Skip to content

Add a framework-agnostic data layer to the web core library#631

Merged
jacobsimionato merged 40 commits intogoogle:mainfrom
jacobsimionato:data-layer-1
Feb 27, 2026
Merged

Add a framework-agnostic data layer to the web core library#631
jacobsimionato merged 40 commits intogoogle:mainfrom
jacobsimionato:data-layer-1

Conversation

@jacobsimionato
Copy link
Collaborator

@jacobsimionato jacobsimionato commented Feb 16, 2026

This PR introduces a framework-agnostic data model for A2UI within the web_core library, following the architecture outlined in Unified Data Model Architecture.

Key Changes

  • New v0.9 Data Model: Implemented the three-layer architecture in renderers/web_core/src/v0_9:
    • Processing Layer: A2uiMessageProcessor for handling the A2UI message stream.
    • Data Layer: Persistent, "dumb" models including SurfaceGroupModel, SurfaceModel, SurfaceComponentsModel, and DataModel.
    • Context Layer: Transient DataContext and ComponentContext to facilitate rendering and scope resolution.
  • Dependency Updates: Added zod and zod-to-json-schema to web_core to support type-safe catalog definitions and schema introspection as planned in the v0.9 evolution.
  • Package Exports: Added a new ./v0_9 export to @a2ui/web_core to allow framework-specific renderers (like Lit or Angular) to opt-in to the new architecture.
  • Improved Testing: Added a test script 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.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@jacobsimionato jacobsimionato marked this pull request as ready for review February 19, 2026 05:36
@jacobsimionato
Copy link
Collaborator Author

I have addressed the feedback. Please take another look.

@jacobsimionato
Copy link
Collaborator Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gemini is kind of right, don't we have an A2UIMessage type for here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True true - I defined a type now.

@google google deleted a comment from gemini-code-assist bot Feb 24, 2026
Copy link
Collaborator

@ditman ditman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments!

@@ -0,0 +1,102 @@
import { z } from 'zod';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this code be part of the artifacts provided by the A2UI spec, instead of being in web_core?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator

@ditman ditman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(this.catalogs being defined directly in the constructor keeps breaking my brain.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that this is called SomethingSomethingSchema. 2 comments:

  • Keep the capitalization consistent, if the A2uiMessageProcessor uses A2ui, this should be A2uiMessageSchema (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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want/need to support relative path traversal? Something like:

"../id" or similar?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this better!

@ditman
Copy link
Collaborator

ditman commented Feb 26, 2026

npm error Invalid: lock file's @types/node@25.2.3 does not satisfy @types/node@24.10.15

You need to commit and push the generated lockfiles, unfortunately (just rebuild the examples)

@jacobsimionato jacobsimionato merged commit e865988 into google:main Feb 27, 2026
8 checks passed
@github-project-automation github-project-automation bot moved this from Todo to Done in A2UI Feb 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants