Migrate package:genui onto package:a2ui_core (state engine + messages)#974
Migrate package:genui onto package:a2ui_core (state engine + messages)#974andrewkolos wants to merge 49 commits into
package:genui onto package:a2ui_core (state engine + messages)#974Conversation
…e JSON
Four substrate-level changes the genui facade migration depends on or
benefits from. Bundled because they're all isolated to a2ui_core and
each is small enough to review at the same sitting.
1. messages.dart: parse-time envelope validation.
- Reject envelopes missing the `version` field or carrying a version
other than 'v0.9' (was: accepted anything).
- Reject envelopes that contain more than one action key
(createSurface/updateComponents/updateDataModel/deleteSurface) —
the v0.9 schema models these as `oneOf`, but the old code silently
dispatched on whichever check matched first.
- UpdateDataModelMessage gains a `hasValue` field plus a
`.removeKey` named constructor. The wire distinction between
`"value": null` (set to null) and an omitted `value` key (remove
the key / sparse-clear a list index) now round-trips losslessly.
Runtime DataModel.set behavior is unchanged; the distinction only
matters for observers/relayers that inspect the message.
2. data_model.dart: defensively copy incoming Map/List values.
Wraps every container the caller hands to DataModel (constructor
`initialData` and `set()` writes) with a deep mutable copy so later
nested writes don't blow up when callers passed `const` literals or
otherwise-unmodifiable views. Test added covering the
"set('/') of a const map, then set('/nested/count')" path.
3. data_path.dart: drop RFC 6901 `~0`/`~1` escape interpretation.
The web_core reference renderer never implemented escaping despite
the v0.9 spec citing RFC 6901; a2ui_core was the only implementation
diverging. Aligns Dart with web_core. Affects only keys containing
literal '~' or '/' — extremely rare in data model usage.
4. reactivity.dart: add `effect` to the re-exported symbols.
preact_signals provides `effect()` as a one-shot wrapper for
`Effect(fn)()`; exporting it lets consumers avoid the
`Effect()..call()` ceremony at the call site.
… facade)
Wraps a2ui_core.DataModel inside genui's existing DataModel/InMemoryDataModel
public API. Adds a2ui_core to the genui pubspec. Preserves the existing
GenUI surface (DataPath, ValueListenable subscriptions, getValue<T>,
update(DataPath, value), bindExternalState, DataModelTypeException) so
no consumer rename is required.
Substantive pieces inside this commit:
1. data_model.dart: InMemoryDataModel becomes a compat facade.
- Constructor `InMemoryDataModel()` constructs a fresh core.DataModel
and owns it. `InMemoryDataModel.wrap(core.DataModel)` is @internal
and lets the controller share a single live model with the surface.
- update(DataPath, value) -> core.set(path.toString(), value).
- subscribe<T>(DataPath) returns a _SignalNotifier — a ChangeNotifier
backed by a preact_signals effect over core.watch<Object?>. The
effect forces a notification on every source change (including
`==`-equal values) so in-place Map/List mutations propagate.
- getValue<T>(DataPath) preserves the typed-read throw via
DataModelTypeException so legacy callers keep their type check.
- bindExternalState is preserved as both instance method and a
top-level helper.
- DataContext keeps its existing public surface (subscribe,
subscribeStream, getValue, update, nested, resolvePath, resolve,
evaluateConditionStream). Internally it routes through the
wrapped DataModel.
2. widget_utilities.dart: Bound* widgets rewritten on preact_signals
internally, but the constructor parameters and builder signature are
unchanged. Two non-obvious wrinkles preserved from this branch's
earlier exploration:
- BoundValue uses a signal+listener bridge (not core.computed)
because computed gates downstream notifications on `==` equality,
which for Maps/Lists is identity-based — so a2ui_core's bubble
notifications on in-place mutations would be silently dropped.
The bridge force-mirrors the source value on every change.
- _SignalBuilder is a small private StatefulWidget that subscribes
to a ReadonlySignal via preact_signals' effect(). signals_flutter
is NOT compatible (different signals package).
3. client_function.dart: ExecutionContext interface picks up the
compat-facade signatures (`Object` for path args so callers can
pass either DataPath or String). format_string.dart drops a couple
of redundant DataPath constructors at call sites.
4. test/widgets/widget_utilities_test.dart: regression for the
BoundObject/BoundList in-place mutation case — without the bridge
fix, these would silently miss bubble-notifications.
Test/dev_tools/example fallout from this commit is minimal because
the public API shape is unchanged.
…n facade Surfaces internally become live core.SurfaceModel instances managed by core.SurfaceGroupModel, with the existing SurfaceDefinition / Component public API preserved as a snapshot facade. GenUI's own renderer reads the live model for granular per-component rebuilds; external code that implements SurfaceContext keeps using the legacy snapshot API. Substantive pieces: 1. surface_registry.dart owns the live core.SurfaceModel for each active surface and exposes both: - watchSurface(id): ValueListenable<core.SurfaceModel?> (live) - watchDefinition(id): ValueListenable<SurfaceDefinition?> (snapshot) The latter materializes a SurfaceDefinition.fromCore lazily when listeners actually read it, and re-materializes on per-component updates. removeSurface only nulls the notifier values — the notifiers stay alive across delete so re-create-with-same-id finds widgets still attached. 2. interfaces/surface_context.dart exposes the legacy `definition: ValueListenable<SurfaceDefinition?>` plus a new @internal LiveSurfaceContext extension that adds `surface: ValueListenable<core.SurfaceModel?>`. External SurfaceContext implementations only need to provide the legacy `definition` snapshot path; GenUI's own controller provides both. 3. widgets/surface.dart has two render paths: - _buildLiveWidget: used when the context is LiveSurfaceContext. Wraps each rendered component in a _ComponentBuilder that subscribes to that component's onUpdated, so an UpdateComponents for one component rebuilds only its subtree (flutter#811 §3.5). - _buildWidgetFromDefinition: used when the context only provides the legacy snapshot path. Rebuilds from the snapshot on every definition change (pre-flutter#811 behavior). This dual path is the cost of the compat-facade strategy. Custom SurfaceContext implementations stay on the legacy path; granular reactivity is opt-in by implementing LiveSurfaceContext. 4. ui_models.dart: SurfaceDefinition gains a .fromCore(SurfaceModel) factory and keeps its validate(Schema), copyWith, asContextDescription methods. SurfaceUpdate.{SurfaceAdded,ComponentsUpdated} carries both the live `surface` and a lazy `definition` snapshot — lifecycle-only listeners pay nothing. 5. engine/data_model_store.dart is kept as a compat facade. Its lookup callback now redirects to InMemoryDataModel.wrap of the live surface.dataModel for active surfaces, falling back to standalone models when no live surface exists. Carries an explicit dartdoc marking it as compatibility-only. 6. model/parts/ui.dart's UiPart.create accepts either SurfaceDefinition or a raw JsonMap as `definition`. Parsing always re-materializes a SurfaceDefinition snapshot so consumers don't see the wire-shape difference. 7. Legacy SurfaceDefinition.validate(Schema) is preserved. Tests covering surface lifecycle live in commit 4 with their controller/registry counterparts.
…ssor
SurfaceController becomes a thin Flutter-side wrapper around
core.MessageProcessor. The substrate owns canonical state mutation
(create/update/delete surfaces and their components/data models);
this class adds the Flutter-specific concerns: pre-create message
buffering, schema validation, a SurfaceUpdate stream the facade
subscribes to, and the GenUI-named A2uiMessage facade.
Substantive pieces:
1. engine/surface_controller.dart: rewrite.
- Public `handleMessage(A2uiMessage)` accepts the genui-facade
message type. It converts to core via `message.toCoreMessage()`
and delegates to a private `_handleCoreMessage(core.A2uiMessage)`.
The buffered/flushed path uses the private method directly so
no double-conversion happens.
- Holds a core.MessageProcessor built from each catalog's
`coreCatalog` adapter; subscribes to its
groupModel.onSurfaceCreated/onSurfaceDeleted and forwards them
to the SurfaceRegistry.
- Pre-create buffering: UpdateComponents/UpdateDataModel that
arrive before their createSurface are held in a per-surfaceId
queue with `pendingUpdateTimeout` cleanup, then flushed when
the surface is created.
- Lenient unknown-catalogId behavior is preserved (registers an
empty stub core.Catalog so the substrate's "not found" check
passes; the genui Surface widget surfaces FallbackWidget at
render time as before).
- Schema validation runs after each UpdateComponents via the new
shared schema_validation helper (item 5 below). Mutation is not
rolled back on failure — the error is reported and the caller
decides.
2. model/a2ui_message.dart: GenUI message facade classes.
`A2uiMessage`, `CreateSurface`, `UpdateComponents`,
`UpdateDataModel`, `DeleteSurface` each wrap the corresponding
core message. `.fromJson(JsonMap)` parses via core then converts;
`.fromCore(core.A2uiMessage)` adapts an existing core message;
`.toCoreMessage()` converts back. `UpdateDataModel` preserves
the value-omitted vs value-null wire distinction via a
`hasValue` flag + `.removeKey` named constructor matching
a2ui_core's new shape.
3. model/catalog.dart: `coreCatalog` extension exposes a
core.Catalog<core.ComponentApi> view of a genui Catalog so the
substrate has real component metadata for the processor.
Functions and theme are not adapted yet — Node Layer (#1282)
will redo this boundary; until then catalog widgets resolve
functions through the genui DataContext, not the core one.
4. model/catalog_item.dart: CatalogItemContext keeps both the
legacy public unnamed constructor (which builds a stand-alone
substrate context internally) AND an @internal fromCore
constructor used by the live render path. `withOverrides({...})`
is the rebind-callbacks-while-preserving-substrate-context
pattern Catalog.buildWidget uses.
5. model/schema_validation.dart (new): shared validator extracted
from both SurfaceDefinition.validate and the controller's
post-mutation validation. Takes an iterable of
`({id, type, json})` records plus the catalog Schema; both
call sites adapt their representation into the iterable.
6. transport/a2ui_parser_transformer.dart: routes through
core.A2uiMessage.fromJson then wraps the result in the facade
A2uiMessage. Envelope detection (the `_looksLikeA2uiEnvelope`
helper) treats `version` as an envelope marker, so a payload
like {"version":"v0.9","unknownAction":{}} is surfaced as a
validation error rather than falling through to TextEvent.
7. New regression tests:
- data_model_edge_cases_test.dart: covers the
mutable-copy-of-const-Map case the substrate fix in commit 1
introduced.
- surface_controller_test.dart: store-facade test, duplicate
createSurface, pre-create dataModel access.
- a2ui_message_test.dart: facade round-trip cases.
- core_widgets_test.dart: minor accommodation for the new
contextFor(...)/dataModel path.
Short guide for consumers. The substrate moved into a2ui_core but the existing GenUI public API names are preserved as compat facades, so most catalog widget authors and example apps need no source changes. The guide: 1. Lists what stays source-compatible: CreateSurface, DataPath, DataModel, InMemoryDataModel, SurfaceDefinition, Component, SurfaceContext.definition, SurfaceController.store / DataModelStore, ActionDelegate.handleEvent's existing signature, CatalogItemContext public fields, UiPart.create(definition: SurfaceDefinition(...)). 2. Documents what changed internally: SurfaceController delegates to MessageProcessor; InMemoryDataModel wraps core.DataModel; the Surface widget uses live core models for granular rebuilds when its context is GenUI's own controller context, and falls back to the legacy snapshot path for custom SurfaceContexts. 3. Lists residual behavior changes to watch for: stricter DataModel writes, defensively-copied stored containers, signal-backed reactivity inside Bound*, stricter envelope validation, duplicate createSurface as an error, RFC 6901 escapes no longer interpreted. 4. Notes that the GenUI-named compat types are the current public API, not short-lived shims. A future PR may add a2ui_core-shaped aliases for cross-language parity, but the existing names will not be removed without a separate deprecation cycle.
…akage Pre-create writes to `SurfaceContext.dataModel` were lost when the live surface arrived (the fallback `InMemoryDataModel` stayed shadowed by the new live wrapper). `DataModelStore` now exposes `attachLive(id, model)`, called from `_onCoreSurfaceCreated`, which snapshots any fallback data into the live core model and disposes the fallback. `_ControllerContext.dataModel` is routed through the store so post-create re-fetches see the migrated data. Marks `SurfaceRegistry`, `SurfaceAdded.surface`, and `ComponentsUpdated.surface` `@internal` so the public consumer surface stays GenUI-typed. Updates the migration guide to call out the small set of integrator-facing APIs that still expose `a2ui_core.SurfaceModel`. Adds tests covering pre-create dataModel access, fallback->live data migration on createSurface, and facade-level UpdateDataModel toCoreMessage/fromCore round-trip preservation of the explicit-null vs omitted distinction. Adds CHANGELOG entries to both packages.
- _onCoreSurfaceCreated: migrate fallback data via store.attachLive BEFORE registry.addSurface notifies listeners, so a synchronous ValueListener callback that calls contextFor.dataModel sees the populated live model rather than racing the migration. - DataModelStore.getDataModel: check the _liveDataModels cache before invoking the lookup callback. Avoids redundant wrap calls and avoids the race where a getDataModel call cached one wrapper just before attachLive cached another. - DataModelStore.attachLive: drop the null-snapshot guard so a pre-create `update(root, null)` is preserved across createSurface. Adds a test. - Revert the genui.dart hide of SurfaceRegistry. SurfaceRegistry existed as a public type pre-migration; hiding it would be an unrelated source break. The new core-typed `surface` fields on SurfaceAdded / SurfaceUpdated remain `@internal`. - Migration guide: add an explicit Scope section listing the deferred follow-ups (catalog widget bodies, action dispatch, sendDataModel, GenericBinder, typed-props), the flutter#938 dependency for the hasValue/remove runtime split, and the A2UI#1499 RFC 6901 tracking.
`SurfaceRegistry.watchSurface` and `getSurface` are reverted to their pre-migration return types (`SurfaceDefinition`-typed), and `SurfaceAdded` / `SurfaceUpdated` regain their `definition: SurfaceDefinition` field. Existing consumers of those names see no signature change. Live core access moves to new `@internal` siblings: `watchLiveSurface`, `getLiveSurface`, and the existing `.surface` field on the registry events. The migration guide is updated to reflect the actual set of internal-marked APIs. Internal callers (the lookup-callback in `DataModelStore`, `_findCatalogForSurface`, and `LiveSurfaceContext.surface`) move to the new live methods.
`SurfaceAdded` and `SurfaceUpdated` regain their pre-migration public constructor signature `(surfaceId, SurfaceDefinition)`. The live core surface is moved to an `@internal SurfaceAdded.fromCore` / `SurfaceUpdated.fromCore` named constructor used by `SurfaceRegistry`; the `surface` field becomes nullable + `@internal` (null when constructed via the public ctor, populated when emitted by the registry). The controller's surfaceUpdates mapper unwraps `surface!` since registry- emitted events always use `.fromCore`. Adds a regression test exercising `watchSurface` / `getSurface` against the `SurfaceDefinition` snapshot.
Same pattern as the registry-event compat fix, applied to the user- facing `SurfaceAdded` and `ComponentsUpdated` events in `packages/genui/lib/src/model/ui_models.dart`: - Public ctor takes `(surfaceId, SurfaceDefinition)` (pre-migration shape), leaves `surface` null. - `@internal SurfaceAdded.fromCore` / `ComponentsUpdated.fromCore` populate both the snapshot and the live `core.SurfaceModel`. - `surface` field is now `core.SurfaceModel?` and `@internal`. - The surfaceUpdates mapper in SurfaceController uses `.fromCore`. Marks `SurfaceRegistry.addSurface` and `notifyUpdated` `@internal` (new in this branch; only SurfaceController calls them). The pre-migration `SurfaceRegistry.updateSurface(SurfaceDefinition)` is intentionally not restored; the live-model registry can't honor the definition-only push path without diverging from the substrate state. The removal is now called out in the migration guide. Adds a regression test for the public SurfaceUpdate ctors.
… updateSurface removal - Adds `const` to public `SurfaceAdded` and `ComponentsUpdated` constructors to match the pre-migration shape (and the existing `SurfaceRemoved` ctor). - Promotes the `SurfaceRegistry.updateSurface(...)` removal to a BREAKING bullet in the CHANGELOG. It is a pre-migration public API removal; preserving it would require diverging the registry from the live core surface model.
Per reviewer nit: "replacements" implied external users should reach for them, which is the opposite of the @internal intent. Reads now as "internal lifecycle hooks."
schema_validation.dart imported ui_models.dart only to throw A2uiValidationException, while ui_models.dart imported schema_validation.dart for SurfaceDefinition.validate. That cycle failed layerlens --fail-on-cycles. A2uiValidationException is a cross-cutting error type thrown across the model, engine, and transport layers, and ui_models.dart never used the one it defined. Move it to a primitives leaf so schema_validation no longer depends on the model layer and the graph becomes an acyclic DAG. primitives.dart re-exports it, so package:genui consumers are unchanged.
handleUiEvent guarded on `event is! UserActionEvent`, but extension types erase to their representation at runtime, so the check was always false and every UiEvent was submitted to the AI as an action. Add a data-based discriminator (UiEvent.isUserAction, keyed on the action `name`) and use it in handleUiEvent and Surface._dispatchEvent, which had the same erased check. Empty surfaceId was rejected only for CreateSurface; UpdateComponents, UpdateDataModel, and DeleteSurface with an empty id buffered under "" until timeout. Reject an empty surfaceId on any message that carries one. Regression tests cover both.
Add a widget test for live per-component rebuilds (the migration's headline behavior) and a test for the new duplicate-createSurface error. The migration nets a deletion of well-tested bespoke code, which lowered packages/genui aggregate line coverage, so re-mark the high-water baseline from 79.71% to its post-migration value (79.38%).
Remove the live per-component render path from Surface: the _ComponentBuilder, the LiveSurfaceContext render subscription, and the component create/delete listeners. The widget now always rebuilds from SurfaceContext.definition, which the registry keeps current on every create/update; data changes still propagate through the Bound* widgets. Each component widget is keyed by id to preserve widget identity across rebuilds, which the live builder previously supplied. The live core.SurfaceModel remains tracked for the data adapter; only the render-path subscription is removed.
Delete the genui-side A2UI message facade classes (A2uiMessage, CreateSurface, UpdateComponents, UpdateDataModel, DeleteSurface). Production code now passes a2ui_core message types straight through: handleMessage, the transport interface/adapter, the parser, and A2uiMessageEvent all use core.A2uiMessage. The only genui-specific piece retained from the old file is the catalog-parameterized message schema builder (a2uiMessageSchema). The parser preserves the friendly "version must be v0.9" error message. Tests build core messages via a small test-only helper (test/test_infra/message_builders.dart) that mirrors the old named-argument shape; component setup uses raw wire JSON. The facade-only message test is removed (those types no longer exist; a2ui_core covers them). Downstream consumers (genui_a2a, examples, dev_tools) are not yet migrated.
…es directly The A2UI message types are renderer-agnostic (pure-Dart a2ui_core), so message- layer consumers depend on a2ui_core directly rather than reaching through the Flutter renderer, mirroring how the React/Lit renderers consume web_core. - genui_a2a: agent connector stream + parsing use core.A2uiMessage. - catalog_gallery: sample parser/view + tests use core message types (components are raw wire maps now: component['component'], not Component.type). - verdure: ai_provider matches core.CreateSurfaceMessage. Adds a2ui_core as a direct dependency of each.
DataModelStore existed only to preserve the legacy SurfaceController.store API; nothing outside genui used it. Inline its small pre-create buffering (a writable model handed out by contextFor(id).dataModel before the surface exists, migrated into the live core model on creation) directly into SurfaceController as private state, and drop the public `store` getter, the class, and its barrel export. Post-create, data models come from the live core surface; the dead attachSurface/detachSurface bookkeeping is gone.
…gration The 0.10.0 entry described the abandoned "all public types preserved as facades" approach. Update it to reflect reality: A2UI message types and DataModelStore are removed (not facaded), with the breaking changes spelled out; SurfaceDefinition/Component are noted as the retained snapshot types pending flutter#801. Add the matching breaking-change entry to genui_a2a.
The guide described the abandoned facade approach: kept message classes, a SurfaceController.store facade, and granular per-component rebuilds. Rewrite it around what actually ships: A2UI messages and DataModelStore are removed (with before/after code), the catalog authoring API + surface/data-model snapshots are the retained types pending flutter#801, and the renderer rebuilds from the SurfaceDefinition snapshot.
The data-substrate migration widened the catalog dataContext path parameters
(ExecutionContext / DataContext subscribe, subscribeStream, getValue, update,
nested, resolvePath, and the constructor) from DataPath to Object, with a
runtime _toDataPath('$path') converter, to also accept raw string paths.
main typed these as DataPath, every caller already passes DataPath(...), and the
widening was used by exactly two test lines. It dropped compile-time checking
and loosened the frozen catalog authoring API. Restore DataPath and remove the
converter.
The live per-component render path was removed earlier; this drops the scaffolding that only existed to feed it: - LiveSurfaceContext (and its surface getter) and SurfaceRegistry.watchLiveSurface had no remaining readers. - The @internal `surface` fields on SurfaceAdded/ComponentsUpdated were write-only; fromCore still snapshots the definition from the core surface. - SurfaceRegistry._surfaces is now a plain map instead of a ValueNotifier map (nothing watches it; getLiveSurface still reads it for the data model). Migration guide updated to drop the removed @internal live-surface APIs.
The migration guide documented internals consumers never touch: the controller-only updateSurface, @internal lifecycle hooks, the internal wrap/ snapshot mechanics, signal backing, mutable-copy storage, and the rendering strategy. Cut all of it. The guide now covers only what a consumer changes (message types, store) or observes (stricter DataModel/validation, duplicate- create error, pointer escapes, null-removes-key). Also drop a stale CHANGELOG entry that still described the SurfaceAdded/ ComponentsUpdated @internal `surface` fields, which were removed, and trim the updateSurface entry's internal rationale.
These two genui consumers were missed in the message-layer migration. Add the a2ui_core dependency and use core.A2uiMessage types directly; in composer, replace the removed SurfaceController.store with contextFor(id).dataModel.
Import message types unprefixed via `show` rather than `as core`; they don't collide with anything genui exports. Scope the message section to custom-transport and direct message construction, and demote the niche build/parse details. Simplify the DataModel strictness note.
The hasValue flag and removeKey constructor preserved a wire distinction (explicit value:null vs absent value) that nothing consumes; the processor collapses both to a key removal. fromJson always reads value, toJson always emits it.
Drop implementation detail, roadmap notes, and spec cross-references that belong in a PR description rather than a library changelog.
Match the migration_<from>_to_<to>.md convention of the existing guides; retitle to "0.9.1 to 0.10.0" and update the CHANGELOG link.
Rebuild bound values via ValueListenableBuilder over the ValueListenable that subscribe() already returns, instead of bridging it into a preact_signals signal and a hand-rolled _SignalBuilder. The signal layer was residue of the deprioritized granular-rebuild work; the in-place mutation requirement is already satisfied by _SignalNotifier in the data layer.
Restore the frozen authoring-API shape (getComponent as a field, data as Object) and drop the synthesized core ComponentContext backing plus the unused fromCore/withOverrides/forTesting members. Catalog.buildWidget reconstructs the context via the public constructor.
Replace the hand-rolled definition listener lifecycle with a ValueListenableBuilder over SurfaceContext.definition, which is still a plain ValueListenable.
Drop the dead SurfaceDefinition payload and the fromCore constructors from SurfaceAdded/SurfaceUpdated; the sole consumer reads the surface and re-derives the definition itself.
Remove the leftover UpdateDataModelMessage doc that described removed hasValue machinery, and reword the unknown-catalog stub comment to state the behavior instead of narrating the migration.
…tate Move DataContext/resolveContext back above the model classes to match main (top-level order is behavior-neutral) and inline the single-caller _bindExternalState helper into InMemoryDataModel. Cuts the file's diff vs main from 542 to 283 lines.
Restore main's behavior; the retain-on-remove was not necessitated by the migration.
Move the KeyedSubtree back to where main had it (behavior-equivalent), removing the duplicate keying from Surface.
The RFC 6901 ~0/~1 DataPath change and the DataModel.set deep-copy are independent a2ui_core changes the migration does not need: genui never uses ~0/~1 escapes, and the deep-copy is unnecessary (verified by neutralizing it and running genui's suite, 275/275). Revert both to main and drop their CHANGELOG / migration-guide mentions; they belong in a separate a2ui_core PR.
… by the migration)
…ct/JsonMap path is dead)
There was a problem hiding this comment.
Code Review
This pull request refactors package:genui to run on the shared package:a2ui_core runtime, replacing GenUI-specific message classes with core message types and removing DataModelStore in favor of direct context-based data model access. The reviewer feedback highlights several opportunities to improve robustness and platform compatibility, particularly on Web platforms. Key recommendations include avoiding generic type parameters in pattern matching for JSON-decoded structures, using safe casting (.cast<String, Object?>()) to prevent runtime TypeErrors, preserving the DataPath context in _SignalNotifier for more accurate error reporting, and ensuring fallback data is only applied when non-null to avoid overwriting live model defaults.
Note: Security Review did not run due to the size of the PR.
| void _setup() { | ||
| final Object? raw = widget.value; | ||
| if (raw is Map && raw.containsKey('path')) { | ||
| _listenable = widget.dataContext.subscribe<Object?>( | ||
| DataPath(raw['path'] as String), | ||
| ); |
There was a problem hiding this comment.
Check if raw['path'] is a String instead of just checking raw.containsKey('path') to prevent a TypeError when the path is null, missing, or of an unexpected type (e.g., if the map represents a non-binding structure that happens to contain a 'path' key).
| void _setup() { | |
| final Object? raw = widget.value; | |
| if (raw is Map && raw.containsKey('path')) { | |
| _listenable = widget.dataContext.subscribe<Object?>( | |
| DataPath(raw['path'] as String), | |
| ); | |
| void _setup() { | |
| final Object? raw = widget.value; | |
| if (raw is Map && raw['path'] is String) { | |
| _listenable = widget.dataContext.subscribe<Object?>( | |
| DataPath(raw['path'] as String), | |
| ); |
A non-String path (e.g. malformed AI output) reached `as String` and threw;
match main's {'path': String} guard and fall through to the literal case.
_SignalNotifier hardcoded DataPath.root; thread the subscribed path so a type mismatch points at the right location (parity with main).
Package publishing
Documentation at https://github.com/dart-lang/ecosystem/wiki/Publishing-automation. |
Advances #811. Supersedes #956 (which used a compatibility facade approach that was abandoned).
Migrates
package:genui's runtime onto the shared, pure-Dartpackage:a2ui_core: protocol parsing, message processing, and surface/component/data-model state.SurfaceControllerbecomes a thin wrapper overa2ui_core.MessageProcessor; genui no longer maintains its own parallel A2UI runtime.The catalog-widget authoring API is unchanged; only the A2UI message types move onto
a2ui_core. Unifying the catalog/component types with the core models is a separate follow-up (#801) that also changes the authoring API.Breaking changes
a2ui_coremessage types are now used over genui's bespoke ones. The genui message classes (A2uiMessage,CreateSurface,UpdateComponents,UpdateDataModel,DeleteSurface) are removed.SurfaceController.handleMessage,Transport.incomingMessages, andA2uiMessageEvent.messageusecore.A2uiMessage. Construct withcore.CreateSurfaceMessage(...)etc.;core.UpdateComponentsMessagecarries raw component JSON maps. Message-handling consumers should depend ona2ui_coredirectly (those types don't collide with genui'sDataModel/DataPath).SurfaceController.store/DataModelStoreremoved. Access a surface's data model viaSurfaceController.contextFor(id).dataModel.SurfaceRegistry.updateSurface(...)removed. Surface lifecycle flows throughSurfaceController.handleMessage.Behavior changes (inherited from
a2ui_core, not renames)DataModelwrites are stricter (errors on type-mismatched intermediate paths and very large list indices; sparse list writes fill skipped entries withnull).createSurfacefor an active surface id is now an error.Full list in
packages/genui/CHANGELOG.md; consumer-facing details in the migration guide.Note for reviewers
The significant changes are found across less than 10 files. Everything else is mechanical renames and tests.
Some files of note:
packages/genui/lib/src/engine/surface_controller.dart:MessageProcessordelegation,handleMessage(core.A2uiMessage), pre-create data buffering.packages/genui/lib/src/model/data_model.dart:InMemoryDataModelovercore.DataModel;_SignalNotifierexposes core signals asValueListenables.packages/genui/lib/src/model/ui_models.dart:SurfaceDefinition/Componentsnapshots;SurfaceUpdateevents.packages/genui/lib/src/engine/surface_registry.dart: surface lifecycle.packages/genui/lib/src/widgets/surface.dart: definition-snapshot render path.packages/genui/lib/src/model/a2ui_message.dart: now just the catalog-parameterized message schema (the message classes are gone).packages/genui/lib/src/model/schema_validation.dart,widget_utilities.dart.