Skip to content

Commit

Permalink
tree2: Better and more consistent field cursor support (microsoft#18234)
Browse files Browse the repository at this point in the history
## Description

This better formalizes the convention of using above root placeholder
nodes, leverages this to provide field cursor builders, and makes cursor
related function naming more consistent.

## Breaking Changes

Some cursor related functions have been renamed and had argument order
changed, but nothing was removed.
  • Loading branch information
CraigMacomber authored Nov 9, 2023
1 parent 8969798 commit ed4ffda
Show file tree
Hide file tree
Showing 58 changed files with 513 additions and 267 deletions.
9 changes: 6 additions & 3 deletions experimental/dds/tree2/api-report/tree2.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1763,15 +1763,18 @@ export class SimpleDependee implements Dependee {
// @alpha
export function singleJsonCursor(root: JsonCompatible): ITreeCursorSynchronous;

// @alpha
export function singleStackTreeCursor<TNode>(root: TNode, adapter: CursorAdapter<TNode>): CursorWithNode<TNode>;

// @alpha
export function singleTextCursor(root: JsonableTree): ITreeCursorSynchronous;

// @alpha
export type StableNodeKey = Brand<StableId, "Stable Node Key">;

// @alpha
export function stackTreeFieldCursor<TNode>(adapter: CursorAdapter<TNode>, root: TNode, detachedField?: DetachedField): CursorWithNode<TNode>;

// @alpha
export function stackTreeNodeCursor<TNode>(adapter: CursorAdapter<TNode>, root: TNode): CursorWithNode<TNode>;

// @alpha
export interface StoredSchemaCollection {
readonly nodeSchema: ReadonlyMap<TreeNodeSchemaIdentifier, TreeNodeStoredSchema>;
Expand Down
1 change: 1 addition & 0 deletions experimental/dds/tree2/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export {
DetachedFieldIndex,
ForestRootId,
getDetachedFieldContainingPath,
aboveRootPlaceholder,
} from "./tree";

export {
Expand Down
1 change: 1 addition & 0 deletions experimental/dds/tree2/src/core/tree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export {
rootFieldKey,
NodeData,
rootField,
aboveRootPlaceholder,
} from "./types";
export { DeltaVisitor, visitDelta } from "./visitDelta";
export {
Expand Down
7 changes: 7 additions & 0 deletions experimental/dds/tree2/src/core/tree/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,10 @@ export interface NodeData {
*/
readonly type: TreeNodeSchemaIdentifier;
}

/**
* Use this type to indicate that a node sits above the detached fields, and thus is not a real node and who's type should not matter.
*/
export const aboveRootPlaceholder: TreeNodeSchemaIdentifier = brand(
"com.fluidframework.placeholder.aboveRoot",
);
4 changes: 2 additions & 2 deletions experimental/dds/tree2/src/domains/json/jsonCursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
ITreeCursorSynchronous,
} from "../../core";
import { JsonCompatible } from "../../util";
import { CursorAdapter, isPrimitiveValue, singleStackTreeCursor } from "../../feature-libraries";
import { CursorAdapter, isPrimitiveValue, stackTreeNodeCursor } from "../../feature-libraries";
import { leaf } from "../leafDomain";
import { jsonArray, jsonObject } from "./jsonDomainSchema";

Expand Down Expand Up @@ -94,7 +94,7 @@ const adapter: CursorAdapter<JsonCompatible> = {
* @alpha
*/
export function singleJsonCursor(root: JsonCompatible): ITreeCursorSynchronous {
return singleStackTreeCursor(root, adapter);
return stackTreeNodeCursor(adapter, root);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
PlaceIndex,
Range,
ITreeCursorSynchronous,
aboveRootPlaceholder,
} from "../../core";
import { assertValidRange, brand, fail, getOrAddEmptyToMap } from "../../util";
import { createEmitter } from "../../events";
Expand All @@ -35,7 +36,7 @@ import { basicChunkTree, chunkTree, IChunker } from "./chunkTree";
import { ChunkedCursor, TreeChunk } from "./chunk";

function makeRoot(): BasicChunk {
return new BasicChunk(brand("above root placeholder"), new Map());
return new BasicChunk(aboveRootPlaceholder, new Map());
}

interface StackNode {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
TreeNodeSchema,
allowedTypesToTypeSet,
} from "./typed-schema";
import { singleMapTreeCursor } from "./mapTreeCursor";
import { cursorForMapTreeNode } from "./mapTreeCursor";
import { AllowedTypesToTypedTrees, ApiMode, TypedField, TypedNode } from "./schema-aware";

/**
Expand Down Expand Up @@ -485,7 +485,7 @@ export function cursorFromContextualData(
data: ContextuallyTypedNodeData,
): ITreeCursorSynchronous {
const mapTree = applyTypesFromContext(context, typeSet, data);
return singleMapTreeCursor(mapTree);
return cursorForMapTreeNode(mapTree);
}

/**
Expand Down Expand Up @@ -534,7 +534,7 @@ export function cursorsFromContextualData(
data: ContextuallyTypedNodeData | undefined,
): ITreeCursorSynchronous[] {
const mapTrees = applyFieldTypesFromContext(context, field, data);
return mapTrees.map(singleMapTreeCursor);
return mapTrees.map(cursorForMapTreeNode);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
makeDetachedNodeId,
} from "../../core";
import { fail, Mutable, IdAllocator, SizedNestedMap } from "../../util";
import { singleTextCursor, jsonableTreeFromCursor } from "../treeTextCursor";
import { cursorForJsonableTreeNode, jsonableTreeFromCursor } from "../treeTextCursor";
import {
ToDelta,
FieldChangeRebaser,
Expand Down Expand Up @@ -556,7 +556,7 @@ export function optionalFieldIntoDelta(
} else {
if (Object.prototype.hasOwnProperty.call(update, "set")) {
const setUpdate = update as { set: JsonableTree; buildId: ChangeAtomId };
const content = [singleTextCursor(setUpdate.set)];
const content = [cursorForJsonableTreeNode(setUpdate.set)];
const buildId = makeDetachedNodeId(
setUpdate.buildId.revision ?? change.fieldChange.revision ?? revision,
setUpdate.buildId.localId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
import { assertValidIndex, assertValidRangeIndices, brand, disposeSymbol, fail } from "../../util";
import { AllowedTypes, TreeFieldSchema } from "../typed-schema";
import { LocalNodeKey, StableNodeKey, nodeKeyTreeIdentifier } from "../node-key";
import { mapTreeFromCursor, singleMapTreeCursor } from "../mapTreeCursor";
import { mapTreeFromCursor, cursorForMapTreeNode } from "../mapTreeCursor";
import { Context } from "./context";
import {
FlexibleNodeContent,
Expand Down Expand Up @@ -541,7 +541,7 @@ function prepareFieldCursorForInsert(cursor: ITreeCursorSynchronous): ITreeCurso
// Convert from the desired API (single field cursor) to the currently required API (array of node cursors).
// This is inefficient, and particularly bad if the data was efficiently chunked using uniform chunks.
// TODO: update editing APIs to take in field cursors not arrays of node cursors, then remove this copying conversion.
return mapCursorField(cursor, () => singleMapTreeCursor(mapTreeFromCursor(cursor)));
return mapCursorField(cursor, () => cursorForMapTreeNode(mapTreeFromCursor(cursor)));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from "../core";
import { Summarizable, SummaryElementParser, SummaryElementStringifier } from "../shared-tree-core";
import { idAllocatorFromMaxId } from "../util";
import { jsonableTreeFromCursor, singleTextCursor } from "./treeTextCursor";
import { jsonableTreeFromCursor, cursorForJsonableTreeNode } from "./treeTextCursor";

/**
* The storage key for the blob in the summary containing tree data
Expand Down Expand Up @@ -99,7 +99,7 @@ export class ForestSummarizer implements Summarizable {
return [
fieldKey,
{
build: [{ id: buildId, trees: content.map(singleTextCursor) }],
build: [{ id: buildId, trees: content.map(cursorForJsonableTreeNode) }],
local: [{ count: content.length, attach: buildId }],
},
];
Expand Down
8 changes: 5 additions & 3 deletions experimental/dds/tree2/src/feature-libraries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,23 @@ export {
} from "./contextuallyTyped";

export { ForestSummarizer } from "./forestSummarizer";
export { singleMapTreeCursor, mapTreeFromCursor } from "./mapTreeCursor";
export { cursorForMapTreeField, cursorForMapTreeNode, mapTreeFromCursor } from "./mapTreeCursor";
export { MemoizedIdRangeAllocator, IdRange } from "./memoizedIdRangeAllocator";
export { buildForest } from "./object-forest";
export { SchemaSummarizer, SchemaEditor, encodeTreeSchema } from "./schemaSummarizer";
// This is exported because its useful for doing comparisons of schema in tests.
export { makeSchemaCodec } from "./schemaIndexFormat";
export {
singleStackTreeCursor,
stackTreeNodeCursor,
CursorAdapter,
prefixPath,
prefixFieldPath,
CursorWithNode,
stackTreeFieldCursor,
} from "./treeCursorUtils";
export {
singleTextCursor,
cursorForJsonableTreeNode,
cursorForJsonableTreeField,
jsonableTreeFromCursor,
jsonableTreeFromFieldCursor,
jsonableTreeFromForest,
Expand Down
43 changes: 38 additions & 5 deletions experimental/dds/tree2/src/feature-libraries/mapTreeCursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,47 @@
*/

import { assert } from "@fluidframework/core-utils";
import { FieldKey, MapTree, ITreeCursor, CursorLocationType, mapCursorField } from "../core";
import { CursorAdapter, CursorWithNode, singleStackTreeCursor } from "./treeCursorUtils";
import {
FieldKey,
MapTree,
ITreeCursor,
CursorLocationType,
mapCursorField,
DetachedField,
detachedFieldAsKey,
rootField,
aboveRootPlaceholder,
} from "../core";
import {
CursorAdapter,
CursorWithNode,
stackTreeFieldCursor,
stackTreeNodeCursor,
} from "./treeCursorUtils";

/**
* @returns an ITreeCursorSynchronous for a single MapTree.
* @returns an {@link ITreeCursorSynchronous} in nodes mode for a single MapTree.
*/
export function singleMapTreeCursor(root: MapTree): CursorWithNode<MapTree> {
return singleStackTreeCursor(root, adapter);
export function cursorForMapTreeNode(root: MapTree): CursorWithNode<MapTree> {
return stackTreeNodeCursor(adapter, root);
}

/**
* @returns an {@link ITreeCursorSynchronous} in fields mode for a MapTree field.
*/
export function cursorForMapTreeField(
root: MapTree[],
detachedField: DetachedField = rootField,
): CursorWithNode<MapTree> {
const key = detachedFieldAsKey(detachedField);
return stackTreeFieldCursor(
adapter,
{
type: aboveRootPlaceholder,
fields: new Map([[key, root]]),
},
detachedField,
);
}

const adapter: CursorAdapter<MapTree> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
PlaceIndex,
Value,
ITreeCursorSynchronous,
aboveRootPlaceholder,
} from "../../core";
import {
brand,
Expand All @@ -41,12 +42,12 @@ import {
assertNonNegativeSafeInteger,
} from "../../util";
import { CursorWithNode, SynchronousCursor } from "../treeCursorUtils";
import { mapTreeFromCursor, singleMapTreeCursor } from "../mapTreeCursor";
import { mapTreeFromCursor, cursorForMapTreeNode } from "../mapTreeCursor";
import { createEmitter } from "../../events";

function makeRoot(): MapTree {
return {
type: brand("above root placeholder"),
type: aboveRootPlaceholder,
fields: new Map(),
};
}
Expand Down Expand Up @@ -89,7 +90,7 @@ class ObjectForest extends SimpleDependee implements IEditableForest {
// they are assumed to be copy on write. See TODO on NodeData.
forest.roots.fields.set(
key,
value.map((v) => mapTreeFromCursor(singleMapTreeCursor(v))),
value.map((v) => mapTreeFromCursor(cursorForMapTreeNode(v))),
);
}
return forest;
Expand Down Expand Up @@ -323,7 +324,7 @@ class ObjectForest extends SimpleDependee implements IEditableForest {
}

public getCursorAboveDetachedFields(): ITreeCursorSynchronous {
return singleMapTreeCursor(this.roots);
return cursorForMapTreeNode(this.roots);
}
}

Expand Down Expand Up @@ -462,7 +463,7 @@ class Cursor extends SynchronousCursor implements ITreeSubscriptionCursor {
);
this.clear();
this.state = ITreeSubscriptionCursorState.Current;
this.innerCursor = singleMapTreeCursor(this.forest.roots);
this.innerCursor = cursorForMapTreeNode(this.forest.roots);
this.forest.currentCursors.add(this);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { assert, unreachableCase } from "@fluidframework/core-utils";
import { fail, Mutable } from "../../util";
import { Delta, TaggedChange, areEqualChangeAtomIds, makeDetachedNodeId } from "../../core";
import { nodeIdFromChangeAtom } from "../deltaUtils";
import { singleTextCursor } from "../treeTextCursor";
import { cursorForJsonableTreeNode } from "../treeTextCursor";
import { MarkList, NoopMarkType } from "./format";
import {
areInputCellsEmpty,
Expand Down Expand Up @@ -66,7 +66,7 @@ export function sequenceFieldToDelta<TNodeChange>(
if (mark.attach.type === "Insert" && mark.attach.content !== undefined) {
build.push({
id: oldId,
trees: mark.attach.content.map(singleTextCursor),
trees: mark.attach.content.map(cursorForJsonableTreeNode),
});
}
rename.push({
Expand Down Expand Up @@ -142,7 +142,7 @@ export function sequenceFieldToDelta<TNodeChange>(
);
build.push({
id: buildId,
trees: mark.content.map(singleTextCursor),
trees: mark.content.map(cursorForJsonableTreeNode),
});
}
local.push(deltaMark);
Expand Down
32 changes: 25 additions & 7 deletions experimental/dds/tree2/src/feature-libraries/treeCursorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
FieldUpPath,
PathRootPrefix,
CursorMarker,
DetachedField,
rootField,
detachedFieldAsKey,
} from "../core";
import { fail } from "../util";

Expand Down Expand Up @@ -43,21 +46,36 @@ export interface CursorWithNode<TNode> extends ITreeCursorSynchronous {
/**
* Create a cursor, in `nodes` mode at the root of the provided tree.
*
* @returns an {@link ITreeCursorSynchronous} for a single root.
* @returns an {@link ITreeCursorSynchronous} for a single root in `nodes` mode.
* @alpha
*
* @privateRemarks
* A version of this API which produces field cursors should be provided.
*/
export function singleStackTreeCursor<TNode>(
root: TNode,
export function stackTreeNodeCursor<TNode>(
adapter: CursorAdapter<TNode>,
root: TNode,
): CursorWithNode<TNode> {
return new StackCursor(adapter, [], [], [root], 0);
}

/**
* Provides functionality to allow a {@link singleStackTreeCursor} to implement a cursor.
* Create a cursor, in `fields` mode at the `detachedField` under the provided `root`.
*
* @returns an {@link ITreeCursorSynchronous} for `detachedField` of `root` in `fields` mode.
* @alpha
*/
export function stackTreeFieldCursor<TNode>(
adapter: CursorAdapter<TNode>,
root: TNode,
detachedField: DetachedField = rootField,
): CursorWithNode<TNode> {
const cursor = stackTreeNodeCursor(adapter, root);
// Because the root node in `stackTreeNodeCursor` is treated as the above detached fields node,
// using it then just entering the correct field doesn't mess up the paths reported by the cursor.
cursor.enterField(detachedFieldAsKey(detachedField));
return cursor;
}

/**
* Provides functionality to allow a {@link stackTreeNodeCursor} and {@link stackTreeFieldCursor} to implement cursors.
* @alpha
*/
export interface CursorAdapter<TNode> {
Expand Down
Loading

0 comments on commit ed4ffda

Please sign in to comment.