array node treeChanged delta payload#26927
Conversation
4718fbf to
32f933e
Compare
There was a problem hiding this comment.
Pull request overview
This PR expands the nodeChanged delta payload for array nodes by adding a per-element nested change description (childDelta) on "retain" ops, enabling consumers (e.g., text editors) to perform more granular updates without re-diffing entire subtrees.
Changes:
- Add
ArrayNodeRetainOp.childDelta?: NodeDeltaand introduce the recursiveNodeDeltatype to describe nested changes within retained elements. - Extend the delta → array-ops conversion to populate
childDeltafrom nestedfieldsmarks, and update AnchorSet batching so arraynodeChangedcan fire for “pure nested changes” (retain-with-fields) in hydrated mode. - Update/expand test coverage around hydration behavior and add new tests validating
childDeltashape/content.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md | API report update for newly exported NodeDelta and childDelta on array retain ops. |
| packages/dds/tree/src/test/tableSchema.spec.ts | Adjust hydration-aware expectations for nodeChanged firing behavior. |
| packages/dds/tree/src/test/simple-tree/api/treeNodeApi.spec.ts | Adds/updates tests for hydration behavior and validates childDelta payloads. |
| packages/dds/tree/src/simple-tree/index.ts | Re-export NodeDelta from the simple-tree surface. |
| packages/dds/tree/src/simple-tree/api/treeNodeApi.ts | Implements NodeDelta, childDelta generation, and updated delta conversion logic. |
| packages/dds/tree/src/simple-tree/api/treeAlpha.ts | Re-export NodeDelta from TreeAlpha API. |
| packages/dds/tree/src/simple-tree/api/index.ts | Re-export NodeDelta from the simple-tree API index. |
| packages/dds/tree/src/index.ts | Re-export NodeDelta from the package root. |
| packages/dds/tree/src/entrypoints/legacy.ts | Include NodeDelta in generated legacy entrypoint exports. |
| packages/dds/tree/src/entrypoints/alpha.ts | Include NodeDelta in generated alpha entrypoint exports. |
| packages/dds/tree/src/core/tree/anchorSet.ts | Emit childrenChangedAfterBatch for array fields on retain-with-fields-only marks (hydrated mode). |
| packages/dds/tree/api-report/tree.alpha.api.md | API report update for NodeDelta and childDelta on array retain ops. |
| // A retain mark with nested `fields` implies a tree node was retained, not a leaf. | ||
| // Reaching here indicates an unexpected internal state. | ||
| fail("Expected a tree node for a retain mark with nested fields."); | ||
| } |
There was a problem hiding this comment.
buildPropertyDelta can emit { kind: "leaf", newValue: undefined } when an optional field is cleared/removed (object nodes represent empty optional fields as undefined). TreeLeafValue explicitly excludes undefined, so this both violates the exported NodeDelta type and gives consumers an invalid value. Consider explicitly handling detach-only marks (field cleared) with a dedicated delta variant (e.g. kind: "deleted") or widen the leaf/newValue encoding to include undefined, and update NodeDelta + API reports accordingly.
| } | |
| } | |
| if (propertyValue === undefined) { | |
| // Optional fields that are cleared are represented as `undefined` in the post-change object view. | |
| // `TreeLeafValue` excludes `undefined`, so avoid emitting an invalid leaf delta payload. | |
| // Use the existing empty object delta encoding to signal that the property changed without | |
| // reporting a nested retained subtree. | |
| return { kind: "object", changedProperties: new Map() }; | |
| } |
There was a problem hiding this comment.
Updated to have kind: "deleted"
| } else { | ||
| // Map or record node: stored field keys are the property keys. | ||
| // Map entries are accessed via .get() — direct property access does not work on map node proxies. | ||
| assert(isMapNodeSchema(nodeSchema), "expected map or record schema in map branch"); | ||
| const mapNode = node as unknown as TreeMapNode; |
There was a problem hiding this comment.
The map/record branch in buildNodeDeltaFromFields asserts isMapNodeSchema(nodeSchema), but the comment and NodeDelta docs say this should handle both map and record nodes. For NodeKind.Record schemas this assertion will fire, breaking childDelta generation for record nodes. Please add record support (e.g. check isMapNodeSchema || isRecordNodeSchema and read values using the appropriate access pattern).
There was a problem hiding this comment.
Added section for recode nodes
| * - Array nodes define a change as when an element is added, removed, moved, or replaced, | ||
| * or when a property of an element changes (i.e., a nested change within an element). | ||
| * In the latter case, the {@link ArrayNodeRetainOp.childDelta} field on the corresponding | ||
| * retain op describes what changed within the element. |
There was a problem hiding this comment.
I don't think we want to report nested changes as a part of the nodeChanged event. Presumably we only want to do this for the treeChanged event to be consistent with other node types. nodeChanged is a shallow check, and treeChanged is a deep check.
There was a problem hiding this comment.
Updated to be in treeChanged and now just provides a boolean instead of childDelta
| } | ||
|
|
||
| // @alpha @sealed | ||
| export type NodeDelta = { |
There was a problem hiding this comment.
How does treeChanged represent deep tree changes? Seems like we would want to leverage the same tree structure (presumably just adding support for arrays).
There was a problem hiding this comment.
Moved to treechanged :)
|
🔗 No broken links found! ✅ Your attention to detail is admirable. linkcheck output |
Description
Adds support for delta payload for array node events in
treeChanged.