diff --git a/api-report/tree2.api.md b/api-report/tree2.api.md index b07edc446a0f..6b8c66cab280 100644 --- a/api-report/tree2.api.md +++ b/api-report/tree2.api.md @@ -71,7 +71,7 @@ export type AnchorsCompare = CompareFunction; // @alpha @sealed export class AnchorSet implements ISubscribable, AnchorLocator { - applyDelta(delta: Delta.Root): void; + acquireVisitor(): DeltaVisitor; // (undocumented) forget(anchor: Anchor): void; internalizePath(originalPath: UpPath): UpPath; @@ -428,6 +428,28 @@ declare namespace Delta { } export { Delta } +// @alpha +export interface DeltaVisitor { + // (undocumented) + enterField(key: FieldKey): void; + // (undocumented) + enterNode(index: number): void; + // (undocumented) + exitField(key: FieldKey): void; + // (undocumented) + exitNode(index: number): void; + // (undocumented) + free(): void; + // (undocumented) + onDelete(index: number, count: number): void; + // (undocumented) + onInsert(index: number, content: Delta.ProtoNodes): void; + // (undocumented) + onMoveIn(index: number, count: number, id: Delta.MoveId): void; + // (undocumented) + onMoveOut(index: number, count: number, id: Delta.MoveId): void; +} + // @alpha export interface Dependee extends NamedComputation { registerDependent(dependent: Dependent): boolean; @@ -747,8 +769,8 @@ export const forbiddenFieldKindIdentifier = "Forbidden"; // @alpha export interface ForestEvents { - afterDelta(delta: Delta.Root): void; - beforeDelta(delta: Delta.Root): void; + afterChange(): void; + beforeChange(): void; } // @alpha @@ -849,8 +871,9 @@ export interface IdRange { // @alpha export interface IEditableForest extends IForestSubscription { + // (undocumented) + acquireVisitor(): DeltaVisitor; readonly anchors: AnchorSet; - applyDelta(delta: Delta.Root): void; } // @alpha diff --git a/experimental/dds/tree2/src/core/forest/editableForest.ts b/experimental/dds/tree2/src/core/forest/editableForest.ts index d67aeff3963b..b4b144b155a5 100644 --- a/experimental/dds/tree2/src/core/forest/editableForest.ts +++ b/experimental/dds/tree2/src/core/forest/editableForest.ts @@ -12,6 +12,8 @@ import { Anchor, ITreeCursorSynchronous, rootFieldKey, + DeltaVisitor, + applyDelta, } from "../tree"; import { IForestSubscription, ITreeSubscriptionCursor } from "./forest"; @@ -31,10 +33,13 @@ export interface IEditableForest extends IForestSubscription { readonly anchors: AnchorSet; /** - * Applies the supplied Delta to the forest. - * Does NOT update anchors. + * @returns a visitor that can be used to mutate the forest. + * + * Mutating the forest does NOT update anchors. + * The visitor must be released after use. + * It is invalid to acquire a visitor without releasing the previous one. */ - applyDelta(delta: Delta.Root): void; + acquireVisitor(): DeltaVisitor; } /** @@ -50,7 +55,7 @@ export function initializeForest( ): void { assert(forest.isEmpty, 0x747 /* forest must be empty */); const insert: Delta.Insert = { type: Delta.MarkType.Insert, content }; - forest.applyDelta(new Map([[rootFieldKey, [insert]]])); + applyDelta(new Map([[rootFieldKey, [insert]]]), forest); } // TODO: Types below here may be useful for input into edit building APIs, but are no longer used here directly. diff --git a/experimental/dds/tree2/src/core/forest/forest.ts b/experimental/dds/tree2/src/core/forest/forest.ts index 9542a51d5f9b..215aba20d1ff 100644 --- a/experimental/dds/tree2/src/core/forest/forest.ts +++ b/experimental/dds/tree2/src/core/forest/forest.ts @@ -10,7 +10,6 @@ import { StoredSchemaRepository, FieldKey } from "../schema-stored"; import { Anchor, AnchorSet, - Delta, DetachedField, detachedFieldAsKey, ITreeCursor, @@ -35,14 +34,16 @@ import type { IEditableForest } from "./editableForest"; */ export interface ForestEvents { /** - * Delta is about to be applied to forest. + * The forest is about to be changed. + * Emitted before the first change in a batch of changes. */ - beforeDelta(delta: Delta.Root): void; + beforeChange(): void; /** - * Delta was just applied to forest. + * The forest was just changed. + * Emitted after the last change in a batch of changes. */ - afterDelta(delta: Delta.Root): void; + afterChange(): void; } /** diff --git a/experimental/dds/tree2/src/core/index.ts b/experimental/dds/tree2/src/core/index.ts index 859e241e6f89..d119ff667605 100644 --- a/experimental/dds/tree2/src/core/index.ts +++ b/experimental/dds/tree2/src/core/index.ts @@ -55,6 +55,7 @@ export { detachedFieldAsKey, keyAsDetachedField, visitDelta, + applyDelta, setGenericTreeField, DeltaVisitor, PathVisitor, diff --git a/experimental/dds/tree2/src/core/tree/anchorSet.ts b/experimental/dds/tree2/src/core/tree/anchorSet.ts index 19cd4aeb3be4..0c92eeef71b5 100644 --- a/experimental/dds/tree2/src/core/tree/anchorSet.ts +++ b/experimental/dds/tree2/src/core/tree/anchorSet.ts @@ -19,7 +19,7 @@ import { FieldKey } from "../schema-stored"; import { UpPath } from "./pathTree"; import { Value, detachedFieldAsKey, DetachedField, EmptyKey } from "./types"; import { PathVisitor } from "./visitPath"; -import { visitDelta, DeltaVisitor } from "./visitDelta"; +import { DeltaVisitor } from "./visitDelta"; import * as Delta from "./delta"; /** @@ -214,6 +214,8 @@ export class AnchorSet implements ISubscribable, AnchorLoca // For now use this more encapsulated approach with maps. private readonly anchorToPath: Map = new Map(); + private activeVisitor?: DeltaVisitor; + public on( eventName: K, listener: AnchorSetRootEvents[K], @@ -556,128 +558,154 @@ export class AnchorSet implements ISubscribable, AnchorLoca /** * Updates the anchors according to the changes described in the given delta */ - public applyDelta(delta: Delta.Root): void { - let parentField: FieldKey | undefined; - let parent: UpPath | undefined; + public acquireVisitor(): DeltaVisitor { + assert( + this.activeVisitor === undefined, + "Must release existing visitor before acquiring another", + ); const moveTable = new Map(); - // Run `withNode` on anchorNode for parent if there is such an anchorNode. - // If at root, run `withRoot` instead. - const maybeWithNode: ( - withNode: (anchorNode: PathNode) => void, - withRoot?: () => void, - ) => void = (withNode, withRoot) => { - if (parent === undefined && withRoot !== undefined) { - withRoot(); - } else { - assert(parent !== undefined, 0x5b0 /* parent must exist */); - // TODO:Perf: - // When traversing to a depth D when there are not anchors in that subtree, this goes O(D^2). - // Delta traversal should early out in this case because no work is needed (and all move outs are known to not contain anchors). - parent = this.internalizePath(parent); - if (parent instanceof PathNode) { - withNode(parent); + const visitor = { + anchorSet: this, + // Run `withNode` on anchorNode for parent if there is such an anchorNode. + // If at root, run `withRoot` instead. + maybeWithNode(withNode: (anchorNode: PathNode) => void, withRoot?: () => void) { + if (this.parent === undefined && withRoot !== undefined) { + withRoot(); + } else { + assert(this.parent !== undefined, 0x5b0 /* parent must exist */); + // TODO:Perf: + // When traversing to a depth D when there are not anchors in that subtree, this goes O(D^2). + // Delta traversal should early out in this case because no work is needed (and all move outs are known to not contain anchors). + this.parent = this.anchorSet.internalizePath(this.parent); + if (this.parent instanceof PathNode) { + withNode(this.parent); + } } - } - }; - - // Lookup table for path visitors collected from {@link AnchorEvents.visitSubtreeChanging} emitted events. - // The key is the path of the node that the visitor is registered on. The code ensures that the path visitor visits only the appropriate subtrees - // by maintaining the mapping only during time between the {@link DeltaVisitor.enterNode} and {@link DeltaVisitor.exitNode} calls for a given anchorNode. - const pathVisitors: Map> = new Map(); - - const visitor: DeltaVisitor = { - onDelete: (start: number, count: number): void => { - assert(parentField !== undefined, 0x3a7 /* Must be in a field to delete */); - maybeWithNode( + }, + // Lookup table for path visitors collected from {@link AnchorEvents.visitSubtreeChanging} emitted events. + // The key is the path of the node that the visitor is registered on. The code ensures that the path visitor visits only the appropriate subtrees + // by maintaining the mapping only during time between the {@link DeltaVisitor.enterNode} and {@link DeltaVisitor.exitNode} calls for a given anchorNode. + pathVisitors: new Map>(), + parentField: undefined as FieldKey | undefined, + parent: undefined as UpPath | undefined, + free() { + assert( + this.anchorSet.activeVisitor !== undefined, + "Multiple free calls for same visitor", + ); + this.anchorSet.activeVisitor = undefined; + }, + onDelete(start: number, count: number): void { + assert(this.parentField !== undefined, 0x3a7 /* Must be in a field to delete */); + this.maybeWithNode( (p) => { p.events.emit("childrenChanging", p); }, - () => this.events.emit("childrenChanging", this), + () => this.anchorSet.events.emit("childrenChanging", this.anchorSet), ); const upPath: UpPath = { - parent, - parentField, + parent: this.parent, + parentField: this.parentField, parentIndex: start, }; - for (const visitors of pathVisitors.values()) { + for (const visitors of this.pathVisitors.values()) { for (const pathVisitor of visitors) { pathVisitor.onDelete(upPath, count); } } - this.removeChildren( + this.anchorSet.removeChildren( { - parent, - parentField, + parent: this.parent, + parentField: this.parentField, parentIndex: start, }, count, ); }, - onInsert: (start: number, content: Delta.ProtoNodes): void => { - assert(parentField !== undefined, 0x3a8 /* Must be in a field to insert */); - maybeWithNode( + onInsert(start: number, content: Delta.ProtoNodes): void { + assert(this.parentField !== undefined, 0x3a8 /* Must be in a field to insert */); + this.maybeWithNode( (p) => p.events.emit("childrenChanging", p), - () => this.events.emit("childrenChanging", this), + () => this.anchorSet.events.emit("childrenChanging", this.anchorSet), ); const upPath: UpPath = { - parent, - parentField, + parent: this.parent, + parentField: this.parentField, parentIndex: start, }; - for (const visitors of pathVisitors.values()) { + for (const visitors of this.pathVisitors.values()) { for (const pathVisitor of visitors) { pathVisitor.onInsert(upPath, content); } } - this.offsetChildren( + this.anchorSet.offsetChildren( { - parent, - parentField, + parent: this.parent, + parentField: this.parentField, parentIndex: start, }, content.length, ); }, - onMoveOut: (start: number, count: number, id: Delta.MoveId): void => { - assert(parentField !== undefined, 0x3a9 /* Must be in a field to move out */); - maybeWithNode( + onMoveOut(start: number, count: number, id: Delta.MoveId): void { + assert(this.parentField !== undefined, 0x3a9 /* Must be in a field to move out */); + this.maybeWithNode( (p) => p.events.emit("childrenChanging", p), - () => this.events.emit("childrenChanging", this), + () => this.anchorSet.events.emit("childrenChanging", this.anchorSet), ); - const fieldKey = this.createEmptyDetachedField(); - const source = { parent, parentField, parentIndex: start }; - const destination = { parent: this.root, parentField: fieldKey, parentIndex: 0 }; - this.moveChildren(source, destination, count); + const fieldKey = this.anchorSet.createEmptyDetachedField(); + const source = { + parent: this.parent, + parentField: this.parentField, + parentIndex: start, + }; + const destination = { + parent: this.anchorSet.root, + parentField: fieldKey, + parentIndex: 0, + }; + this.anchorSet.moveChildren(source, destination, count); moveTable.set(id, destination); }, - onMoveIn: (start: number, count: number, id: Delta.MoveId): void => { - assert(parentField !== undefined, 0x3aa /* Must be in a field to move in */); - maybeWithNode( + onMoveIn(start: number, count: number, id: Delta.MoveId): void { + assert(this.parentField !== undefined, 0x3aa /* Must be in a field to move in */); + this.maybeWithNode( (p) => p.events.emit("childrenChanging", p), - () => this.events.emit("childrenChanging", this), + () => this.anchorSet.events.emit("childrenChanging", this.anchorSet), ); const sourcePath = moveTable.get(id) ?? fail("Must visit a move in after its move out"); moveTable.delete(id); - this.moveChildren(sourcePath, { parent, parentField, parentIndex: start }, count); + this.anchorSet.moveChildren( + sourcePath, + { parent: this.parent, parentField: this.parentField, parentIndex: start }, + count, + ); }, - enterNode: (index: number): void => { - assert(parentField !== undefined, 0x3ab /* Must be in a field to enter node */); - parent = { parent, parentField, parentIndex: index }; - parentField = undefined; - maybeWithNode((p) => { + enterNode(index: number): void { + assert( + this.parentField !== undefined, + 0x3ab /* Must be in a field to enter node */, + ); + this.parent = { + parent: this.parent, + parentField: this.parentField, + parentIndex: index, + }; + this.parentField = undefined; + this.maybeWithNode((p) => { // avoid multiple pass side-effects - if (!pathVisitors.has(p)) { + if (!this.pathVisitors.has(p)) { const visitors: (PathVisitor | void)[] = p.events.emitAndCollect( "subtreeChanging", p, ); if (visitors.length > 0) { - pathVisitors.set( + this.pathVisitors.set( p, new Set(visitors.filter((v): v is PathVisitor => v !== undefined)), ); @@ -685,24 +713,27 @@ export class AnchorSet implements ISubscribable, AnchorLoca } }); }, - exitNode: (index: number): void => { - assert(parent !== undefined, 0x3ac /* Must have parent node */); - maybeWithNode((p) => { + exitNode(index: number): void { + assert(this.parent !== undefined, 0x3ac /* Must have parent node */); + this.maybeWithNode((p) => { // Remove subtree path visitors added at this node if there are any - pathVisitors.delete(p); + this.pathVisitors.delete(p); }); - parentField = parent.parentField; - parent = parent.parent; + const parent = this.parent; + assert(parent !== undefined, "Unable to exit root node"); + this.parentField = parent.parentField; + this.parent = parent.parent; }, - enterField: (key: FieldKey): void => { - parentField = key; + enterField(key: FieldKey): void { + this.parentField = key; }, - exitField: (key: FieldKey): void => { - parentField = undefined; + exitField(key: FieldKey): void { + this.parentField = undefined; }, }; this.events.emit("treeChanging", this); - visitDelta(delta, visitor); + this.activeVisitor = visitor; + return visitor; } } diff --git a/experimental/dds/tree2/src/core/tree/index.ts b/experimental/dds/tree2/src/core/tree/index.ts index 0011542ffba5..7fc9db6e00f2 100644 --- a/experimental/dds/tree2/src/core/tree/index.ts +++ b/experimental/dds/tree2/src/core/tree/index.ts @@ -67,7 +67,7 @@ export { NodeData, rootField, } from "./types"; -export { DeltaVisitor, visitDelta } from "./visitDelta"; +export { DeltaVisitor, visitDelta, applyDelta } from "./visitDelta"; export { PathVisitor } from "./visitPath"; // Split this up into separate import and export for compatibility with API-Extractor. diff --git a/experimental/dds/tree2/src/core/tree/visitDelta.ts b/experimental/dds/tree2/src/core/tree/visitDelta.ts index 75fe303dff33..ea333b837fcd 100644 --- a/experimental/dds/tree2/src/core/tree/visitDelta.ts +++ b/experimental/dds/tree2/src/core/tree/visitDelta.ts @@ -97,15 +97,25 @@ export function visitDelta(delta: Delta.Root, visitor: DeltaVisitor): void { } } +export function applyDelta( + delta: Delta.Root, + deltaProcessor: { acquireVisitor: () => DeltaVisitor }, +): void { + const visitor = deltaProcessor.acquireVisitor(); + visitDelta(delta, visitor); + visitor.free(); +} +/** + * Visitor for changes in a delta. + * Must be freed after use. + * @alpha + */ export interface DeltaVisitor { + free(): void; onDelete(index: number, count: number): void; onInsert(index: number, content: Delta.ProtoNodes): void; onMoveOut(index: number, count: number, id: Delta.MoveId): void; onMoveIn(index: number, count: number, id: Delta.MoveId): void; - // TODO: better align this with ITreeCursor: - // maybe rename its up and down to enter / exit? Maybe Also)? - // Maybe also have cursor have "current field key" state to allow better handling of empty fields and better match - // this visitor? enterNode(index: number): void; exitNode(index: number): void; enterField(key: FieldKey): void; diff --git a/experimental/dds/tree2/src/feature-libraries/chunked-forest/chunkedForest.ts b/experimental/dds/tree2/src/feature-libraries/chunked-forest/chunkedForest.ts index ef25ad393b02..77b0133731f5 100644 --- a/experimental/dds/tree2/src/feature-libraries/chunked-forest/chunkedForest.ts +++ b/experimental/dds/tree2/src/feature-libraries/chunked-forest/chunkedForest.ts @@ -18,12 +18,12 @@ import { Delta, UpPath, Anchor, - visitDelta, FieldAnchor, ForestEvents, ITreeSubscriptionCursorState, rootFieldKey, mapCursorField, + DeltaVisitor, } from "../../core"; import { brand, fail, getOrAddEmptyToMap } from "../../util"; import { createEmitter } from "../../events"; @@ -48,6 +48,8 @@ interface StackNode { class ChunkedForest extends SimpleDependee implements IEditableForest { private readonly dependent = new SimpleObservingDependent(() => this.invalidateDependents()); + private activeVisitor?: DeltaVisitor; + private readonly events = createEmitter(); /** @@ -82,9 +84,12 @@ class ChunkedForest extends SimpleDependee implements IEditableForest { this.anchors.forget(anchor); } - public applyDelta(delta: Delta.Root): void { - this.events.emit("beforeDelta", delta); - this.invalidateDependents(); + public acquireVisitor(): DeltaVisitor { + assert( + this.activeVisitor === undefined, + "Must release existing visitor before acquiring another", + ); + this.events.emit("beforeChange"); const moves: Map = new Map(); @@ -92,51 +97,72 @@ class ChunkedForest extends SimpleDependee implements IEditableForest { this.roots = this.roots.clone(); } - // Current location in the tree, as a non-shared BasicChunk (TODO: support in-place modification of other chunk formats when possible). - // Start above root detached sequences. - const mutableChunkStack: StackNode[] = []; - let mutableChunk: BasicChunk | undefined = this.roots; - - const getParent = () => { - assert(mutableChunkStack.length > 0, 0x532 /* invalid access to root's parent */); - return mutableChunkStack[mutableChunkStack.length - 1]; - }; - - const moveIn = (index: number, toAttach: DetachedField): number => { - const detachedKey = detachedFieldAsKey(toAttach); - const children = this.roots.fields.get(detachedKey) ?? []; - this.roots.fields.delete(detachedKey); - if (children.length === 0) { - return 0; // Prevent creating 0 sized fields when inserting empty into empty. - } + const visitor = { + forest: this, + // Current location in the tree, as a non-shared BasicChunk (TODO: support in-place modification of other chunk formats when possible). + // Start above root detached sequences. + mutableChunkStack: [] as StackNode[], + mutableChunk: this.roots as BasicChunk | undefined, + getParent() { + assert( + this.mutableChunkStack.length > 0, + 0x532 /* invalid access to root's parent */, + ); + return this.mutableChunkStack[this.mutableChunkStack.length - 1]; + }, + moveIn( + index: number, + toAttach: DetachedField, + invalidateDependents: boolean = true, + ): number { + if (invalidateDependents) { + this.forest.invalidateDependents(); + } + const detachedKey = detachedFieldAsKey(toAttach); + const children = this.forest.roots.fields.get(detachedKey) ?? []; + this.forest.roots.fields.delete(detachedKey); + if (children.length === 0) { + return 0; // Prevent creating 0 sized fields when inserting empty into empty. + } - const parent = getParent(); - const destinationField = getOrAddEmptyToMap(parent.mutableChunk.fields, parent.key); - // TODO: this will fail for very large moves due to argument limits. - destinationField.splice(index, 0, ...children); + const parent = this.getParent(); + const destinationField = getOrAddEmptyToMap(parent.mutableChunk.fields, parent.key); + // TODO: this will fail for very large moves due to argument limits. + destinationField.splice(index, 0, ...children); - return children.length; - }; - const visitor = { - onDelete: (index: number, count: number): void => { - visitor.onMoveOut(index, count); + return children.length; + }, + free(): void { + this.mutableChunk = undefined; + this.mutableChunkStack.length = 0; + assert( + this.forest.activeVisitor !== undefined, + "Multiple free calls for same visitor", + ); + this.forest.activeVisitor = undefined; + this.forest.events.emit("afterChange"); }, - onInsert: (index: number, content: Delta.ProtoNodes): void => { - const chunks: TreeChunk[] = content.map((c) => chunkTree(c, this.chunker)); - const field = this.newDetachedField(); - this.roots.fields.set(detachedFieldAsKey(field), chunks); - moveIn(index, field); + onDelete(index: number, count: number): void { + this.onMoveOut(index, count); }, - onMoveOut: (index: number, count: number, id?: Delta.MoveId): void => { - const parent = getParent(); + onInsert(index: number, content: Delta.ProtoNodes): void { + this.forest.invalidateDependents(); + const chunks: TreeChunk[] = content.map((c) => chunkTree(c, this.forest.chunker)); + const field = this.forest.newDetachedField(); + this.forest.roots.fields.set(detachedFieldAsKey(field), chunks); + this.moveIn(index, field, false); + }, + onMoveOut(index: number, count: number, id?: Delta.MoveId): void { + this.forest.invalidateDependents(); + const parent = this.getParent(); const sourceField = parent.mutableChunk.fields.get(parent.key) ?? []; const newField = sourceField.splice(index, count); if (id !== undefined) { - const detached = this.newDetachedField(); + const detached = this.forest.newDetachedField(); const key = detachedFieldAsKey(detached); if (newField.length > 0) { - this.roots.fields.set(key, newField); + this.forest.roots.fields.set(key, newField); } moves.set(id, detached); } else { @@ -148,15 +174,15 @@ class ChunkedForest extends SimpleDependee implements IEditableForest { parent.mutableChunk.fields.delete(parent.key); } }, - onMoveIn: (index: number, count: number, id: Delta.MoveId): void => { + onMoveIn(index: number, count: number, id: Delta.MoveId): void { const toAttach = moves.get(id) ?? fail("move in without move out"); moves.delete(id); - const countMoved = moveIn(index, toAttach); + const countMoved = this.moveIn(index, toAttach); assert(countMoved === count, 0x533 /* counts must match */); }, - enterNode: (index: number): void => { - assert(mutableChunk === undefined, 0x535 /* should be in field */); - const parent = getParent(); + enterNode(index: number): void { + assert(this.mutableChunk === undefined, 0x535 /* should be in field */); + const parent = this.getParent(); const chunks = parent.mutableChunk.fields.get(parent.key) ?? fail("missing edited field"); let indexWithinChunk = index; @@ -176,7 +202,7 @@ class ChunkedForest extends SimpleDependee implements IEditableForest { // // Maybe build path when visitor navigates then lazily sync to chunk tree when editing? const newChunks = mapCursorField(found.cursor(), (cursor) => - basicChunkTree(cursor, this.chunker), + basicChunkTree(cursor, this.forest.chunker), ); // TODO: this could fail for really long chunks being split (due to argument count limits). // Current implementations of chunks shouldn't ever be that long, but it could be an issue if they get bigger. @@ -187,30 +213,29 @@ class ChunkedForest extends SimpleDependee implements IEditableForest { } assert(found instanceof BasicChunk, 0x536 /* chunk should have been normalized */); if (found.isShared()) { - mutableChunk = chunks[indexOfChunk] = found.clone(); + this.mutableChunk = chunks[indexOfChunk] = found.clone(); found.referenceRemoved(); } else { - mutableChunk = found; + this.mutableChunk = found; } }, - exitNode: (index: number): void => { - assert(mutableChunk !== undefined, 0x537 /* should be in node */); - mutableChunk = undefined; + exitNode(index: number): void { + assert(this.mutableChunk !== undefined, 0x537 /* should be in node */); + this.mutableChunk = undefined; }, - enterField: (key: FieldKey): void => { - assert(mutableChunk !== undefined, 0x538 /* should be in node */); - mutableChunkStack.push({ key, mutableChunk }); - mutableChunk = undefined; + enterField(key: FieldKey): void { + assert(this.mutableChunk !== undefined, 0x538 /* should be in node */); + this.mutableChunkStack.push({ key, mutableChunk: this.mutableChunk }); + this.mutableChunk = undefined; }, - exitField: (key: FieldKey): void => { - const top = mutableChunkStack.pop() ?? fail("should not be at root"); - assert(mutableChunk === undefined, 0x539 /* should be in field */); - mutableChunk = top.mutableChunk; + exitField(key: FieldKey): void { + const top = this.mutableChunkStack.pop() ?? fail("should not be at root"); + assert(this.mutableChunk === undefined, 0x539 /* should be in field */); + this.mutableChunk = top.mutableChunk; }, }; - visitDelta(delta, visitor); - - this.events.emit("afterDelta", delta); + this.activeVisitor = visitor; + return visitor; } private nextDetachedFieldIdentifier = 0; diff --git a/experimental/dds/tree2/src/feature-libraries/editable-tree/editableTreeContext.ts b/experimental/dds/tree2/src/feature-libraries/editable-tree/editableTreeContext.ts index c386f1bff251..18e192174cb7 100644 --- a/experimental/dds/tree2/src/feature-libraries/editable-tree/editableTreeContext.ts +++ b/experimental/dds/tree2/src/feature-libraries/editable-tree/editableTreeContext.ts @@ -131,7 +131,7 @@ export class ProxyContext implements EditableTreeContext { public readonly nodeKeyFieldKey?: FieldKey, ) { this.eventUnregister = [ - this.forest.on("beforeDelta", () => { + this.forest.on("beforeChange", () => { this.prepareForEdit(); }), ]; diff --git a/experimental/dds/tree2/src/feature-libraries/forestRepairDataStore.ts b/experimental/dds/tree2/src/feature-libraries/forestRepairDataStore.ts index 39e33f5265cd..e799cd19b89a 100644 --- a/experimental/dds/tree2/src/feature-libraries/forestRepairDataStore.ts +++ b/experimental/dds/tree2/src/feature-libraries/forestRepairDataStore.ts @@ -6,6 +6,7 @@ import { assert, unreachableCase } from "@fluidframework/core-utils"; import { AnchorSet, + applyDelta, castCursorToSynchronous, Delta, EmptyKey, @@ -206,7 +207,7 @@ export class ForestRepairDataStoreProvider implements IRepairDataStoreP public applyChange(change: TChange): void { if (this.frozenForest === undefined) { - this.forest.applyDelta(this.intoDelta(change)); + applyDelta(this.intoDelta(change), this.forest); } } diff --git a/experimental/dds/tree2/src/feature-libraries/object-forest/objectForest.ts b/experimental/dds/tree2/src/feature-libraries/object-forest/objectForest.ts index 60e88867df21..f4475eee181c 100644 --- a/experimental/dds/tree2/src/feature-libraries/object-forest/objectForest.ts +++ b/experimental/dds/tree2/src/feature-libraries/object-forest/objectForest.ts @@ -19,7 +19,6 @@ import { Delta, UpPath, Anchor, - visitDelta, ITreeCursor, CursorLocationType, TreeSchemaIdentifier, @@ -30,6 +29,7 @@ import { FieldUpPath, ForestEvents, PathRootPrefix, + DeltaVisitor, } from "../../core"; import { brand, fail, assertValidIndex } from "../../util"; import { CursorWithNode, SynchronousCursor } from "../treeCursorUtils"; @@ -52,6 +52,8 @@ function makeRoot(): MapTree { class ObjectForest extends SimpleDependee implements IEditableForest { private readonly dependent = new SimpleObservingDependent(() => this.invalidateDependents()); + private activeVisitor?: DeltaVisitor; + public readonly roots: MapTree = makeRoot(); // All cursors that are in the "Current" state. Must be empty when editing. @@ -89,9 +91,13 @@ class ObjectForest extends SimpleDependee implements IEditableForest { this.anchors.forget(anchor); } - public applyDelta(delta: Delta.Root): void { - this.events.emit("beforeDelta", delta); - this.invalidateDependents(); + public acquireVisitor(): DeltaVisitor { + assert( + this.activeVisitor === undefined, + "Must release existing visitor before acquiring another", + ); + this.events.emit("beforeChange"); + assert( this.currentCursors.size === 0, 0x374 /* No cursors can be current when modifying forest */, @@ -106,7 +112,8 @@ class ObjectForest extends SimpleDependee implements IEditableForest { const moves: Map = new Map(); const cursor: Cursor = this.allocateCursor(); cursor.setToAboveDetachedSequences(); - const moveIn = (index: number, toAttach: DetachedField): number => { + const moveIn = (index: number, toAttach: DetachedField, moveInCursor: Cursor): number => { + this.invalidateDependents(); const detachedKey = detachedFieldAsKey(toAttach); const children = getMapTreeField(this.roots, detachedKey, false); this.roots.fields.delete(detachedKey); @@ -114,7 +121,7 @@ class ObjectForest extends SimpleDependee implements IEditableForest { return 0; // Prevent creating 0 sized fields when inserting empty into empty. } - const [parent, key] = cursor.getParent(); + const [parent, key] = moveInCursor.getParent(); const destinationField = getMapTreeField(parent, key, true); assertValidIndex(index, destinationField, true); // TODO: this will fail for very large moves due to argument limits. @@ -123,14 +130,26 @@ class ObjectForest extends SimpleDependee implements IEditableForest { return children.length; }; const visitor = { - onDelete: (index: number, count: number): void => { - visitor.onMoveOut(index, count); + forest: this, + cursor, + free() { + this.cursor.free(); + assert( + this.forest.activeVisitor !== undefined, + "Multiple free calls for same visitor", + ); + this.forest.activeVisitor = undefined; + this.forest.events.emit("afterChange"); }, - onInsert: (index: number, content: Delta.ProtoNode[]): void => { - const range = this.add(content); - moveIn(index, range); + onDelete(index: number, count: number): void { + this.onMoveOut(index, count); }, - onMoveOut: (index: number, count: number, id?: Delta.MoveId): void => { + onInsert(index: number, content: Delta.ProtoNode[]): void { + const range = this.forest.add(content); + moveIn(index, range, this.cursor); + }, + onMoveOut(index: number, count: number, id?: Delta.MoveId): void { + this.forest.invalidateDependents(); const [parent, key] = cursor.getParent(); const sourceField = getMapTreeField(parent, key, false); const startIndex = index; @@ -142,31 +161,37 @@ class ObjectForest extends SimpleDependee implements IEditableForest { 0x371 /* detached range's end must be after its start */, ); const newField = sourceField.splice(startIndex, endIndex - startIndex); - const field = this.addFieldAsDetached(newField); + const field = this.forest.addFieldAsDetached(newField); if (id !== undefined) { moves.set(id, field); } else { - this.delete(field); + this.forest.delete(field); } if (sourceField.length === 0) { parent.fields.delete(key); } }, - onMoveIn: (index: number, count: number, id: Delta.MoveId): void => { + onMoveIn(index: number, count: number, id: Delta.MoveId): void { const toAttach = moves.get(id) ?? fail("move in without move out"); moves.delete(id); - const countMoved = moveIn(index, toAttach); + const countMoved = moveIn(index, toAttach, this.cursor); assert(countMoved === count, 0x369 /* counts must match */); }, - enterNode: (index: number): void => cursor.enterNode(index), - exitNode: (index: number): void => cursor.exitNode(), - enterField: (key: FieldKey): void => cursor.enterField(key), - exitField: (key: FieldKey): void => cursor.exitField(), + enterNode(index: number): void { + this.cursor.enterNode(index); + }, + exitNode(index: number): void { + this.cursor.exitNode(); + }, + enterField(key: FieldKey): void { + this.cursor.enterField(key); + }, + exitField(key: FieldKey): void { + this.cursor.exitField(); + }, }; - visitDelta(delta, visitor); - cursor.free(); - - this.events.emit("afterDelta", delta); + this.activeVisitor = visitor; + return visitor; } private nextRange = 0; diff --git a/experimental/dds/tree2/src/index.ts b/experimental/dds/tree2/src/index.ts index 46d5ef798749..ea80962beabb 100644 --- a/experimental/dds/tree2/src/index.ts +++ b/experimental/dds/tree2/src/index.ts @@ -24,6 +24,7 @@ export { RootField, ChildCollection, ChildLocation, + DeltaVisitor, FieldMapObject, NodeData, GenericTreeNode, diff --git a/experimental/dds/tree2/src/shared-tree/sharedTreeView.ts b/experimental/dds/tree2/src/shared-tree/sharedTreeView.ts index 09c02bf1e842..b70a20fbacdf 100644 --- a/experimental/dds/tree2/src/shared-tree/sharedTreeView.ts +++ b/experimental/dds/tree2/src/shared-tree/sharedTreeView.ts @@ -17,6 +17,7 @@ import { UndoRedoManager, LocalCommitSource, schemaDataIsEmpty, + applyDelta, } from "../core"; import { HasListeners, IEmitter, ISubscribable, createEmitter } from "../events"; import { @@ -432,8 +433,8 @@ export class SharedTreeView implements ISharedTreeBranchView { branch.on("change", ({ change }) => { if (change !== undefined) { const delta = this.changeFamily.intoDelta(change); - this.forest.anchors.applyDelta(delta); - this.forest.applyDelta(delta); + applyDelta(delta, this.forest.anchors); + applyDelta(delta, this.forest); this.nodeKeyIndex.scanKeys(this.context); this.events.emit("afterBatch"); } diff --git a/experimental/dds/tree2/src/test/feature-libraries/chunked-forest/chunkedForest.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/chunked-forest/chunkedForest.spec.ts index 0021341557bf..7c29147e7c31 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/chunked-forest/chunkedForest.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/chunked-forest/chunkedForest.spec.ts @@ -34,6 +34,7 @@ import { Delta, IForestSubscription, StoredSchemaRepository, + applyDelta, } from "../../../core"; import { jsonObject } from "../../../domains"; import { @@ -140,7 +141,7 @@ describe("ChunkedForest", () => { // Captured reference owns a ref count making it shared. assert(chunk.isShared()); // Delete from forest, removing the forest's ref, making chunk not shared again. - forest.applyDelta(delta); + applyDelta(delta, forest); assert(!chunk.isShared()); compareForest(forest, []); diff --git a/experimental/dds/tree2/src/test/feature-libraries/default-field-kinds/defaultChangeFamily.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/default-field-kinds/defaultChangeFamily.spec.ts index 2f52e1317dae..84d3b88ad05e 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/default-field-kinds/defaultChangeFamily.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/default-field-kinds/defaultChangeFamily.spec.ts @@ -17,6 +17,7 @@ import { rootFieldKey, TaggedChange, UpPath, + applyDelta, } from "../../../core"; import { jsonNumber, jsonObject, jsonString } from "../../../domains"; import { @@ -118,7 +119,7 @@ function initializeEditableForest(data?: JsonableTree): { changes.push({ revision: currentRevision, change }); const delta = defaultChangeFamily.intoDelta(change); deltas.push(delta); - forest.applyDelta(delta); + applyDelta(delta, forest); currentRevision = mintRevisionTag(); }); return { diff --git a/experimental/dds/tree2/src/test/feature-libraries/editable-tree/editableTreeContext.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/editable-tree/editableTreeContext.spec.ts index 04254ede121d..8a3a2912dda9 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/editable-tree/editableTreeContext.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/editable-tree/editableTreeContext.spec.ts @@ -40,7 +40,7 @@ describe("editable-tree context", () => { allowedSchemaModifications: AllowedUpdateType.None, }); - view.context.on("afterDelta", () => { + view.context.on("afterChange", () => { view.context.clear(); }); diff --git a/experimental/dds/tree2/src/test/feature-libraries/forestRepairDataStore.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/forestRepairDataStore.spec.ts index 5a503daedf81..a914591baf64 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/forestRepairDataStore.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/forestRepairDataStore.spec.ts @@ -12,6 +12,7 @@ import { RevisionTag, rootFieldKey, UpPath, + applyDelta, } from "../../core"; import { jsonNumber, jsonObject } from "../../domains"; import { @@ -80,7 +81,7 @@ describe("ForestRepairDataStore", () => { ], ]); store.capture(delta1, revision1); - forest.applyDelta(delta1); + applyDelta(delta1, forest); const delta2 = new Map([ [ rootFieldKey, diff --git a/experimental/dds/tree2/src/test/forestTestSuite.ts b/experimental/dds/tree2/src/test/forestTestSuite.ts index a893291a7b99..a1e0f9fe191a 100644 --- a/experimental/dds/tree2/src/test/forestTestSuite.ts +++ b/experimental/dds/tree2/src/test/forestTestSuite.ts @@ -24,6 +24,7 @@ import { EmptyKey, ValueSchema, FieldUpPath, + applyDelta, } from "../core"; import { cursorToJsonObject, @@ -167,7 +168,7 @@ export function testForest(config: ForestTestConfiguration): void { type: Delta.MarkType.Insert, content: [singleJsonCursor([])], }; - forest.applyDelta(new Map([[brand("different root"), [insert]]])); + applyDelta(new Map([[brand("different root"), [insert]]]), forest); assert(!forest.isEmpty); }); @@ -329,8 +330,8 @@ export function testForest(config: ForestTestConfiguration): void { const mark: Delta.Delete = { type: Delta.MarkType.Delete, count: 1 }; const delta: Delta.Root = new Map([[rootFieldKey, [mark]]]); - forest.applyDelta(delta); - forest.anchors.applyDelta(delta); + applyDelta(delta, forest); + applyDelta(delta, forest.anchors); assert.equal( forest.tryMoveCursorToNode(firstNodeAnchor, cursor), @@ -412,7 +413,7 @@ export function testForest(config: ForestTestConfiguration): void { const clone = forest.clone(schema, forest.anchors); const mark: Delta.Delete = { type: Delta.MarkType.Delete, count: 1 }; const delta: Delta.Root = new Map([[rootFieldKey, [mark]]]); - clone.applyDelta(delta); + applyDelta(delta, clone); // Check the clone has the new value const cloneReader = clone.allocateCursor(); @@ -437,7 +438,7 @@ export function testForest(config: ForestTestConfiguration): void { const mark: Delta.Delete = { type: Delta.MarkType.Delete, count: 1 }; const delta: Delta.Root = new Map([[rootFieldKey, [mark]]]); - assert.throws(() => forest.applyDelta(delta)); + assert.throws(() => applyDelta(delta, forest)); }); } @@ -466,7 +467,7 @@ export function testForest(config: ForestTestConfiguration): void { ]), }; const delta: Delta.Root = new Map([[rootFieldKey, [setField]]]); - forest.applyDelta(delta); + applyDelta(delta, forest); const reader = forest.allocateCursor(); moveToDetachedField(forest, reader); @@ -483,7 +484,7 @@ export function testForest(config: ForestTestConfiguration): void { const mark: Delta.Delete = { type: Delta.MarkType.Delete, count: 1 }; const delta: Delta.Root = new Map([[rootFieldKey, [0, mark]]]); - forest.applyDelta(delta); + applyDelta(delta, forest); // Inspect resulting tree: should just have `2`. const reader = forest.allocateCursor(); @@ -506,7 +507,7 @@ export function testForest(config: ForestTestConfiguration): void { const skip: Delta.Skip = 1; const mark: Delta.Delete = { type: Delta.MarkType.Delete, count: 1 }; const delta: Delta.Root = new Map([[rootFieldKey, [skip, mark]]]); - forest.applyDelta(delta); + applyDelta(delta, forest); // Inspect resulting tree: should just have `1`. const reader = forest.allocateCursor(); @@ -525,7 +526,7 @@ export function testForest(config: ForestTestConfiguration): void { content: [singleJsonCursor(3)], }; const delta: Delta.Root = new Map([[rootFieldKey, [mark]]]); - forest.applyDelta(delta); + applyDelta(delta, forest); const reader = forest.allocateCursor(); moveToDetachedField(forest, reader); @@ -560,7 +561,7 @@ export function testForest(config: ForestTestConfiguration): void { fields: new Map([[xField, [moveOut]]]), }; const delta: Delta.Root = new Map([[rootFieldKey, [mark, moveIn]]]); - forest.applyDelta(delta); + applyDelta(delta, forest); const reader = forest.allocateCursor(); moveToDetachedField(forest, reader); @@ -592,7 +593,7 @@ export function testForest(config: ForestTestConfiguration): void { ]), }; const delta: Delta.Root = new Map([[rootFieldKey, [modify]]]); - forest.applyDelta(delta); + applyDelta(delta, forest); const reader = forest.allocateCursor(); moveToDetachedField(forest, reader); assert(reader.firstNode()); @@ -626,7 +627,7 @@ export function testForest(config: ForestTestConfiguration): void { ]), }; const delta: Delta.Root = new Map([[rootFieldKey, [mark]]]); - forest.applyDelta(delta); + applyDelta(delta, forest); const reader = forest.allocateCursor(); moveToDetachedField(forest, reader); @@ -659,7 +660,7 @@ export function testForest(config: ForestTestConfiguration): void { const delta: Delta.Root = new Map([ [rootFieldKey, [mark, { type: Delta.MarkType.MoveIn, count: 1, moveId }]], ]); - forest.applyDelta(delta); + applyDelta(delta, forest); const reader = forest.allocateCursor(); moveToDetachedField(forest, reader); @@ -703,7 +704,7 @@ export function testForest(config: ForestTestConfiguration): void { ]), }; const delta: Delta.Root = new Map([[rootFieldKey, [mark]]]); - forest.applyDelta(delta); + applyDelta(delta, forest); const reader = forest.allocateCursor(); moveToDetachedField(forest, reader); @@ -735,15 +736,15 @@ export function testForest(config: ForestTestConfiguration): void { const delta: Delta.Root = new Map([[rootFieldKey, [insert]]]); assert.deepEqual(dependent.tokens, []); - forest.applyDelta(delta); + applyDelta(delta, forest); assert.deepEqual(dependent.tokens.length, 1); - forest.applyDelta(delta); + applyDelta(delta, forest); assert.deepEqual(dependent.tokens.length, 2); // Remove the dependency so the dependent stops getting invalidation messages forest.removeDependent(dependent); - forest.applyDelta(delta); + applyDelta(delta, forest); assert.deepEqual(dependent.tokens.length, 2); }); @@ -785,7 +786,7 @@ export function testForest(config: ForestTestConfiguration): void { ]); const expected: JsonCompatible[] = [{ y: 1 }]; initializeForest(forest, [singleJsonCursor(nestedContent)]); - forest.applyDelta(delta); + applyDelta(delta, forest); const readCursor = forest.allocateCursor(); moveToDetachedField(forest, readCursor); const actual = mapCursorField(readCursor, cursorToJsonObject); @@ -827,7 +828,7 @@ export function testForest(config: ForestTestConfiguration): void { ]), }; const delta: Delta.Root = new Map([[rootFieldKey, [modify]]]); - forest.applyDelta(delta); + applyDelta(delta, forest); const expectedCursor = cursorForTypedTreeData({ schema }, root, { x: [], y: [1, 2], diff --git a/experimental/dds/tree2/src/test/tree/anchorSet.spec.ts b/experimental/dds/tree2/src/test/tree/anchorSet.spec.ts index 5e3e1ad100a9..39725d338b67 100644 --- a/experimental/dds/tree2/src/test/tree/anchorSet.spec.ts +++ b/experimental/dds/tree2/src/test/tree/anchorSet.spec.ts @@ -16,6 +16,7 @@ import { UpPath, clonePath, rootFieldKey, + applyDelta, } from "../../core"; import { brand } from "../../util"; import { expectEqualPaths } from "../utils"; @@ -61,7 +62,7 @@ describe("AnchorSet", () => { }; const delta = new Map([[rootFieldKey, [1, moveOut, 1, moveIn]]]); - anchors.applyDelta(delta); + applyDelta(delta, anchors); checkEquality(anchors.locate(anchor0), makePath([rootFieldKey, 0])); checkEquality(anchors.locate(anchor1), makePath([rootFieldKey, 2])); checkEquality(anchors.locate(anchor2), makePath([rootFieldKey, 1])); @@ -76,7 +77,7 @@ describe("AnchorSet", () => { content: [node, node].map(singleTextCursor), }; - anchors.applyDelta(makeDelta(insert, makePath([fieldFoo, 4]))); + applyDelta(makeDelta(insert, makePath([fieldFoo, 4])), anchors); checkEquality(anchors.locate(anchor1), makePath([fieldFoo, 7], [fieldBar, 4])); checkEquality(anchors.locate(anchor2), makePath([fieldFoo, 3], [fieldBaz, 2])); @@ -90,7 +91,7 @@ describe("AnchorSet", () => { count: 1, }; - anchors.applyDelta(makeDelta(deleteMark, makePath([fieldFoo, 4]))); + applyDelta(makeDelta(deleteMark, makePath([fieldFoo, 4])), anchors); checkEquality(anchors.locate(anchor1), makePath([fieldFoo, 4], [fieldBar, 4])); checkEquality(anchors.locate(anchor2), path2); assert.equal(anchors.locate(anchor3), undefined); @@ -105,7 +106,7 @@ describe("AnchorSet", () => { count: 1, }; - anchors.applyDelta(makeDelta(deleteMark, makePath([fieldFoo, 5]))); + applyDelta(makeDelta(deleteMark, makePath([fieldFoo, 5])), anchors); assert.equal(anchors.locate(anchor4), undefined); assert.equal(anchors.locate(anchor1), undefined); assert.doesNotThrow(() => anchors.forget(anchor4)); @@ -116,14 +117,14 @@ describe("AnchorSet", () => { assert.throws(() => anchors.locate(anchor1)); checkEquality(anchors.locate(anchor2), path2); - anchors.applyDelta(makeDelta(deleteMark, makePath([fieldFoo, 3]))); + applyDelta(makeDelta(deleteMark, makePath([fieldFoo, 3])), anchors); checkEquality(anchors.locate(anchor2), undefined); assert.doesNotThrow(() => anchors.forget(anchor2)); assert.throws(() => anchors.locate(anchor2)); // The index of anchor3 has changed from 4 to 3 because of the deletion of the node at index 3. checkEquality(anchors.locate(anchor3), makePath([fieldFoo, 3])); - anchors.applyDelta(makeDelta(deleteMark, makePath([fieldFoo, 3]))); + applyDelta(makeDelta(deleteMark, makePath([fieldFoo, 3])), anchors); checkEquality(anchors.locate(anchor3), undefined); assert.doesNotThrow(() => anchors.forget(anchor3)); assert.throws(() => anchors.locate(anchor3)); @@ -149,7 +150,7 @@ describe("AnchorSet", () => { }; const delta = new Map([[fieldFoo, [3, moveOut, 1, modify]]]); - anchors.applyDelta(delta); + applyDelta(delta, anchors); checkEquality(anchors.locate(anchor1), makePath([fieldFoo, 4], [fieldBar, 5])); checkEquality( anchors.locate(anchor2), @@ -232,7 +233,7 @@ describe("AnchorSet", () => { }; log.expect([]); - anchors.applyDelta(new Map([[rootFieldKey, [0, deleteMark]]])); + applyDelta(new Map([[rootFieldKey, [0, deleteMark]]]), anchors); log.expect([ ["root childrenChange", 1], @@ -253,7 +254,7 @@ describe("AnchorSet", () => { type: Delta.MarkType.Insert, content: [singleTextCursor({ type: jsonString.name, value: "x" })], }; - anchors.applyDelta(new Map([[rootFieldKey, [deleteMark, insertMark]]])); + applyDelta(new Map([[rootFieldKey, [deleteMark, insertMark]]]), anchors); log.expect([ ["afterDelete", 1], @@ -262,12 +263,12 @@ describe("AnchorSet", () => { ]); log.clear(); - anchors.applyDelta(makeDelta(insertMark, makePath([rootFieldKey, 0], [fieldFoo, 5]))); + applyDelta(makeDelta(insertMark, makePath([rootFieldKey, 0], [fieldFoo, 5])), anchors); log.expect([["root treeChange", 1]]); log.clear(); - anchors.applyDelta(new Map([[rootFieldKey, [0, deleteMark]]])); + applyDelta(new Map([[rootFieldKey, [0, deleteMark]]]), anchors); log.expect([ ["root childrenChange", 1], ["root treeChange", 1], @@ -285,7 +286,7 @@ describe("AnchorSet", () => { }; const log = new UnorderedTestLogger(); const anchors = new AnchorSet(); - anchors.applyDelta(makeDelta(insertMark, makePath([rootFieldKey, 0], [fieldFoo, 3]))); + applyDelta(makeDelta(insertMark, makePath([rootFieldKey, 0], [fieldFoo, 3])), anchors); const anchor0 = anchors.track(makePath([rootFieldKey, 0])); const node0 = anchors.locate(anchor0) ?? assert.fail(); const pathVisitor: PathVisitor = { @@ -303,14 +304,14 @@ describe("AnchorSet", () => { }, }; const unsubscribePathVisitor = node0.on("subtreeChanging", (n: AnchorNode) => pathVisitor); - anchors.applyDelta(makeDelta(insertMark, makePath([rootFieldKey, 0], [fieldFoo, 4]))); + applyDelta(makeDelta(insertMark, makePath([rootFieldKey, 0], [fieldFoo, 4])), anchors); log.expect([["visitSubtreeChange.onInsert-foo-4", 1]]); log.clear(); - anchors.applyDelta(makeDelta(deleteMark, makePath([rootFieldKey, 0], [fieldFoo, 5]))); + applyDelta(makeDelta(deleteMark, makePath([rootFieldKey, 0], [fieldFoo, 5])), anchors); log.expect([["visitSubtreeChange.onDelete-foo-5-1", 1]]); log.clear(); unsubscribePathVisitor(); - anchors.applyDelta(makeDelta(insertMark, makePath([rootFieldKey, 0], [fieldFoo, 4]))); + applyDelta(makeDelta(insertMark, makePath([rootFieldKey, 0], [fieldFoo, 4])), anchors); log.expect([]); }); });