Skip to content

fix(dart): implement atomic two-pass component validation in MessageProcessor - #2374

Open
diegolopezrm wants to merge 2 commits into
a2ui-project:mainfrom
diegolopezrm:fix-dart-atomic-component-validation
Open

fix(dart): implement atomic two-pass component validation in MessageProcessor#2374
diegolopezrm wants to merge 2 commits into
a2ui-project:mainfrom
diegolopezrm:fix-dart-atomic-component-validation

Conversation

@diegolopezrm

Copy link
Copy Markdown

Summary

Ports the atomic two-pass component validation from #2332 (Swift) to the Dart implementation.

MessageProcessor._processUpdateComponents validated and applied components in a single pass. If an update payload contained a valid component followed by an invalid one, the valid component was already committed to SurfaceComponentsModel when the error was raised, leaving the surface in a half-updated state that no message describes and the client has no way to recover.

This mirrors the structure #2332 introduced in Swift:

  1. Validation pass — every component in the batch is checked for a required id and, when the component does not already exist, a required component type. Any failure throws and the batch is abandoned without touching surface state.
  2. Mutation pass — runs only once the whole batch has passed validation.

Reproduction

Before this change, the following left root committed even though the batch was rejected:

processor.processMessages([
  CreateSurfaceMessage(surfaceId: 's1', catalogId: catalog.id),
]);

// throws A2uiValidationError for the second component...
processor.processMessages([
  UpdateComponentsMessage(surfaceId: 's1', components: [
    {'id': 'root', 'component': 'Text', 'text': 'valid'},
    {'component': 'Text', 'text': 'no id'},
  ]),
]);

// ...but 'root' was already in the surface.
surface.componentsModel.get('root'); // non-null before, null after

Behavioural note

The "cannot create a component without a type" check moves from the mutation site into the validation pass. It keeps the same meaning — the type is required only when the component does not already exist, so updating an existing component without repeating its type still works — but it is now evaluated for the whole batch before anything is applied. A test covers that case explicitly so the relaxation is not lost.

Tests

Five tests added to dart/a2ui_core/test/processor_test.dart:

  • a fully valid batch still applies every component;
  • a batch rejected for a missing id leaves the surface untouched;
  • a batch rejected for a missing component type leaves the surface untouched;
  • a rejected batch does not overwrite components applied by an earlier accepted batch;
  • updating an existing component without repeating its type keeps working.

The three regression tests fail on main and pass with this change. The other two guard against over-restriction.

dart test in dart/a2ui_core: 74 passing (69 before). dart analyze reports the same two pre-existing unnecessary_library_directive infos as main and nothing new.

…rocessor

_processUpdateComponents validated and applied components in a single
pass, so a batch containing a valid component followed by an invalid one
committed the valid one to SurfaceComponentsModel before the error was
raised. The surface was left in a half-updated state that no message
describes, and the client had no way to recover it.

Split the work into two passes, mirroring the Swift implementation:

1. Validation pass, which checks every component in the batch for a
   required id and, when the component does not already exist, a
   required component type. Any failure throws and the batch is
   abandoned without touching surface state.
2. Mutation pass, which runs only once the whole batch has passed.

Adds five tests: a fully valid batch still applies; a batch rejected for
a missing id or a missing type leaves the surface untouched; a rejected
batch does not overwrite components applied by an earlier accepted
batch; and updating an existing component without repeating its type
keeps working.
@google-cla

google-cla Bot commented Aug 24, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a two-pass processing mechanism in MessageProcessor to ensure transactional integrity when updating components. The first pass validates all components in a batch (checking for missing IDs or missing types for new components) before any mutations are applied in the second pass, preventing the surface from entering a half-updated state. Comprehensive unit tests have been added to verify this behavior under various success and failure scenarios. There are no review comments, and I have no feedback to provide.

@diegolopezrm

Copy link
Copy Markdown
Author

@googlebot I signed it!

2 similar comments
@diegolopezrm

Copy link
Copy Markdown
Author

@googlebot I signed it!

@diegolopezrm

Copy link
Copy Markdown
Author

@googlebot I signed it!

@github-actions github-actions Bot added the status: needs-triage auto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs label Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: needs-triage auto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant