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

feat(tree): create refreshers during delta visit #20303

Merged
merged 6 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 56 additions & 14 deletions packages/dds/tree/src/core/tree/visitDelta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

import { assert } from "@fluidframework/core-utils";

import { NestedMap } from "../../index.js";
import { setInNestedMap, tryGetFromNestedMap } from "../../util/index.js";
import { FieldKey } from "../schema-stored/index.js";
import { ITreeCursorSynchronous } from "./cursor.js";
import * as Delta from "./delta.js";
import { ProtoNodes } from "./delta.js";
import {
Expand All @@ -16,6 +19,7 @@ import {
offsetDetachId,
} from "./deltaUtil.js";
import { DetachedFieldIndex, ForestRootId } from "./detachedFieldIndex.js";
import { Major, Minor } from "./detachedFieldIndexTypes.js";
import { NodeIndex, PlaceIndex, Range } from "./pathTree.js";

/**
Expand Down Expand Up @@ -70,8 +74,16 @@ export function visitDelta(
const attachPassRoots: Map<ForestRootId, Delta.FieldMap> = new Map();
const rootTransfers: Delta.DetachedNodeRename[] = [];
const rootDestructions: Delta.DetachedNodeDestruction[] = [];
const refreshers: NestedMap<Major, Minor, ITreeCursorSynchronous> = new Map();
delta.refreshers?.forEach(({ id: { major, minor }, trees }) => {
for (let i = 0; i < trees.length; i += 1) {
const offsettedId = minor + i;
setInNestedMap(refreshers, major, offsettedId, trees[i]);
}
});
const detachConfig: PassConfig = {
func: detachPass,
refreshers,
detachedFieldIndex,
detachPassRoots,
attachPassRoots,
Expand All @@ -84,6 +96,7 @@ export function visitDelta(
transferRoots(rootTransfers, attachPassRoots, detachedFieldIndex, visitor);
const attachConfig: PassConfig = {
func: attachPass,
refreshers,
detachedFieldIndex,
detachPassRoots,
attachPassRoots,
Expand Down Expand Up @@ -289,7 +302,11 @@ export interface DeltaVisitor {
interface PassConfig {
readonly func: Pass;
readonly detachedFieldIndex: DetachedFieldIndex;

/**
* A mapping between forest root id and trees that represent refresher data. Each entry is only
* created in the forest once needed.
*/
readonly refreshers: NestedMap<Major, Minor, ITreeCursorSynchronous>;
/**
* Nested changes on roots that need to be visited as part of the detach pass.
* Each entry is removed when its associated changes are visited.
Expand Down Expand Up @@ -359,7 +376,13 @@ function visitNode(
function detachPass(delta: Delta.FieldChanges, visitor: DeltaVisitor, config: PassConfig): void {
if (delta.global !== undefined) {
for (const { id, fields } of delta.global) {
const root = config.detachedFieldIndex.getEntry(id);
let root = config.detachedFieldIndex.tryGetEntry(id);
if (root === undefined) {
const tree = tryGetFromNestedMap(config.refreshers, id.major, id.minor);
assert(tree !== undefined, "refresher data not found");
buildTrees(id, [tree], config, visitor);
root = config.detachedFieldIndex.getEntry(id);
}
config.detachPassRoots.set(root, fields);
config.attachPassRoots.set(root, fields);
}
Expand Down Expand Up @@ -396,24 +419,33 @@ function detachPass(delta: Delta.FieldChanges, visitor: DeltaVisitor, config: Pa
}
}

function buildTrees(
id: Delta.DetachedNodeId,
trees: readonly ITreeCursorSynchronous[],
config: PassConfig,
visitor: DeltaVisitor,
) {
for (let i = 0; i < trees.length; i += 1) {
const offsettedId = offsetDetachId(id, i);
let root = config.detachedFieldIndex.tryGetEntry(offsettedId);
// Tree building is idempotent. We can therefore ignore build instructions for trees that already exist.
// The idempotence is leveraged by undo/redo as well as sandwich rebasing.
if (root === undefined) {
root = config.detachedFieldIndex.createEntry(offsettedId);
const field = config.detachedFieldIndex.toFieldKey(root);
visitor.create([trees[i]], field);
}
}
}

function processBuilds(
builds: readonly Delta.DetachedNodeBuild[] | undefined,
config: PassConfig,
visitor: DeltaVisitor,
) {
if (builds !== undefined) {
for (const { id, trees } of builds) {
for (let i = 0; i < trees.length; i += 1) {
const offsettedId = offsetDetachId(id, i);
let root = config.detachedFieldIndex.tryGetEntry(offsettedId);
// Tree building is idempotent. We can therefore ignore build instructions for trees that already exist.
// The idempotence is leveraged by undo/redo as well as sandwich rebasing.
if (root === undefined) {
root = config.detachedFieldIndex.createEntry(offsettedId);
const field = config.detachedFieldIndex.toFieldKey(root);
visitor.create([trees[i]], field);
}
}
buildTrees(id, trees, config, visitor);
}
}
}
Expand Down Expand Up @@ -441,7 +473,17 @@ function attachPass(delta: Delta.FieldChanges, visitor: DeltaVisitor, config: Pa
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);
const sourceRoot = config.detachedFieldIndex.getEntry(offsetAttachId);
let sourceRoot = config.detachedFieldIndex.tryGetEntry(offsetAttachId);
if (sourceRoot === undefined) {
const tree = tryGetFromNestedMap(
config.refreshers,
offsetAttachId.major,
offsetAttachId.minor,
);
assert(tree !== undefined, "refresher data not found");
buildTrees(offsetAttachId, [tree], config, visitor);
sourceRoot = config.detachedFieldIndex.getEntry(offsetAttachId);
}
const sourceField = config.detachedFieldIndex.toFieldKey(sourceRoot);
const offsetIndex = index + i;
if (isReplaceMark(mark)) {
Expand Down
196 changes: 196 additions & 0 deletions packages/dds/tree/src/test/tree/visitDelta.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,202 @@ describe("visitDelta", () => {
{ id: node1, root: 3 },
]);
});

describe("refreshers", () => {
it("for restores at the root", () => {
const index = makeDetachedFieldIndex("", testRevisionTagCodec);
const node = { minor: 42 };
const rootFieldDelta: DeltaFieldChanges = {
local: [{ count: 1, attach: node }],
};
const delta: DeltaRoot = {
refreshers: [{ id: node, trees: [content] }],
fields: new Map([[rootKey, rootFieldDelta]]),
};
const expected: VisitScript = [
["enterField", rootKey],
["exitField", rootKey],
["enterField", rootKey],
["create", [content], field0],
["attach", field0, 1, 0],
["exitField", rootKey],
];
testVisit(delta, expected, index);
assert.equal(index.entries().next().done, true);
});

it("for restores under a child", () => {
const index = makeDetachedFieldIndex("", testRevisionTagCodec);
const buildId = { minor: 42 };
const rootFieldDelta: DeltaFieldChanges = {
local: [
{
count: 1,
fields: new Map([[fooKey, { local: [{ count: 1, attach: buildId }] }]]),
},
],
};
const expected: VisitScript = [
["enterField", rootKey],
["enterNode", 0],
["enterField", fooKey],
["exitField", fooKey],
["exitNode", 0],
["exitField", rootKey],
["enterField", rootKey],
["enterNode", 0],
["enterField", fooKey],
["create", [content], field0],
["attach", field0, 1, 0],
["exitField", fooKey],
["exitNode", 0],
["exitField", rootKey],
];
const delta: DeltaRoot = {
refreshers: [{ id: buildId, trees: [content] }],
fields: new Map([[rootKey, rootFieldDelta]]),
};
testVisit(delta, expected, index);
assert.equal(index.entries().next().done, true);
});

it("for partial restores", () => {
const index = makeDetachedFieldIndex("", testRevisionTagCodec);
const node = { minor: 42 };
const rootFieldDelta: DeltaFieldChanges = {
local: [{ count: 1, attach: { minor: 43 } }],
};
const delta: DeltaRoot = {
refreshers: [{ id: node, trees: [content, content] }],
fields: new Map([[rootKey, rootFieldDelta]]),
};
const expected: VisitScript = [
["enterField", rootKey],
["exitField", rootKey],
["enterField", rootKey],
["create", [content], field0],
["attach", field0, 1, 0],
["exitField", rootKey],
];
testVisit(delta, expected, index);
assert.equal(index.entries().next().done, true);
});

it("for changes to repair data", () => {
const index = makeDetachedFieldIndex("", testRevisionTagCodec);
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],
["attach", field0, 1, 0],
["exitField", fooKey],
["exitNode", 0],
["exitField", field1],
];
const delta: DeltaRoot = {
refreshers: [{ id: refresherId, trees: [content] }],
build: [{ id: buildId, trees: [content] }],
fields: new Map([[rootKey, rootFieldDelta]]),
};
testVisit(delta, expected, index);
});
});

describe("tolerates superfluous refreshers", () => {
it("when the delta can be applied without the refresher", () => {
const index = makeDetachedFieldIndex("", testRevisionTagCodec);
const node = { minor: 42 };
const node2 = { minor: 43 };
const rootFieldDelta: DeltaFieldChanges = {
local: [{ count: 1, attach: node2 }],
};
const delta: DeltaRoot = {
refreshers: [
{ id: node, trees: [content] },
{ id: node2, trees: [content] },
],
fields: new Map([[rootKey, rootFieldDelta]]),
};
const expected: VisitScript = [
["enterField", rootKey],
["exitField", rootKey],
["enterField", rootKey],
["create", [content], field0],
["attach", field0, 1, 0],
["exitField", rootKey],
];
testVisit(delta, expected, index);
assert.equal(index.entries().next().done, true);
});

it("when the refreshed tree already exists in the forest", () => {
yann-achard-MS marked this conversation as resolved.
Show resolved Hide resolved
const index = makeDetachedFieldIndex("", testRevisionTagCodec);
const node = { minor: 42 };
index.createEntry(node, 1);
const rootFieldDelta: DeltaFieldChanges = {
local: [{ count: 1, attach: node }],
};
const delta: DeltaRoot = {
refreshers: [{ id: node, trees: [content] }],
fields: new Map([[rootKey, rootFieldDelta]]),
};
const expected: VisitScript = [
["enterField", rootKey],
["exitField", rootKey],
["enterField", rootKey],
["attach", field0, 1, 0],
["exitField", rootKey],
];
testVisit(delta, expected, index);
assert.equal(index.entries().next().done, true);
});

it("when the refreshed tree is included in the builds", () => {
const index = makeDetachedFieldIndex("", testRevisionTagCodec);
const node = { minor: 42 };
const rootFieldDelta: DeltaFieldChanges = {
local: [{ count: 1, attach: node }],
};
const delta: DeltaRoot = {
build: [{ id: node, trees: [content] }],
refreshers: [{ id: node, trees: [content] }],
fields: new Map([[rootKey, rootFieldDelta]]),
};
const expected: VisitScript = [
["create", [content], field0],
["enterField", rootKey],
["exitField", rootKey],
["enterField", rootKey],
["attach", field0, 1, 0],
["exitField", rootKey],
];
testVisit(delta, expected, index);
assert.equal(index.entries().next().done, true);
});
});

describe("rename chains", () => {
const pointA = { minor: 1 };
for (const cycle of [false, true]) {
Expand Down
Loading