diff --git a/packages/dds/tree/src/feature-libraries/flex-tree/context.ts b/packages/dds/tree/src/feature-libraries/flex-tree/context.ts index 810afa850946..765bb77e26b2 100644 --- a/packages/dds/tree/src/feature-libraries/flex-tree/context.ts +++ b/packages/dds/tree/src/feature-libraries/flex-tree/context.ts @@ -159,7 +159,7 @@ export class Context implements FlexTreeHydratedContext, IDisposable { assert(this.disposed === false, 0x804 /* use after dispose */); const cursor = this.checkout.forest.allocateCursor("root"); moveToDetachedField(this.checkout.forest, cursor); - const field = makeField(this, this.flexSchema.rootFieldSchema, cursor); + const field = makeField(this, this.schema.rootFieldSchema, cursor); cursor.free(); return field; } diff --git a/packages/dds/tree/src/feature-libraries/flex-tree/flexTreeTypes.ts b/packages/dds/tree/src/feature-libraries/flex-tree/flexTreeTypes.ts index 4bfaa49bb9c2..5673cbdbc430 100644 --- a/packages/dds/tree/src/feature-libraries/flex-tree/flexTreeTypes.ts +++ b/packages/dds/tree/src/feature-libraries/flex-tree/flexTreeTypes.ts @@ -20,7 +20,6 @@ import type { OptionalFieldEditBuilder, } from "../default-schema/index.js"; import type { FlexFieldKind } from "../modular-schema/index.js"; -import type { FlexTreeNodeSchema } from "../typed-schema/index.js"; import type { FlexTreeContext } from "./context.js"; @@ -126,11 +125,7 @@ export enum TreeStatus { * the same {@link FlexTreeNode} instance will be used in the new location. * Similarly, edits applied to a node's sub-tree concurrently with the move of the node will still be applied to its subtree in its new location. * - * * @remarks - * Down-casting (via {@link FlexTreeNode#is}) is required to access Schema-Aware APIs, including editing. - * All content in the tree is accessible without down-casting, but if the schema is known, - * the schema aware API may be more ergonomic. * All editing is actually done via {@link FlexTreeField}s: the nodes are immutable other than that they contain mutable fields. */ export interface FlexTreeNode extends FlexTreeEntity { @@ -162,11 +157,6 @@ export interface FlexTreeNode extends FlexTreeEntity { */ readonly parentField: { readonly parent: FlexTreeField; readonly index: number }; - /** - * Type guard for narrowing / down-casting to a specific schema. - */ - is(schema: FlexTreeNodeSchema): boolean; - boxedIterator(): IterableIterator; /** diff --git a/packages/dds/tree/src/feature-libraries/flex-tree/lazyEntity.ts b/packages/dds/tree/src/feature-libraries/flex-tree/lazyEntity.ts index f00355cdae37..b9d8b4fd09b5 100644 --- a/packages/dds/tree/src/feature-libraries/flex-tree/lazyEntity.ts +++ b/packages/dds/tree/src/feature-libraries/flex-tree/lazyEntity.ts @@ -43,15 +43,12 @@ export function assertFlexTreeEntityNotFreed(entity: FlexTreeEntity): void { /** * This is a base class for lazy (cursor based) UntypedEntity implementations, which uniformly handles cursors and anchors. */ -export abstract class LazyEntity - implements FlexTreeEntity, IDisposable -{ +export abstract class LazyEntity implements FlexTreeEntity, IDisposable { readonly #lazyCursor: ITreeSubscriptionCursor; public readonly [anchorSymbol]: TAnchor; protected constructor( public readonly context: Context, - public readonly flexSchema: TSchema, cursor: ITreeSubscriptionCursor, anchor: TAnchor, ) { diff --git a/packages/dds/tree/src/feature-libraries/flex-tree/lazyField.ts b/packages/dds/tree/src/feature-libraries/flex-tree/lazyField.ts index 994fcf50b360..09c58e2897bd 100644 --- a/packages/dds/tree/src/feature-libraries/flex-tree/lazyField.ts +++ b/packages/dds/tree/src/feature-libraries/flex-tree/lazyField.ts @@ -10,6 +10,7 @@ import { type ExclusiveMapTree, type FieldAnchor, type FieldKey, + type FieldKindIdentifier, type FieldUpPath, type ITreeCursorSynchronous, type ITreeSubscriptionCursor, @@ -27,7 +28,6 @@ import { type ValueFieldEditBuilder, } from "../default-schema/index.js"; import type { FlexFieldKind } from "../modular-schema/index.js"; -import type { FlexFieldSchema } from "../typed-schema/index.js"; import type { Context } from "./context.js"; import { @@ -75,7 +75,7 @@ const fieldCache: WeakMap> = new Weak export function makeField( context: Context, - schema: FlexFieldSchema, + schema: TreeFieldStoredSchema, cursor: ITreeSubscriptionCursor, ): FlexTreeField { const fieldAnchor = cursor.buildFieldAnchor(); @@ -125,19 +125,12 @@ export function makeField( /** * Base type for fields implementing {@link FlexTreeField} using cursors. */ -export abstract class LazyField - extends LazyEntity, FieldAnchor> - implements FlexTreeField -{ +export abstract class LazyField extends LazyEntity implements FlexTreeField { public get [flexTreeMarker](): FlexTreeEntityKind.Field { return FlexTreeEntityKind.Field; } public readonly key: FieldKey; - public get schema(): TreeFieldStoredSchema { - return this.flexSchema.stored; - } - /** * If this field ends its lifetime before the Anchor does, this needs to be invoked to avoid a double free * if/when the Anchor is destroyed. @@ -146,11 +139,11 @@ export abstract class LazyField public constructor( context: Context, - schema: FlexFieldSchema, + public readonly schema: TreeFieldStoredSchema, cursor: ITreeSubscriptionCursor, fieldAnchor: FieldAnchor, ) { - super(context, schema, cursor, fieldAnchor); + super(context, cursor, fieldAnchor); assert(cursor.mode === CursorLocationType.Fields, 0x77b /* must be in fields mode */); this.key = cursor.getFieldKey(); // Fields currently live as long as their parent does. @@ -171,7 +164,7 @@ export abstract class LazyField 0xa26 /* Narrowing must be done to a kind that exists in this context */, ); - return this.flexSchema.kind === (kind as unknown); + return this.schema.kind === kind.identifier; } public get parent(): FlexTreeNode | undefined { @@ -259,19 +252,7 @@ export abstract class LazyField } } -export class LazySequence - extends LazyField - implements FlexTreeSequenceField -{ - public constructor( - context: Context, - schema: FlexFieldSchema, - cursor: ITreeSubscriptionCursor, - fieldAnchor: FieldAnchor, - ) { - super(context, schema, cursor, fieldAnchor); - } - +export class LazySequence extends LazyField implements FlexTreeSequenceField { public at(index: number): FlexTreeUnknownUnboxed | undefined { const finalIndex = indexForAt(index, this.length); @@ -302,19 +283,7 @@ export class LazySequence } } -export class ReadonlyLazyValueField - extends LazyField - implements FlexTreeRequiredField -{ - public constructor( - context: Context, - schema: FlexFieldSchema, - cursor: ITreeSubscriptionCursor, - fieldAnchor: FieldAnchor, - ) { - super(context, schema, cursor, fieldAnchor); - } - +export class ReadonlyLazyValueField extends LazyField implements FlexTreeRequiredField { public editor: ValueFieldEditBuilder = { set: (newContent) => { assert(false, 0xa0c /* Unexpected set of readonly field */); @@ -327,15 +296,6 @@ export class ReadonlyLazyValueField } export class LazyValueField extends ReadonlyLazyValueField implements FlexTreeRequiredField { - public constructor( - context: Context, - schema: FlexFieldSchema, - cursor: ITreeSubscriptionCursor, - fieldAnchor: FieldAnchor, - ) { - super(context, schema, cursor, fieldAnchor); - } - public override editor: ValueFieldEditBuilder = { set: (newContent) => { this.valueFieldEditor().set(cursorForMapTreeNode(newContent)); @@ -353,19 +313,7 @@ export class LazyValueField extends ReadonlyLazyValueField implements FlexTreeRe } } -export class LazyOptionalField - extends LazyField - implements FlexTreeOptionalField -{ - public constructor( - context: Context, - schema: FlexFieldSchema, - cursor: ITreeSubscriptionCursor, - fieldAnchor: FieldAnchor, - ) { - super(context, schema, cursor, fieldAnchor); - } - +export class LazyOptionalField extends LazyField implements FlexTreeOptionalField { public editor: OptionalFieldEditBuilder = { set: (newContent, wasEmpty) => { this.optionalEditor().set( @@ -386,25 +334,22 @@ export class LazyOptionalField } } -export class LazyForbiddenField extends LazyField {} +export class LazyForbiddenField extends LazyField {} type Builder = new ( context: Context, // Correct use of these builders requires the builder of the matching type to be used. - // Since this has to be done at runtime anyway, trying to use safer typing than `any` here (such as `never`, which is only slightly safer) - // does not seem worth it (ends up requiring type casts that are just as unsafe). - // eslint-disable-next-line @typescript-eslint/no-explicit-any - schema: FlexFieldSchema, + schema: TreeFieldStoredSchema, cursor: ITreeSubscriptionCursor, fieldAnchor: FieldAnchor, -) => LazyField; - -const builderList: [FlexFieldKind, Builder][] = [ - [FieldKinds.forbidden, LazyForbiddenField], - [FieldKinds.optional, LazyOptionalField], - [FieldKinds.sequence, LazySequence], - [FieldKinds.required, LazyValueField], - [FieldKinds.identifier, LazyValueField], +) => LazyField; + +const builderList: [FieldKindIdentifier, Builder][] = [ + [FieldKinds.forbidden.identifier, LazyForbiddenField], + [FieldKinds.optional.identifier, LazyOptionalField], + [FieldKinds.sequence.identifier, LazySequence], + [FieldKinds.required.identifier, LazyValueField], + [FieldKinds.identifier.identifier, LazyValueField], ]; -const kindToClass: ReadonlyMap = new Map(builderList); +const kindToClass: ReadonlyMap = new Map(builderList); diff --git a/packages/dds/tree/src/feature-libraries/flex-tree/lazyNode.ts b/packages/dds/tree/src/feature-libraries/flex-tree/lazyNode.ts index e13a3d8c030e..840dca8d9014 100644 --- a/packages/dds/tree/src/feature-libraries/flex-tree/lazyNode.ts +++ b/packages/dds/tree/src/feature-libraries/flex-tree/lazyNode.ts @@ -13,6 +13,7 @@ import { type ITreeSubscriptionCursor, type TreeNavigationResult, type TreeNodeSchemaIdentifier, + type TreeNodeStoredSchema, type Value, inCursorField, mapCursorFields, @@ -52,10 +53,9 @@ export function makeTree(context: Context, cursor: ITreeSubscriptionCursor): Laz context.checkout.forest.anchors.forget(anchor); assert(cached.context === context, 0x782 /* contexts must match */); assert(cached instanceof LazyTreeNode, 0x92c /* Expected LazyTreeNode */); - return cached as LazyTreeNode; + return cached; } - const schema = context.flexSchema.nodeSchema.get(cursor.type) ?? fail("missing schema"); - return new LazyTreeNode(context, schema, cursor, anchorNode, anchor); + return new LazyTreeNode(context, cursor.type, cursor, anchorNode, anchor); } function cleanupTree(anchor: AnchorNode): void { @@ -67,47 +67,37 @@ function cleanupTree(anchor: AnchorNode): void { /** * Lazy implementation of {@link FlexTreeNode}. */ -export class LazyTreeNode - extends LazyEntity - implements FlexTreeNode -{ +export class LazyTreeNode extends LazyEntity implements FlexTreeNode { public get [flexTreeMarker](): FlexTreeEntityKind.Node { return FlexTreeEntityKind.Node; } - public get schema(): TreeNodeSchemaIdentifier { - return this.flexSchema.name; - } - // Using JS private here prevents it from showing up as a enumerable own property, or conflicting with struct fields. readonly #removeDeleteCallback: () => void; + private readonly storedSchema: TreeNodeStoredSchema; + private readonly flexSchema: FlexTreeNodeSchema; + public constructor( context: Context, - schema: TSchema, + public readonly schema: TreeNodeSchemaIdentifier, cursor: ITreeSubscriptionCursor, public readonly anchorNode: AnchorNode, anchor: Anchor, ) { - super(context, schema, cursor, anchor); + super(context, cursor, anchor); + this.storedSchema = context.schema.nodeSchema.get(this.schema) ?? fail("missing schema"); + this.flexSchema = context.flexSchema.nodeSchema.get(this.schema) ?? fail("missing schema"); assert(cursor.mode === CursorLocationType.Nodes, 0x783 /* must be in nodes mode */); anchorNode.slots.set(flexTreeSlot, this); this.#removeDeleteCallback = anchorNode.on("afterDestroy", cleanupTree); assert( - this.context.flexSchema.nodeSchema.get(this.flexSchema.name) !== undefined, + this.context.flexSchema.nodeSchema.get(this.schema) !== undefined, 0x784 /* There is no explicit schema for this node type. Ensure that the type is correct and the schema for it was added to the TreeStoredSchema */, ); } - public is(schema: FlexTreeNodeSchema): boolean { - assert( - this.context.flexSchema.nodeSchema.get(schema.name) === schema, - 0x785 /* Narrowing must be done to a schema that exists in this context */, - ); - return this.flexSchema === (schema as unknown); - } - protected override [tryMoveCursorToAnchorSymbol]( cursor: ITreeSubscriptionCursor, ): TreeNavigationResult { @@ -133,20 +123,24 @@ export class LazyTreeNode { - return makeField(this.context, fieldSchema, cursor); + return makeField(this.context, fieldSchema.stored, cursor); }); } public boxedIterator(): IterableIterator { return mapCursorFields(this[cursorSymbol], (cursor) => - makeField(this.context, this.flexSchema.getFieldSchema(cursor.getFieldKey()), cursor), + makeField( + this.context, + this.flexSchema.getFieldSchema(cursor.getFieldKey()).stored, + cursor, + ), ).values(); } @@ -194,7 +188,7 @@ export class LazyTreeNode { { jsonValidator: typeboxValidator }, ); const dummyEditor = new DefaultEditBuilder(new DefaultChangeFamily(codec), changeReceiver); - const checkout = new MockTreeCheckout(forest, dummyEditor as unknown as ISharedTreeEditor); + const checkout = new MockTreeCheckout(forest, { + editor: dummyEditor as unknown as ISharedTreeEditor, + }); checkout.editor .sequenceField({ field: rootFieldKey, parent: undefined }) .insert(0, chunk.cursor()); diff --git a/packages/dds/tree/src/test/feature-libraries/flex-tree/lazyField.spec.ts b/packages/dds/tree/src/test/feature-libraries/flex-tree/lazyField.spec.ts index 487b37d75140..a707acd29acc 100644 --- a/packages/dds/tree/src/test/feature-libraries/flex-tree/lazyField.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/flex-tree/lazyField.spec.ts @@ -12,6 +12,7 @@ import { validateAssertionError } from "@fluidframework/test-runtime-utils/inter import { type FieldAnchor, type FieldKey, + TreeStoredSchemaRepository, type UpPath, rootFieldKey, } from "../../../core/index.js"; @@ -25,14 +26,16 @@ import { import { FieldKinds, FlexFieldSchema, + MockNodeKeyManager, cursorForJsonableTreeNode, defaultSchemaPolicy, + getTreeContext, + intoStoredSchema, mapTreeFromCursor, - type FlexFieldKind, type FlexTreeSchema, } from "../../../feature-libraries/index.js"; import { brand, disposeSymbol } from "../../../util/index.js"; -import { flexTreeViewWithContent, forestWithContent } from "../../utils.js"; +import { flexTreeViewWithContent, forestWithContent, MockTreeCheckout } from "../../utils.js"; import { getReadonlyContext, @@ -48,7 +51,7 @@ import { stringSchema, toFlexSchema, } from "../../../simple-tree/index.js"; -import { getFlexSchema } from "../../../simple-tree/toFlexSchema.js"; +import { getFlexSchema, toStoredSchema } from "../../../simple-tree/toFlexSchema.js"; import { JsonObject, singleJsonCursor } from "../../json/index.js"; const detachedField: FieldKey = brand("detached"); @@ -57,27 +60,27 @@ const detachedFieldAnchor: FieldAnchor = { parent: undefined, fieldKey: detached /** * Test {@link LazyField} implementation. */ -class TestLazyField extends LazyField {} +class TestLazyField extends LazyField {} describe("LazyField", () => { it("LazyField implementations do not allow edits to detached trees", () => { - const schema = toFlexSchema(JsonObject); + const schema = toStoredSchema(JsonObject); const forest = forestWithContent({ schema, initialTree: singleJsonCursor({}), }); - const context = getReadonlyContext(forest, schema); + const context = getReadonlyContext(forest, JsonObject); const cursor = initializeCursor(context, detachedFieldAnchor); const optionalField = new LazyOptionalField( context, - FlexFieldSchema.create(FieldKinds.optional, [getFlexSchema(JsonObject)]), + FlexFieldSchema.create(FieldKinds.optional, [getFlexSchema(JsonObject)]).stored, cursor, detachedFieldAnchor, ); const valueField = new LazyValueField( context, - FlexFieldSchema.create(FieldKinds.required, [getFlexSchema(JsonObject)]), + FlexFieldSchema.create(FieldKinds.required, [getFlexSchema(JsonObject)]).stored, cursor, detachedFieldAnchor, ); @@ -99,12 +102,11 @@ describe("LazyField", () => { const builder = new SchemaFactory("test"); const rootSchema = builder.optional([builder.object("object", {})]); - const schema = toFlexSchema(rootSchema); // Note: this tree initialization is strictly to enable construction of the lazy field. // The test cases below are strictly in terms of the schema of the created fields. const { context, cursor } = readonlyTreeWithContent({ - schema, + schema: rootSchema, initialTree: singleJsonCursor({}), }); @@ -114,7 +116,7 @@ describe("LazyField", () => { const booleanOptionalField = new LazyOptionalField( context, - FlexFieldSchema.create(FieldKinds.optional, [getFlexSchema(booleanSchema)]), + FlexFieldSchema.create(FieldKinds.optional, [getFlexSchema(booleanSchema)]).stored, cursor, detachedFieldAnchor, ); @@ -130,10 +132,10 @@ describe("LazyField", () => { class Struct extends factory.object("Struct", { foo: factory.number, }) {} - const schema = toFlexSchema(Struct); + const schema = toStoredSchema(Struct); const { context, cursor } = readonlyTreeWithContent({ - schema, + schema: Struct, initialTree: cursorFromInsertable(Struct, { foo: 5 }), }); @@ -158,7 +160,7 @@ describe("LazyField", () => { const leafField = new TestLazyField( context, - toFlexSchema(factory.number).rootFieldSchema, + toFlexSchema(factory.number).rootFieldSchema.stored, cursor, { parent: parentAnchor, @@ -170,12 +172,12 @@ describe("LazyField", () => { it("Disposes when context is disposed", () => { const factory = new SchemaFactory("LazyField"); - const schema = toFlexSchema(factory.number); + const schema = toStoredSchema(factory.number); const forest = forestWithContent({ schema, initialTree: cursorFromInsertable(factory.number, 5), }); - const context = getReadonlyContext(forest, schema); + const context = getReadonlyContext(forest, factory.number); const cursor = initializeCursor(context, detachedFieldAnchor); const field = new TestLazyField( @@ -193,15 +195,15 @@ describe("LazyField", () => { it("Disposes when parent is disposed", () => { const factory = new SchemaFactory("LazyField"); class Holder extends factory.object("holder", { f: factory.number }) {} - const schema = toFlexSchema(Holder); + const schema = toStoredSchema(Holder); const forest = forestWithContent({ schema, initialTree: cursorFromInsertable(Holder, { f: 5 }), }); - const context = getReadonlyContext(forest, schema); + const context = getReadonlyContext(forest, Holder); const holder = [...context.root.boxedIterator()][0]; - assert(holder.is(getFlexSchema(Holder))); + assert(holder.schema === Holder.identifier); const field = holder.getBoxed(brand("f")); assert(field instanceof LazyField); @@ -217,15 +219,15 @@ describe("LazyField", () => { it("Disposes when context then parent is disposed", () => { const factory = new SchemaFactory("LazyField"); class Holder extends factory.object("holder", { f: factory.number }) {} - const schema = toFlexSchema(Holder); + const schema = toStoredSchema(Holder); const forest = forestWithContent({ schema, initialTree: cursorFromInsertable(Holder, { f: 5 }), }); - const context = getReadonlyContext(forest, schema); + const context = getReadonlyContext(forest, Holder); const holder = [...context.root.boxedIterator()][0]; - assert(holder.is(getFlexSchema(Holder))); + assert(holder.schema === Holder.identifier); const field = holder.getBoxed(brand("f")); assert(field instanceof LazyField); @@ -240,8 +242,9 @@ describe("LazyField", () => { describe("LazyOptionalField", () => { const builder = new SchemaFactory("test"); - const schema = toFlexSchema(builder.optional(builder.number)); - const rootSchema = schema.rootFieldSchema as FlexFieldSchema; + const schema = builder.optional(builder.number); + const storedSchema = toStoredSchema(schema); + const rootSchema = storedSchema.rootFieldSchema; describe("Field with value", () => { const { context, cursor } = readonlyTreeWithContent({ @@ -329,8 +332,9 @@ describe("LazyOptionalField", () => { describe("LazyValueField", () => { const builder = new SchemaFactory("test"); - const schema = toFlexSchema(builder.required(builder.string)); - const rootSchema = schema.rootFieldSchema as FlexFieldSchema; + const schema = builder.required(builder.string); + const schemaStored = toStoredSchema(schema); + const rootSchema = schemaStored.rootFieldSchema; const initialTree = "Hello world"; const { context, cursor } = readonlyTreeWithContent({ @@ -394,11 +398,21 @@ describe("LazySequence", () => { * Creates a tree with a sequence of numbers at the root, and returns the sequence */ function testSequence(data: number[]) { - const { context, cursor } = readonlyTreeWithContent({ - schema, - initialTree: data.map((n) => singleJsonCursor(n)), + const content = data.map((n) => singleJsonCursor(n)); + const forest = forestWithContent({ + schema: intoStoredSchema(schema), + initialTree: content, }); - return new LazySequence(context, rootSchema, cursor, rootFieldAnchor); + const context = getTreeContext( + schema, + new MockTreeCheckout(forest, { + schema: new TreeStoredSchemaRepository(intoStoredSchema(schema)), + }), + new MockNodeKeyManager(), + ); + const cursor = initializeCursor(context, rootFieldAnchor); + + return new LazySequence(context, rootSchema.stored, cursor, rootFieldAnchor); } it("atIndex", () => { diff --git a/packages/dds/tree/src/test/feature-libraries/flex-tree/lazyNode.spec.ts b/packages/dds/tree/src/test/feature-libraries/flex-tree/lazyNode.spec.ts index dd3746c978d0..998da8e19ae3 100644 --- a/packages/dds/tree/src/test/feature-libraries/flex-tree/lazyNode.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/flex-tree/lazyNode.spec.ts @@ -11,73 +11,25 @@ import { type Anchor, type AnchorNode, EmptyKey, - type FieldAnchor, type FieldKey, type ITreeSubscriptionCursor, type MapTree, - TreeNavigationResult, rootFieldKey, } from "../../../core/index.js"; import type { Context } from "../../../feature-libraries/flex-tree/context.js"; import { LazyTreeNode } from "../../../feature-libraries/flex-tree/lazyNode.js"; -import type { - FlexAllowedTypes, - FlexFieldKind, - FlexTreeField, - FlexTreeNode, - FlexTreeNodeSchema, -} from "../../../feature-libraries/index.js"; -import type { TreeContent } from "../../../shared-tree/index.js"; - -import { contextWithContentReadonly } from "./utils.js"; -import { - cursorFromInsertable, - SchemaFactory, - toFlexSchema, -} from "../../../simple-tree/index.js"; -import { getFlexSchema } from "../../../simple-tree/toFlexSchema.js"; -import { JsonArray, JsonObject, singleJsonCursor } from "../../json/index.js"; -import { stringSchema } from "../../../simple-tree/leafNodeSchema.js"; - -const rootFieldAnchor: FieldAnchor = { parent: undefined, fieldKey: rootFieldKey }; - -/** - * Creates a cursor from the provided `context` and moves it to the provided `anchor`. - */ -function initializeCursor(context: Context, anchor: FieldAnchor): ITreeSubscriptionCursor { - const cursor = context.checkout.forest.allocateCursor(); - - assert.equal( - context.checkout.forest.tryMoveCursorToField(anchor, cursor), - TreeNavigationResult.Ok, - ); - return cursor; -} +import type { FlexTreeField, FlexTreeNode } from "../../../feature-libraries/index.js"; -/** - * Initializes a test tree, context, and cursor, and moves the cursor to the tree's root. - * - * @returns The initialized context and cursor. - */ -function initializeTreeWithContent( - treeContent: TreeContent, -): { - context: Context; - cursor: ITreeSubscriptionCursor; -} { - const context = contextWithContentReadonly(treeContent); - const cursor = initializeCursor(context, rootFieldAnchor); - - return { - context, - cursor, - }; -} +import { readonlyTreeWithContent } from "./utils.js"; +import { cursorFromInsertable, SchemaFactory } from "../../../simple-tree/index.js"; +import { JsonObject, singleJsonCursor } from "../../json/index.js"; +import { stringSchema } from "../../../simple-tree/leafNodeSchema.js"; +import { brand } from "../../../util/index.js"; /** * Test {@link LazyTreeNode} implementation. */ -class TestLazyTree extends LazyTreeNode {} +class TestLazyTree extends LazyTreeNode {} /** * Creates an {@link Anchor} and an {@link AnchorNode} for the provided cursor's location. @@ -94,33 +46,12 @@ function createAnchors( describe("LazyNode", () => { describe("LazyNode", () => { - it("is", () => { - const { context, cursor } = initializeTreeWithContent({ - schema: toFlexSchema(JsonObject), - initialTree: singleJsonCursor({}), - }); - cursor.enterNode(0); - - const { anchor, anchorNode } = createAnchors(context, cursor); - - const node = new TestLazyTree( - context, - getFlexSchema(JsonObject), - cursor, - anchorNode, - anchor, - ); - - assert(node.is(getFlexSchema(JsonObject))); - assert(!node.is(getFlexSchema(JsonArray))); - }); - it("parent", () => { const schemaFactory = new SchemaFactory("test"); const ParentNode = schemaFactory.map("map", schemaFactory.string); - const { context, cursor } = initializeTreeWithContent({ - schema: toFlexSchema(ParentNode), + const { context, cursor } = readonlyTreeWithContent({ + schema: ParentNode, initialTree: cursorFromInsertable(ParentNode, { [EmptyKey]: "test" }), }); cursor.enterNode(0); @@ -129,7 +60,7 @@ describe("LazyNode", () => { const node = new TestLazyTree( context, - getFlexSchema(ParentNode), + brand(ParentNode.identifier), cursor, anchorNode, anchor, @@ -141,15 +72,15 @@ describe("LazyNode", () => { it("keys", () => { { - const { context, cursor } = initializeTreeWithContent({ - schema: toFlexSchema(JsonObject), + const { context, cursor } = readonlyTreeWithContent({ + schema: JsonObject, initialTree: singleJsonCursor({}), }); cursor.enterNode(0); const { anchor, anchorNode } = createAnchors(context, cursor); const node = new TestLazyTree( context, - getFlexSchema(JsonObject), + brand(JsonObject.identifier), cursor, anchorNode, anchor, @@ -157,15 +88,15 @@ describe("LazyNode", () => { assert.deepEqual([...node.keys()], []); } { - const { context, cursor } = initializeTreeWithContent({ - schema: toFlexSchema(JsonObject), + const { context, cursor } = readonlyTreeWithContent({ + schema: JsonObject, initialTree: singleJsonCursor({ x: 5 }), }); cursor.enterNode(0); const { anchor, anchorNode } = createAnchors(context, cursor); const node = new TestLazyTree( context, - getFlexSchema(JsonObject), + brand(JsonObject.identifier), cursor, anchorNode, anchor, @@ -175,8 +106,8 @@ describe("LazyNode", () => { }); it("leaf", () => { - const { context, cursor } = initializeTreeWithContent({ - schema: toFlexSchema(stringSchema), + const { context, cursor } = readonlyTreeWithContent({ + schema: stringSchema, initialTree: singleJsonCursor("Hello world"), }); cursor.enterNode(0); @@ -185,7 +116,7 @@ describe("LazyNode", () => { const node = new LazyTreeNode( context, - getFlexSchema(stringSchema), + brand(stringSchema.identifier), cursor, anchorNode, anchor, diff --git a/packages/dds/tree/src/test/feature-libraries/flex-tree/unboxed.spec.ts b/packages/dds/tree/src/test/feature-libraries/flex-tree/unboxed.spec.ts index 738ba9a2f041..619f25491f0d 100644 --- a/packages/dds/tree/src/test/feature-libraries/flex-tree/unboxed.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/flex-tree/unboxed.spec.ts @@ -7,67 +7,17 @@ import { strict as assert } from "node:assert"; -import { - type FieldAnchor, - type ITreeSubscriptionCursor, - TreeNavigationResult, - rootFieldKey, -} from "../../../core/index.js"; -import type { Context } from "../../../feature-libraries/flex-tree/context.js"; import { unboxedFlexNode } from "../../../feature-libraries/flex-tree/unboxed.js"; -import { - isFlexTreeNode, - type FlexAllowedTypes, - type FlexFieldKind, -} from "../../../feature-libraries/index.js"; -import type { TreeContent } from "../../../shared-tree/index.js"; +import { isFlexTreeNode } from "../../../feature-libraries/index.js"; -import { contextWithContentReadonly } from "./utils.js"; -import { toFlexSchema } from "../../../simple-tree/index.js"; +import { readonlyTreeWithContent } from "./utils.js"; import { stringSchema } from "../../../simple-tree/leafNodeSchema.js"; import { JsonUnion, singleJsonCursor } from "../../json/index.js"; -const rootFieldAnchor: FieldAnchor = { parent: undefined, fieldKey: rootFieldKey }; - -/** - * Creates a cursor from the provided `context` and moves it to the provided `anchor`. - */ -function initializeCursor(context: Context, anchor: FieldAnchor): ITreeSubscriptionCursor { - const cursor = context.checkout.forest.allocateCursor(); - - assert.equal( - context.checkout.forest.tryMoveCursorToField(anchor, cursor), - TreeNavigationResult.Ok, - ); - return cursor; -} - -/** - * Initializes a test tree, context, and cursor, and moves the cursor to the tree's root. - * - * @returns The initialized context and cursor. - */ -function initializeTreeWithContent( - treeContent: TreeContent, -): { - context: Context; - cursor: ITreeSubscriptionCursor; -} { - const context = contextWithContentReadonly(treeContent); - const cursor = initializeCursor(context, rootFieldAnchor); - - return { - context, - cursor, - }; -} - describe("unboxedFlexNode", () => { it("Leaf", () => { - const schema = toFlexSchema(stringSchema); - - const { context, cursor } = initializeTreeWithContent({ - schema, + const { context, cursor } = readonlyTreeWithContent({ + schema: stringSchema, initialTree: singleJsonCursor("Hello world"), }); cursor.enterNode(0); // Root node field has 1 node; move into it @@ -76,10 +26,8 @@ describe("unboxedFlexNode", () => { }); it("Non-Leaf", () => { - const schema = toFlexSchema(JsonUnion); - - const { context, cursor } = initializeTreeWithContent({ - schema, + const { context, cursor } = readonlyTreeWithContent({ + schema: JsonUnion, initialTree: singleJsonCursor({}), }); cursor.enterNode(0); // Root node field has 1 node; move into it diff --git a/packages/dds/tree/src/test/feature-libraries/flex-tree/utils.ts b/packages/dds/tree/src/test/feature-libraries/flex-tree/utils.ts index 3d8dd5f0bb3a..9a5cc2e98571 100644 --- a/packages/dds/tree/src/test/feature-libraries/flex-tree/utils.ts +++ b/packages/dds/tree/src/test/feature-libraries/flex-tree/utils.ts @@ -8,23 +8,33 @@ import { strict as assert } from "node:assert"; import { type FieldAnchor, type IEditableForest, + type ITreeCursorSynchronous, type ITreeSubscriptionCursor, TreeNavigationResult, + TreeStoredSchemaRepository, rootFieldKey, } from "../../../core/index.js"; // eslint-disable-next-line import/no-internal-modules import { type Context, getTreeContext } from "../../../feature-libraries/flex-tree/context.js"; -import { - type FlexAllowedTypes, - type FlexFieldKind, - type FlexTreeSchema, - MockNodeKeyManager, -} from "../../../feature-libraries/index.js"; -import type { TreeContent } from "../../../shared-tree/index.js"; +import { MockNodeKeyManager } from "../../../feature-libraries/index.js"; import { MockTreeCheckout, forestWithContent } from "../../utils.js"; +import { + toFlexSchema, + toStoredSchema, + type ImplicitFieldSchema, +} from "../../../simple-tree/index.js"; -export function getReadonlyContext(forest: IEditableForest, schema: FlexTreeSchema): Context { - return getTreeContext(schema, new MockTreeCheckout(forest), new MockNodeKeyManager()); +export function getReadonlyContext( + forest: IEditableForest, + schema: ImplicitFieldSchema, +): Context { + return getTreeContext( + toFlexSchema(schema), + new MockTreeCheckout(forest, { + schema: new TreeStoredSchemaRepository(toStoredSchema(schema)), + }), + new MockNodeKeyManager(), + ); } /** @@ -34,11 +44,23 @@ export function getReadonlyContext(forest: IEditableForest, schema: FlexTreeSche * * @returns The created context. */ -export function contextWithContentReadonly(content: TreeContent): Context { - const forest = forestWithContent(content); +export function contextWithContentReadonly(content: TreeSimpleContent): Context { + const forest = forestWithContent({ ...content, schema: toStoredSchema(content.schema) }); return getReadonlyContext(forest, content.schema); } +/** + * Content that can populate a `SharedTree`. + */ +export interface TreeSimpleContent { + readonly schema: ImplicitFieldSchema; + /** + * Default tree content to initialize the tree with iff the tree is uninitialized + * (meaning it does not even have any schema set at all). + */ + readonly initialTree: readonly ITreeCursorSynchronous[] | ITreeCursorSynchronous | undefined; +} + /** * Creates a cursor from the provided `context` and moves it to the provided `anchor`. */ @@ -61,12 +83,7 @@ export const rootFieldAnchor: FieldAnchor = { parent: undefined, fieldKey: rootF * * @returns The initialized context and cursor. */ -export function readonlyTreeWithContent< - Kind extends FlexFieldKind, - Types extends FlexAllowedTypes, ->( - treeContent: TreeContent, -): { +export function readonlyTreeWithContent(treeContent: TreeSimpleContent): { context: Context; cursor: ITreeSubscriptionCursor; } { diff --git a/packages/dds/tree/src/test/scalableTestTrees.ts b/packages/dds/tree/src/test/scalableTestTrees.ts index 5781616c57ce..cba84a72922e 100644 --- a/packages/dds/tree/src/test/scalableTestTrees.ts +++ b/packages/dds/tree/src/test/scalableTestTrees.ts @@ -24,6 +24,8 @@ import { import type { TreeStoredContent } from "../shared-tree/schematizeTree.js"; // eslint-disable-next-line import/no-internal-modules import { toFlexSchema, toStoredSchema } from "../simple-tree/toFlexSchema.js"; +// eslint-disable-next-line import/no-internal-modules +import type { TreeSimpleContent } from "./feature-libraries/flex-tree/utils.js"; /** * Test trees which can be parametrically scaled to any size. @@ -66,13 +68,24 @@ export function makeJsDeepTree(depth: number, leafValue: number): JSDeepTree | n } export function makeDeepContent(depth: number, leafValue: number = 1): TreeContent { + const content = makeDeepContentSimple(depth, leafValue); + return { + ...content, + schema: toFlexSchema(content.schema), + }; +} + +export function makeDeepContentSimple( + depth: number, + leafValue: number = 1, +): TreeSimpleContent { // Implicit type conversion is needed here to make this compile. const initialTree = makeJsDeepTree(depth, leafValue); return { // Types do not allow implicitly constructing recursive types, so cast is required. // TODO: Find a better alternative. initialTree: cursorFromInsertable(LinkedList, initialTree as LinkedList), - schema: toFlexSchema(LinkedList), + schema: LinkedList, }; } @@ -80,13 +93,10 @@ export function makeDeepStoredContent( depth: number, leafValue: number = 1, ): TreeStoredContent { - // Implicit type conversion is needed here to make this compile. - const initialTree = makeJsDeepTree(depth, leafValue); + const content = makeDeepContentSimple(depth, leafValue); return { - // Types do now allow implicitly constructing recursive types, so cast is required. - // TODO: Find a better alternative. - initialTree: cursorFromInsertable(LinkedList, initialTree as LinkedList), - schema: toStoredSchema(LinkedList), + ...content, + schema: toStoredSchema(content.schema), }; } @@ -96,15 +106,32 @@ export function makeDeepStoredContent( * @param endLeafValue - the value of the end leaf of the tree. If not provided its index is used. * @returns a tree with specified number of nodes, with the end leaf node set to the endLeafValue */ -export function makeWideContentWithEndValue( +export function makeWideContentWithEndValueSimple( numberOfNodes: number, endLeafValue?: number, -): TreeContent { +): TreeSimpleContent { // Implicit type conversion is needed here to make this compile. const initialTree = makeJsWideTreeWithEndValue(numberOfNodes, endLeafValue); return { initialTree: cursorFromInsertable(WideRoot, initialTree), - schema: toFlexSchema(WideRoot), + schema: WideRoot, + }; +} + +/** + * + * @param numberOfNodes - number of nodes of the tree + * @param endLeafValue - the value of the end leaf of the tree. If not provided its index is used. + * @returns a tree with specified number of nodes, with the end leaf node set to the endLeafValue + */ +export function makeWideContentWithEndValue( + numberOfNodes: number, + endLeafValue?: number, +): TreeContent { + const content = makeWideContentWithEndValueSimple(numberOfNodes, endLeafValue); + return { + ...content, + schema: toFlexSchema(content.schema), }; } @@ -117,11 +144,10 @@ export function makeWideStoredContentWithEndValue( numberOfNodes: number, endLeafValue?: number, ): TreeStoredContent { - // Implicit type conversion is needed here to make this compile. - const initialTree = makeJsWideTreeWithEndValue(numberOfNodes, endLeafValue); + const content = makeWideContentWithEndValueSimple(numberOfNodes, endLeafValue); return { - initialTree: cursorFromInsertable(WideRoot, initialTree), - schema: toStoredSchema(WideRoot), + ...content, + schema: toStoredSchema(content.schema), }; } diff --git a/packages/dds/tree/src/test/shared-tree/sharedTree.bench.ts b/packages/dds/tree/src/test/shared-tree/sharedTree.bench.ts index 281d2d04a768..638dd324916f 100644 --- a/packages/dds/tree/src/test/shared-tree/sharedTree.bench.ts +++ b/packages/dds/tree/src/test/shared-tree/sharedTree.bench.ts @@ -28,11 +28,11 @@ import { WideRoot, deepPath, localFieldKey, - makeDeepContent, + makeDeepContentSimple, makeDeepStoredContent, makeJsDeepTree, makeJsWideTreeWithEndValue, - makeWideContentWithEndValue, + makeWideContentWithEndValueSimple, makeWideStoredContentWithEndValue, readDeepCursorTree, readDeepFlexTree, @@ -158,7 +158,7 @@ describe("SharedTree benchmarks", () => { type: benchmarkType, title: `Deep Tree with cursor: reads with ${numberOfNodes} nodes`, before: () => { - tree = flexTreeViewWithContent(makeDeepContent(numberOfNodes)); + tree = flexTreeViewWithContent(makeDeepContentSimple(numberOfNodes)); }, benchmarkFn: () => { const { depth, value } = readDeepCursorTree(tree); @@ -180,7 +180,7 @@ describe("SharedTree benchmarks", () => { expected += index; } tree = flexTreeViewWithContent( - makeWideContentWithEndValue(numberOfNodes, numberOfNodes - 1), + makeWideContentWithEndValueSimple(numberOfNodes, numberOfNodes - 1), ); }, benchmarkFn: () => { @@ -198,7 +198,7 @@ describe("SharedTree benchmarks", () => { type: benchmarkType, title: `Deep Tree with Flex Tree: reads with ${numberOfNodes} nodes`, before: () => { - tree = flexTreeViewWithContent(makeDeepContent(numberOfNodes)); + tree = flexTreeViewWithContent(makeDeepContentSimple(numberOfNodes)); }, benchmarkFn: () => { const { depth, value } = readDeepFlexTree(tree); @@ -215,7 +215,7 @@ describe("SharedTree benchmarks", () => { title: `Wide Tree with Flex Tree: reads with ${numberOfNodes} nodes`, before: () => { expected = ((numberOfNodes - 1) * numberOfNodes) / 2; // Arithmetic sum of [0, numberOfNodes) - tree = flexTreeViewWithContent(makeWideContentWithEndValue(numberOfNodes)); + tree = flexTreeViewWithContent(makeWideContentWithEndValueSimple(numberOfNodes)); }, benchmarkFn: () => { const { nodesCount, sum } = readWideFlexTree(tree); diff --git a/packages/dds/tree/src/test/utils.ts b/packages/dds/tree/src/test/utils.ts index 65e4da945e4e..7542b2db1f53 100644 --- a/packages/dds/tree/src/test/utils.ts +++ b/packages/dds/tree/src/test/utils.ts @@ -141,6 +141,7 @@ import { toStoredSchema, type TreeViewEvents, type TreeView, + toFlexSchema, } from "../simple-tree/index.js"; import { type JsonCompatible, @@ -154,6 +155,8 @@ import { isFluidHandle, toFluidHandleInternal } from "@fluidframework/runtime-ut import type { Client } from "@fluid-private/test-dds-utils"; import { cursorFromInsertable } from "../simple-tree/index.js"; 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"; // Testing utilities @@ -722,8 +725,8 @@ function createCheckoutWithContent( return { checkout, logger }; } -export function flexTreeViewWithContent( - content: TreeContent, +export function flexTreeViewWithContent( + content: TreeSimpleContent, args?: { events?: Listenable & IEmitter & @@ -732,12 +735,12 @@ export function flexTreeViewWithContent( }, ): CheckoutFlexTreeView { const view = checkoutWithContent( - { initialTree: content.initialTree, schema: intoStoredSchema(content.schema) }, + { initialTree: content.initialTree, schema: toStoredSchema(content.schema) }, args, ); return new CheckoutFlexTreeView( view, - content.schema, + toFlexSchema(content.schema), args?.nodeKeyManager ?? new MockNodeKeyManager(), ); } @@ -1212,13 +1215,13 @@ export function viewCheckout( * A mock implementation of `ITreeCheckout` that provides read access to the forest, and nothing else. */ export class MockTreeCheckout implements ITreeCheckout { - private readonly _editor: ISharedTreeEditor | undefined; public constructor( public readonly forest: IForestSubscription, - editor?: ISharedTreeEditor, - ) { - this._editor = editor; - } + private readonly options?: { + schema?: TreeStoredSchemaSubscription; + editor?: ISharedTreeEditor; + }, + ) {} public viewWith( config: TreeViewConfiguration, @@ -1227,13 +1230,16 @@ export class MockTreeCheckout implements ITreeCheckout { } public get storedSchema(): TreeStoredSchemaSubscription { - throw new Error("'storedSchema' property not implemented in MockTreeCheckout."); + if (this.options?.schema === undefined) { + throw new Error("No schema provided to MockTreeCheckout."); + } + return this.options.schema; } public get editor(): ISharedTreeEditor { - if (this._editor === undefined) { + if (this.options?.editor === undefined) { throw new Error("No editor provided to MockTreeCheckout."); } - return this._editor; + return this.options.editor; } public get transaction(): ITransaction { throw new Error("'transaction' property not implemented in MockTreeCheckout.");