Add a framework-agnostic data layer to the web core library#631
Add a framework-agnostic data layer to the web core library#631jacobsimionato merged 40 commits intogoogle:mainfrom
Conversation
There was a problem hiding this comment.
Code Review
The pull request introduces a framework-agnostic data layer for the web core library, aligning it with the v0.9 specification and the Flutter implementation. It features a reactive DataModel with JSON Pointer support, a MessageProcessor for protocol handling, and Zod-based schema validation for components. The architecture is well-decoupled, allowing for easier integration with different web frameworks. However, the relaxation of TypeScript strictness in tsconfig.json is a concern, and there are potential edge cases in the data model's path resolution and array handling that should be addressed to ensure robustness.
Renames the 'A2uiModel' class to 'SurfaceGroupModel' and its corresponding files to better reflect its role in managing groups of surfaces. Updates all internal references and tests accordingly.
…d ComponentsModel Refactors SurfaceGroupModel and ComponentsModel to accept instances via `addSurface` and `addComponent` respectively, improving encapsulation. Implements an observable pattern for action handling in SurfaceModel and SurfaceGroupModel, removing the ActionHandler dependency from the SurfaceModel constructor.
Renames ComponentsModel to SurfaceComponentsModel to better reflect its scope and relationship with SurfaceModel. Updates all references and exports.
|
I have addressed the feedback. Please take another look. |
|
/gemini review |
There was a problem hiding this comment.
Code Review
The pull request introduces a framework-agnostic data layer for A2UI, following a clean three-layer architecture (Processing, Data, and Context). The implementation is well-structured and includes a good suite of tests using the native Node.js test runner. The main issues identified relate to the non-incremental nature of component property updates, which could lead to data loss, and opportunities to leverage the newly added Zod library for better type safety in the message processing layer. There are also some TODOs in the rendering context that should be addressed to ensure full protocol support.
| this.model.removeLifecycleListener(listener); | ||
| } | ||
|
|
||
| processMessages(messages: any[]): void { |
There was a problem hiding this comment.
The processMessages method uses any[] for the input messages. Since zod has been added as a dependency and schemas are being defined in schema_types.ts, it is highly recommended to define a schema for the A2UI messages and validate them here. This would provide better type safety at the system boundary and catch malformed messages early.
There was a problem hiding this comment.
Gemini is kind of right, don't we have an A2UIMessage type for here?
There was a problem hiding this comment.
True true - I defined a type now.
| @@ -0,0 +1,102 @@ | |||
| import { z } from 'zod'; | |||
There was a problem hiding this comment.
Should this code be part of the artifacts provided by the A2UI spec, instead of being in web_core?
There was a problem hiding this comment.
Are we going to have different schema_types in different parts of the code? I wonder if it'd make sense to have a "schemas" directory to put them all there:
schemas
|- catalog
-- something_else
Maybe? IDK, but now I don't think these should be vended from the spec, unless it vends something that we can use to codegen the ones in TS (and maybe Dart) from a single source of truth.
There was a problem hiding this comment.
I moved things to a "schema" folder, which reflects the structure of the specification. Maybe we can generate this from json schema one day, but for now, I think translating it once per spec revision is fine.
| this.model.removeLifecycleListener(listener); | ||
| } | ||
|
|
||
| processMessages(messages: any[]): void { |
There was a problem hiding this comment.
Gemini is kind of right, don't we have an A2UIMessage type for here?
| async dispatchAction(action: any): Promise<void> { | ||
| for (const listener of this.actionListeners) { | ||
| try { | ||
| await listener(action); |
There was a problem hiding this comment.
(Same comment as above about awaiting a promise that may never be resolved)
| @@ -0,0 +1,10 @@ | |||
|
|
|||
| export * from './state/data-model.js'; | |||
| export * from './rendering/data-context.js'; | |||
There was a problem hiding this comment.
Should each of these be a different library, so you can import from web_core/v0.9/state or web_core/v0.9/rendering, instead of from a big file? That would also alleviate issues with name collisions down the line?
There was a problem hiding this comment.
This makes sense. Maybe we can figure it out later though?
| A dedicated store for the surface's application data (the "Model" in MVVM). | ||
|
|
||
| ```typescript | ||
| class DataModel { |
There was a problem hiding this comment.
Would it be nice to have a way of create SomethingModel extends DataModel and have type-safe versions of the models that "hide" the string paths and T details from users?
There was a problem hiding this comment.
We can't do that though, because the DataModel is an untyped thing. The exact structure is defined at runtime according to updateDataModel messages etc.
ditman
left a comment
There was a problem hiding this comment.
Small note about the naming of Schema types (make them all SomethingSomethingSchema?)
| const { surfaceId, catalogId, theme } = payload; | ||
|
|
||
| // Find catalog | ||
| const catalog = this.catalogs.find(c => c.id === catalogId); |
There was a problem hiding this comment.
(this.catalogs being defined directly in the constructor keeps breaking my brain.)
There was a problem hiding this comment.
We should lobby the Dart team to add this! Then it will be the norm in our brains! :-D
| @@ -0,0 +1,23 @@ | |||
| import { z } from 'zod'; | |||
|
|
|||
| export const A2UIMessageSchema = z.object({ | |||
There was a problem hiding this comment.
I like that this is called SomethingSomethingSchema. 2 comments:
- Keep the capitalization consistent, if the
A2uiMessageProcessorusesA2ui, this should beA2uiMessageSchema(same everywhere) - If this is a SomethingSomethingSchema, I'd argue that everything that is defined using Zod should be a SomethingSomethingSchema as well... It's more verbose, but also more descriptive, and frees the "good names" for the actual objects that we may want to use within the rest of the framework.
There was a problem hiding this comment.
Great point. I updated this to make all the zod schemas be called SomethingSchema. And then when we do inferType from it, we just name that as Something rather than SomethingDef.
| return this.path; | ||
| } | ||
|
|
||
| // Normalize current path (remove trailing slash if exists, unless root) |
There was a problem hiding this comment.
Do we want/need to support relative path traversal? Something like:
"../id" or similar?
There was a problem hiding this comment.
Ummm that's interesting. It's not in the spec right now, so we don't need it. I'm sure we'll find more edge cases to handle here, but at least we will have things more centralized now so it's not so hard to tighten the implementation.
| called = true; | ||
| }); | ||
| model.set('/user/name', 'Charlie'); | ||
| assert.strictEqual(called, true, 'Callback was never called'); |
You need to commit and push the generated lockfiles, unfortunately (just rebuild the examples) |
This PR introduces a framework-agnostic data model for A2UI within the
web_corelibrary, following the architecture outlined in Unified Data Model Architecture.Key Changes
renderers/web_core/src/v0_9:A2uiMessageProcessorfor handling the A2UI message stream.SurfaceGroupModel,SurfaceModel,SurfaceComponentsModel, andDataModel.DataContextandComponentContextto facilitate rendering and scope resolution.zodandzod-to-json-schematoweb_coreto support type-safe catalog definitions and schema introspection as planned in the v0.9 evolution../v0_9export to@a2ui/web_coreto allow framework-specific renderers (like Lit or Angular) to opt-in to the new architecture.testscript using the native Node.js test runner for the new model.Architectural Alignment
The implementation adheres to the principles of separation of concerns, mutable but observable "dumb" models, and granular reactivity through standard observer patterns, ensuring it remains framework-agnostic and high-performance.