Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(tree): move global and rename effects to the delta root #23484

Merged
merged 19 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading