Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 18 additions & 3 deletions packages/dds/tree/api-report/tree.alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ export interface ArrayNodeCustomizableSchemaUnsafe<out TName extends string, in
}, false, T, undefined, TCustomMetadata>, SimpleArrayNodeSchema<SchemaType.View, TCustomMetadata> {
}

// @beta @sealed
export type ArrayNodeDeltaOp = {
readonly type: "retain";
readonly count: number;
} | {
readonly type: "insert";
readonly count: number;
} | {
readonly type: "remove";
readonly count: number;
};

// @alpha @sealed @system
export interface ArrayNodePojoEmulationSchema<out TName extends string = string, in out T extends ImplicitAllowedTypes = ImplicitAllowedTypes, out ImplicitlyConstructable extends boolean = true, out TCustomMetadata = unknown> extends TreeNodeSchemaNonClass<TName, NodeKind.Array, TreeArrayNode<T> & WithType<TName, NodeKind.Array, T>, Iterable<InsertableTreeNodeFromImplicitAllowedTypes<T>>, ImplicitlyConstructable, T, undefined, TCustomMetadata>, SimpleArrayNodeSchema<SchemaType.View, TCustomMetadata> {
}
Expand Down Expand Up @@ -392,7 +404,7 @@ export namespace FluidSerializableAsTree {
export type Data = JsonCompatible<IFluidHandle>;
const // @system
_APIExtractorWorkaroundObjectBase: TreeNodeSchemaClass_2<"com.fluidframework.serializable.object", NodeKind_2.Record, TreeRecordNodeUnsafe_2<readonly [() => typeof FluidSerializableObject, () => typeof Array, LeafSchema_2<"string", string>, LeafSchema_2<"number", number>, LeafSchema_2<"boolean", boolean>, LeafSchema_2<"null", null>, LeafSchema_2<"handle", IFluidHandle<unknown>>]> & WithType_2<"com.fluidframework.serializable.object", NodeKind_2.Record, unknown>, {
readonly [x: string]: string | number | IFluidHandle<unknown> | System_Unsafe_2.InsertableTypedNodeUnsafe<LeafSchema_2<"boolean", boolean>, LeafSchema_2<"boolean", boolean>> | FluidSerializableObject | Array | null;
readonly [x: string]: string | number | IFluidHandle<unknown> | FluidSerializableObject | Array | System_Unsafe_2.InsertableTypedNodeUnsafe<LeafSchema_2<"boolean", boolean>, LeafSchema_2<"boolean", boolean>> | null;
}, false, readonly [() => typeof FluidSerializableObject, () => typeof Array, LeafSchema_2<"string", string>, LeafSchema_2<"number", number>, LeafSchema_2<"boolean", boolean>, LeafSchema_2<"null", null>, LeafSchema_2<"handle", IFluidHandle<unknown>>], undefined, unknown>;
// @sealed
export class FluidSerializableObject extends _APIExtractorWorkaroundObjectBase {
Expand All @@ -401,7 +413,7 @@ export namespace FluidSerializableAsTree {
export type _RecursiveArrayWorkaroundJsonArray = FixRecursiveArraySchema<typeof Array>;
const // @system
_APIExtractorWorkaroundArrayBase: TreeNodeSchemaClass_2<"com.fluidframework.serializable.array", NodeKind_2.Array, System_Unsafe_2.TreeArrayNodeUnsafe<readonly [() => typeof FluidSerializableObject, () => typeof Array, LeafSchema_2<"string", string>, LeafSchema_2<"number", number>, LeafSchema_2<"boolean", boolean>, LeafSchema_2<"null", null>, LeafSchema_2<"handle", IFluidHandle<unknown>>]> & WithType_2<"com.fluidframework.serializable.array", NodeKind_2.Array, unknown>, {
[Symbol.iterator](): Iterator<string | number | IFluidHandle<unknown> | System_Unsafe_2.InsertableTypedNodeUnsafe<LeafSchema_2<"boolean", boolean>, LeafSchema_2<"boolean", boolean>> | FluidSerializableObject | Array | null, any, undefined>;
[Symbol.iterator](): Iterator<string | number | IFluidHandle<unknown> | FluidSerializableObject | Array | System_Unsafe_2.InsertableTypedNodeUnsafe<LeafSchema_2<"boolean", boolean>, LeafSchema_2<"boolean", boolean>> | null, any, undefined>;
}, false, readonly [() => typeof FluidSerializableObject, () => typeof Array, LeafSchema_2<"string", string>, LeafSchema_2<"number", number>, LeafSchema_2<"boolean", boolean>, LeafSchema_2<"null", null>, LeafSchema_2<"handle", IFluidHandle<unknown>>], undefined>;
// (undocumented)
export type Tree = TreeNodeFromImplicitAllowedTypes<typeof Tree>;
Expand Down Expand Up @@ -778,6 +790,7 @@ type NodeBuilderData<T extends TreeNodeSchemaCore<string, NodeKind, boolean>> =
// @beta @sealed
export interface NodeChangedData<TNode extends TreeNode = TreeNode> {
readonly changedProperties?: ReadonlySet<TNode extends WithType<string, NodeKind.Object, infer TInfo> ? string & keyof TInfo : string>;
readonly delta?: readonly ArrayNodeDeltaOp[];
}

// @public
Expand Down Expand Up @@ -1608,7 +1621,9 @@ export interface TreeChangeEvents {

// @beta @sealed
export interface TreeChangeEventsBeta<TNode extends TreeNode = TreeNode> extends TreeChangeEvents {
nodeChanged: (data: NodeChangedData<TNode> & (TNode extends WithType<string, NodeKind.Map | NodeKind.Object | NodeKind.Record> ? Required<Pick<NodeChangedData<TNode>, "changedProperties">> : unknown)) => void;
nodeChanged: (data: NodeChangedData<TNode> & (TNode extends WithType<string, NodeKind.Map | NodeKind.Object | NodeKind.Record> ? Required<Pick<NodeChangedData<TNode>, "changedProperties">> : TNode extends WithType<string, NodeKind.Array> ? {
readonly delta: readonly ArrayNodeDeltaOp[] | undefined;
} : unknown)) => void;
}

// @alpha
Expand Down
21 changes: 18 additions & 3 deletions packages/dds/tree/api-report/tree.beta.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ type ApplyKindInput<T, Kind extends FieldKind, DefaultsAreOptional extends boole
Kind
] extends [FieldKind.Required] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : [Kind] extends [FieldKind.Identifier] ? DefaultsAreOptional extends true ? T | undefined : T : never;

// @beta @sealed
export type ArrayNodeDeltaOp = {
readonly type: "retain";
readonly count: number;
} | {
readonly type: "insert";
readonly count: number;
} | {
readonly type: "remove";
readonly count: number;
};

// @beta
export function asBeta<TSchema extends ImplicitFieldSchema>(view: TreeView<TSchema>): TreeViewBeta<TSchema>;

Expand Down Expand Up @@ -211,7 +223,7 @@ export namespace FluidSerializableAsTree {
export type Data = JsonCompatible<IFluidHandle>;
const // @system
_APIExtractorWorkaroundObjectBase: TreeNodeSchemaClass_2<"com.fluidframework.serializable.object", NodeKind_2.Record, TreeRecordNodeUnsafe_2<readonly [() => typeof FluidSerializableObject, () => typeof Array, LeafSchema_2<"string", string>, LeafSchema_2<"number", number>, LeafSchema_2<"boolean", boolean>, LeafSchema_2<"null", null>, LeafSchema_2<"handle", IFluidHandle<unknown>>]> & WithType_2<"com.fluidframework.serializable.object", NodeKind_2.Record, unknown>, {
readonly [x: string]: string | number | IFluidHandle<unknown> | System_Unsafe_2.InsertableTypedNodeUnsafe<LeafSchema_2<"boolean", boolean>, LeafSchema_2<"boolean", boolean>> | FluidSerializableObject | Array | null;
readonly [x: string]: string | number | IFluidHandle<unknown> | FluidSerializableObject | Array | System_Unsafe_2.InsertableTypedNodeUnsafe<LeafSchema_2<"boolean", boolean>, LeafSchema_2<"boolean", boolean>> | null;
}, false, readonly [() => typeof FluidSerializableObject, () => typeof Array, LeafSchema_2<"string", string>, LeafSchema_2<"number", number>, LeafSchema_2<"boolean", boolean>, LeafSchema_2<"null", null>, LeafSchema_2<"handle", IFluidHandle<unknown>>], undefined, unknown>;
// @sealed
export class FluidSerializableObject extends _APIExtractorWorkaroundObjectBase {
Expand All @@ -220,7 +232,7 @@ export namespace FluidSerializableAsTree {
export type _RecursiveArrayWorkaroundJsonArray = FixRecursiveArraySchema<typeof Array>;
const // @system
_APIExtractorWorkaroundArrayBase: TreeNodeSchemaClass_2<"com.fluidframework.serializable.array", NodeKind_2.Array, System_Unsafe_2.TreeArrayNodeUnsafe<readonly [() => typeof FluidSerializableObject, () => typeof Array, LeafSchema_2<"string", string>, LeafSchema_2<"number", number>, LeafSchema_2<"boolean", boolean>, LeafSchema_2<"null", null>, LeafSchema_2<"handle", IFluidHandle<unknown>>]> & WithType_2<"com.fluidframework.serializable.array", NodeKind_2.Array, unknown>, {
[Symbol.iterator](): Iterator<string | number | IFluidHandle<unknown> | System_Unsafe_2.InsertableTypedNodeUnsafe<LeafSchema_2<"boolean", boolean>, LeafSchema_2<"boolean", boolean>> | FluidSerializableObject | Array | null, any, undefined>;
[Symbol.iterator](): Iterator<string | number | IFluidHandle<unknown> | FluidSerializableObject | Array | System_Unsafe_2.InsertableTypedNodeUnsafe<LeafSchema_2<"boolean", boolean>, LeafSchema_2<"boolean", boolean>> | null, any, undefined>;
}, false, readonly [() => typeof FluidSerializableObject, () => typeof Array, LeafSchema_2<"string", string>, LeafSchema_2<"number", number>, LeafSchema_2<"boolean", boolean>, LeafSchema_2<"null", null>, LeafSchema_2<"handle", IFluidHandle<unknown>>], undefined>;
// (undocumented)
export type Tree = TreeNodeFromImplicitAllowedTypes<typeof Tree>;
Expand Down Expand Up @@ -373,6 +385,7 @@ type NodeBuilderData<T extends TreeNodeSchemaCore<string, NodeKind, boolean>> =
// @beta @sealed
export interface NodeChangedData<TNode extends TreeNode = TreeNode> {
readonly changedProperties?: ReadonlySet<TNode extends WithType<string, NodeKind.Object, infer TInfo> ? string & keyof TInfo : string>;
readonly delta?: readonly ArrayNodeDeltaOp[];
}

// @public
Expand Down Expand Up @@ -887,7 +900,9 @@ export interface TreeChangeEvents {

// @beta @sealed
export interface TreeChangeEventsBeta<TNode extends TreeNode = TreeNode> extends TreeChangeEvents {
nodeChanged: (data: NodeChangedData<TNode> & (TNode extends WithType<string, NodeKind.Map | NodeKind.Object | NodeKind.Record> ? Required<Pick<NodeChangedData<TNode>, "changedProperties">> : unknown)) => void;
nodeChanged: (data: NodeChangedData<TNode> & (TNode extends WithType<string, NodeKind.Map | NodeKind.Object | NodeKind.Record> ? Required<Pick<NodeChangedData<TNode>, "changedProperties">> : TNode extends WithType<string, NodeKind.Array> ? {
readonly delta: readonly ArrayNodeDeltaOp[] | undefined;
} : unknown)) => void;
}

// @beta @input
Expand Down
21 changes: 18 additions & 3 deletions packages/dds/tree/api-report/tree.legacy.beta.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ type ApplyKindInput<T, Kind extends FieldKind, DefaultsAreOptional extends boole
Kind
] extends [FieldKind.Required] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : [Kind] extends [FieldKind.Identifier] ? DefaultsAreOptional extends true ? T | undefined : T : never;

// @beta @sealed
export type ArrayNodeDeltaOp = {
readonly type: "retain";
readonly count: number;
} | {
readonly type: "insert";
readonly count: number;
} | {
readonly type: "remove";
readonly count: number;
};

// @beta
export function asBeta<TSchema extends ImplicitFieldSchema>(view: TreeView<TSchema>): TreeViewBeta<TSchema>;

Expand Down Expand Up @@ -214,7 +226,7 @@ export namespace FluidSerializableAsTree {
export type Data = JsonCompatible<IFluidHandle>;
const // @system
_APIExtractorWorkaroundObjectBase: TreeNodeSchemaClass_2<"com.fluidframework.serializable.object", NodeKind_2.Record, TreeRecordNodeUnsafe_2<readonly [() => typeof FluidSerializableObject, () => typeof Array, LeafSchema_2<"string", string>, LeafSchema_2<"number", number>, LeafSchema_2<"boolean", boolean>, LeafSchema_2<"null", null>, LeafSchema_2<"handle", IFluidHandle<unknown>>]> & WithType_2<"com.fluidframework.serializable.object", NodeKind_2.Record, unknown>, {
readonly [x: string]: string | number | IFluidHandle<unknown> | System_Unsafe_2.InsertableTypedNodeUnsafe<LeafSchema_2<"boolean", boolean>, LeafSchema_2<"boolean", boolean>> | FluidSerializableObject | Array | null;
readonly [x: string]: string | number | IFluidHandle<unknown> | FluidSerializableObject | Array | System_Unsafe_2.InsertableTypedNodeUnsafe<LeafSchema_2<"boolean", boolean>, LeafSchema_2<"boolean", boolean>> | null;
}, false, readonly [() => typeof FluidSerializableObject, () => typeof Array, LeafSchema_2<"string", string>, LeafSchema_2<"number", number>, LeafSchema_2<"boolean", boolean>, LeafSchema_2<"null", null>, LeafSchema_2<"handle", IFluidHandle<unknown>>], undefined, unknown>;
// @sealed
export class FluidSerializableObject extends _APIExtractorWorkaroundObjectBase {
Expand All @@ -223,7 +235,7 @@ export namespace FluidSerializableAsTree {
export type _RecursiveArrayWorkaroundJsonArray = FixRecursiveArraySchema<typeof Array>;
const // @system
_APIExtractorWorkaroundArrayBase: TreeNodeSchemaClass_2<"com.fluidframework.serializable.array", NodeKind_2.Array, System_Unsafe_2.TreeArrayNodeUnsafe<readonly [() => typeof FluidSerializableObject, () => typeof Array, LeafSchema_2<"string", string>, LeafSchema_2<"number", number>, LeafSchema_2<"boolean", boolean>, LeafSchema_2<"null", null>, LeafSchema_2<"handle", IFluidHandle<unknown>>]> & WithType_2<"com.fluidframework.serializable.array", NodeKind_2.Array, unknown>, {
[Symbol.iterator](): Iterator<string | number | IFluidHandle<unknown> | System_Unsafe_2.InsertableTypedNodeUnsafe<LeafSchema_2<"boolean", boolean>, LeafSchema_2<"boolean", boolean>> | FluidSerializableObject | Array | null, any, undefined>;
[Symbol.iterator](): Iterator<string | number | IFluidHandle<unknown> | FluidSerializableObject | Array | System_Unsafe_2.InsertableTypedNodeUnsafe<LeafSchema_2<"boolean", boolean>, LeafSchema_2<"boolean", boolean>> | null, any, undefined>;
}, false, readonly [() => typeof FluidSerializableObject, () => typeof Array, LeafSchema_2<"string", string>, LeafSchema_2<"number", number>, LeafSchema_2<"boolean", boolean>, LeafSchema_2<"null", null>, LeafSchema_2<"handle", IFluidHandle<unknown>>], undefined>;
// (undocumented)
export type Tree = TreeNodeFromImplicitAllowedTypes<typeof Tree>;
Expand Down Expand Up @@ -376,6 +388,7 @@ type NodeBuilderData<T extends TreeNodeSchemaCore<string, NodeKind, boolean>> =
// @beta @sealed
export interface NodeChangedData<TNode extends TreeNode = TreeNode> {
readonly changedProperties?: ReadonlySet<TNode extends WithType<string, NodeKind.Object, infer TInfo> ? string & keyof TInfo : string>;
readonly delta?: readonly ArrayNodeDeltaOp[];
}

// @public
Expand Down Expand Up @@ -899,7 +912,9 @@ export interface TreeChangeEvents {

// @beta @sealed
export interface TreeChangeEventsBeta<TNode extends TreeNode = TreeNode> extends TreeChangeEvents {
nodeChanged: (data: NodeChangedData<TNode> & (TNode extends WithType<string, NodeKind.Map | NodeKind.Object | NodeKind.Record> ? Required<Pick<NodeChangedData<TNode>, "changedProperties">> : unknown)) => void;
nodeChanged: (data: NodeChangedData<TNode> & (TNode extends WithType<string, NodeKind.Map | NodeKind.Object | NodeKind.Record> ? Required<Pick<NodeChangedData<TNode>, "changedProperties">> : TNode extends WithType<string, NodeKind.Array> ? {
readonly delta: readonly ArrayNodeDeltaOp[] | undefined;
} : unknown)) => void;
}

// @beta @input
Expand Down
70 changes: 60 additions & 10 deletions packages/dds/tree/src/core/tree/anchorSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,17 @@ export interface AnchorEvents {
*
* Compare to {@link AnchorEvents.childrenChanged} which is emitted in the middle of the batch/delta-visit.
*/
childrenChangedAfterBatch(arg: { changedFields: ReadonlySet<FieldKey> }): void;
childrenChangedAfterBatch(arg: {
changedFields: ReadonlySet<FieldKey>;
/**
* The sequential delta marks for each changed field, keyed by field key.
* @remarks
* A field is absent from this map when no mark data was captured for it
* (e.g. for unhydrated nodes, or when marks were invalidated by multiple
* sequential batches touching the same field before the buffer was flushed).
*/
fieldMarks: ReadonlyMap<FieldKey, readonly Delta.Mark[]>;
}): void;

/**
* Emitted in the middle of applying a batch of changes (i.e. during a delta a visit), if something in the subtree
Expand Down Expand Up @@ -726,6 +736,12 @@ export class AnchorSet implements AnchorLocator {
*/
bufferedEvents: [] as BufferedEvent[],

/**
* Field marks delivered via {@link DeltaVisitor.fieldMarks}, keyed by (PathNode, FieldKey).
* Populated during the first (detach) pass and consumed during "free" when emitting events.
*/
storedFieldMarks: new Map<PathNode, Map<FieldKey, readonly Delta.Mark[]>>(),

/**
* 'currentDepth' and 'depthThresholdForSubtreeChanged' serve to keep track of when do we need to emit
* subtreeChangedAfterBatch events.
Expand Down Expand Up @@ -764,15 +780,31 @@ export class AnchorSet implements AnchorLocator {
}
this.anchorSet.activeVisitor = undefined;

// Aggregate changedFields by node.
// Aggregate changedFields and fieldMarks by node.
const eventsByNode: Map<PathNode, Set<FieldKey>> = new Map();
for (const { node, event, changedField } of this.bufferedEvents) {
const marksByNode: Map<PathNode, Map<FieldKey, readonly Delta.Mark[]>> = new Map();
for (const { node, event, changedField, fieldMarks } of this.bufferedEvents) {
if (event === "childrenChangedAfterBatch") {
const keys = getOrCreate(eventsByNode, node, () => new Set());
keys.add(
const resolvedField: FieldKey =
changedField ??
fail(0xb57 /* childrenChangedAfterBatch events should have a changedField */),
);
fail(0xb57 /* childrenChangedAfterBatch events should have a changedField */);
const keys = getOrCreate(eventsByNode, node, () => new Set());
keys.add(resolvedField);
if (fieldMarks !== undefined) {
const nodeMarks = getOrCreate(marksByNode, node, () => new Map());
// First-wins is safe here because `fieldMarks` is called at most once per
// field per delta visit (the hook fires during the detach pass only).
// Within a single delta a field therefore produces at most one marks entry,
// so there is never a legitimate second entry to prefer over the first.
// If the same field is touched by two separate deltas before the buffer is
// flushed (e.g. because a schema change forced a second delta in the same
// batch), `KernelEventBuffer` detects the collision and removes the entry
// for that field entirely, so the consumer receives `undefined` rather than
// stale marks.
if (!nodeMarks.has(resolvedField)) {
nodeMarks.set(resolvedField, fieldMarks);
}
}
}
}

Expand All @@ -787,7 +819,9 @@ export class AnchorSet implements AnchorLocator {
const changedFields =
eventsByNode.get(node) ??
fail(0xaeb /* childrenChangedAfterBatch events should have changedFields */);
node.events.emit(event, { changedFields });
const fieldMarks: ReadonlyMap<FieldKey, readonly Delta.Mark[]> =
marksByNode.get(node) ?? new Map();
node.events.emit(event, { changedFields, fieldMarks });
} else {
node.events.emit(event);
}
Expand All @@ -800,17 +834,20 @@ export class AnchorSet implements AnchorLocator {
);
},
notifyChildrenChanged(): void {
const parentField = this.parentField;
this.maybeWithNode(
(p) => {
assert(
this.parentField !== undefined,
parentField !== undefined,
0xa24 /* Must be in a field to modify its contents */,
);
p.events.emit("childrenChanged", p);
const fieldMarks = this.storedFieldMarks.get(p)?.get(parentField);
this.bufferedEvents.push({
node: p,
event: "childrenChangedAfterBatch",
changedField: this.parentField,
changedField: parentField,
fieldMarks,
});
},
() => {},
Expand Down Expand Up @@ -934,6 +971,14 @@ export class AnchorSet implements AnchorLocator {
exitField(key: FieldKey): void {
this.parentField = undefined;
},
fieldMarks(key: FieldKey, marks: readonly Delta.Mark[]): void {
if (this.parent !== undefined) {
const interned = this.anchorSet.internalizePath(this.parent);
if (interned instanceof PathNode) {
getOrCreate(this.storedFieldMarks, interned, () => new Map()).set(key, marks);
}
}
},
};
this.#events.emit("treeChanging", this);
this.activeVisitor = visitor;
Expand Down Expand Up @@ -1241,4 +1286,9 @@ interface BufferedEvent {
* Some events, such as afterDestroy, do not involve a key, and thus leave this undefined.
*/
changedField?: FieldKey;
/**
* The sequential delta marks for the impacted field, if available.
* Populated from {@link DeltaVisitor.fieldMarks} during the first (detach) pass.
*/
fieldMarks?: readonly Delta.Mark[];
}
Loading
Loading