Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6ab973e
Add gemignore
jacobsimionato Jan 29, 2026
89db879
Add design docs
jacobsimionato Feb 16, 2026
4f43a8e
Add core renderer APIs not trimmed
jacobsimionato Feb 16, 2026
3b9ce30
Merge branch 'main' into data-layer-1
jacobsimionato Feb 16, 2026
d2f663b
Make a scaled down version of web-core
jacobsimionato Feb 16, 2026
aa1196d
Fix client capabilities test
jacobsimionato Feb 16, 2026
ca866e3
Update renderers web core
jacobsimionato Feb 16, 2026
a01b828
Update data model
jacobsimionato Feb 18, 2026
53e9469
refactor(web_core): rename A2uiModel to SurfaceGroupModel in v0.9
jacobsimionato Feb 18, 2026
5e62b6d
refactor(web_core): use standard add pattern for SurfaceGroupModel an…
jacobsimionato Feb 18, 2026
b77455c
refactor(web_core): rename ComponentsModel to SurfaceComponentsModel
jacobsimionato Feb 18, 2026
a15804d
replace many docs with one doc
jacobsimionato Feb 18, 2026
d82673e
Fix doc
jacobsimionato Feb 19, 2026
7647e3d
Add manual edits
jacobsimionato Feb 19, 2026
310fc80
Improve framework renderer section
jacobsimionato Feb 19, 2026
951fa0b
Simplify design
jacobsimionato Feb 19, 2026
98b26c6
Merge branch 'main' into data-layer-1
jacobsimionato Feb 19, 2026
8a088f2
Enable scrict mode again
jacobsimionato Feb 22, 2026
b412047
fix: Address PR feedback
jacobsimionato Feb 22, 2026
14e2ca5
Fix message processor
jacobsimionato Feb 24, 2026
6e15d44
Fix docs
jacobsimionato Feb 24, 2026
e3b2f65
Address feedback
jacobsimionato Feb 26, 2026
68efefc
Improve tests
jacobsimionato Feb 26, 2026
bb1e362
Improve subscription API
jacobsimionato Feb 26, 2026
95ab135
fix wishy washy types
jacobsimionato Feb 26, 2026
dd34140
Fix path comment
jacobsimionato Feb 26, 2026
d70d5ee
Add todos for type checks
jacobsimionato Feb 26, 2026
b38c982
Rename to renderer guide
jacobsimionato Feb 26, 2026
b331957
Update message processor to remove getSurface
jacobsimionato Feb 26, 2026
5733cef
Fix minor issues
jacobsimionato Feb 26, 2026
248e574
Update renderer guide
jacobsimionato Feb 26, 2026
93bec2b
Updates including event emitter etc
jacobsimionato Feb 26, 2026
48edaed
reduce node version
jacobsimionato Feb 26, 2026
f5e3f2d
A few updates
jacobsimionato Feb 27, 2026
ed611f1
Update renderer_guide
jacobsimionato Feb 27, 2026
b13accd
Fix schema names
jacobsimionato Feb 27, 2026
a4fa67b
Some improvements
jacobsimionato Feb 27, 2026
144f703
Update surface components and renderer guide
jacobsimionato Feb 27, 2026
4839444
remvoe design alternatives
jacobsimionato Feb 27, 2026
6a5b9b6
Remove flutter
jacobsimionato Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions renderers/web_core/package-lock.json
Comment thread
ditman marked this conversation as resolved.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion renderers/web_core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"prepack": "npm run build",
"build": "wireit",
"build:tsc": "wireit",
"copy-spec": "wireit"
"test": "wireit"
},
"wireit": {
"copy-spec": {
Expand Down Expand Up @@ -68,6 +68,12 @@
"!dist/**/*.min.js{,.map}"
],
"clean": "if-file-deleted"
},
"test": {
"command": "node --test dist/**/*.test.js",
"dependencies": [
"build"
]
}
},
"author": "Google",
Expand All @@ -76,5 +82,9 @@
"@types/node": "^24.10.1",
"typescript": "^5.8.3",
"wireit": "^0.15.0-pre.2"
},
"dependencies": {
"zod": "^3.25.76",
"zod-to-json-schema": "^3.25.1"
}
}
38 changes: 38 additions & 0 deletions renderers/web_core/src/v0_9/catalog/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { z } from 'zod';

/**
* A definition of a UI component's API.
* This interface defines the contract for a component's capabilities and properties,
* independent of any specific rendering implementation.
*/
export interface ComponentApi {
/** The name of the component as it appears in the A2UI JSON (e.g., 'Button'). */
name: string;

/**
* The Zod schema describing the **properties** of this component.
*
* - MUST include catalog-specific common properties (e.g. 'weight', 'accessibility').
* - MUST NOT include 'component' or 'id' as those are handled by the framework/envelope.
*/
readonly schema: z.ZodType<any>;
}

export class Catalog<T extends ComponentApi> {
readonly id: string;

/**
* A map of available components.
* This is readonly to encourage immutable extension patterns.
*/
readonly components: ReadonlyMap<string, T>;

constructor(id: string, components: T[]) {
this.id = id;
const map = new Map<string, T>();
for (const comp of components) {
map.set(comp.name, comp);
}
this.components = map;
}
}
44 changes: 44 additions & 0 deletions renderers/web_core/src/v0_9/common/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/** Standard cleanup interface returned by all subscriptions. */
export interface Subscription {
unsubscribe(): void;
}

/** The listener function signature. */
export type EventListener<T> = (data: T) => void | Promise<void>;

/**
* Public interface exposed by models.
* Allows ONLY subscribing to events.
*/
export interface EventSource<T> {
subscribe(listener: EventListener<T>): Subscription;
}

/**
* Internal implementation used by the model.
* Implements EventSource but also provides the 'emit' method.
*/
export class EventEmitter<T> implements EventSource<T> {
private listeners = new Set<EventListener<T>>();

subscribe(listener: EventListener<T>): Subscription {
this.listeners.add(listener);
return {
unsubscribe: () => this.listeners.delete(listener)
};
}

async emit(data: T): Promise<void> {
for (const listener of this.listeners) {
try {
await listener(data);
} catch (e) {
console.error('EventEmitter error:', e);
}
}
}

dispose(): void {
this.listeners.clear();
}
}
12 changes: 12 additions & 0 deletions renderers/web_core/src/v0_9/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

export * from './state/data-model.js';
export * from './common/events.js';
export * from './rendering/data-context.js';
Copy link
Copy Markdown
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
Copy Markdown
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?

export * from './state/surface-model.js';
export * from './processing/message-processor.js';
export * from './catalog/types.js';
export * from './rendering/component-context.js';
export * from './state/surface-group-model.js';
export * from './state/surface-components-model.js';
export * from './schema/index.js';

133 changes: 133 additions & 0 deletions renderers/web_core/src/v0_9/processing/message-processor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import assert from 'node:assert';
import { test, describe, it, beforeEach } from 'node:test';
import { MessageProcessor } from './message-processor.js';
import { Catalog, ComponentApi } from '../catalog/types.js';

describe('MessageProcessor', () => {
let processor: MessageProcessor<ComponentApi>;
let testCatalog: Catalog<ComponentApi>;
let actions: any[] = [];

beforeEach(() => {
actions = [];
testCatalog = new Catalog('test-catalog', []);
processor = new MessageProcessor<ComponentApi>([testCatalog], async (a) => { actions.push(a); });
});

it('creates surface', () => {
processor.processMessages([{
createSurface: {
surfaceId: 's1',
catalogId: 'test-catalog',
theme: {}
}
}]);
const surface = processor.model.getSurface('s1');
assert.ok(surface);
assert.strictEqual(surface.id, 's1');
});

it('updates components on correct surface', () => {
processor.processMessages([{
createSurface: { surfaceId: 's1', catalogId: 'test-catalog' }
}]);

processor.processMessages([{
updateComponents: {
surfaceId: 's1',
components: [{ id: 'root', component: 'Box' }]
}
}]);

const surface = processor.model.getSurface('s1');
assert.ok(surface?.componentsModel.get('root'));
});

it('updates existing components via message', () => {
processor.processMessages([{
createSurface: { surfaceId: 's1', catalogId: 'test-catalog' }
}]);

// Create
processor.processMessages([{
updateComponents: {
surfaceId: 's1',
components: [{ id: 'btn', component: 'Button', label: 'Initial' }]
}
}]);

const surface = processor.model.getSurface('s1');
const btn = surface?.componentsModel.get('btn');
assert.strictEqual(btn?.properties.label, 'Initial');

// Update
processor.processMessages([{
updateComponents: {
surfaceId: 's1',
components: [{ id: 'btn', component: 'Button', label: 'Updated' }]
}
}]);

assert.strictEqual(btn?.properties.label, 'Updated');
});

it('deletes surface', () => {
processor.processMessages([{
createSurface: { surfaceId: 's1', catalogId: 'test-catalog' }
}]);
assert.ok(processor.model.getSurface('s1'));

processor.processMessages([{
deleteSurface: { surfaceId: 's1' }
}]);
assert.strictEqual(processor.model.getSurface('s1'), undefined);
});

it('routes data model updates', () => {
processor.processMessages([{
createSurface: { surfaceId: 's1', catalogId: 'test-catalog' }
}]);

processor.processMessages([{
updateDataModel: {
surfaceId: 's1',
path: '/foo',
value: 'bar'
}
}]);

const surface = processor.model.getSurface('s1');
assert.strictEqual(surface?.dataModel.get('/foo'), 'bar');
});

it('notifies lifecycle listeners', () => {
let created: any = null;
let deletedId: string | null = null;

const sub = processor.onSurfaceCreated((s) => { created = s; });
const sub2 = processor.onSurfaceDeleted((id) => { deletedId = id; });

// Create
processor.processMessages([{
createSurface: { surfaceId: 's1', catalogId: 'test-catalog' }
}]);
assert.ok(created);
assert.strictEqual(created.id, 's1');

// Delete
processor.processMessages([{
deleteSurface: { surfaceId: 's1' }
}]);
assert.strictEqual(deletedId, 's1');

// Test Unsubscribe
created = null;
sub.unsubscribe();
processor.processMessages([{
createSurface: { surfaceId: 's2', catalogId: 'test-catalog' }
}]);
assert.strictEqual(created, null);

sub2.unsubscribe();
});
});
Loading
Loading