Skip to content

Commit

Permalink
refactor(tree): move global and rename effects to the delta root (#23484
Browse files Browse the repository at this point in the history
)

This change moves the global and renames effects out of the delta field
changes and into the delta root object.

---------

Co-authored-by: yann-achard-MS <97201204+yann-achard-MS@users.noreply.github.com>
  • Loading branch information
jenn-le and yann-achard-MS authored Jan 16, 2025
1 parent de5bfac commit e533d64
Show file tree
Hide file tree
Showing 30 changed files with 690 additions and 824 deletions.
2 changes: 0 additions & 2 deletions packages/dds/tree/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,6 @@ export {
forEachField,
type PathRootPrefix,
deltaForRootInitialization,
emptyFieldChanges,
isEmptyFieldChanges,
makeDetachedNodeId,
offsetDetachId,
emptyDelta,
Expand Down
47 changes: 21 additions & 26 deletions packages/dds/tree/src/core/tree/delta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@ export interface Root<TTree = ProtoNode> {
* The ordering has no significance.
*/
readonly refreshers?: readonly DetachedNodeBuild<TTree>[];
/**
* 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[];
}

/**
Expand Down Expand Up @@ -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[];
17 changes: 1 addition & 16 deletions packages/dds/tree/src/core/tree/deltaUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import { rootFieldKey } from "./types.js";

export const emptyDelta: Root<never> = {};

export const emptyFieldChanges: FieldChanges = {};

export function isAttachMark(mark: Mark): boolean {
return mark.attach !== undefined && mark.detach === undefined;
}
Expand All @@ -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;
Expand All @@ -42,12 +32,7 @@ export function deltaForRootInitialization(content: readonly ITreeCursorSynchron
const delta: Root = {
build: [{ id: buildId, trees: content }],
fields: new Map<FieldKey, FieldChanges>([
[
rootFieldKey,
{
local: [{ count: content.length, attach: buildId }],
},
],
[rootFieldKey, [{ count: content.length, attach: buildId }]],
]),
};
return delta;
Expand Down
2 changes: 0 additions & 2 deletions packages/dds/tree/src/core/tree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,6 @@ export { SparseNode, getDescendant } from "./sparseTree.js";

export {
deltaForRootInitialization,
emptyFieldChanges,
isEmptyFieldChanges,
makeDetachedNodeId,
offsetDetachId,
emptyDelta,
Expand Down
205 changes: 108 additions & 97 deletions packages/dds/tree/src/core/tree/visitDelta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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;
}
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<DeltaDetachedNodeId> => [],
isEmpty: (change: 0) => true,
getNestedChanges: (change: 0) => [],
Expand Down
Loading

0 comments on commit e533d64

Please sign in to comment.