AnchorNode ↔ base-component field-name collisions

When a TypeScript AnchorNode extends a custom SceneGraph base component, its inputFields and outputFields share one field namespace with the base component's own <interface> fields. This document describes what the Roku render thread does for each way that the names can collide, including one fatal case, to help you avoid problems.

You must not declare an AnchorNode input or output whose name matches a base field but whose type differs. This is fatal: the app exits at node creation. Reusing a name with the same type is safe but rarely what you want; therefore, you should use unique names.

Environment under test

The tests ran in the following environment:

ItemValue
DeviceRoku Ultra 4802X (US)
Firmware15.3.5 build 700 (ui-software-version 15.3.5)
SDKworkspace-pinned @roku-sdk/* (external workspace)
Repro appdevice-tests/rsg-ts/anchor-node/app (workspace root)
Locked in bydevice-tests/rsg-ts/anchor-node/field-collision.test.ts, device-tests/rsg-ts/anchor-node/addfield-clobber.test.ts

An AnchorNode that uses extends requires Roku OS 15.3 or later. The tests mount each case in its own launch (POST /launch/dev?case=<ID>) so that a fatal case cannot mask the others.

Understanding collisions with base interface fields (Track A)

Track A tests an AnchorNode input or output against a field in the base component's <interface>. The following table lists each collision and the observed behavior:

#CollisionNative behaviorVerdict
A0control: unique input name (tsOnlyText:string)TypeScript round-trips (typeof=string); base fields independentWorks (baseline)
A1input name == base name, same type (string)Both the TypeScript input and the base observer see the write; no shadow, no invalidateWorks
A2input name == base name, type mismatch (string, versus array)roSGNode: Field baseTags type mismatch during creation; createChild → invalid; app EXIT_SYSTEM_KILFatal
A3output name == base integer fieldBase observer sees the TypeScript-written value (baseCount=99)Works
A4input reuses a field the base actively writesThe TypeScript input tracks the base's writeWorks
A5input default 0, versus base XML value="7" (default mismatch)Divergence: the base keeps 7, and the TypeScript input layer reads 0; the two are not reconciledWorks with divergence
A6descriptor declares one name as both input and outputThe field mounts once and is usable from TypeScriptWorks
A7input name == base name, type mismatch (array, versus assocarray)Field baseData type mismatch during creation; createChild → invalid; app EXIT_SYSTEM_KILFatal

What the fatal case looks like (A2 and A7)

The device log shows the following error:

BRIGHTSCRIPT: ERROR: roSGNode: Field baseTags type mismatch during creation: pkg:/components/CollisionScene.brs(NN)

The node is rejected at creation. Under static XML placement, this aborts the whole scene. Through createChild, the call returns invalid and the app exits (EXIT_SYSTEM_KIL) shortly after. In either case, the collision is uncontained: it takes the app down, not only the offending node.

Understanding addField against an already-declared field (Track B)

The addField method is the BrightScript-side analogue of a collision. Track B tests what happens when you call addField with a name that already exists on the node. The following table lists each case and the observed behavior:

#addField call, versus existing fieldObservedVerdict
B0control: fresh name, no prior field=true, usable roStringWorks
B1same name, same type=true, type unchanged, still settableWorks (idempotent no-op)
B2same name, different type (integer over string)=true, but the type stays roString; an integer set is coerced to "42"Silent no-op (misleading)
B3same name + alwaysNotify=true=true, but the notify flag is unchanged (2 identical sets fire the observer once)Silent no-op (misleading)
B5preset value, then redundant addField=true, current value preservedWorks

The B4 case is intentionally absent. It was reserved for an addField call with a default value, but addField(name, type, alwaysNotify) does not have a default-value parameter; therefore, the case cannot be expressed. B5 covers value preservation instead.

The addField method cannot redefine an existing field's type or alwaysNotify flag. It returns true, which is misleading; therefore, code that expects a redefinition silently keeps the original definition.

Summarizing the findings

The tests produced the following findings:

  • A type-mismatch name collision is fatal. An AnchorNode input or output whose name matches a base field but whose type differs crashes the app at node creation (A2, A7).
  • Same-type collisions are safe but ambiguous. A same-name, same-type input (A1), an output over a base field (A3), reuse of a base-written field (A4), and a descriptor input and output duplicate (A6) all round-trip cleanly.
  • A default mismatch is not reconciled (A5). The base keeps its XML default, and the TypeScript input layer reports its own. Two defaults coexist silently.
  • The addField method cannot change an existing field's type or notify flag (B2, B3). It returns true but does nothing.

Avoiding field-name collisions

The findings lead to the following recommendations:

  • You should use distinct names for AnchorNode inputFields and outputFields. Do not reuse the names of the base component's custom <interface> fields.
  • If you need to bridge to a base field, you should give the AnchorNode field a different name and forward it inside the base component's BrightScript (observeField, then assign) instead of relying on a same-name collision.
  • You must not declare a same-name field with a different type than the base. It is fatal on the device, and the build does not currently guard against it.
  • You should treat addField as create-only because it does not change an existing field's type or alwaysNotify.

Reproducing the results

The following commands reproduce the cases on a device (Roku OS 15.3 or later):

# One case per launch on a device (Roku OS 15.3 or later)
cd device-tests/rsg-ts/anchor-node/app && npx rk dev --host <ip> --password <pw>
curl -d '' "http://<ip>:8060/launch/dev?case=A2"   # fatal type-mismatch
curl -d '' "http://<ip>:8060/launch/dev?case=B2"   # addField silent no-op

# Automated regression:
RK_DEVICE_IP=<ip> RK_DEVICE_PASSWORD=<pw> \
  pnpm -F device-tests test --testPathPatterns='rsg-ts/anchor-node/(field-collision|addfield-clobber)'

Did this page help you?