diff --git a/packages/dds/tree/src/core/index.ts b/packages/dds/tree/src/core/index.ts index 496e19aed616..5e414810ce21 100644 --- a/packages/dds/tree/src/core/index.ts +++ b/packages/dds/tree/src/core/index.ts @@ -67,8 +67,6 @@ export { forEachField, type PathRootPrefix, deltaForRootInitialization, - emptyFieldChanges, - isEmptyFieldChanges, makeDetachedNodeId, offsetDetachId, emptyDelta, diff --git a/packages/dds/tree/src/core/tree/delta.ts b/packages/dds/tree/src/core/tree/delta.ts index fc1ad6c8beb6..fd4382929a98 100644 --- a/packages/dds/tree/src/core/tree/delta.ts +++ b/packages/dds/tree/src/core/tree/delta.ts @@ -98,6 +98,22 @@ export interface Root { * The ordering has no significance. */ readonly refreshers?: readonly DetachedNodeBuild[]; + /** + * Changes to apply to detached nodes. + * The ordering has no significance. + * + * Nested changes for a root that is undergoing a rename should be listed under the starting name. + * For example, if one wishes to change a tree which is being renamed from ID A to ID B, + * then the changes should be listed under ID A. + */ + readonly global?: readonly DetachedNodeChanges[]; + /** + * Detached roots whose associated ID needs to be updated. + * The ordering has no significance. + * Note that the renames may need to be performed in a specific order to avoid collisions. + * This ordering problem is left to the consumer of this format. + */ + readonly rename?: readonly DetachedNodeRename[]; } /** @@ -199,30 +215,9 @@ export interface DetachedNodeRename { } /** - * Represents the changes to perform on a given field. + * Represents a list of changes to the nodes in the field. + * The index of each mark within the range of nodes, before + * applying any of the changes, is not represented explicitly. + * It corresponds to the sum of `mark.count` values for all previous marks for which `isAttachMark(mark)` is false. */ -export interface FieldChanges { - /** - * Represents a list of changes to the nodes in the field. - * The index of each mark within the range of nodes, before - * applying any of the changes, is not represented explicitly. - * It corresponds to the sum of `mark.count` values for all previous marks for which `isAttachMark(mark)` is false. - */ - readonly local?: readonly Mark[]; - /** - * Changes to apply to detached nodes. - * The ordering has no significance. - * - * Nested changes for a root that is undergoing a rename should be listed under the starting name. - * For example, if one wishes to change a tree which is being renamed from ID A to ID B, - * then the changes should be listed under ID A. - */ - readonly global?: readonly DetachedNodeChanges[]; - /** - * Detached whose associated ID needs to be updated. - * The ordering has no significance. - * Note that the renames may need to be performed in a specific order to avoid collisions. - * This ordering problem is left to the consumer of this format. - */ - readonly rename?: readonly DetachedNodeRename[]; -} +export type FieldChanges = readonly Mark[]; diff --git a/packages/dds/tree/src/core/tree/deltaUtil.ts b/packages/dds/tree/src/core/tree/deltaUtil.ts index 47ca9755fe97..97eeb2660b7f 100644 --- a/packages/dds/tree/src/core/tree/deltaUtil.ts +++ b/packages/dds/tree/src/core/tree/deltaUtil.ts @@ -12,8 +12,6 @@ import { rootFieldKey } from "./types.js"; export const emptyDelta: Root = {}; -export const emptyFieldChanges: FieldChanges = {}; - export function isAttachMark(mark: Mark): boolean { return mark.attach !== undefined && mark.detach === undefined; } @@ -26,14 +24,6 @@ export function isReplaceMark(mark: Mark): boolean { return mark.detach !== undefined && mark.attach !== undefined; } -export function isEmptyFieldChanges(fieldChanges: FieldChanges): boolean { - return ( - fieldChanges.local === undefined && - fieldChanges.global === undefined && - fieldChanges.rename === undefined - ); -} - export function deltaForRootInitialization(content: readonly ITreeCursorSynchronous[]): Root { if (content.length === 0) { return emptyDelta; @@ -42,12 +32,7 @@ export function deltaForRootInitialization(content: readonly ITreeCursorSynchron const delta: Root = { build: [{ id: buildId, trees: content }], fields: new Map([ - [ - rootFieldKey, - { - local: [{ count: content.length, attach: buildId }], - }, - ], + [rootFieldKey, [{ count: content.length, attach: buildId }]], ]), }; return delta; diff --git a/packages/dds/tree/src/core/tree/index.ts b/packages/dds/tree/src/core/tree/index.ts index edab69d49277..c12b921f5ae6 100644 --- a/packages/dds/tree/src/core/tree/index.ts +++ b/packages/dds/tree/src/core/tree/index.ts @@ -104,8 +104,6 @@ export { SparseNode, getDescendant } from "./sparseTree.js"; export { deltaForRootInitialization, - emptyFieldChanges, - isEmptyFieldChanges, makeDetachedNodeId, offsetDetachId, emptyDelta, diff --git a/packages/dds/tree/src/core/tree/visitDelta.ts b/packages/dds/tree/src/core/tree/visitDelta.ts index af8bd19945da..50e8d7ac2749 100644 --- a/packages/dds/tree/src/core/tree/visitDelta.ts +++ b/packages/dds/tree/src/core/tree/visitDelta.ts @@ -105,6 +105,8 @@ export function visitDelta( rootDestructions, }; processBuilds(delta.build, detachConfig, visitor); + processGlobal(delta.global, detachConfig, visitor); + processRename(delta.rename, detachConfig); visitFieldMarks(delta.fields, visitor, detachConfig); fixedPointVisitOfRoots(visitor, detachPassRoots, detachConfig); transferRoots( @@ -420,52 +422,32 @@ function visitNode( * (because we want to wait until we are sure content to attach is available as a root) */ function detachPass( - delta: Delta.FieldChanges, + fieldChanges: Delta.FieldChanges, visitor: DeltaVisitor, config: PassConfig, ): void { - if (delta.global !== undefined) { - for (const { id, fields } of delta.global) { - let root = config.detachedFieldIndex.tryGetEntry(id); - if (root === undefined) { - const tree = tryGetFromNestedMap(config.refreshers, id.major, id.minor); - assert(tree !== undefined, 0x928 /* refresher data not found */); - buildTrees(id, [tree], config.detachedFieldIndex, config.latestRevision, visitor); - root = config.detachedFieldIndex.getEntry(id); - } - // the revision is updated for any refresher data included in the delta that is used - config.detachedFieldIndex.updateLatestRevision(id, config.latestRevision); - config.detachPassRoots.set(root, fields); - config.attachPassRoots.set(root, fields); + let index = 0; + for (const mark of fieldChanges) { + if (mark.fields !== undefined) { + assert( + mark.attach === undefined || mark.detach !== undefined, + 0x7d0 /* Invalid nested changes on an additive mark */, + ); + visitNode(index, mark.fields, visitor, config); } - } - if (delta.rename !== undefined) { - config.rootTransfers.push(...delta.rename); - } - if (delta.local !== undefined) { - let index = 0; - for (const mark of delta.local) { - if (mark.fields !== undefined) { - assert( - mark.attach === undefined || mark.detach !== undefined, - 0x7d0 /* Invalid nested changes on an additive mark */, - ); - visitNode(index, mark.fields, visitor, config); - } - if (isDetachMark(mark)) { - for (let i = 0; i < mark.count; i += 1) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const id = offsetDetachId(mark.detach!, i); - const root = config.detachedFieldIndex.createEntry(id, config.latestRevision); - if (mark.fields !== undefined) { - config.attachPassRoots.set(root, mark.fields); - } - const field = config.detachedFieldIndex.toFieldKey(root); - visitor.detach({ start: index, end: index + 1 }, field); + if (isDetachMark(mark)) { + for (let i = 0; i < mark.count; i += 1) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const id = offsetDetachId(mark.detach!, i); + const root = config.detachedFieldIndex.createEntry(id, config.latestRevision); + if (mark.fields !== undefined) { + config.attachPassRoots.set(root, mark.fields); } - } else if (!isAttachMark(mark)) { - index += mark.count; + const field = config.detachedFieldIndex.toFieldKey(root); + visitor.detach({ start: index, end: index + 1 }, field); } + } else if (!isAttachMark(mark)) { + index += mark.count; } } } @@ -499,6 +481,37 @@ function processBuilds( } } +function processGlobal( + global: readonly Delta.DetachedNodeChanges[] | undefined, + config: PassConfig, + visitor: DeltaVisitor, +): void { + if (global !== undefined) { + for (const { id, fields } of global) { + let root = config.detachedFieldIndex.tryGetEntry(id); + if (root === undefined) { + const tree = tryGetFromNestedMap(config.refreshers, id.major, id.minor); + assert(tree !== undefined, 0x928 /* refresher data not found */); + buildTrees(id, [tree], config.detachedFieldIndex, config.latestRevision, visitor); + root = config.detachedFieldIndex.getEntry(id); + } + // the revision is updated for any refresher data included in the delta that is used + config.detachedFieldIndex.updateLatestRevision(id, config.latestRevision); + config.detachPassRoots.set(root, fields); + config.attachPassRoots.set(root, fields); + } + } +} + +function processRename( + rename: readonly Delta.DetachedNodeRename[] | undefined, + config: PassConfig, +): void { + if (rename !== undefined) { + config.rootTransfers.push(...rename); + } +} + function collectDestroys( destroys: readonly Delta.DetachedNodeDestruction[] | undefined, config: PassConfig, @@ -515,69 +528,67 @@ function collectDestroys( * - Collects detached roots (from replaces) that need an attach pass */ function attachPass( - delta: Delta.FieldChanges, + fieldChanges: Delta.FieldChanges, visitor: DeltaVisitor, config: PassConfig, ): void { - if (delta.local !== undefined) { - let index = 0; - for (const mark of delta.local) { - if (isAttachMark(mark) || isReplaceMark(mark)) { - for (let i = 0; i < mark.count; i += 1) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const offsetAttachId = offsetDetachId(mark.attach!, i); - let sourceRoot = config.detachedFieldIndex.tryGetEntry(offsetAttachId); - if (sourceRoot === undefined) { - const tree = tryGetFromNestedMap( - config.refreshers, - offsetAttachId.major, - offsetAttachId.minor, - ); - assert(tree !== undefined, 0x92a /* refresher data not found */); - buildTrees( - offsetAttachId, - [tree], - config.detachedFieldIndex, - config.latestRevision, - visitor, - ); - sourceRoot = config.detachedFieldIndex.getEntry(offsetAttachId); - } - const sourceField = config.detachedFieldIndex.toFieldKey(sourceRoot); - const offsetIndex = index + i; - if (isReplaceMark(mark)) { - const rootDestination = config.detachedFieldIndex.createEntry( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - offsetDetachId(mark.detach!, i), - config.latestRevision, - ); - const destinationField = config.detachedFieldIndex.toFieldKey(rootDestination); - visitor.replace( - sourceField, - { start: offsetIndex, end: offsetIndex + 1 }, - destinationField, - ); - // We may need to do a second pass on the detached nodes - if (mark.fields !== undefined) { - config.attachPassRoots.set(rootDestination, mark.fields); - } - } else { - // This a simple attach - visitor.attach(sourceField, 1, offsetIndex); - } - config.detachedFieldIndex.deleteEntry(offsetAttachId); - const fields = config.attachPassRoots.get(sourceRoot); - if (fields !== undefined) { - config.attachPassRoots.delete(sourceRoot); - visitNode(offsetIndex, fields, visitor, config); + let index = 0; + for (const mark of fieldChanges) { + if (isAttachMark(mark) || isReplaceMark(mark)) { + for (let i = 0; i < mark.count; i += 1) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const offsetAttachId = offsetDetachId(mark.attach!, i); + let sourceRoot = config.detachedFieldIndex.tryGetEntry(offsetAttachId); + if (sourceRoot === undefined) { + const tree = tryGetFromNestedMap( + config.refreshers, + offsetAttachId.major, + offsetAttachId.minor, + ); + assert(tree !== undefined, 0x92a /* refresher data not found */); + buildTrees( + offsetAttachId, + [tree], + config.detachedFieldIndex, + config.latestRevision, + visitor, + ); + sourceRoot = config.detachedFieldIndex.getEntry(offsetAttachId); + } + const sourceField = config.detachedFieldIndex.toFieldKey(sourceRoot); + const offsetIndex = index + i; + if (isReplaceMark(mark)) { + const rootDestination = config.detachedFieldIndex.createEntry( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + offsetDetachId(mark.detach!, i), + config.latestRevision, + ); + const destinationField = config.detachedFieldIndex.toFieldKey(rootDestination); + visitor.replace( + sourceField, + { start: offsetIndex, end: offsetIndex + 1 }, + destinationField, + ); + // We may need to do a second pass on the detached nodes + if (mark.fields !== undefined) { + config.attachPassRoots.set(rootDestination, mark.fields); } + } else { + // This a simple attach + visitor.attach(sourceField, 1, offsetIndex); + } + config.detachedFieldIndex.deleteEntry(offsetAttachId); + const fields = config.attachPassRoots.get(sourceRoot); + if (fields !== undefined) { + config.attachPassRoots.delete(sourceRoot); + visitNode(offsetIndex, fields, visitor, config); } - } else if (!isDetachMark(mark) && mark.fields !== undefined) { - visitNode(index, mark.fields, visitor, config); - } - if (!isDetachMark(mark)) { - index += mark.count; } + } else if (!isDetachMark(mark) && mark.fields !== undefined) { + visitNode(index, mark.fields, visitor, config); + } + if (!isDetachMark(mark)) { + index += mark.count; } } } diff --git a/packages/dds/tree/src/feature-libraries/default-schema/defaultFieldKinds.ts b/packages/dds/tree/src/feature-libraries/default-schema/defaultFieldKinds.ts index 84f8d5dd91c9..51c05b0bcca4 100644 --- a/packages/dds/tree/src/feature-libraries/default-schema/defaultFieldKinds.ts +++ b/packages/dds/tree/src/feature-libraries/default-schema/defaultFieldKinds.ts @@ -6,13 +6,13 @@ import { type ChangeAtomId, type DeltaDetachedNodeId, - type DeltaFieldChanges, type FieldKindIdentifier, forbiddenFieldKindIdentifier, Multiplicity, } from "../../core/index.js"; import { fail } from "../../util/index.js"; import { + type FieldChangeDelta, type FieldChangeHandler, type FieldEditor, type FieldKindConfiguration, @@ -43,7 +43,7 @@ export const noChangeHandler: FieldChangeHandler<0> = { }), codecsFactory: () => noChangeCodecFamily, editor: { buildChildChange: (index, change) => fail("Child changes not supported") }, - intoDelta: (change, deltaFromChild: ToDelta): DeltaFieldChanges => ({}), + intoDelta: (change, deltaFromChild: ToDelta): FieldChangeDelta => ({}), relevantRemovedRoots: (change): Iterable => [], isEmpty: (change: 0) => true, getNestedChanges: (change: 0) => [], diff --git a/packages/dds/tree/src/feature-libraries/deltaUtils.ts b/packages/dds/tree/src/feature-libraries/deltaUtils.ts index cd25f76070d8..090d45c4d372 100644 --- a/packages/dds/tree/src/feature-libraries/deltaUtils.ts +++ b/packages/dds/tree/src/feature-libraries/deltaUtils.ts @@ -39,5 +39,18 @@ export function mapRootChanges( trees: trees.map(func), })); } + if (root.global !== undefined) { + out.global = root.global.map(({ id, fields }) => ({ + id, + fields, + })); + } + if (root.rename !== undefined) { + out.rename = root.rename.map(({ count, oldId, newId }) => ({ + count, + oldId, + newId, + })); + } return out; } diff --git a/packages/dds/tree/src/feature-libraries/forest-summary/forestSummarizer.ts b/packages/dds/tree/src/feature-libraries/forest-summary/forestSummarizer.ts index 52e11d40b567..c58a50ee12c8 100644 --- a/packages/dds/tree/src/feature-libraries/forest-summary/forestSummarizer.ts +++ b/packages/dds/tree/src/feature-libraries/forest-summary/forestSummarizer.ts @@ -150,12 +150,7 @@ export class ForestSummarizer implements Summarizable { id: buildId, trees: nodeCursors, }); - fieldChanges.push([ - fieldKey, - { - local: [{ count: nodeCursors.length, attach: buildId }], - }, - ]); + fieldChanges.push([fieldKey, [{ count: nodeCursors.length, attach: buildId }]]); } assert(this.forest.isEmpty, 0x797 /* forest must be empty */); diff --git a/packages/dds/tree/src/feature-libraries/modular-schema/fieldChangeHandler.ts b/packages/dds/tree/src/feature-libraries/modular-schema/fieldChangeHandler.ts index 0a25a7d97eb3..55f4b5849218 100644 --- a/packages/dds/tree/src/feature-libraries/modular-schema/fieldChangeHandler.ts +++ b/packages/dds/tree/src/feature-libraries/modular-schema/fieldChangeHandler.ts @@ -6,7 +6,9 @@ import type { ICodecFamily, IJsonCodec } from "../../codec/index.js"; import type { ChangeEncodingContext, + DeltaDetachedNodeChanges, DeltaDetachedNodeId, + DeltaDetachedNodeRename, DeltaFieldChanges, DeltaFieldMap, EncodedRevisionTag, @@ -25,6 +27,24 @@ export type NestedChangesIndices = [ number | undefined /* outputIndex */, ][]; +/** + * The return value of calling {@link FieldChangeHandler.intoDelta}. + */ +export interface FieldChangeDelta { + /** + * {@inheritdoc DeltaFieldChanges} + */ + readonly local?: DeltaFieldChanges; + /** + * {@inheritdoc DeltaRoot.global} + */ + readonly global?: readonly DeltaDetachedNodeChanges[]; + /** + * {@inheritdoc DeltaRoot.rename} + */ + readonly rename?: readonly DeltaDetachedNodeRename[]; +} + /** * Functionality provided by a field kind which will be composed with other `FieldChangeHandler`s to * implement a unified ChangeFamily supporting documents with multiple field kinds. @@ -44,7 +64,7 @@ export interface FieldChangeHandler< >, ) => ICodecFamily; readonly editor: TEditor; - intoDelta(change: TChangeset, deltaFromChild: ToDelta): DeltaFieldChanges; + intoDelta(change: TChangeset, deltaFromChild: ToDelta): FieldChangeDelta; /** * Returns the set of removed roots that should be in memory for the given change to be applied. * A removed root is relevant if any of the following is true: diff --git a/packages/dds/tree/src/feature-libraries/modular-schema/genericFieldKind.ts b/packages/dds/tree/src/feature-libraries/modular-schema/genericFieldKind.ts index fbf5b1ad823b..254984d483f6 100644 --- a/packages/dds/tree/src/feature-libraries/modular-schema/genericFieldKind.ts +++ b/packages/dds/tree/src/feature-libraries/modular-schema/genericFieldKind.ts @@ -5,7 +5,6 @@ import { type DeltaDetachedNodeId, - type DeltaFieldChanges, type DeltaMark, type RevisionMetadataSource, Multiplicity, @@ -16,6 +15,7 @@ import { type IdAllocator, fail } from "../../util/index.js"; import { assert } from "@fluidframework/core-utils/internal"; import type { CrossFieldManager } from "./crossFieldQueries.js"; import type { + FieldChangeDelta, FieldChangeHandler, NestedChangesIndices, NodeChangeComposer, @@ -47,7 +47,7 @@ export const genericChangeHandler: FieldChangeHandler = { return newGenericChangeset([[index, change]]); }, }, - intoDelta: (change: GenericChangeset, deltaFromChild: ToDelta): DeltaFieldChanges => { + intoDelta: (change: GenericChangeset, deltaFromChild: ToDelta): FieldChangeDelta => { let nodeIndex = 0; const markList: DeltaMark[] = []; for (const [index, nodeChange] of change.entries()) { diff --git a/packages/dds/tree/src/feature-libraries/modular-schema/index.ts b/packages/dds/tree/src/feature-libraries/modular-schema/index.ts index d829136aed98..035db01d7505 100644 --- a/packages/dds/tree/src/feature-libraries/modular-schema/index.ts +++ b/packages/dds/tree/src/feature-libraries/modular-schema/index.ts @@ -29,6 +29,7 @@ export { FlexFieldKind, type FullSchemaPolicy } from "./fieldKind.js"; export { FieldKindWithEditor } from "./fieldKindWithEditor.js"; export { type FieldChangeHandler, + type FieldChangeDelta, type FieldChangeRebaser, type FieldEditor, type NodeChangeComposer, diff --git a/packages/dds/tree/src/feature-libraries/modular-schema/modularChangeFamily.ts b/packages/dds/tree/src/feature-libraries/modular-schema/modularChangeFamily.ts index 76d47e4ad5b5..aeeaaf428cb2 100644 --- a/packages/dds/tree/src/feature-libraries/modular-schema/modularChangeFamily.ts +++ b/packages/dds/tree/src/feature-libraries/modular-schema/modularChangeFamily.ts @@ -30,7 +30,6 @@ import { type RevisionTag, type TaggedChange, type UpPath, - isEmptyFieldChanges, makeDetachedNodeId, mapCursorField, replaceAtomRevisions, @@ -40,6 +39,8 @@ import { areEqualChangeAtomIdOpts, tagChange, makeAnonChange, + type DeltaDetachedNodeChanges, + type DeltaDetachedNodeRename, } from "../../core/index.js"; import { type IdAllocationState, @@ -1997,6 +1998,8 @@ export function updateRefreshers( } /** + * Converts a change into the delta format. + * * @param change - The change to convert into a delta. * @param fieldKinds - The field kinds to delegate to. */ @@ -2006,13 +2009,27 @@ export function intoDelta( ): DeltaRoot { const change = taggedChange.change; const rootDelta: Mutable = {}; + const global: DeltaDetachedNodeChanges[] = []; + const rename: DeltaDetachedNodeRename[] = []; if (!hasConflicts(change)) { // If there are no constraint violations, then tree changes apply. - const fieldDeltas = intoDeltaImpl(change.fieldChanges, change.nodeChanges, fieldKinds); + const fieldDeltas = intoDeltaImpl( + change.fieldChanges, + change.nodeChanges, + fieldKinds, + global, + rename, + ); if (fieldDeltas.size > 0) { rootDelta.fields = fieldDeltas; } + if (global.length > 0) { + rootDelta.global = global; + } + if (rename.length > 0) { + rootDelta.rename = rename; + } } // Constraint violations should not prevent nodes from being built @@ -2060,19 +2077,28 @@ function intoDeltaImpl( change: FieldChangeMap, nodeChanges: ChangeAtomIdBTree, fieldKinds: ReadonlyMap, + global: DeltaDetachedNodeChanges[], + rename: DeltaDetachedNodeRename[], ): Map { const delta: Map = new Map(); + for (const [field, fieldChange] of change) { - const deltaField = getChangeHandler(fieldKinds, fieldChange.fieldKind).intoDelta( + const { + local: fieldChanges, + global: fieldGlobal, + rename: fieldRename, + } = getChangeHandler(fieldKinds, fieldChange.fieldKind).intoDelta( fieldChange.change, (childChange): DeltaFieldMap => { const nodeChange = nodeChangeFromId(nodeChanges, childChange); - return deltaFromNodeChange(nodeChange, nodeChanges, fieldKinds); + return deltaFromNodeChange(nodeChange, nodeChanges, fieldKinds, global, rename); }, ); - if (!isEmptyFieldChanges(deltaField)) { - delta.set(field, deltaField); + if (fieldChanges !== undefined && fieldChanges.length > 0) { + delta.set(field, fieldChanges); } + fieldGlobal?.forEach((c) => global.push(c)); + fieldRename?.forEach((r) => rename.push(r)); } return delta; } @@ -2081,9 +2107,11 @@ function deltaFromNodeChange( change: NodeChangeset, nodeChanges: ChangeAtomIdBTree, fieldKinds: ReadonlyMap, + global: DeltaDetachedNodeChanges[], + rename: DeltaDetachedNodeRename[], ): DeltaFieldMap { if (change.fieldChanges !== undefined) { - return intoDeltaImpl(change.fieldChanges, nodeChanges, fieldKinds); + return intoDeltaImpl(change.fieldChanges, nodeChanges, fieldKinds, global, rename); } // TODO: update the API to allow undefined to be returned here return new Map(); diff --git a/packages/dds/tree/src/feature-libraries/optional-field/optionalField.ts b/packages/dds/tree/src/feature-libraries/optional-field/optionalField.ts index 408cbc6320c9..4d14c23ee9d4 100644 --- a/packages/dds/tree/src/feature-libraries/optional-field/optionalField.ts +++ b/packages/dds/tree/src/feature-libraries/optional-field/optionalField.ts @@ -11,7 +11,6 @@ import { type ChangesetLocalId, type DeltaDetachedNodeChanges, type DeltaDetachedNodeId, - type DeltaFieldChanges, type DeltaMark, type RevisionTag, areEqualChangeAtomIds, @@ -40,6 +39,7 @@ import { type RelevantRemovedRootsFromChild, type ToDelta, type NestedChangesIndices, + type FieldChangeDelta, } from "../modular-schema/index.js"; import type { @@ -657,8 +657,8 @@ export const optionalFieldEditor: OptionalFieldEditor = { export function optionalFieldIntoDelta( change: OptionalChangeset, deltaFromChild: ToDelta, -): DeltaFieldChanges { - const delta: Mutable = {}; +): FieldChangeDelta { + const delta: Mutable = {}; let markIsANoop = true; const mark: Mutable = { count: 1 }; diff --git a/packages/dds/tree/src/feature-libraries/sequence-field/sequenceFieldToDelta.ts b/packages/dds/tree/src/feature-libraries/sequence-field/sequenceFieldToDelta.ts index 4198ed092080..0d6ebaad39c3 100644 --- a/packages/dds/tree/src/feature-libraries/sequence-field/sequenceFieldToDelta.ts +++ b/packages/dds/tree/src/feature-libraries/sequence-field/sequenceFieldToDelta.ts @@ -8,7 +8,6 @@ import { assert, unreachableCase } from "@fluidframework/core-utils/internal"; import { type DeltaDetachedNodeChanges, type DeltaDetachedNodeRename, - type DeltaFieldChanges, type DeltaMark, areEqualChangeAtomIds, } from "../../core/index.js"; @@ -25,12 +24,12 @@ import { getInputCellId, isAttachAndDetachEffect, } from "./utils.js"; -import type { ToDelta } from "../modular-schema/index.js"; +import type { FieldChangeDelta, ToDelta } from "../modular-schema/index.js"; export function sequenceFieldToDelta( change: MarkList, deltaFromChild: ToDelta, -): DeltaFieldChanges { +): FieldChangeDelta { const local: DeltaMark[] = []; const global: DeltaDetachedNodeChanges[] = []; const rename: DeltaDetachedNodeRename[] = []; @@ -187,7 +186,7 @@ export function sequenceFieldToDelta( } local.pop(); } - const delta: Mutable = {}; + const delta: Mutable = {}; if (local.length > 0) { delta.local = local; } diff --git a/packages/dds/tree/src/test/changesetWrapper.ts b/packages/dds/tree/src/test/changesetWrapper.ts index 0d46959f52b9..20969493f3f6 100644 --- a/packages/dds/tree/src/test/changesetWrapper.ts +++ b/packages/dds/tree/src/test/changesetWrapper.ts @@ -7,7 +7,6 @@ import { strict } from "node:assert"; import { assert } from "@fluidframework/core-utils/internal"; import { type ChangeAtomIdMap, - type DeltaFieldChanges, type RevisionTag, type TaggedChange, makeAnonChange, @@ -31,6 +30,8 @@ import { tryGetFromNestedMap, } from "../util/index.js"; import { TestChange } from "./testChange.js"; +// eslint-disable-next-line import/no-internal-modules +import type { FieldChangeDelta } from "../feature-libraries/modular-schema/fieldChangeHandler.js"; export interface ChangesetWrapper { fieldChange: T; @@ -205,8 +206,8 @@ function prune( function toDelta( change: ChangesetWrapper, - fieldToDelta: (change: T, deltaFromChild: ToDelta) => DeltaFieldChanges, -): DeltaFieldChanges { + fieldToDelta: (change: T, deltaFromChild: ToDelta) => FieldChangeDelta, +): FieldChangeDelta { const deltaFromChild = (id: NodeId) => { const node = tryGetFromNestedMap(change.nodes, id.revision, id.localId); assert(node !== undefined, "Unknown node ID"); diff --git a/packages/dds/tree/src/test/feature-libraries/deltaUtils.spec.ts b/packages/dds/tree/src/test/feature-libraries/deltaUtils.spec.ts index 85e689d4e136..e62854c61c55 100644 --- a/packages/dds/tree/src/test/feature-libraries/deltaUtils.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/deltaUtils.spec.ts @@ -34,15 +34,13 @@ describe("DeltaUtils", () => { const nestedCursorInsert = new Map([ [ fooField, - { - local: [ - { count: 42 }, - { - count: 1, - attach: detachId, - }, - ], - }, + [ + { count: 42 }, + { + count: 1, + attach: detachId, + }, + ], ], ]); const input: DeltaRoot = { @@ -50,32 +48,28 @@ describe("DeltaUtils", () => { fields: new Map([ [ fooField, - { - local: [ - { - count: 1, - fields: nestedCursorInsert, - }, - ], - global: [{ id: detachId, fields: nestedCursorInsert }], - }, + [ + { + count: 1, + fields: nestedCursorInsert, + }, + ], ], ]), + global: [{ id: detachId, fields: nestedCursorInsert }], }; deepFreeze(input); const actual = mapRootChanges(input, mapTreeFromCursor); const nestedMapTreeInsert = new Map([ [ fooField, - { - local: [ - { count: 42 }, - { - count: 1, - attach: detachId, - }, - ], - }, + [ + { count: 42 }, + { + count: 1, + attach: detachId, + }, + ], ], ]); const expected: DeltaRoot = { @@ -83,17 +77,15 @@ describe("DeltaUtils", () => { fields: new Map([ [ fooField, - { - local: [ - { - count: 1, - fields: nestedMapTreeInsert, - }, - ], - global: [{ id: detachId, fields: nestedMapTreeInsert }], - }, + [ + { + count: 1, + fields: nestedMapTreeInsert, + }, + ], ], ]), + global: [{ id: detachId, fields: nestedMapTreeInsert }], }; deepFreeze(expected); assert.deepEqual(actual, expected); diff --git a/packages/dds/tree/src/test/feature-libraries/modular-schema/basicRebasers.ts b/packages/dds/tree/src/test/feature-libraries/modular-schema/basicRebasers.ts index 5fed15bc38c6..1bc78ffc252f 100644 --- a/packages/dds/tree/src/test/feature-libraries/modular-schema/basicRebasers.ts +++ b/packages/dds/tree/src/test/feature-libraries/modular-schema/basicRebasers.ts @@ -6,12 +6,9 @@ import { type TUnsafe, Type } from "@sinclair/typebox"; import { makeCodecFamily } from "../../../codec/index.js"; +import { makeDetachedNodeId, Multiplicity } from "../../../core/index.js"; import { - type DeltaFieldChanges, - makeDetachedNodeId, - Multiplicity, -} from "../../../core/index.js"; -import { + type FieldChangeDelta, type FieldChangeEncodingContext, type FieldChangeHandler, type FieldChangeRebaser, @@ -19,7 +16,7 @@ import { referenceFreeFieldChangeRebaser, // eslint-disable-next-line import/no-internal-modules } from "../../../feature-libraries/modular-schema/index.js"; -import { type Mutable, fail } from "../../../util/index.js"; +import { fail, type Mutable } from "../../../util/index.js"; import { makeValueCodec } from "../../codec/index.js"; /** @@ -85,8 +82,8 @@ export const valueHandler = { ]), editor: { buildChildChange: (index, change) => fail("Child changes not supported") }, - intoDelta: (change): DeltaFieldChanges => { - const delta: Mutable = {}; + intoDelta: (change): FieldChangeDelta => { + const delta: Mutable = {}; if (change !== 0) { // We use the new and old numbers as the node ids. // These would have no real meaning to a delta consumer, but these delta are only used for testing. diff --git a/packages/dds/tree/src/test/feature-libraries/modular-schema/genericFieldKind.spec.ts b/packages/dds/tree/src/test/feature-libraries/modular-schema/genericFieldKind.spec.ts index 30077fdfc620..4aaf761db50a 100644 --- a/packages/dds/tree/src/test/feature-libraries/modular-schema/genericFieldKind.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/modular-schema/genericFieldKind.spec.ts @@ -7,7 +7,6 @@ import { strict as assert } from "node:assert"; import type { SessionId } from "@fluidframework/id-compressor"; import type { GenericChangeset, CrossFieldManager } from "../../../feature-libraries/index.js"; -import type { DeltaFieldChanges } from "../../../core/index.js"; import { fakeIdAllocator, brand, idAllocatorFromMaxId } from "../../../util/index.js"; import { type EncodingTestData, @@ -18,6 +17,7 @@ import { testRevisionTagCodec, } from "../../utils.js"; import { + type FieldChangeDelta, type FieldChangeEncodingContext, type NodeId, type RebaseRevisionMetadata, @@ -185,7 +185,7 @@ describe("GenericField", () => { [2, nodeChange2], ]); - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { local: [ { count: 1, fields: TestNodeId.deltaFromChild(nodeChange1) }, { count: 1 }, diff --git a/packages/dds/tree/src/test/feature-libraries/modular-schema/modularChangeFamily.spec.ts b/packages/dds/tree/src/test/feature-libraries/modular-schema/modularChangeFamily.spec.ts index a34ae17c3dff..e56a583466ab 100644 --- a/packages/dds/tree/src/test/feature-libraries/modular-schema/modularChangeFamily.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/modular-schema/modularChangeFamily.spec.ts @@ -92,6 +92,7 @@ import { } from "../../../feature-libraries/modular-schema/modularChangeFamily.js"; import type { EncodedNodeChangeset, + FieldChangeDelta, FieldChangeEncodingContext, // eslint-disable-next-line import/no-internal-modules } from "../../../feature-libraries/modular-schema/index.js"; @@ -139,7 +140,7 @@ const singleNodeHandler: FieldChangeHandler = { rebaser: singleNodeRebaser, codecsFactory: (revisionTagCodec) => makeCodecFamily([[1, singleNodeCodec]]), editor: singleNodeEditor, - intoDelta: (change, deltaFromChild): DeltaFieldChanges => ({ + intoDelta: (change, deltaFromChild): FieldChangeDelta => ({ local: [{ count: 1, fields: change !== undefined ? deltaFromChild(change) : undefined }], }), relevantRemovedRoots: (change, relevantRemovedRootsFromChild) => @@ -1047,26 +1048,19 @@ describe("ModularChangeFamily", () => { describe("intoDelta", () => { it("fieldChanges", () => { - const nodeDelta: DeltaFieldChanges = { - local: [ - { - count: 1, - fields: new Map([ - [ - fieldA, - { - local: [{ count: 1, detach: { minor: 0 }, attach: { minor: 1 } }], - }, - ], - ]), - }, - ], - }; + const nodeDelta: DeltaFieldChanges = [ + { + count: 1, + fields: new Map([ + [fieldA, [{ count: 1, detach: { minor: 0 }, attach: { minor: 1 } }]], + ]), + }, + ]; const expectedDelta: DeltaRoot = { fields: new Map([ [fieldA, nodeDelta], - [fieldB, { local: [{ count: 1, detach: { minor: 1 }, attach: { minor: 2 } }] }], + [fieldB, [{ count: 1, detach: { minor: 1 }, attach: { minor: 2 } }]], ]), }; diff --git a/packages/dds/tree/src/test/feature-libraries/modularChangeFamilyIntegration.spec.ts b/packages/dds/tree/src/test/feature-libraries/modularChangeFamilyIntegration.spec.ts index 5675014a60a6..6387a3a32d2c 100644 --- a/packages/dds/tree/src/test/feature-libraries/modularChangeFamilyIntegration.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/modularChangeFamilyIntegration.spec.ts @@ -566,30 +566,23 @@ describe("ModularChangeFamily integration", () => { const composedDelta = normalizeDelta(intoDelta(makeAnonChange(composed), fieldKinds)); const nodeAChanges: DeltaFieldMap = new Map([ - [fieldB, { local: [{ count: 1, attach: { minor: 1, major: tagForCompare } }] }], + [fieldB, [{ count: 1, attach: { minor: 1, major: tagForCompare } }]], ]); const nodeBChanges: DeltaFieldMap = new Map([ - [ - fieldC, - { - local: [{ count: 1, attach: { minor: 2, major: tagForCompare } }], - }, - ], + [fieldC, [{ count: 1, attach: { minor: 2, major: tagForCompare } }]], ]); const nodeCChanges: DeltaFieldMap = new Map([ - [fieldC, { local: [{ count: 1, detach: { minor: 3, major: tagForCompare } }] }], + [fieldC, [{ count: 1, detach: { minor: 3, major: tagForCompare } }]], ]); - const fieldAChanges: DeltaFieldChanges = { - local: [ - { count: 1, detach: { minor: 0, major: tagForCompare }, fields: nodeAChanges }, - { count: 1, attach: { minor: 0, major: tagForCompare } }, - { count: 1, detach: { minor: 1, major: tagForCompare }, fields: nodeBChanges }, - { count: 1, detach: { minor: 2, major: tagForCompare }, fields: nodeCChanges }, - ], - }; + const fieldAChanges: DeltaFieldChanges = [ + { count: 1, detach: { minor: 0, major: tagForCompare }, fields: nodeAChanges }, + { count: 1, attach: { minor: 0, major: tagForCompare } }, + { count: 1, detach: { minor: 1, major: tagForCompare }, fields: nodeBChanges }, + { count: 1, detach: { minor: 2, major: tagForCompare }, fields: nodeCChanges }, + ]; const expectedDelta: DeltaRoot = normalizeDelta({ fields: new Map([[fieldA, fieldAChanges]]), @@ -630,29 +623,17 @@ describe("ModularChangeFamily integration", () => { fields: new Map([ [ fieldA, - { - local: [ - { - count: 1, - detach: { minor: 0, major: tagForCompare }, - fields: new Map([ - [ - fieldC, - { - local: [{ count: 1, attach: { minor: 2, major: tagForCompare } }], - }, - ], - ]), - }, - ], - }, - ], - [ - fieldB, - { - local: [{ count: 1, attach: { minor: 0, major: tagForCompare } }], - }, + [ + { + count: 1, + detach: { minor: 0, major: tagForCompare }, + fields: new Map([ + [fieldC, [{ count: 1, attach: { minor: 2, major: tagForCompare } }]], + ]), + }, + ], ], + [fieldB, [{ count: 1, attach: { minor: 0, major: tagForCompare } }]], ]), }; @@ -707,22 +688,13 @@ describe("ModularChangeFamily integration", () => { fields: new Map([ [ fieldB, - { - local: [ - { count: 1 }, - { - count: 1, - fields: new Map([ - [ - fieldC, - { - local: [{ count: 1, attach: { major: tag2, minor: 2 } }], - }, - ], - ]), - }, - ], - }, + [ + { count: 1 }, + { + count: 1, + fields: new Map([[fieldC, [{ count: 1, attach: { major: tag2, minor: 2 } }]]]), + }, + ], ], ]), }; @@ -968,8 +940,8 @@ describe("ModularChangeFamily integration", () => { }; const expected: DeltaRoot = { fields: new Map([ - [brand("foo"), { local: [moveOut1, moveIn1] }], - [brand("bar"), { local: [moveOut2, moveIn2] }], + [brand("foo"), [moveOut1, moveIn1]], + [brand("bar"), [moveOut2, moveIn2]], ]), }; const actual = intoDelta(makeAnonChange(change), family.fieldKinds); @@ -996,6 +968,19 @@ function normalizeDelta( trees, })); } + if (delta.global !== undefined && delta.global.length > 0) { + normalized.global = delta.global.map(({ id, fields }) => ({ + id: normalizeDeltaDetachedNodeId(id, genId, map), + fields: normalizeDeltaFieldMap(fields, genId, map), + })); + } + if (delta.rename !== undefined && delta.rename.length > 0) { + normalized.rename = delta.rename.map(({ oldId, count, newId }) => ({ + oldId: normalizeDeltaDetachedNodeId(oldId, genId, map), + count, + newId: normalizeDeltaDetachedNodeId(newId, genId, map), + })); + } return normalized; } @@ -1017,25 +1002,11 @@ function normalizeDeltaFieldChanges( genId: IdAllocator, idMap: Map, ): DeltaFieldChanges { - const normalized: Mutable = {}; - if (delta.local !== undefined && delta.local.length > 0) { - normalized.local = delta.local.map((mark) => normalizeDeltaMark(mark, genId, idMap)); - } - if (delta.global !== undefined && delta.global.length > 0) { - normalized.global = delta.global.map(({ id, fields }) => ({ - id: normalizeDeltaDetachedNodeId(id, genId, idMap), - fields: normalizeDeltaFieldMap(fields, genId, idMap), - })); - } - if (delta.rename !== undefined && delta.rename.length > 0) { - normalized.rename = delta.rename.map(({ oldId, count, newId }) => ({ - oldId: normalizeDeltaDetachedNodeId(oldId, genId, idMap), - count, - newId: normalizeDeltaDetachedNodeId(newId, genId, idMap), - })); + if (delta.length > 0) { + return delta.map((mark) => normalizeDeltaMark(mark, genId, idMap)); } - return normalized; + return delta; } function normalizeDeltaMark( diff --git a/packages/dds/tree/src/test/feature-libraries/optional-field/optionalChangeRebaser.test.ts b/packages/dds/tree/src/test/feature-libraries/optional-field/optionalChangeRebaser.test.ts index 13189bf7e58b..c13c135b5529 100644 --- a/packages/dds/tree/src/test/feature-libraries/optional-field/optionalChangeRebaser.test.ts +++ b/packages/dds/tree/src/test/feature-libraries/optional-field/optionalChangeRebaser.test.ts @@ -11,7 +11,6 @@ import { type ChangeAtomId, type ChangeAtomIdMap, type ChangesetLocalId, - type DeltaFieldChanges, type RevisionMetadataSource, type RevisionTag, type TaggedChange, @@ -21,6 +20,7 @@ import { tagRollbackInverse, } from "../../../core/index.js"; import { + type FieldChangeDelta, type NodeChangeComposer, type NodeChangeRebaser, type NodeId, @@ -108,7 +108,7 @@ const failCrossFieldManager: CrossFieldManager = { function toDelta( change: OptionalChangeset, deltaFromChild: ToDelta = TestNodeId.deltaFromChild, -): DeltaFieldChanges { +): FieldChangeDelta { return optionalFieldIntoDelta(change, deltaFromChild); } @@ -266,7 +266,8 @@ function composeWrapped( } function isWrappedChangeEmpty(change: WrappedChangeset): boolean { - return !isDeltaVisible(toDeltaWrapped(makeAnonChange(change))); + const delta = toDeltaWrapped(makeAnonChange(change)).local; + return delta === undefined || !isDeltaVisible(delta); } function assertWrappedChangesetsEquivalent( @@ -526,7 +527,7 @@ function runSingleEditRebaseAxiomSuite(initialState: OptionalFieldTestState) { const inv = invertWrapped(change, tag1, true); const actual = composeWrapped(change, tagRollbackInverse(inv, tag1, change.revision)); const delta = toDeltaWrapped(makeAnonChange(actual)); - assert.equal(isDeltaVisible(delta), false); + assert.equal(isDeltaVisible(delta.local), false); }); } }); @@ -541,7 +542,7 @@ function runSingleEditRebaseAxiomSuite(initialState: OptionalFieldTestState) { ); const actual = composeWrapped(inv, change); const delta = toDeltaWrapped(makeAnonChange(actual)); - assert.equal(isDeltaVisible(delta), false); + assert.equal(isDeltaVisible(delta.local), false); }); } }); diff --git a/packages/dds/tree/src/test/feature-libraries/optional-field/optionalField.spec.ts b/packages/dds/tree/src/test/feature-libraries/optional-field/optionalField.spec.ts index 7c573f81fdc3..600be056cdd3 100644 --- a/packages/dds/tree/src/test/feature-libraries/optional-field/optionalField.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/optional-field/optionalField.spec.ts @@ -7,7 +7,6 @@ import { strict as assert, fail } from "node:assert"; import { type ChangeAtomId, - type DeltaFieldChanges, type TaggedChange, makeAnonChange, makeDetachedNodeId, @@ -43,8 +42,11 @@ import { testRebaserAxioms } from "./optionalChangeRebaser.test.js"; import { testCodecs } from "./optionalFieldChangeCodecs.test.js"; import { deepFreeze } from "@fluidframework/test-runtime-utils/internal"; import { testReplaceRevisions } from "./replaceRevisions.test.js"; -// eslint-disable-next-line import/no-internal-modules -import type { NestedChangesIndices } from "../../../feature-libraries/modular-schema/fieldChangeHandler.js"; +import type { + FieldChangeDelta, + NestedChangesIndices, + // eslint-disable-next-line import/no-internal-modules +} from "../../../feature-libraries/modular-schema/fieldChangeHandler.js"; /** * A change to a child encoding as a simple placeholder string. @@ -671,7 +673,7 @@ describe("optionalField", () => { describe("IntoDelta", () => { it("can be converted to a delta when field was empty", () => { const outerNodeId = makeDetachedNodeId(tag, 41); - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { global: [ { id: outerNodeId, @@ -686,7 +688,7 @@ describe("optionalField", () => { }); it("can be converted to a delta when restoring content", () => { - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { local: [ { count: 1, @@ -701,7 +703,7 @@ describe("optionalField", () => { }); it("can be converted to a delta with only child changes", () => { - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { local: [ { count: 1, diff --git a/packages/dds/tree/src/test/feature-libraries/sequence-field/sequenceFieldToDelta.test.ts b/packages/dds/tree/src/test/feature-libraries/sequence-field/sequenceFieldToDelta.test.ts index ca66ea2b5255..8377bf1b4cf3 100644 --- a/packages/dds/tree/src/test/feature-libraries/sequence-field/sequenceFieldToDelta.test.ts +++ b/packages/dds/tree/src/test/feature-libraries/sequence-field/sequenceFieldToDelta.test.ts @@ -8,12 +8,10 @@ import { strict as assert, fail } from "node:assert"; import { type ChangesetLocalId, type DeltaDetachedNodeId, - type DeltaFieldChanges, type DeltaFieldMap, type DeltaMark, type FieldKey, type RevisionTag, - emptyFieldChanges, tagChange, } from "../../../core/index.js"; import { type NodeId, SequenceField as SF } from "../../../feature-libraries/index.js"; @@ -24,6 +22,8 @@ import { TestNodeId } from "../../testNodeId.js"; import { ChangeMaker as Change, MarkMaker as Mark } from "./testEdits.js"; import { inlineRevision, toDelta } from "./utils.js"; import { deepFreeze } from "@fluidframework/test-runtime-utils/internal"; +// eslint-disable-next-line import/no-internal-modules +import type { FieldChangeDelta } from "../../../feature-libraries/modular-schema/fieldChangeHandler.js"; const moveId = brand(4242); const moveId2 = brand(4343); @@ -34,7 +34,7 @@ const fooField = brand("foo"); const cellId = { revision: tag1, localId: brand(0) }; const deltaNodeId: DeltaDetachedNodeId = { major: cellId.revision, minor: cellId.localId }; -function toDeltaShallow(change: SF.Changeset): DeltaFieldChanges { +function toDeltaShallow(change: SF.Changeset): FieldChangeDelta { deepFreeze(change); return SF.sequenceFieldToDelta(change, () => fail("Unexpected call to child ToDelta")); } @@ -44,6 +44,8 @@ const childChange1 = TestNodeId.create(nodeId1, TestChange.mint([0], 1)); const childChange1Delta = TestChange.toDelta(tagChange(childChange1.testChange, tag)); const detachId = { major: tag, minor: 42 }; +export const emptyFieldChanges: FieldChangeDelta = {}; + export function testToDelta() { describe("toDelta", () => { it("empty mark list", () => { @@ -54,14 +56,14 @@ export function testToDelta() { it("child change", () => { const actual = toDelta(inlineRevision(Change.modify(0, childChange1), tag)); const markList: DeltaMark[] = [{ count: 1, fields: childChange1Delta }]; - const expected: DeltaFieldChanges = { local: markList }; + const expected: FieldChangeDelta = { local: markList }; assert.deepEqual(actual, expected); }); it("child change under removed node", () => { const modify = [Mark.modify(childChange1, { revision: tag, localId: brand(42) })]; const actual = toDelta(inlineRevision(modify, tag)); - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { global: [{ id: detachId, fields: childChange1Delta }], }; assertFieldChangesEqual(actual, expected); @@ -86,7 +88,7 @@ export function testToDelta() { it("revive => restore", () => { const changeset = Change.revive(0, 1, { revision: tag, localId: brand(0) }, tag2); const actual = toDelta(changeset); - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { local: [ { count: 1, @@ -102,13 +104,13 @@ export function testToDelta() { const changeset = [ Mark.revive(1, { revision: tag, localId: brand(0) }, { changes: nodeId }), ]; - const fieldChanges = new Map([[fooField, {}]]); + const fieldChanges = new Map([[fooField, []]]); const deltaFromChild = (child: NodeId): DeltaFieldMap => { assert.deepEqual(child, nodeId); return fieldChanges; }; const actual = SF.sequenceFieldToDelta(changeset, deltaFromChild); - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { local: [ { count: 1, @@ -127,7 +129,7 @@ export function testToDelta() { it("remove", () => { const changeset = [Mark.remove(10, brand(42))]; - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { local: [ { count: 10, @@ -142,7 +144,7 @@ export function testToDelta() { it("remove with override", () => { const detachIdOverride: SF.CellId = { revision: tag2, localId: brand(1) }; const changeset = [Mark.remove(10, brand(42), { idOverride: detachIdOverride })]; - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { local: [ { count: 10, @@ -170,7 +172,7 @@ export function testToDelta() { count: 10, }; const markList: DeltaMark[] = [{ count: 42 }, moveOut, { count: 8 }, moveIn]; - const expected: DeltaFieldChanges = { local: markList }; + const expected: FieldChangeDelta = { local: markList }; const actual = toDelta(changeset); assert.deepStrictEqual(actual, expected); }); @@ -209,7 +211,7 @@ export function testToDelta() { count: 3, }; const markList: DeltaMark[] = [moveOut1, moveIn1, moveOut2, moveIn2, moveOut3, moveIn3]; - const expected: DeltaFieldChanges = { local: markList }; + const expected: FieldChangeDelta = { local: markList }; const actual = toDelta(changeset); assert.deepStrictEqual(actual, expected); }); @@ -237,7 +239,7 @@ export function testToDelta() { { count: 1 }, { count: 1, fields: childChange1Delta }, ]; - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { local: markList, }; const actual = toDelta(inlineRevision(changeset, tag)); @@ -247,7 +249,7 @@ export function testToDelta() { it("insert and modify => insert", () => { const changeset = [Mark.insert(1, brand(0), { changes: childChange1 })]; const buildId = { major: tag, minor: 0 }; - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { global: [{ id: buildId, fields: childChange1Delta }], local: [{ count: 1, attach: buildId }], }; @@ -257,7 +259,7 @@ export function testToDelta() { it("modify and remove => remove", () => { const changeset = [Mark.remove(1, brand(42), { changes: childChange1 })]; - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { local: [{ count: 1, detach: detachId, fields: childChange1Delta }], }; const actual = toDelta(inlineRevision(changeset, tag)); @@ -266,7 +268,7 @@ export function testToDelta() { it("modify and move-out => move-out", () => { const changeset = [Mark.moveOut(1, moveId, { changes: childChange1 })]; - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { local: [ { count: 1, detach: { major: tag, minor: moveId }, fields: childChange1Delta }, ], @@ -280,10 +282,10 @@ export function testToDelta() { const changeset = [Mark.insert(1, brand(0), { changes: nodeId })]; const nestedMoveDelta = new Map([ - [fooField, { local: [{ attach: { minor: moveId }, count: 42 }] }], + [fooField, [{ attach: { minor: moveId }, count: 42 }]], ]); const buildId = { minor: 0 }; - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { global: [{ id: buildId, fields: nestedMoveDelta }], local: [{ count: 1, attach: buildId }], }; @@ -301,7 +303,7 @@ export function testToDelta() { const changeset = [Mark.remove(2, brand(2), { cellId: { localId: brand(0) } })]; const delta = toDelta(changeset); const buildId = { minor: 0 }; - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { rename: [{ count: 2, oldId: buildId, newId: { minor: 2 } }], }; assertFieldChangesEqual(delta, expected); @@ -317,7 +319,7 @@ export function testToDelta() { const delta = toDelta(changeset); const buildId = { minor: 0 }; const id = { minor: 2 }; - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { rename: [{ oldId: buildId, newId: id, count: 2 }], local: [{ count: 1 }, { count: 2, attach: id }], }; @@ -334,7 +336,7 @@ export function testToDelta() { const delta = toDelta(changeset); const id = { minor: 0 }; - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { local: [{ count: 2, detach: id }], rename: [{ count: 2, oldId: id, newId: { minor: 4 } }], }; @@ -352,7 +354,7 @@ export function testToDelta() { const buildId = { minor: 0 }; const id1 = { minor: 2 }; const id2 = { minor: 6 }; - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { rename: [ { count: 2, oldId: buildId, newId: id1 }, { count: 2, oldId: id1, newId: id2 }, @@ -373,7 +375,7 @@ export function testToDelta() { const delta = toDelta(changeset); const id = { minor: 0 }; - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { local: [ { count: 2, detach: id }, { count: 1 }, @@ -393,7 +395,7 @@ export function testToDelta() { ]; const actual = toDelta(move); - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { rename: [{ count: 1, oldId: deltaNodeId, newId: { minor: 0 } }], local: [{ count: 1, attach: { minor: 0 } }], }; @@ -404,7 +406,7 @@ export function testToDelta() { it("remove", () => { const deletion = [Mark.remove(1, brand(0), { cellId })]; const actual = toDelta(inlineRevision(deletion, tag)); - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { rename: [ { count: 1, @@ -419,7 +421,7 @@ export function testToDelta() { it("modify and remove", () => { const deletion = [Mark.remove(1, brand(0), { cellId, changes: childChange1 })]; const actual = toDelta(inlineRevision(deletion, tag)); - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { rename: [ { count: 1, @@ -443,7 +445,7 @@ export function testToDelta() { Mark.pin(1, brand(1), { changes: childChange1 }), ]; const actual = toDelta(inlineRevision(changeset, tag)); - const expected: DeltaFieldChanges = { + const expected: FieldChangeDelta = { local: [{ count: 1 }, { count: 1, fields: childChange1Delta }], }; assertFieldChangesEqual(actual, expected); diff --git a/packages/dds/tree/src/test/feature-libraries/sequence-field/utils.ts b/packages/dds/tree/src/test/feature-libraries/sequence-field/utils.ts index 7a312e06f858..3c7566c25a48 100644 --- a/packages/dds/tree/src/test/feature-libraries/sequence-field/utils.ts +++ b/packages/dds/tree/src/test/feature-libraries/sequence-field/utils.ts @@ -12,7 +12,6 @@ import { type ChangeAtomId, type ChangeAtomIdMap, type ChangesetLocalId, - type DeltaFieldChanges, type RevisionInfo, type RevisionMetadataSource, type RevisionTag, @@ -29,6 +28,7 @@ import { type CrossFieldManager, type CrossFieldQuerySet, CrossFieldTarget, + type FieldChangeDelta, type NodeId, type RebaseRevisionMetadata, setInCrossFieldMap, @@ -492,7 +492,7 @@ export function checkDeltaEquality(actual: SF.Changeset, expected: SF.Changeset) assertFieldChangesEqual(toDelta(actual), toDelta(expected)); } -export function toDelta(change: SF.Changeset): DeltaFieldChanges { +export function toDelta(change: SF.Changeset): FieldChangeDelta { deepFreeze(change); return SF.sequenceFieldToDelta(change, TestNodeId.deltaFromChild); } diff --git a/packages/dds/tree/src/test/forestTestSuite.ts b/packages/dds/tree/src/test/forestTestSuite.ts index f36ca568f500..9cfbcfe39e22 100644 --- a/packages/dds/tree/src/test/forestTestSuite.ts +++ b/packages/dds/tree/src/test/forestTestSuite.ts @@ -180,16 +180,10 @@ export function testForest(config: ForestTestConfiguration): void { const forest = factory(new TreeStoredSchemaRepository(toStoredSchema(JsonArray))); assert(forest.isEmpty); - const insert: DeltaFieldChanges = { - local: [{ count: 1, attach: { minor: 1 } }], - }; - applyTestDelta( - new Map([[brand("different root"), insert]]), - forest, - undefined, - undefined, - [{ id: { minor: 1 }, trees: [singleJsonCursor([])] }], - ); + const insert: DeltaFieldChanges = [{ count: 1, attach: { minor: 1 } }]; + applyTestDelta(new Map([[brand("different root"), insert]]), forest, { + build: [{ id: { minor: 1 }, trees: [singleJsonCursor([])] }], + }); assert(!forest.isEmpty); }); @@ -411,13 +405,9 @@ export function testForest(config: ForestTestConfiguration): void { cursor.clear(); const mark: DeltaMark = { count: 1, detach: detachId }; - const delta: DeltaFieldMap = new Map([[rootFieldKey, { local: [mark] }]]); - applyTestDelta(delta, forest, undefined, undefined, undefined, [ - { id: detachId, count: 1 }, - ]); - applyTestDelta(delta, forest.anchors, undefined, undefined, undefined, [ - { id: detachId, count: 1 }, - ]); + const delta: DeltaFieldMap = new Map([[rootFieldKey, [mark]]]); + applyTestDelta(delta, forest, { destroy: [{ id: detachId, count: 1 }] }); + applyTestDelta(delta, forest.anchors, { destroy: [{ id: detachId, count: 1 }] }); assert.equal( forest.tryMoveCursorToNode(firstNodeAnchor, cursor), @@ -447,9 +437,9 @@ export function testForest(config: ForestTestConfiguration): void { { jsonValidator: typeboxValidator }, ); const delta: DeltaFieldMap = new Map([ - [rootFieldKey, { local: [mark] }], + [rootFieldKey, [mark]], ]); - applyTestDelta(delta, forest, detachedFieldIndex); + applyTestDelta(delta, forest, { detachedFieldIndex }); const detachedField: DetachedField = brand( detachedFieldIndex.toFieldKey(0 as ForestRootId), @@ -563,7 +553,7 @@ export function testForest(config: ForestTestConfiguration): void { const clone = forest.clone(schema, forest.anchors); const mark: DeltaMark = { count: 1, detach: detachId }; - const delta: DeltaFieldMap = new Map([[rootFieldKey, { local: [mark] }]]); + const delta: DeltaFieldMap = new Map([[rootFieldKey, [mark]]]); applyTestDelta(delta, clone); // Check the clone has the new value @@ -593,7 +583,7 @@ export function testForest(config: ForestTestConfiguration): void { moveToDetachedField(forest, cursor); const mark: DeltaMark = { count: 1, detach: detachId }; - const delta: DeltaFieldMap = new Map([[rootFieldKey, { local: [mark] }]]); + const delta: DeltaFieldMap = new Map([[rootFieldKey, [mark]]]); assert.throws(() => applyTestDelta(delta, forest)); }); @@ -614,7 +604,7 @@ export function testForest(config: ForestTestConfiguration): void { }); const mark: DeltaMark = { count: 1, detach: detachId }; - const delta: DeltaFieldMap = new Map([[rootFieldKey, { local: [mark] }]]); + const delta: DeltaFieldMap = new Map([[rootFieldKey, [mark]]]); assert.throws(() => applyTestDelta(delta, forest)); assert.deepEqual(log, ["beforeChange"]); }); @@ -635,7 +625,7 @@ export function testForest(config: ForestTestConfiguration): void { }); const mark: DeltaMark = { count: 1, detach: detachId }; - const delta: DeltaFieldMap = new Map([[rootFieldKey, { local: [mark] }]]); + const delta: DeltaFieldMap = new Map([[rootFieldKey, [mark]]]); applyTestDelta(delta, forest); assert.deepEqual(log, ["beforeChange"]); }); @@ -651,27 +641,22 @@ export function testForest(config: ForestTestConfiguration): void { const setField: DeltaMark = { count: 1, - fields: new Map([ - [ - xField, - { - local: [{ count: 1, detach: detachId, attach: buildId }], - }, - ], - ]), + fields: new Map([[xField, [{ count: 1, detach: detachId, attach: buildId }]]]), }; - const delta: DeltaFieldMap = new Map([[rootFieldKey, { local: [setField] }]]); - applyTestDelta(delta, forest, undefined, undefined, [ - { - id: buildId, - trees: [ - cursorForJsonableTreeNode({ - type: brand(booleanSchema.identifier), - value: true, - }), - ], - }, - ]); + const delta: DeltaFieldMap = new Map([[rootFieldKey, [setField]]]); + applyTestDelta(delta, forest, { + build: [ + { + id: buildId, + trees: [ + cursorForJsonableTreeNode({ + type: brand(booleanSchema.identifier), + value: true, + }), + ], + }, + ], + }); const reader = forest.allocateCursor(); moveToDetachedField(forest, reader); @@ -692,22 +677,22 @@ export function testForest(config: ForestTestConfiguration): void { const setField: DeltaMark = { count: 1, - fields: new Map([ - [xField, { local: [{ count: 1, detach: detachId, attach: buildId }] }], - ]), + fields: new Map([[xField, [{ count: 1, detach: detachId, attach: buildId }]]]), }; - const delta: DeltaFieldMap = new Map([[rootFieldKey, { local: [setField] }]]); - applyTestDelta(delta, forest, undefined, undefined, [ - { - id: buildId, - trees: [ - cursorForJsonableTreeNode({ - type: brand(booleanSchema.identifier), - value: true, - }), - ], - }, - ]); + const delta: DeltaFieldMap = new Map([[rootFieldKey, [setField]]]); + applyTestDelta(delta, forest, { + build: [ + { + id: buildId, + trees: [ + cursorForJsonableTreeNode({ + type: brand(booleanSchema.identifier), + value: true, + }), + ], + }, + ], + }); const reader = forest.allocateCursor(); moveToDetachedField(forest, reader); @@ -731,7 +716,7 @@ export function testForest(config: ForestTestConfiguration): void { count: 1, detach: detachId, }; - const delta: DeltaFieldMap = new Map([[rootFieldKey, { local: [mark] }]]); + const delta: DeltaFieldMap = new Map([[rootFieldKey, [mark]]]); applyTestDelta(delta, forest); // Inspect resulting tree: should just have `2`. @@ -762,7 +747,7 @@ export function testForest(config: ForestTestConfiguration): void { count: 1, detach: detachId, }; - const delta: DeltaFieldMap = new Map([[rootFieldKey, { local: [skip, mark] }]]); + const delta: DeltaFieldMap = new Map([[rootFieldKey, [skip, mark]]]); applyTestDelta(delta, forest); // Inspect resulting tree: should just have `1`. @@ -783,11 +768,11 @@ export function testForest(config: ForestTestConfiguration): void { ); const delta: DeltaFieldMap = new Map([ - [rootFieldKey, { local: [{ count: 1, attach: buildId }] }], - ]); - applyTestDelta(delta, forest, undefined, undefined, [ - { id: buildId, trees: [singleJsonCursor(3)] }, + [rootFieldKey, [{ count: 1, attach: buildId }]], ]); + applyTestDelta(delta, forest, { + build: [{ id: buildId, trees: [singleJsonCursor(3)] }], + }); const reader = forest.allocateCursor(); moveToDetachedField(forest, reader); @@ -812,24 +797,16 @@ export function testForest(config: ForestTestConfiguration): void { count: 1, attach: moveId, }; - const delta: DeltaFieldMap = new Map([ - [ - rootFieldKey, + const delta: DeltaFieldMap = new Map([[rootFieldKey, [moveIn]]]); + applyTestDelta(delta, forest, { + build: [{ id: buildId, trees: [singleJsonCursor({ x: 0 })] }], + global: [ { - global: [ - { - id: buildId, - fields: new Map([[xField, { local: [moveOut] }]]), - }, - ], - local: [moveIn], - relocate: [{ id: buildId, count: 1, destination: detachId }], + id: buildId, + fields: new Map([[xField, [moveOut]]]), }, ], - ]); - applyTestDelta(delta, forest, undefined, undefined, [ - { id: buildId, trees: [singleJsonCursor({ x: 0 })] }, - ]); + }); const reader = forest.allocateCursor(); moveToDetachedField(forest, reader); @@ -859,11 +836,11 @@ export function testForest(config: ForestTestConfiguration): void { const modify: DeltaMark = { count: 1, fields: new Map([ - [xField, { local: [moveOut] }], - [yField, { local: [{ count: 1 }, moveIn] }], + [xField, [moveOut]], + [yField, [{ count: 1 }, moveIn]], ]), }; - const delta: DeltaFieldMap = new Map([[rootFieldKey, { local: [modify] }]]); + const delta: DeltaFieldMap = new Map([[rootFieldKey, [modify]]]); applyTestDelta(delta, forest); const reader = forest.allocateCursor(); moveToDetachedField(forest, reader); @@ -886,46 +863,36 @@ export function testForest(config: ForestTestConfiguration): void { ); const delta: DeltaFieldMap = new Map([ - [ - rootFieldKey, + [rootFieldKey, [{ count: 1, attach: buildId }]], + ]); + applyTestDelta(delta, forest, { + build: [ { - global: [ - { - id: buildId, - fields: new Map([ - [ - brand("newField"), - { - local: [{ count: 1, attach: buildId2 }], - }, - ], - ]), - }, + id: buildId, + trees: [ + cursorForJsonableTreeNode({ + type: brand(numberSchema.identifier), + value: 3, + }), + ], + }, + { + id: buildId2, + trees: [ + cursorForJsonableTreeNode({ + type: brand(numberSchema.identifier), + value: 4, + }), ], - local: [{ count: 1, attach: buildId }], }, ], - ]); - applyTestDelta(delta, forest, undefined, undefined, [ - { - id: buildId, - trees: [ - cursorForJsonableTreeNode({ - type: brand(numberSchema.identifier), - value: 3, - }), - ], - }, - { - id: buildId2, - trees: [ - cursorForJsonableTreeNode({ - type: brand(numberSchema.identifier), - value: 4, - }), - ], - }, - ]); + global: [ + { + id: buildId, + fields: new Map([[brand("newField"), [{ count: 1, attach: buildId2 }]]]), + }, + ], + }); const reader = forest.allocateCursor(); moveToDetachedField(forest, reader); @@ -956,10 +923,10 @@ export function testForest(config: ForestTestConfiguration): void { const mark: DeltaMark = { count: 1, detach: detachId, - fields: new Map([[xField, { local: [{ count: 1, detach: moveId }] }]]), + fields: new Map([[xField, [{ count: 1, detach: moveId }]]]), }; const delta: DeltaFieldMap = new Map([ - [rootFieldKey, { local: [mark, { count: 1, attach: moveId }] }], + [rootFieldKey, [mark, { count: 1, attach: moveId }]], ]); applyTestDelta(delta, forest); @@ -986,36 +953,32 @@ export function testForest(config: ForestTestConfiguration): void { fields: new Map([ [ xField, - { - local: [ - { - count: 1, - detach: moveId, - fields: new Map([ + [ + { + count: 1, + detach: moveId, + fields: new Map([ + [ + fooField, [ - fooField, { - local: [ - { - count: 1, - detach: detachId, - attach: buildId, - }, - ], + count: 1, + detach: detachId, + attach: buildId, }, ], - ]), - }, - ], - }, + ], + ]), + }, + ], ], - [yField, { local: [{ count: 1, attach: moveId }] }], + [yField, [{ count: 1, attach: moveId }]], ]), }; - const delta: DeltaFieldMap = new Map([[rootFieldKey, { local: [mark] }]]); - applyTestDelta(delta, forest, undefined, undefined, [ - { id: buildId, trees: [singleJsonCursor(3)] }, - ]); + const delta: DeltaFieldMap = new Map([[rootFieldKey, [mark]]]); + applyTestDelta(delta, forest, { + build: [{ id: buildId, trees: [singleJsonCursor(3)] }], + }); const reader = forest.allocateCursor(); moveToDetachedField(forest, reader); @@ -1040,26 +1003,22 @@ export function testForest(config: ForestTestConfiguration): void { const delta: DeltaFieldMap = new Map([ [ rootFieldKey, - { - local: [ - { - count: 1, - fields: new Map([ + [ + { + count: 1, + fields: new Map([ + [ + xField, [ - xField, { - local: [ - { - count: 1, - detach: detachId, - }, - ], + count: 1, + detach: detachId, }, ], - ]), - }, - ], - }, + ], + ]), + }, + ], ], ]); const expected: JsonCompatible[] = [{ y: 1 }]; @@ -1104,11 +1063,11 @@ export function testForest(config: ForestTestConfiguration): void { const modify: DeltaMark = { count: 1, fields: new Map([ - [xField, { local: [moveOut] }], - [yField, { local: [moveIn] }], + [xField, [moveOut]], + [yField, [moveIn]], ]), }; - const delta: DeltaFieldMap = new Map([[rootFieldKey, { local: [modify] }]]); + const delta: DeltaFieldMap = new Map([[rootFieldKey, [modify]]]); applyTestDelta(delta, forest); const expectedCursor = cursorFromInsertable(NodeSchema, { y: 2 }); const expected: JsonableTree[] = [jsonableTreeFromCursor(expectedCursor)]; @@ -1140,17 +1099,15 @@ export function testForest(config: ForestTestConfiguration): void { ); forest.registerAnnouncedVisitor(acquireVisitor); - const delta: DeltaFieldMap = new Map([ - [rootFieldKey, { local: [{ count: 1, attach: buildId }] }], - ]); - applyTestDelta(delta, forest, undefined, undefined, [ - { id: buildId, trees: [singleJsonCursor(3)] }, - ]); + const delta: DeltaFieldMap = new Map([[rootFieldKey, [{ count: 1, attach: buildId }]]]); + applyTestDelta(delta, forest, { + build: [{ id: buildId, trees: [singleJsonCursor(3)] }], + }); forest.deregisterAnnouncedVisitor(acquireVisitor); - applyTestDelta(delta, forest, undefined, undefined, [ - { id: buildId, trees: [singleJsonCursor(4)] }, - ]); + applyTestDelta(delta, forest, { + build: [{ id: buildId, trees: [singleJsonCursor(4)] }], + }); assert.equal(treesCreated, 1); }); diff --git a/packages/dds/tree/src/test/testChange.spec.ts b/packages/dds/tree/src/test/testChange.spec.ts index 72b6e2a76bfd..9fe642d64cec 100644 --- a/packages/dds/tree/src/test/testChange.spec.ts +++ b/packages/dds/tree/src/test/testChange.spec.ts @@ -74,14 +74,7 @@ describe("TestChange", () => { const tag = mintRevisionTag(); const delta = TestChange.toDelta(tagChange(change1, tag)); const field: FieldKey = brand("testIntentions"); - const expected = new Map([ - [ - field, - { - local: [{ count: 2 }, { count: 3 }], - }, - ], - ]); + const expected = new Map([[field, [{ count: 2 }, { count: 3 }]]]); assert.deepEqual(delta, expected); assert.deepEqual( diff --git a/packages/dds/tree/src/test/testChange.ts b/packages/dds/tree/src/test/testChange.ts index 360a0afd5332..ef8953cbae32 100644 --- a/packages/dds/tree/src/test/testChange.ts +++ b/packages/dds/tree/src/test/testChange.ts @@ -193,7 +193,7 @@ function toDelta({ change }: TaggedChange): DeltaFieldMap { // We represent the intentions as a list if node offsets in some imaginary field "testIntentions". // This is purely for the sake of testing. brand("testIntentions"), - { local: change.intentions.map((i) => ({ count: i })) }, + change.intentions.map((i) => ({ count: i })), ], ]); } @@ -337,7 +337,7 @@ export function asDelta(intentions: number[]): DeltaRoot { return intentions.length === 0 ? emptyDelta : { - fields: new Map([[rootKey, { local: intentions.map((i) => ({ count: i })) }]]), + fields: new Map([[rootKey, intentions.map((i) => ({ count: i }))]]), }; } diff --git a/packages/dds/tree/src/test/tree/anchorSet.spec.ts b/packages/dds/tree/src/test/tree/anchorSet.spec.ts index cacff34f634d..c287eeaab6d8 100644 --- a/packages/dds/tree/src/test/tree/anchorSet.spec.ts +++ b/packages/dds/tree/src/test/tree/anchorSet.spec.ts @@ -83,9 +83,7 @@ describe("AnchorSet", () => { attach: moveId, }; - const delta = new Map([ - [rootFieldKey, { local: [{ count: 1 }, moveOut, { count: 1 }, moveIn] }], - ]); + const delta = new Map([[rootFieldKey, [{ count: 1 }, moveOut, { count: 1 }, moveIn]]]); announceTestDelta(delta, anchors); checkEquality(anchors.locate(anchor0), makePath([rootFieldKey, 0])); checkEquality(anchors.locate(anchor1), makePath([rootFieldKey, 2])); @@ -97,16 +95,10 @@ describe("AnchorSet", () => { const [anchors, anchor1, anchor2, anchor3] = setup(); const trees = [node, node].map(cursorForJsonableTreeNode); - const fieldChanges: DeltaFieldChanges = { - local: [{ count: 4 }, { count: 2, attach: buildId }], - }; - announceTestDelta( - makeFieldDelta(fieldChanges, makeFieldPath(fieldFoo)), - anchors, - undefined, - undefined, - [{ id: buildId, trees }], - ); + const fieldChanges: DeltaFieldChanges = [{ count: 4 }, { count: 2, attach: buildId }]; + announceTestDelta(makeFieldDelta(fieldChanges, makeFieldPath(fieldFoo)), anchors, { + build: [{ id: buildId, trees }], + }); checkEquality(anchors.locate(anchor1), makePath([fieldFoo, 7], [fieldBar, 4])); checkEquality(anchors.locate(anchor2), makePath([fieldFoo, 3], [fieldBaz, 2])); @@ -233,12 +225,10 @@ describe("AnchorSet", () => { const modify: DeltaMark = { count: 1, - fields: new Map([[fieldBar, { local: [{ count: 3 }, moveIn] }]]), + fields: new Map([[fieldBar, [{ count: 3 }, moveIn]]]), }; - const delta = new Map([ - [fieldFoo, { local: [{ count: 3 }, moveOut, { count: 1 }, modify] }], - ]); + const delta = new Map([[fieldFoo, [{ count: 3 }, moveOut, { count: 1 }, modify]]]); announceTestDelta(delta, anchors); checkEquality(anchors.locate(anchor1), makePath([fieldFoo, 4], [fieldBar, 5])); checkEquality( @@ -260,11 +250,9 @@ describe("AnchorSet", () => { testIdCompressor, ); - announceTestDelta( - makeDelta(detachMark, makePath([fieldFoo, 3])), - anchors, + announceTestDelta(makeDelta(detachMark, makePath([fieldFoo, 3])), anchors, { detachedFieldIndex, - ); + }); checkEquality(anchors.locate(anchor1), makePath([fieldFoo, 4], [fieldBar, 4])); checkRemoved(anchors.locate(anchor2), brand("repair-0")); checkEquality(anchors.locate(anchor3), makePath([fieldFoo, 3])); @@ -275,11 +263,9 @@ describe("AnchorSet", () => { attach: detachId, }; - announceTestDelta( - makeDelta(restoreMark, makePath([fieldFoo, 3])), - anchors, + announceTestDelta(makeDelta(restoreMark, makePath([fieldFoo, 3])), anchors, { detachedFieldIndex, - ); + }); checkEquality(anchors.locate(anchor1), path1); checkEquality(anchors.locate(anchor2), path2); checkEquality(anchors.locate(anchor3), path3); @@ -298,11 +284,9 @@ describe("AnchorSet", () => { testIdCompressor, ); - announceTestDelta( - makeDelta(detachMark, makePath([fieldFoo, 3])), - anchors, + announceTestDelta(makeDelta(detachMark, makePath([fieldFoo, 3])), anchors, { detachedFieldIndex, - ); + }); checkRemoved(anchors.locate(anchor1), brand("repair-2")); checkRemoved(anchors.locate(anchor2), brand("repair-0")); checkRemoved(anchors.locate(anchor3), brand("repair-1")); @@ -313,11 +297,9 @@ describe("AnchorSet", () => { attach: { minor: 44 }, }; - announceTestDelta( - makeDelta(restoreMark, makePath([fieldFoo, 3])), - anchors, + announceTestDelta(makeDelta(restoreMark, makePath([fieldFoo, 3])), anchors, { detachedFieldIndex, - ); + }); checkEquality(anchors.locate(anchor1), makePath([fieldFoo, 3], [fieldBar, 4])); checkRemoved(anchors.locate(anchor2), brand("repair-0")); checkRemoved(anchors.locate(anchor3), brand("repair-1")); @@ -425,7 +407,7 @@ describe("AnchorSet", () => { }; log.expect([]); - announceTestDelta(new Map([[rootFieldKey, { local: [detachMark] }]]), anchors); + announceTestDelta(new Map([[rootFieldKey, [detachMark]]]), anchors); log.expect([ ["root childrenChange", 1], @@ -455,20 +437,7 @@ describe("AnchorSet", () => { ], }, ]; - announceTestDelta( - new Map([ - [ - rootFieldKey, - { - local: [detachMark, insertMark], - }, - ], - ]), - anchors, - undefined, - undefined, - build, - ); + announceTestDelta(new Map([[rootFieldKey, [detachMark, insertMark]]]), anchors, { build }); log.expect([ ["root childrenChange", 2], @@ -477,17 +446,15 @@ describe("AnchorSet", () => { log.clear(); const insertAtFoo5 = makeFieldDelta( - { - local: [{ count: 5 }, insertMark], - }, + [{ count: 5 }, insertMark], makeFieldPath(fieldFoo, [rootFieldKey, 0]), ); - announceTestDelta(insertAtFoo5, anchors, undefined, undefined, build); + announceTestDelta(insertAtFoo5, anchors, { build }); log.expect([["root treeChange", 1]]); log.clear(); - announceTestDelta(new Map([[rootFieldKey, { local: [detachMark] }]]), anchors); + announceTestDelta(new Map([[rootFieldKey, [detachMark]]]), anchors); log.expect([ ["root childrenChange", 1], ["root treeChange", 1], @@ -547,9 +514,7 @@ describe("AnchorSet", () => { }, ]; const insertAtFoo4 = makeFieldDelta( - { - local: [{ count: 4 }, { count: 1, attach: buildId }], - }, + [{ count: 4 }, { count: 1, attach: buildId }], makeFieldPath(fieldFoo, [rootFieldKey, 0]), ); const detachMark: DeltaMark = { @@ -562,23 +527,17 @@ describe("AnchorSet", () => { detach: { minor: 42 }, }; const replaceAtFoo5 = makeFieldDelta( - { - local: [{ count: 5 }, replaceMark], - }, + [{ count: 5 }, replaceMark], makeFieldPath(fieldFoo, [rootFieldKey, 0]), ); const log = new UnorderedTestLogger(); const anchors = new AnchorSet(); const trees = [cursorForJsonableTreeNode(node)]; - const fieldChanges: DeltaFieldChanges = { - local: [{ count: 3 }, { count: 1, attach: buildId }], - }; + const fieldChanges: DeltaFieldChanges = [{ count: 3 }, { count: 1, attach: buildId }]; announceTestDelta( makeFieldDelta(fieldChanges, makeFieldPath(fieldFoo, [rootFieldKey, 0])), anchors, - undefined, - undefined, - [{ id: buildId, trees }], + { build: [{ id: buildId, trees }] }, ); const anchor0 = anchors.track(makePath([rootFieldKey, 0])); const node0 = anchors.locate(anchor0) ?? assert.fail(); @@ -657,13 +616,13 @@ describe("AnchorSet", () => { "subtreeChanging", (n: AnchorNode) => pathVisitor, ); - announceTestDelta(insertAtFoo4, anchors, undefined, undefined, build); + announceTestDelta(insertAtFoo4, anchors, { build }); log.expect([ ["visitSubtreeChange.beforeAttach-src:Temp-0[0, 1]-dst:foo[4]", 1], ["visitSubtreeChange.afterAttach-src:Temp-0[0]-dst:foo[4, 5]", 1], ]); log.clear(); - announceTestDelta(replaceAtFoo5, anchors, undefined, undefined, build); + announceTestDelta(replaceAtFoo5, anchors, { build }); log.expect([ ["visitSubtreeChange.beforeReplace-old:foo[5, 6]-new:Temp-0[0, 1]", 1], ["visitSubtreeChange.afterReplace-old:Temp-1[0, 1]-new:foo[5, 6]", 1], @@ -679,7 +638,7 @@ describe("AnchorSet", () => { ]); log.clear(); unsubscribePathVisitor(); - announceTestDelta(insertAtFoo4, anchors, undefined, undefined, build); + announceTestDelta(insertAtFoo4, anchors, { build }); log.expect([]); }); @@ -783,7 +742,7 @@ function checkRemoved(path: UpPath | undefined, expected: FieldKey = brand("Temp function makeDelta(mark: DeltaMark, path: UpPath): DeltaFieldMap { const fields: DeltaFieldMap = new Map([ - [path.parentField, { local: [{ count: path.parentIndex }, mark] }], + [path.parentField, [{ count: path.parentIndex }, mark]], ]); if (path.parent === undefined) { return fields; diff --git a/packages/dds/tree/src/test/tree/visitDelta.spec.ts b/packages/dds/tree/src/test/tree/visitDelta.spec.ts index 562994fb5c46..eb3acbcc1f95 100644 --- a/packages/dds/tree/src/test/tree/visitDelta.spec.ts +++ b/packages/dds/tree/src/test/tree/visitDelta.spec.ts @@ -6,9 +6,7 @@ import { strict as assert } from "node:assert"; import { - type DeltaDetachedNodeBuild, type DeltaDetachedNodeChanges, - type DeltaDetachedNodeDestruction, type DeltaDetachedNodeRename, type DeltaFieldChanges, type DeltaMark, @@ -26,6 +24,7 @@ import { rootFromDeltaFieldMap, testIdCompressor, testRevisionTagCodec, + type DeltaParams, } from "../utils.js"; import { deepFreeze } from "@fluidframework/test-runtime-utils/internal"; import { singleJsonCursor } from "../json/index.js"; @@ -65,7 +64,15 @@ const visitorMethods: (keyof DeltaVisitor)[] = [ "exitField", ]; -function testVisit( +/** + * Calls visitDelta on a DeltaRoot and checks that the result is as expected. + * + * @param delta - the root delta to visit + * @param expected - the expected result of the call to visitDelta + * @param detachedFieldIndex - an optional detached field index to get refresher data from + * @param revision - an optional revision for the delta being visited + */ +function testDeltaVisit( delta: DeltaRoot, expected: Readonly, detachedFieldIndex?: DetachedFieldIndex, @@ -89,16 +96,23 @@ function testVisit( assert.deepEqual(result, expected); } +/** + * Creates a DeltaRoot from the provided parameters and calls `testDeltaVisit` on the result. + */ function testTreeVisit( marks: DeltaFieldChanges, expected: Readonly, - detachedFieldIndex?: DetachedFieldIndex, - revision?: RevisionTag, - build?: readonly DeltaDetachedNodeBuild[], - destroy?: readonly DeltaDetachedNodeDestruction[], + params?: DeltaParams, ): void { - const rootDelta = rootFromDeltaFieldMap(new Map([[rootKey, marks]]), build, destroy); - testVisit(rootDelta, expected, detachedFieldIndex, revision); + const { detachedFieldIndex, revision, global, rename, build, destroy } = params ?? {}; + const rootDelta = rootFromDeltaFieldMap( + new Map([[rootKey, marks]]), + global, + rename, + build, + destroy, + ); + testDeltaVisit(rootDelta, expected, detachedFieldIndex, revision); } const rootKey: FieldKey = brand("root"); @@ -112,19 +126,20 @@ const field3: FieldKey = brand("-3"); describe("visitDelta", () => { it("empty delta", () => { - testTreeVisit({}, [ - ["enterField", rootKey], - ["exitField", rootKey], - ["enterField", rootKey], - ["exitField", rootKey], - ]); + testTreeVisit( + [], + [ + ["enterField", rootKey], + ["exitField", rootKey], + ["enterField", rootKey], + ["exitField", rootKey], + ], + ); }); it("insert root", () => { const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); const node = { minor: 42 }; - const rootFieldDelta: DeltaFieldChanges = { - local: [{ count: 1, attach: node }], - }; + const rootFieldDelta: DeltaFieldChanges = [{ count: 1, attach: node }]; const delta: DeltaRoot = { build: [{ id: node, trees: [content] }], fields: new Map([[rootKey, rootFieldDelta]]), @@ -137,34 +152,30 @@ describe("visitDelta", () => { ["attach", field0, 1, 0], ["exitField", rootKey], ]; - testVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.equal(index.entries().next().done, true); }); it("throws on build of existing tree", () => { const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); const node = { minor: 42 }; index.createEntry(node); - const rootFieldDelta: DeltaFieldChanges = { - local: [{ count: 1, attach: node }], - }; + const rootFieldDelta: DeltaFieldChanges = [{ count: 1, attach: node }]; const delta: DeltaRoot = { build: [{ id: node, trees: [content] }], fields: new Map([[rootKey, rootFieldDelta]]), }; - assert.throws(() => testVisit(delta, [], index)); + assert.throws(() => testDeltaVisit(delta, [], index)); assert.deepEqual(Array.from(index.entries()), [{ id: { minor: 42 }, root: 0 }]); }); it("insert child", () => { const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); const buildId = { minor: 42 }; - const rootFieldDelta: DeltaFieldChanges = { - local: [ - { - count: 1, - fields: new Map([[fooKey, { local: [{ count: 1, attach: buildId }] }]]), - }, - ], - }; + const rootFieldDelta: DeltaFieldChanges = [ + { + count: 1, + fields: new Map([[fooKey, [{ count: 1, attach: buildId }]]]), + }, + ]; const expected: VisitScript = [ ["create", [content], field0], ["enterField", rootKey], @@ -185,7 +196,7 @@ describe("visitDelta", () => { build: [{ id: buildId, trees: [content] }], fields: new Map([[rootKey, rootFieldDelta]]), }; - testVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.equal(index.entries().next().done, true); }); it("remove root", () => { @@ -194,9 +205,9 @@ describe("visitDelta", () => { count: 2, detach: { minor: 42 }, }; - const delta = { local: [{ count: 1 }, mark] }; + const marks = [{ count: 1 }, mark]; testTreeVisit( - delta, + marks, [ ["enterField", rootKey], ["detach", { start: 1, end: 2 }, field0], @@ -205,7 +216,9 @@ describe("visitDelta", () => { ["enterField", rootKey], ["exitField", rootKey], ], - index, + { + detachedFieldIndex: index, + }, ); assert.deepEqual(Array.from(index.entries()), [ { id: { minor: 42 }, root: 0 }, @@ -220,9 +233,8 @@ describe("visitDelta", () => { }; const mark: DeltaMark = { count: 1, - fields: new Map([[fooKey, { local: [{ count: 42 }, remove] }]]), + fields: new Map([[fooKey, [{ count: 42 }, remove]]]), }; - const delta = { local: [mark] }; const expected: VisitScript = [ ["enterField", rootKey], ["enterNode", 0], @@ -238,7 +250,7 @@ describe("visitDelta", () => { ["exitNode", 0], ["exitField", rootKey], ]; - testTreeVisit(delta, expected, index); + testTreeVisit([mark], expected, { detachedFieldIndex: index }); assert.deepEqual(Array.from(index.entries()), [{ id: { minor: 42 }, root: 0 }]); }); it("changes under insert", () => { @@ -252,14 +264,15 @@ describe("visitDelta", () => { count: 1, attach: moveId, }; - const delta: DeltaFieldChanges = { + const delta: DeltaRoot = { global: [ { id: { minor: 43 }, - fields: new Map([[fooKey, { local: [{ count: 42 }, moveOut, moveIn] }]]), + fields: new Map([[fooKey, [{ count: 42 }, moveOut, moveIn]]]), }, ], - local: [{ count: 1, attach: { minor: 43 } }], + build: [{ id: { minor: 43 }, trees: [content] }], + fields: new Map([[rootKey, [{ count: 1, attach: { minor: 43 } }]]]), }; const expected: VisitScript = [ ["create", [content], field0], @@ -281,9 +294,7 @@ describe("visitDelta", () => { ["exitNode", 0], ["exitField", rootKey], ]; - testTreeVisit(delta, expected, index, undefined, [ - { id: { minor: 43 }, trees: [content] }, - ]); + testDeltaVisit(delta, expected, index); assert.equal(index.entries().next().done, true); }); it("move node to the right", () => { @@ -298,7 +309,7 @@ describe("visitDelta", () => { count: 1, attach: moveId, }; - const delta = { local: [{ count: 1 }, moveOut, { count: 1 }, moveIn] }; + const marks = [{ count: 1 }, moveOut, { count: 1 }, moveIn]; const expected: VisitScript = [ ["enterField", rootKey], ["detach", { start: 1, end: 2 }, field0], @@ -307,7 +318,7 @@ describe("visitDelta", () => { ["attach", field0, 1, 2], ["exitField", rootKey], ]; - testTreeVisit(delta, expected, index); + testTreeVisit(marks, expected, { detachedFieldIndex: index }); assert.equal(index.entries().next().done, true); }); it("move children to the left", () => { @@ -323,9 +334,9 @@ describe("visitDelta", () => { }; const modify: DeltaMark = { count: 1, - fields: new Map([[fooKey, { local: [{ count: 2 }, moveIn, { count: 3 }, moveOut] }]]), + fields: new Map([[fooKey, [{ count: 2 }, moveIn, { count: 3 }, moveOut]]]), }; - const delta = { local: [modify] }; + const marks = [modify]; const expected: VisitScript = [ ["enterField", rootKey], ["enterNode", 0], @@ -344,7 +355,7 @@ describe("visitDelta", () => { ["exitNode", 0], ["exitField", rootKey], ]; - testTreeVisit(delta, expected, index); + testTreeVisit(marks, expected, { detachedFieldIndex: index }); assert.equal(index.entries().next().done, true); }); it("move cousins", () => { @@ -361,11 +372,11 @@ describe("visitDelta", () => { const modify: DeltaMark = { count: 1, fields: new Map([ - [fooKey, { local: [moveIn] }], - [barKey, { local: [moveOut] }], + [fooKey, [moveIn]], + [barKey, [moveOut]], ]), }; - const delta = { local: [modify] }; + const marks = [modify]; const expected: VisitScript = [ ["enterField", rootKey], ["enterNode", 0], @@ -386,7 +397,7 @@ describe("visitDelta", () => { ["exitNode", 0], ["exitField", rootKey], ]; - testTreeVisit(delta, expected, index); + testTreeVisit(marks, expected, { detachedFieldIndex: index }); assert.equal(index.entries().next().done, true); }); it("changes under remove", () => { @@ -403,9 +414,9 @@ describe("visitDelta", () => { const remove: DeltaMark = { detach: { minor: 42 }, count: 1, - fields: new Map([[fooKey, { local: [moveOut, moveIn] }]]), + fields: new Map([[fooKey, [moveOut, moveIn]]]), }; - const delta = { local: [remove] }; + const marks = [remove]; const expected: VisitScript = [ ["enterField", rootKey], ["enterNode", 0], @@ -425,7 +436,7 @@ describe("visitDelta", () => { ["exitNode", 0], ["exitField", field1], ]; - testTreeVisit(delta, expected, index); + testTreeVisit(marks, expected, { detachedFieldIndex: index }); assert.deepEqual(Array.from(index.entries()), [{ id: { minor: 42 }, root: 1 }]); }); it("changes under destroy", () => { @@ -443,14 +454,13 @@ describe("visitDelta", () => { }; const nested: DeltaDetachedNodeChanges = { id: node1, - fields: new Map([[fooKey, { local: [moveOut, moveIn] }]]), + fields: new Map([[fooKey, [moveOut, moveIn]]]), }; - const delta: DeltaFieldChanges = { + const delta: DeltaRoot = { global: [nested], + destroy: [{ id: node1, count: 1 }], }; const expected: VisitScript = [ - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field0], ["enterNode", 0], ["enterField", fooKey], @@ -458,8 +468,6 @@ describe("visitDelta", () => { ["exitField", fooKey], ["exitNode", 0], ["exitField", field0], - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field0], ["enterNode", 0], ["enterField", fooKey], @@ -469,7 +477,7 @@ describe("visitDelta", () => { ["exitField", field0], ["destroy", field0, 1], ]; - testTreeVisit(delta, expected, index, undefined, undefined, [{ id: node1, count: 1 }]); + testDeltaVisit(delta, expected, index); assert.equal(index.entries().next().done, true); }); it("destroy (root level)", () => { @@ -483,35 +491,26 @@ describe("visitDelta", () => { ["destroy", field0, 1], ["destroy", field1, 1], ]; - testVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.equal(index.entries().next().done, true); }); it("build-rename-destroy (field level)", () => { const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); const buildId = { minor: 42 }; const detachId = { minor: 43 }; - const delta: DeltaFieldChanges = { + const delta: DeltaRoot = { rename: [{ oldId: buildId, newId: detachId, count: 1 }], + build: [{ id: buildId, trees: [content] }], + destroy: [{ id: detachId, count: 1 }], }; const expected: VisitScript = [ ["create", [content], field0], - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field0], ["detach", { start: 0, end: 1 }, field1], ["exitField", field0], - ["enterField", rootKey], - ["exitField", rootKey], ["destroy", field1, 1], ]; - testTreeVisit( - delta, - expected, - index, - undefined, - [{ id: buildId, trees: [content] }], - [{ id: detachId, count: 1 }], - ); + testDeltaVisit(delta, expected, index); assert.equal(index.entries().next().done, true); }); it("changes under move-out", () => { @@ -533,9 +532,9 @@ describe("visitDelta", () => { const moveOut1: DeltaMark = { count: 1, detach: moveId1, - fields: new Map([[fooKey, { local: [moveOut2, moveIn2] }]]), + fields: new Map([[fooKey, [moveOut2, moveIn2]]]), }; - const delta = { local: [moveOut1, moveIn1] }; + const marks = [moveOut1, moveIn1]; const expected: VisitScript = [ ["enterField", rootKey], ["enterNode", 0], @@ -554,7 +553,7 @@ describe("visitDelta", () => { ["exitNode", 0], ["exitField", rootKey], ]; - testTreeVisit(delta, expected, index); + testTreeVisit(marks, expected, { detachedFieldIndex: index }); assert.equal(index.entries().next().done, true); }); @@ -577,14 +576,14 @@ describe("visitDelta", () => { const moveOut2: DeltaMark = { count: 1, detach: { minor: 3 }, - fields: new Map([[fooKey, { local: [attach] }]]), + fields: new Map([[fooKey, [attach]]]), }; - const rootChanges: DeltaFieldChanges = { local: [moveOut1, moveOut2, moveIn] }; + const fieldChanges: DeltaFieldChanges = [moveOut1, moveOut2, moveIn]; const delta: DeltaRoot = { build: [{ id: buildId, trees: [content] }], - fields: new Map([[rootKey, rootChanges]]), + fields: new Map([[rootKey, fieldChanges]]), }; const expected: VisitScript = [ @@ -608,7 +607,7 @@ describe("visitDelta", () => { ["exitField", rootKey], ]; - testVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.equal(index.entries().next().done, true); }); @@ -621,10 +620,10 @@ describe("visitDelta", () => { attach: buildId, }; - const rootChanges: DeltaFieldChanges = { local: [replace] }; + const fieldChanges: DeltaFieldChanges = [replace]; const delta: DeltaRoot = { build: [{ id: buildId, trees: [content, content] }], - fields: new Map([[rootKey, rootChanges]]), + fields: new Map([[rootKey, fieldChanges]]), }; const expected: VisitScript = [ @@ -639,7 +638,7 @@ describe("visitDelta", () => { ]; const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); - testVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); }); it("changes under replaced node", () => { @@ -662,9 +661,9 @@ describe("visitDelta", () => { count: 1, detach: { minor: 42 }, attach: moveId1, - fields: new Map([[fooKey, { local: [moveOut2, moveIn2] }]]), + fields: new Map([[fooKey, [moveOut2, moveIn2]]]), }; - const delta = { local: [replace, moveOut1] }; + const marks = [replace, moveOut1]; const expected: VisitScript = [ ["enterField", rootKey], ["enterNode", 0], @@ -685,7 +684,7 @@ describe("visitDelta", () => { ["exitNode", 0], ["exitField", field2], ]; - testTreeVisit(delta, expected, index); + testTreeVisit(marks, expected, { detachedFieldIndex: index }); assert.deepEqual(Array.from(index.entries()), [{ id: { minor: 42 }, root: 2 }]); }); @@ -704,14 +703,14 @@ describe("visitDelta", () => { const moveOut1: DeltaMark = { count: 1, detach: moveId1, - fields: new Map([[fooKey, { local: [moveOut2, moveIn2] }]]), + fields: new Map([[fooKey, [moveOut2, moveIn2]]]), }; const replace: DeltaMark = { count: 1, detach: { minor: 42 }, attach: moveId1, }; - const delta = { local: [replace, moveOut1] }; + const marks = [replace, moveOut1]; const expected: VisitScript = [ ["enterField", rootKey], ["enterNode", 1], @@ -730,27 +729,22 @@ describe("visitDelta", () => { ["exitNode", 0], ["exitField", rootKey], ]; - testTreeVisit(delta, expected, index); + testTreeVisit(marks, expected, { detachedFieldIndex: index }); assert.deepEqual(Array.from(index.entries()), [{ id: { minor: 42 }, root: 2 }]); }); it("transient insert", () => { const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); - const delta: DeltaFieldChanges = { + const delta: DeltaRoot = { + build: [{ id: { minor: 42 }, trees: [content] }], rename: [{ oldId: { minor: 42 }, count: 1, newId: { minor: 43 } }], }; const expected: VisitScript = [ ["create", [content], field0], - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field0], ["detach", { start: 0, end: 1 }, field1], ["exitField", field0], - ["enterField", rootKey], - ["exitField", rootKey], ]; - testTreeVisit(delta, expected, index, undefined, [ - { id: { minor: 42 }, trees: [content] }, - ]); + testDeltaVisit(delta, expected, index); assert.deepEqual(Array.from(index.entries()), [{ id: { minor: 43 }, root: 1 }]); }); it("changes under transient", () => { @@ -766,14 +760,13 @@ describe("visitDelta", () => { }; const buildId = { minor: 42 }; const detachId = { minor: 43 }; - const delta: DeltaFieldChanges = { - global: [{ id: buildId, fields: new Map([[barKey, { local: [moveOut, moveIn] }]]) }], + const delta: DeltaRoot = { + build: [{ id: buildId, trees: [content] }], + global: [{ id: buildId, fields: new Map([[barKey, [moveOut, moveIn]]]) }], rename: [{ oldId: buildId, count: 1, newId: detachId }], }; const expected: VisitScript = [ ["create", [content], field0], // field0: buildId - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field0], ["enterNode", 0], ["enterField", barKey], @@ -784,8 +777,6 @@ describe("visitDelta", () => { ["enterField", field0], ["detach", { start: 0, end: 1 }, field2], // field2: detachId ["exitField", field0], - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field2], ["enterNode", 0], ["enterField", barKey], @@ -794,7 +785,7 @@ describe("visitDelta", () => { ["exitNode", 0], ["exitField", field2], ]; - testTreeVisit(delta, expected, index, undefined, [{ id: buildId, trees: [content] }]); + testDeltaVisit(delta, expected, index); assert.deepEqual(Array.from(index.entries()), [{ id: detachId, root: 2 }]); }); it("restore", () => { @@ -805,7 +796,7 @@ describe("visitDelta", () => { count: 1, attach: node1, }; - const delta = { local: [restore] }; + const marks = [restore]; const expected: VisitScript = [ ["enterField", rootKey], ["exitField", rootKey], @@ -813,7 +804,7 @@ describe("visitDelta", () => { ["attach", field0, 1, 0], ["exitField", rootKey], ]; - testTreeVisit(delta, expected, index); + testTreeVisit(marks, expected, { detachedFieldIndex: index }); assert.equal(index.entries().next().done, true); }); it("move removed node", () => { @@ -830,10 +821,7 @@ describe("visitDelta", () => { count: 1, attach: moveId, }; - const delta = { - rename: [rename], - local: [moveIn], - }; + const marks = [moveIn]; const expected: VisitScript = [ ["enterField", rootKey], ["exitField", rootKey], @@ -844,7 +832,7 @@ describe("visitDelta", () => { ["attach", field1, 1, 0], ["exitField", rootKey], ]; - testTreeVisit(delta, expected, index); + testTreeVisit(marks, expected, { detachedFieldIndex: index, rename: [rename] }); assert.equal(index.entries().next().done, true); }); it("changes under removed node", () => { @@ -862,12 +850,10 @@ describe("visitDelta", () => { }; const modify: DeltaDetachedNodeChanges = { id: { minor: 1 }, - fields: new Map([[fooKey, { local: [moveOut, moveIn] }]]), + fields: new Map([[fooKey, [moveOut, moveIn]]]), }; const delta = { global: [modify] }; const expected: VisitScript = [ - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field0], ["enterNode", 0], ["enterField", fooKey], @@ -875,8 +861,6 @@ describe("visitDelta", () => { ["exitField", fooKey], ["exitNode", 0], ["exitField", field0], - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field0], ["enterNode", 0], ["enterField", fooKey], @@ -885,7 +869,7 @@ describe("visitDelta", () => { ["exitNode", 0], ["exitField", field0], ]; - testTreeVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.deepEqual(Array.from(index.entries()), [{ id: { minor: 1 }, root: 0 }]); }); it("changes under transient move-in", () => { @@ -899,12 +883,10 @@ describe("visitDelta", () => { fields: new Map([ [ fooKey, - { - local: [ - { count: 1, detach: moveId2 }, - { count: 1, attach: moveId2 }, - ], - }, + [ + { count: 1, detach: moveId2 }, + { count: 1, attach: moveId2 }, + ], ], ]), }; @@ -913,7 +895,7 @@ describe("visitDelta", () => { oldId: moveId1, newId: detachId, }; - const delta = { local: [moveOut], rename: [moveIn] }; + const delta: DeltaRoot = { fields: new Map([[rootKey, [moveOut]]]), rename: [moveIn] }; const expected: VisitScript = [ ["enterField", rootKey], ["enterNode", 0], @@ -936,7 +918,7 @@ describe("visitDelta", () => { ["exitNode", 0], ["exitField", field2], ]; - testTreeVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.deepEqual(Array.from(index.entries()), [{ id: detachId, root: 2 }]); }); it("transient restore", () => { @@ -950,15 +932,11 @@ describe("visitDelta", () => { }; const delta = { rename: [restore] }; const expected: VisitScript = [ - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field0], ["detach", { start: 0, end: 1 }, field1], ["exitField", field0], - ["enterField", rootKey], - ["exitField", rootKey], ]; - testTreeVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.deepEqual(Array.from(index.entries()), [{ id: { minor: 42 }, root: 1 }]); }); it("update detached node", () => { @@ -978,22 +956,19 @@ describe("visitDelta", () => { newId: node1, }; const delta = { + build: [{ id: buildId, trees: [content] }], rename: [renameOldNode, renameNewNode], }; const expected: VisitScript = [ ["create", [content], field1], // field1: buildId - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field0], // field0: node1 ["detach", { start: 0, end: 1 }, field2], // field2: detachId ["exitField", field0], ["enterField", field1], ["detach", { start: 0, end: 1 }, field3], // field3: node1 ["exitField", field1], - ["enterField", rootKey], - ["exitField", rootKey], ]; - testTreeVisit(delta, expected, index, undefined, [{ id: buildId, trees: [content] }]); + testDeltaVisit(delta, expected, index); assert.deepEqual(Array.from(index.entries()), [ { id: detachId, root: 2 }, { id: node1, root: 3 }, @@ -1004,9 +979,7 @@ describe("visitDelta", () => { it("for restores at the root", () => { const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); const node = { minor: 42 }; - const rootFieldDelta: DeltaFieldChanges = { - local: [{ count: 1, attach: node }], - }; + const rootFieldDelta: DeltaFieldChanges = [{ count: 1, attach: node }]; const delta: DeltaRoot = { refreshers: [{ id: node, trees: [content] }], fields: new Map([[rootKey, rootFieldDelta]]), @@ -1019,21 +992,19 @@ describe("visitDelta", () => { ["attach", field0, 1, 0], ["exitField", rootKey], ]; - testVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.equal(index.entries().next().done, true); }); it("for restores under a child", () => { const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); const buildId = { minor: 42 }; - const rootFieldDelta: DeltaFieldChanges = { - local: [ - { - count: 1, - fields: new Map([[fooKey, { local: [{ count: 1, attach: buildId }] }]]), - }, - ], - }; + const rootFieldDelta: DeltaFieldChanges = [ + { + count: 1, + fields: new Map([[fooKey, [{ count: 1, attach: buildId }]]]), + }, + ]; const expected: VisitScript = [ ["enterField", rootKey], ["enterNode", 0], @@ -1054,16 +1025,14 @@ describe("visitDelta", () => { refreshers: [{ id: buildId, trees: [content] }], fields: new Map([[rootKey, rootFieldDelta]]), }; - testVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.equal(index.entries().next().done, true); }); it("for partial restores", () => { const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); const node = { minor: 42 }; - const rootFieldDelta: DeltaFieldChanges = { - local: [{ count: 1, attach: { minor: 43 } }], - }; + const rootFieldDelta: DeltaFieldChanges = [{ count: 1, attach: { minor: 43 } }]; const delta: DeltaRoot = { refreshers: [{ id: node, trees: [content, content] }], fields: new Map([[rootKey, rootFieldDelta]]), @@ -1076,7 +1045,7 @@ describe("visitDelta", () => { ["attach", field0, 1, 0], ["exitField", rootKey], ]; - testVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.equal(index.entries().next().done, true); }); @@ -1084,27 +1053,15 @@ describe("visitDelta", () => { const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); const refresherId = { minor: 42 }; const buildId = { minor: 43 }; - const rootFieldDelta: DeltaFieldChanges = { - global: [ - { - id: refresherId, - fields: new Map([[fooKey, { local: [{ count: 1, attach: buildId }] }]]), - }, - ], - }; const expected: VisitScript = [ ["create", [content], field0], - ["enterField", rootKey], ["create", [content], field1], - ["exitField", rootKey], ["enterField", field1], ["enterNode", 0], ["enterField", fooKey], ["exitField", fooKey], ["exitNode", 0], ["exitField", field1], - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field1], ["enterNode", 0], ["enterField", fooKey], @@ -1114,11 +1071,18 @@ describe("visitDelta", () => { ["exitField", field1], ]; const delta: DeltaRoot = { + global: [ + { + id: refresherId, + fields: new Map([[fooKey, [{ count: 1, attach: buildId }]]]), + }, + ], refreshers: [{ id: refresherId, trees: [content] }], build: [{ id: buildId, trees: [content] }], - fields: new Map([[rootKey, rootFieldDelta]]), + // TODO the global was in this so it might've changed the expected value + // fields: new Map([[rootKey, rootFieldDelta]]), }; - testVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); }); }); @@ -1137,16 +1101,12 @@ describe("visitDelta", () => { rename: [rename], }; const expected: VisitScript = [ - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field0], ["detach", { start: 0, end: 1 }, field1], ["exitField", field0], - ["enterField", rootKey], - ["exitField", rootKey], ]; const revision = mintRevisionTag(); - testTreeVisit(delta, expected, index, revision); + testDeltaVisit(delta, expected, index, revision); const iteratorResult = index.entries().next(); assert.equal(iteratorResult.done, false); assert.deepEqual(iteratorResult.value.id, moveId); @@ -1162,7 +1122,7 @@ describe("visitDelta", () => { }; const expected: VisitScript = [["create", [content], field0]]; const revision = mintRevisionTag(); - testVisit(delta, expected, index, revision); + testDeltaVisit(delta, expected, index, revision); assert.deepEqual(Array.from(index.entries()), [ { id: node, root: 0, latestRelevantRevision: revision }, ]); @@ -1183,9 +1143,9 @@ describe("visitDelta", () => { }; const modify: DeltaDetachedNodeChanges = { id: node1, - fields: new Map([[fooKey, { local: [moveOut, moveIn] }]]), + fields: new Map([[fooKey, [moveOut, moveIn]]]), }; - const delta = { global: [modify] }; + const global = [modify]; const expected: VisitScript = [ ["enterField", rootKey], ["exitField", rootKey], @@ -1207,7 +1167,7 @@ describe("visitDelta", () => { ["exitField", field0], ]; const revision = mintRevisionTag(); - testTreeVisit(delta, expected, index, revision); + testTreeVisit([], expected, { detachedFieldIndex: index, revision, global }); assert.deepEqual(Array.from(index.entries()), [ { id: node1, root: 0, latestRelevantRevision: revision }, ]); @@ -1221,7 +1181,7 @@ describe("visitDelta", () => { detach: { minor: 2 }, }; - const rootChanges: DeltaFieldChanges = { local: [replace] }; + const rootChanges: DeltaFieldChanges = [replace]; const delta: DeltaRoot = { build: [{ id: buildId, trees: [content, content] }], fields: new Map([[rootKey, rootChanges]]), @@ -1240,7 +1200,7 @@ describe("visitDelta", () => { const revision = mintRevisionTag(); const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); - testVisit(delta, expected, index, revision); + testDeltaVisit(delta, expected, index, revision); const iteratorResult = index.entries().next(); assert.equal(iteratorResult.done, false); assert.deepEqual(iteratorResult.value.id, buildId); @@ -1253,9 +1213,7 @@ describe("visitDelta", () => { const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); const node = { minor: 42 }; const node2 = { minor: 43 }; - const rootFieldDelta: DeltaFieldChanges = { - local: [{ count: 1, attach: node2 }], - }; + const rootFieldDelta: DeltaFieldChanges = [{ count: 1, attach: node2 }]; const delta: DeltaRoot = { refreshers: [ { id: node, trees: [content] }, @@ -1271,7 +1229,7 @@ describe("visitDelta", () => { ["attach", field0, 1, 0], ["exitField", rootKey], ]; - testVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.equal(index.entries().next().done, true); }); @@ -1279,9 +1237,7 @@ describe("visitDelta", () => { const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); const node = { minor: 42 }; index.createEntry(node, undefined, 1); - const rootFieldDelta: DeltaFieldChanges = { - local: [{ count: 1, attach: node }], - }; + const rootFieldDelta: DeltaFieldChanges = [{ count: 1, attach: node }]; const delta: DeltaRoot = { refreshers: [{ id: node, trees: [content] }], fields: new Map([[rootKey, rootFieldDelta]]), @@ -1293,16 +1249,14 @@ describe("visitDelta", () => { ["attach", field0, 1, 0], ["exitField", rootKey], ]; - testVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.equal(index.entries().next().done, true); }); it("when the refreshed tree is included in the builds", () => { const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); const node = { minor: 42 }; - const rootFieldDelta: DeltaFieldChanges = { - local: [{ count: 1, attach: node }], - }; + const rootFieldDelta: DeltaFieldChanges = [{ count: 1, attach: node }]; const delta: DeltaRoot = { build: [{ id: node, trees: [content] }], refreshers: [{ id: node, trees: [content] }], @@ -1316,7 +1270,7 @@ describe("visitDelta", () => { ["attach", field0, 1, 0], ["exitField", rootKey], ]; - testVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.equal(index.entries().next().done, true); }); }); @@ -1339,22 +1293,13 @@ describe("visitDelta", () => { rename: [rename], }; const expected: VisitScript = cycle - ? [ - ["enterField", rootKey], - ["exitField", rootKey], - ["enterField", rootKey], - ["exitField", rootKey], - ] + ? [] : [ - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field0], ["detach", { start: 0, end: 1 }, field1], ["exitField", field0], - ["enterField", rootKey], - ["exitField", rootKey], ]; - testTreeVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.deepEqual(Array.from(index.entries()), [{ id: end, root: cycle ? 0 : 1 }]); }); }); @@ -1381,18 +1326,14 @@ describe("visitDelta", () => { ][ordering - 1], }; const expected: VisitScript = [ - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field0], ["detach", { start: 0, end: 1 }, field1], ["exitField", field0], ["enterField", field1], ["detach", { start: 0, end: 1 }, field2], ["exitField", field1], - ["enterField", rootKey], - ["exitField", rootKey], ]; - testTreeVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.deepEqual(Array.from(index.entries()), [{ id: end, root: 2 }]); }); } @@ -1428,8 +1369,6 @@ describe("visitDelta", () => { ][ordering - 1], }; const expected: VisitScript = [ - ["enterField", rootKey], - ["exitField", rootKey], ["enterField", field0], ["detach", { start: 0, end: 1 }, field1], ["exitField", field0], @@ -1439,12 +1378,10 @@ describe("visitDelta", () => { ["enterField", field2], ["detach", { start: 0, end: 1 }, field3], ["exitField", field2], - ["enterField", rootKey], - ["exitField", rootKey], ]; const index = makeDetachedFieldIndex("", testRevisionTagCodec, testIdCompressor); index.createEntry(pointA); - testTreeVisit(delta, expected, index); + testDeltaVisit(delta, expected, index); assert.deepEqual(Array.from(index.entries()), [{ id: end, root: 3 }]); }); } diff --git a/packages/dds/tree/src/test/utils.ts b/packages/dds/tree/src/test/utils.ts index 4967499a888b..697ce090de9c 100644 --- a/packages/dds/tree/src/test/utils.ts +++ b/packages/dds/tree/src/test/utils.ts @@ -92,6 +92,8 @@ import { CursorLocationType, type RevertibleAlpha, type RevertibleAlphaFactory, + type DeltaDetachedNodeChanges, + type DeltaDetachedNodeRename, } from "../core/index.js"; import { typeboxValidator } from "../external-utilities/index.js"; import { @@ -156,6 +158,8 @@ import { JsonUnion, cursorToJsonObject, singleJsonCursor } from "./json/index.js // eslint-disable-next-line import/no-internal-modules import type { TreeSimpleContent } from "./feature-libraries/flex-tree/utils.js"; import type { Transactor } from "../shared-tree-core/index.js"; +// eslint-disable-next-line import/no-internal-modules +import type { FieldChangeDelta } from "../feature-libraries/modular-schema/fieldChangeHandler.js"; // Testing utilities @@ -460,8 +464,8 @@ export function spyOnMethod( /** * Determines whether or not the given delta has a visible impact on the document tree. */ -export function isDeltaVisible(delta: DeltaFieldChanges): boolean { - for (const mark of delta.local ?? []) { +export function isDeltaVisible(fieldChanges: DeltaFieldChanges | undefined): boolean { + for (const mark of fieldChanges ?? []) { if (mark.attach !== undefined || mark.detach !== undefined) { return true; } @@ -479,7 +483,7 @@ export function isDeltaVisible(delta: DeltaFieldChanges): boolean { /** * Assert two MarkList are equal, handling cursors. */ -export function assertFieldChangesEqual(a: DeltaFieldChanges, b: DeltaFieldChanges): void { +export function assertFieldChangesEqual(a: FieldChangeDelta, b: FieldChangeDelta): void { assert.deepStrictEqual(a, b); } @@ -1004,15 +1008,22 @@ export function defaultRevInfosFromChanges( return revInfos; } +export interface DeltaParams { + detachedFieldIndex?: DetachedFieldIndex; + revision?: RevisionTag; + global?: readonly DeltaDetachedNodeChanges[]; + rename?: readonly DeltaDetachedNodeRename[]; + build?: readonly DeltaDetachedNodeBuild[]; + destroy?: readonly DeltaDetachedNodeDestruction[]; +} + export function applyTestDelta( delta: DeltaFieldMap, deltaProcessor: { acquireVisitor: () => DeltaVisitor }, - detachedFieldIndex?: DetachedFieldIndex, - revision?: RevisionTag, - build?: readonly DeltaDetachedNodeBuild[], - destroy?: readonly DeltaDetachedNodeDestruction[], + params?: DeltaParams, ): void { - const rootDelta = rootFromDeltaFieldMap(delta, build, destroy); + const { detachedFieldIndex, revision, global, rename, build, destroy } = params ?? {}; + const rootDelta = rootFromDeltaFieldMap(delta, global, rename, build, destroy); applyDelta( rootDelta, revision, @@ -1025,12 +1036,10 @@ export function applyTestDelta( export function announceTestDelta( delta: DeltaFieldMap, deltaProcessor: { acquireVisitor: () => DeltaVisitor & AnnouncedVisitor }, - detachedFieldIndex?: DetachedFieldIndex, - revision?: RevisionTag, - build?: readonly DeltaDetachedNodeBuild[], - destroy?: readonly DeltaDetachedNodeDestruction[], + params?: DeltaParams, ): void { - const rootDelta = rootFromDeltaFieldMap(delta, build, destroy); + const { detachedFieldIndex, revision, global, rename, build, destroy } = params ?? {}; + const rootDelta = rootFromDeltaFieldMap(delta, global, rename, build, destroy); announceDelta( rootDelta, revision, @@ -1042,10 +1051,18 @@ export function announceTestDelta( export function rootFromDeltaFieldMap( delta: DeltaFieldMap, + global?: readonly DeltaDetachedNodeChanges[], + rename?: readonly DeltaDetachedNodeRename[], build?: readonly DeltaDetachedNodeBuild[], destroy?: readonly DeltaDetachedNodeDestruction[], ): Mutable { const rootDelta: Mutable = { fields: delta }; + if (global !== undefined) { + rootDelta.global = global; + } + if (rename !== undefined) { + rootDelta.rename = rename; + } if (build !== undefined) { rootDelta.build = build; }