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
AnchorNodeinput 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:
| Item | Value |
|---|---|
| Device | Roku Ultra 4802X (US) |
| Firmware | 15.3.5 build 700 (ui-software-version 15.3.5) |
| SDK | workspace-pinned @roku-sdk/* (external workspace) |
| Repro app | device-tests/rsg-ts/anchor-node/app (workspace root) |
| Locked in by | device-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:
| # | Collision | Native behavior | Verdict |
|---|---|---|---|
| A0 | control: unique input name (tsOnlyText:string) | TypeScript round-trips (typeof=string); base fields independent | Works (baseline) |
| A1 | input name == base name, same type (string) | Both the TypeScript input and the base observer see the write; no shadow, no invalidate | Works |
| A2 | input name == base name, type mismatch (string, versus array) | roSGNode: Field baseTags type mismatch during creation; createChild → invalid; app EXIT_SYSTEM_KIL | Fatal |
| A3 | output name == base integer field | Base observer sees the TypeScript-written value (baseCount=99) | Works |
| A4 | input reuses a field the base actively writes | The TypeScript input tracks the base's write | Works |
| A5 | input 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 reconciled | Works with divergence |
| A6 | descriptor declares one name as both input and output | The field mounts once and is usable from TypeScript | Works |
| A7 | input name == base name, type mismatch (array, versus assocarray) | Field baseData type mismatch during creation; createChild → invalid; app EXIT_SYSTEM_KIL | Fatal |
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)
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 field | Observed | Verdict |
|---|---|---|---|
| B0 | control: fresh name, no prior field | =true, usable roString | Works |
| B1 | same name, same type | =true, type unchanged, still settable | Works (idempotent no-op) |
| B2 | same name, different type (integer over string) | =true, but the type stays roString; an integer set is coerced to "42" | Silent no-op (misleading) |
| B3 | same name + alwaysNotify=true | =true, but the notify flag is unchanged (2 identical sets fire the observer once) | Silent no-op (misleading) |
| B5 | preset value, then redundant addField | =true, current value preserved | Works |
The B4 case is intentionally absent. It was reserved for an
addFieldcall with a default value, butaddField(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
AnchorNodeinput 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
addFieldmethod cannot change an existing field's type or notify flag (B2, B3). It returnstruebut does nothing.
Avoiding field-name collisions
The findings lead to the following recommendations:
- You should use distinct names for
AnchorNodeinputFieldsandoutputFields. 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
AnchorNodefield 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
addFieldas create-only because it does not change an existing field's type oralwaysNotify.
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)'Updated about 6 hours ago