This is a proposal based on a comment that @yjbanov made on the original design for web_core. After thinking about it a bit more and seeing how the libraries evolved, I think he was right!
Description and Rationale
We are introducing a "Node Layer" architecture into the A2UI core libraries and refactoring the existing rendering frameworks (React, Angular, Lit) to consume it.
Currently, frameworks are responsible for complex tasks: traversing the flat SurfaceModel adjacency list, resolving data bindings using GenericBinder, handling template expansions (e.g., ChildList), and managing subscription lifecycles. This causes duplicated logic across frameworks, risks memory leaks due to forgotten binder disposals, and makes adding new framework adapters difficult.
The Node Layer centralizes this "business logic" into the core library. It transforms the flat component map into a living, reactive view hierarchy. Renderers will simply receive fully-resolved, type-safe Node instances and focus solely on mapping them to native UI primitives (pixels).
Detailed Architecture & Design
The Node Layer represents a shift from frameworks directly managing components and data to having the SurfaceModel manage the entire view hierarchy lifecycle.
1. Core Data Structures: The Node
The new architecture introduces a Node<TProps> interface. A Node represents a living, fully resolved component instance in the view hierarchy. By utilizing generics, the Node interface provides type-safe property access to the rendering frameworks.
instanceId: A stable, unique identifier for this instance in the rendered tree. For templated nodes, this incorporates the data path (e.g., 'item-card-[/users/0]') to ensure React/Flutter keys remain stable.
props: A reactive Signal<TProps>. Dynamic values are primitive strings/numbers/booleans, Actions are callable () => void closures, and Structural props (e.g., child or children) contain actual child Node references (not string IDs).
- Lifecycle:
onDestroyed event and a dispose() method.
2. Internal Node Engine inside SurfaceModel
The management of the Node tree is an implementation detail inside SurfaceModel. Because SurfaceModel encapsulates both the DataModel and the SurfaceComponentsModel, it is perfectly positioned to handle this.
- Public API:
SurfaceModel exposes a single, reactive rootNode: Signal<Node | undefined>. Frameworks simply subscribe to this signal.
- Internal Engine: The
SurfaceModel internally tracks SurfaceComponentsModel changes and DataModel mutations. It manages a private cache of active nodes, automatically instantiating and destroying child nodes as requested by layout containers.
3. 1-to-N Mapping for Templates (ChildLists)
A primary responsibility of the internal Node engine is managing the 1-to-N mapping between a single ComponentModel and multiple rendered Node instances.
When an A2UI payload contains a ChildList acting as a template (e.g., a Card with ID "user-card" iterating over /users), it will be rendered multiple times.
- The internal engine handles subscribing to the
DataModel array path (e.g., /users).
- If the array has 3 items, the engine spawns 3 unique
Node instances for the same componentId.
- Each spawned node receives its own
instanceId, scoped dataPath, and an isolated GenericBinder to resolve its properties.
- If the array length changes (e.g., a 4th user is added), the parent node detects the array mutation and spawns a 4th
Node, emitting an updated ChildList prop containing the 4 nodes.
- When destroyed (e.g., a user is deleted), the Node fires
onDestroyed, disposes its GenericBinder, and destroys its children.
4. Soft Transition for Adapters
Framework adapters will expose Node objects for structural properties, but will maintain backward-compatible shims so component authors don't have to rewrite all their UI code immediately.
buildChild Overload: The buildChild helper will accept child: string | Node. If it receives a Node, it renders it directly.
Node.toString(): Implement Node.prototype.toString() to return its componentId, preventing breakage for logic using ID comparisons.
This is a proposal based on a comment that @yjbanov made on the original design for web_core. After thinking about it a bit more and seeing how the libraries evolved, I think he was right!
Description and Rationale
We are introducing a "Node Layer" architecture into the A2UI core libraries and refactoring the existing rendering frameworks (React, Angular, Lit) to consume it.
Currently, frameworks are responsible for complex tasks: traversing the flat
SurfaceModeladjacency list, resolving data bindings usingGenericBinder, handling template expansions (e.g.,ChildList), and managing subscription lifecycles. This causes duplicated logic across frameworks, risks memory leaks due to forgotten binder disposals, and makes adding new framework adapters difficult.The Node Layer centralizes this "business logic" into the core library. It transforms the flat component map into a living, reactive view hierarchy. Renderers will simply receive fully-resolved, type-safe
Nodeinstances and focus solely on mapping them to native UI primitives (pixels).Detailed Architecture & Design
The Node Layer represents a shift from frameworks directly managing components and data to having the
SurfaceModelmanage the entire view hierarchy lifecycle.1. Core Data Structures: The
NodeThe new architecture introduces a
Node<TProps>interface. ANoderepresents a living, fully resolved component instance in the view hierarchy. By utilizing generics, theNodeinterface provides type-safe property access to the rendering frameworks.instanceId: A stable, unique identifier for this instance in the rendered tree. For templated nodes, this incorporates the data path (e.g.,'item-card-[/users/0]') to ensure React/Flutter keys remain stable.props: A reactiveSignal<TProps>. Dynamic values are primitive strings/numbers/booleans, Actions are callable() => voidclosures, and Structural props (e.g.,childorchildren) contain actual childNodereferences (not string IDs).onDestroyedevent and adispose()method.2. Internal Node Engine inside SurfaceModel
The management of the Node tree is an implementation detail inside
SurfaceModel. BecauseSurfaceModelencapsulates both theDataModeland theSurfaceComponentsModel, it is perfectly positioned to handle this.SurfaceModelexposes a single, reactiverootNode: Signal<Node | undefined>. Frameworks simply subscribe to this signal.SurfaceModelinternally tracksSurfaceComponentsModelchanges andDataModelmutations. It manages a private cache of active nodes, automatically instantiating and destroying child nodes as requested by layout containers.3. 1-to-N Mapping for Templates (ChildLists)
A primary responsibility of the internal Node engine is managing the 1-to-N mapping between a single
ComponentModeland multiple renderedNodeinstances.When an A2UI payload contains a
ChildListacting as a template (e.g., aCardwith ID"user-card"iterating over/users), it will be rendered multiple times.DataModelarray path (e.g.,/users).Nodeinstances for the samecomponentId.instanceId, scopeddataPath, and an isolatedGenericBinderto resolve its properties.Node, emitting an updatedChildListprop containing the 4 nodes.onDestroyed, disposes itsGenericBinder, and destroys its children.4. Soft Transition for Adapters
Framework adapters will expose
Nodeobjects for structural properties, but will maintain backward-compatible shims so component authors don't have to rewrite all their UI code immediately.buildChildOverload: ThebuildChildhelper will acceptchild: string | Node. If it receives aNode, it renders it directly.Node.toString(): ImplementNode.prototype.toString()to return itscomponentId, preventing breakage for logic using ID comparisons.