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 2ccba8f652d1..4bfaa49bb9c2 100644 --- a/packages/dds/tree/src/feature-libraries/flex-tree/flexTreeTypes.ts +++ b/packages/dds/tree/src/feature-libraries/flex-tree/flexTreeTypes.ts @@ -194,12 +194,6 @@ export interface FlexTreeNode extends FlexTreeEntity { * If well-formed, it must follow this schema. */ readonly schema: TreeNodeSchemaIdentifier; - - /** - * Schema for this entity. - * If well-formed, it must follow this schema. - */ - readonly flexSchema: FlexTreeNodeSchema; } /** diff --git a/packages/dds/tree/src/simple-tree/api/create.ts b/packages/dds/tree/src/simple-tree/api/create.ts index b831e6a08fef..d8f337feac2e 100644 --- a/packages/dds/tree/src/simple-tree/api/create.ts +++ b/packages/dds/tree/src/simple-tree/api/create.ts @@ -19,7 +19,6 @@ import { getOrCreateNodeFromInnerNode, UnhydratedFlexTreeNode, type Unhydrated, - UnhydratedContext, } from "../core/index.js"; import { cursorForMapTreeNode, @@ -39,6 +38,7 @@ import { type VerboseTree, type VerboseTreeNode, } from "./verboseTree.js"; +import { getUnhydratedContext } from "../createContext.js"; /** * Construct tree content that is compatible with the field defined by the provided `schema`. @@ -151,12 +151,12 @@ export function createFromCursor( cursor: ITreeCursorSynchronous | undefined, ): Unhydrated> { const mapTrees = cursor === undefined ? [] : [mapTreeFromCursor(cursor)]; - const flexSchema = toFlexSchema(schema); + const context = getUnhydratedContext(schema); + const flexSchema = context.flexContext.flexSchema; const schemaValidationPolicy: SchemaAndPolicy = { policy: defaultSchemaPolicy, - // TODO: optimize: This isn't the most efficient operation since its not cached, and has to convert all the schema. - schema: intoStoredSchema(flexSchema), + schema: context.flexContext.schema, }; const maybeError = isFieldInSchema( @@ -174,8 +174,7 @@ export function createFromCursor( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const mapTree = mapTrees[0]!; const mapTreeNode = UnhydratedFlexTreeNode.getOrCreate( - // TODO: Provide a way to get simple-tree context here, then make UnhydratedFlexTreeNode's hold simple-tree contexts. Use this for InnerNode -> TreeSchemaSchema - new UnhydratedContext(flexSchema), + getUnhydratedContext(schema), mapTree, ); diff --git a/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts b/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts index fa3f85c56e82..89b06385339f 100644 --- a/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts +++ b/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts @@ -11,7 +11,6 @@ import { type TreeStatus, isLazy, isTreeValue, - FlexObjectNodeSchema, FieldKinds, } from "../../feature-libraries/index.js"; import { fail, extractFromOpaque, isReadonlyArray } from "../../util/index.js"; @@ -43,6 +42,7 @@ import { tryGetTreeNodeSchema, getOrCreateNodeFromInnerNode, UnhydratedFlexTreeNode, + typeSchemaSymbol, } from "../core/index.js"; import { isObjectNodeSchema } from "../objectNodeTypes.js"; @@ -230,10 +230,13 @@ export const treeNodeApi: TreeNodeApi = { return tryGetSchema(node) ?? fail("Not a tree node"); }, shortId(node: TreeNode): number | string | undefined { + const schema = node[typeSchemaSymbol]; + if (!isObjectNodeSchema(schema)) { + return undefined; + } + const flexNode = getOrCreateInnerNode(node); - const flexSchema = flexNode.flexSchema; - const identifierFieldKeys = - flexSchema instanceof FlexObjectNodeSchema ? flexSchema.identifierFieldKeys : []; + const identifierFieldKeys = schema.identifierFieldKeys; switch (identifierFieldKeys.length) { case 0: diff --git a/packages/dds/tree/src/simple-tree/arrayNode.ts b/packages/dds/tree/src/simple-tree/arrayNode.ts index 75f6b099a858..b49e941cd413 100644 --- a/packages/dds/tree/src/simple-tree/arrayNode.ts +++ b/packages/dds/tree/src/simple-tree/arrayNode.ts @@ -42,7 +42,7 @@ import { UnhydratedTreeSequenceField, } from "./core/index.js"; import { TreeNodeValid, type MostDerivedData } from "./treeNodeValid.js"; -import { createUnhydratedContext } from "./createContext.js"; +import { getUnhydratedContext } from "./createContext.js"; /** * A generic array type, used to defined types like {@link (TreeArrayNode:interface)}. @@ -956,7 +956,7 @@ export function arraySchema< input: T2, ): UnhydratedFlexTreeNode { return UnhydratedFlexTreeNode.getOrCreate( - unhydratedContext.flexContext, + unhydratedContext, mapTreeFromNodeData(input as object, this as unknown as ImplicitAllowedTypes), ); } @@ -965,7 +965,7 @@ export function arraySchema< protected static override oneTimeSetup(this: typeof TreeNodeValid): Context { const schema = this as unknown as TreeNodeSchema; - unhydratedContext = createUnhydratedContext(schema); + unhydratedContext = getUnhydratedContext(schema); // First run, do extra validation. // TODO: provide a way for TreeConfiguration to trigger this same validation to ensure it gets run early. diff --git a/packages/dds/tree/src/simple-tree/core/schemaCaching.ts b/packages/dds/tree/src/simple-tree/core/schemaCaching.ts index 1017e65ecf54..3dbe893685c4 100644 --- a/packages/dds/tree/src/simple-tree/core/schemaCaching.ts +++ b/packages/dds/tree/src/simple-tree/core/schemaCaching.ts @@ -10,6 +10,8 @@ import { fail } from "../../util/index.js"; import type { TreeNodeSchema } from "./treeNodeSchema.js"; import type { InnerNode } from "./treeNodeKernel.js"; +import { UnhydratedFlexTreeNode } from "./unhydratedFlexTree.js"; +import { SimpleContextSlot, type Context } from "./context.js"; /** * A symbol for storing FlexTreeSchema on TreeNodeSchema. @@ -66,7 +68,20 @@ export function getSimpleNodeSchema(flexSchema: FlexTreeNodeSchema): TreeNodeSch * Gets the {@link TreeNodeSchema} for the {@link InnerNode}. */ export function getSimpleNodeSchemaFromInnerNode(innerNode: InnerNode): TreeNodeSchema { - // TODO: to make this work without depending on flex tree schema, a new caching/lookup mechanism will be required, likely leveraging the FlexTreeContext: - // A SimpleTreeContext could be defined and associated with the FlexTreeContext, and used to look up simple-tree schema by identifier. - return getSimpleNodeSchema(innerNode.flexSchema); + const context: Context = getSimpleContextFromInnerNode(innerNode); + return context.schema.get(innerNode.schema) ?? fail("missing schema from context"); +} + +/** + * Gets the {@link Context} for the {@link InnerNode}. + */ +export function getSimpleContextFromInnerNode(innerNode: InnerNode): Context { + if (innerNode instanceof UnhydratedFlexTreeNode) { + return innerNode.simpleContext; + } + + const context = innerNode.anchorNode.anchorSet.slots.get(SimpleContextSlot); + assert(context !== undefined, "missing simple tree context"); + + return context; } diff --git a/packages/dds/tree/src/simple-tree/core/unhydratedFlexTree.ts b/packages/dds/tree/src/simple-tree/core/unhydratedFlexTree.ts index a52f0a493c43..af5d33e6cd2c 100644 --- a/packages/dds/tree/src/simple-tree/core/unhydratedFlexTree.ts +++ b/packages/dds/tree/src/simple-tree/core/unhydratedFlexTree.ts @@ -37,10 +37,10 @@ import { type FlexTreeSchema, intoStoredSchemaCollection, type FlexFieldKind, - defaultSchemaPolicy, FieldKinds, type SequenceFieldEditBuilder, } from "../../feature-libraries/index.js"; +import type { Context } from "./context.js"; interface UnhydratedTreeSequenceFieldEditBuilder extends SequenceFieldEditBuilder { @@ -85,12 +85,16 @@ export class UnhydratedFlexTreeNode implements UnhydratedFlexTreeNode { * @remarks It must conform to the `nodeSchema`. */ public static getOrCreate( - context: UnhydratedContext, + context: Context, mapTree: ExclusiveMapTree, ): UnhydratedFlexTreeNode { return nodeCache.get(mapTree) ?? new UnhydratedFlexTreeNode(context, mapTree, undefined); } + public get context(): UnhydratedContext { + return this.simpleContext.flexContext; + } + /** * Create a new UnhydratedFlexTreeNode. * @param location - the parentage of this node, if it is being created underneath an existing node and field, or undefined if not @@ -100,7 +104,7 @@ export class UnhydratedFlexTreeNode implements UnhydratedFlexTreeNode { * Instead, it should always be acquired via {@link getOrCreateNode}. */ public constructor( - public readonly context: UnhydratedContext, + public readonly simpleContext: Context, /** The underlying {@link MapTree} that this `UnhydratedFlexTreeNode` reads its data from */ public readonly mapTree: ExclusiveMapTree, private location = unparentedLocation, @@ -198,7 +202,7 @@ export class UnhydratedFlexTreeNode implements UnhydratedFlexTreeNode { for (const [key, mapTrees] of this.mapTree.fields) { const field = getOrCreateField(this, key, this.flexSchema.getFieldSchema(key)); for (let index = 0; index < field.length; index++) { - const child = getOrCreateChild(this.context, mapTrees[index] ?? oob(), { + const child = getOrCreateChild(this.simpleContext, mapTrees[index] ?? oob(), { parent: field, index, }); @@ -240,13 +244,6 @@ export class UnhydratedContext implements FlexTreeContext { // #region Fields -const emptyContext = new UnhydratedContext({ - adapters: {}, - nodeSchema: new Map(), - policy: defaultSchemaPolicy, - rootFieldSchema: FlexFieldSchema.empty, -}); - /** * A special singleton that is the implicit {@link LocationInField} of all un-parented {@link UnhydratedFlexTreeNode}s. * @remarks This exists because {@link UnhydratedFlexTreeNode.parentField} must return a field. @@ -271,7 +268,9 @@ const unparentedLocation: LocationInField = { return undefined; }, schema: FlexFieldSchema.empty.stored, - context: emptyContext, + get context(): never { + return fail("unsupported"); + }, getFieldPath() { fail("unsupported"); }, @@ -286,8 +285,12 @@ class UnhydratedFlexTreeField implements FlexTreeField { return this.flexSchema.stored; } + public get context(): UnhydratedContext { + return this.simpleContext.flexContext; + } + public constructor( - public readonly context: UnhydratedContext, + public readonly simpleContext: Context, public readonly flexSchema: FlexFieldSchema, public readonly key: FieldKey, public readonly parent: UnhydratedFlexTreeNode, @@ -325,7 +328,7 @@ class UnhydratedFlexTreeField implements FlexTreeField { return this.mapTrees .map( (m, index) => - getOrCreateChild(this.context, m, { + getOrCreateChild(this.simpleContext, m, { parent: this, index, }) as FlexTreeNode, @@ -340,7 +343,7 @@ class UnhydratedFlexTreeField implements FlexTreeField { } const m = this.mapTrees[i]; if (m !== undefined) { - return getOrCreateChild(this.context, m, { + return getOrCreateChild(this.simpleContext, m, { parent: this, index: i, }) as FlexTreeNode; @@ -370,16 +373,14 @@ class UnhydratedFlexTreeField implements FlexTreeField { } /** Unboxes leaf nodes to their values */ - protected unboxed( - mapTree: ExclusiveMapTree, - parent: LocationInField, - ): FlexTreeUnknownUnboxed { + protected unboxed(index: number): FlexTreeUnknownUnboxed { + const mapTree: ExclusiveMapTree = this.mapTrees[index] ?? oob(); const value = mapTree.value; if (value !== undefined) { return value; } - return getOrCreateChild(parent.parent.context, mapTree, parent); + return getOrCreateChild(this.simpleContext, mapTree, { parent: this, index }); } } @@ -412,10 +413,7 @@ class EagerMapTreeOptionalField public get content(): FlexTreeUnknownUnboxed | undefined { const value = this.mapTrees[0]; if (value !== undefined) { - return this.unboxed(value, { - parent: this, - index: 0, - }); + return this.unboxed(0); } return undefined; @@ -471,15 +469,15 @@ export class UnhydratedTreeSequenceField if (i === undefined) { return undefined; } - return this.unboxed(this.mapTrees[i] ?? oob(), { parent: this, index: i }); + return this.unboxed(i); } public map(callbackfn: (value: FlexTreeUnknownUnboxed, index: number) => U): U[] { return Array.from(this, callbackfn); } public *[Symbol.iterator](): IterableIterator { - for (const [i, mapTree] of this.mapTrees.entries()) { - yield this.unboxed(mapTree, { parent: this, index: i }); + for (const [i] of this.mapTrees.entries()) { + yield this.unboxed(i); } } } @@ -512,7 +510,7 @@ export function tryUnhydratedFlexTreeNode( /** Helper for creating a `UnhydratedFlexTreeNode` given the parent field (e.g. when "walking down") */ function getOrCreateChild( - context: UnhydratedContext, + context: Context, mapTree: ExclusiveMapTree, parent: LocationInField | undefined, ): UnhydratedFlexTreeNode { @@ -539,18 +537,18 @@ function getOrCreateField( schema.kind.identifier === FieldKinds.required.identifier || schema.kind.identifier === FieldKinds.identifier.identifier ) { - return new EagerMapTreeRequiredField(parent.context, schema, key, parent); + return new EagerMapTreeRequiredField(parent.simpleContext, schema, key, parent); } if (schema.kind.identifier === FieldKinds.optional.identifier) { - return new EagerMapTreeOptionalField(parent.context, schema, key, parent); + return new EagerMapTreeOptionalField(parent.simpleContext, schema, key, parent); } if (schema.kind.identifier === FieldKinds.sequence.identifier) { - return new UnhydratedTreeSequenceField(parent.context, schema, key, parent); + return new UnhydratedTreeSequenceField(parent.simpleContext, schema, key, parent); } - return new UnhydratedFlexTreeField(parent.context, schema, key, parent); + return new UnhydratedFlexTreeField(parent.simpleContext, schema, key, parent); } // #endregion Caching and unboxing utilities diff --git a/packages/dds/tree/src/simple-tree/createContext.ts b/packages/dds/tree/src/simple-tree/createContext.ts index 88b95437a2e3..64d4b51958ec 100644 --- a/packages/dds/tree/src/simple-tree/createContext.ts +++ b/packages/dds/tree/src/simple-tree/createContext.ts @@ -3,13 +3,21 @@ * Licensed under the MIT License. */ -import { Context, type TreeNodeSchema, UnhydratedContext } from "./core/index.js"; +import { getOrCreate } from "../util/index.js"; +import { Context, UnhydratedContext } from "./core/index.js"; +import { normalizeFieldSchema, type ImplicitFieldSchema } from "./schemaTypes.js"; import { toFlexSchema } from "./toFlexSchema.js"; +const contextCache: WeakMap = new WeakMap(); + /** * Utility for creating {@link Context}s for unhydrated nodes. */ -export function createUnhydratedContext(schema: TreeNodeSchema): Context { - const flexContext = new UnhydratedContext(toFlexSchema(schema)); - return new Context([schema], flexContext); +export function getUnhydratedContext(schema: ImplicitFieldSchema): Context { + return getOrCreate(contextCache, schema, (s) => { + const normalized = normalizeFieldSchema(schema); + + const flexContext = new UnhydratedContext(toFlexSchema(normalized)); + return new Context(normalized.allowedTypeSet, flexContext); + }); } diff --git a/packages/dds/tree/src/simple-tree/mapNode.ts b/packages/dds/tree/src/simple-tree/mapNode.ts index 6c4ba993b1b9..094a2eb12ef3 100644 --- a/packages/dds/tree/src/simple-tree/mapNode.ts +++ b/packages/dds/tree/src/simple-tree/mapNode.ts @@ -42,7 +42,7 @@ import { import { brand, count, type RestrictiveStringRecord } from "../util/index.js"; import { TreeNodeValid, type MostDerivedData } from "./treeNodeValid.js"; import type { ExclusiveMapTree } from "../core/index.js"; -import { createUnhydratedContext } from "./createContext.js"; +import { getUnhydratedContext } from "./createContext.js"; /** * A map of string keys to tree objects. @@ -255,7 +255,7 @@ export function mapSchema< input: T2, ): UnhydratedFlexTreeNode { return UnhydratedFlexTreeNode.getOrCreate( - unhydratedContext.flexContext, + unhydratedContext, mapTreeFromNodeData(input as FactoryContent, this as unknown as ImplicitAllowedTypes), ); } @@ -264,7 +264,7 @@ export function mapSchema< protected static override oneTimeSetup(this: typeof TreeNodeValid): Context { const schema = this as unknown as TreeNodeSchema; - unhydratedContext = createUnhydratedContext(schema); + unhydratedContext = getUnhydratedContext(schema); return unhydratedContext; } diff --git a/packages/dds/tree/src/simple-tree/objectNode.ts b/packages/dds/tree/src/simple-tree/objectNode.ts index 9281c6f7068a..3cff8b6fb435 100644 --- a/packages/dds/tree/src/simple-tree/objectNode.ts +++ b/packages/dds/tree/src/simple-tree/objectNode.ts @@ -44,7 +44,7 @@ import { mapTreeFromNodeData, type InsertableContent } from "./toMapTree.js"; import { type RestrictiveStringRecord, fail, type FlattenKeys } from "../util/index.js"; import type { ObjectNodeSchema, ObjectNodeSchemaInternalData } from "./objectNodeTypes.js"; import { TreeNodeValid, type MostDerivedData } from "./treeNodeValid.js"; -import { createUnhydratedContext } from "./createContext.js"; +import { getUnhydratedContext } from "./createContext.js"; /** * Helper used to produce types for object nodes. @@ -151,7 +151,7 @@ function createFlexKeyMapping(fields: Record): Simp * If not provided `{}` is used for the target. */ function createProxyHandler( - flexKeyMap: SimpleKeyMap, + schema: ObjectNodeSchema & ObjectNodeSchemaInternalData, allowAdditionalProperties: boolean, ): ProxyHandler { // To satisfy 'deepEquals' level scrutiny, the target of the proxy must be an object with the same @@ -165,7 +165,7 @@ function createProxyHandler( // a dispatch object to see if it improves performance. const handler: ProxyHandler = { get(target, propertyKey, proxy): unknown { - const fieldInfo = flexKeyMap.get(propertyKey); + const fieldInfo = schema.flexKeyMap.get(propertyKey); if (fieldInfo !== undefined) { const flexNode = getOrCreateInnerNode(proxy); @@ -190,11 +190,20 @@ function createProxyHandler( return undefined; } + // POJO mode objects don't have TreeNode's build in members on their targets, so special case them: + if (propertyKey === typeSchemaSymbol) { + return schema; + } + // eslint-disable-next-line import/no-deprecated + if (propertyKey === typeNameSymbol) { + return schema.identifier; + } + // Pass the proxy as the receiver here, so that any methods on the prototype receive `proxy` as `this`. return Reflect.get(target, propertyKey, proxy); }, set(target, propertyKey, value: InsertableContent | undefined, proxy) { - const fieldInfo = flexKeyMap.get(propertyKey); + const fieldInfo = schema.flexKeyMap.get(propertyKey); if (fieldInfo === undefined) { // Pass the proxy as the receiver here, so that setters on the prototype receive `proxy` as `this`. return allowAdditionalProperties @@ -217,18 +226,18 @@ function createProxyHandler( }, has: (target, propertyKey) => { return ( - flexKeyMap.has(propertyKey) || + schema.flexKeyMap.has(propertyKey) || (allowAdditionalProperties ? Reflect.has(target, propertyKey) : false) ); }, ownKeys: (target) => { return [ - ...flexKeyMap.keys(), + ...schema.flexKeyMap.keys(), ...(allowAdditionalProperties ? Reflect.ownKeys(target) : []), ]; }, getOwnPropertyDescriptor: (target, propertyKey) => { - const fieldInfo = flexKeyMap.get(propertyKey); + const fieldInfo = schema.flexKeyMap.get(propertyKey); if (fieldInfo === undefined) { return allowAdditionalProperties @@ -320,6 +329,13 @@ export function objectSchema< // Performance optimization: cache property key => stored key and schema. const flexKeyMap: SimpleKeyMap = createFlexKeyMapping(info); + const identifierFieldKeys: FieldKey[] = []; + for (const item of flexKeyMap.values()) { + if (item.schema.kind === FieldKind.Identifier) { + identifierFieldKeys.push(item.storedKey); + } + } + const lazyChildTypes = new Lazy( () => new Set(Array.from(flexKeyMap.values(), (f) => [...f.schema.allowedTypeSet]).flat()), ); @@ -342,6 +358,7 @@ export function objectSchema< key as string, ]), ); + public static readonly identifierFieldKeys: readonly FieldKey[] = identifierFieldKeys; public static override prepareInstance( this: typeof TreeNodeValid, @@ -379,7 +396,7 @@ export function objectSchema< input: T2, ): UnhydratedFlexTreeNode { return UnhydratedFlexTreeNode.getOrCreate( - unhydratedContext.flexContext, + unhydratedContext, mapTreeFromNodeData(input as object, this as unknown as ImplicitAllowedTypes), ); } @@ -389,9 +406,9 @@ export function objectSchema< protected static override oneTimeSetup(this: typeof TreeNodeValid): Context { // One time initialization that required knowing the most derived type (from this.constructor) and thus has to be lazy. customizable = (this as unknown) !== CustomObjectNode; - handler = createProxyHandler(flexKeyMap, customizable); - const schema = this as unknown as TreeNodeSchema; - unhydratedContext = createUnhydratedContext(schema); + const schema = this as unknown as ObjectNodeSchema & ObjectNodeSchemaInternalData; + handler = createProxyHandler(schema, customizable); + unhydratedContext = getUnhydratedContext(schema); // First run, do extra validation. // TODO: provide a way for TreeConfiguration to trigger this same validation to ensure it gets run early. diff --git a/packages/dds/tree/src/simple-tree/objectNodeTypes.ts b/packages/dds/tree/src/simple-tree/objectNodeTypes.ts index 33f7a367b9da..3555a5a168fb 100644 --- a/packages/dds/tree/src/simple-tree/objectNodeTypes.ts +++ b/packages/dds/tree/src/simple-tree/objectNodeTypes.ts @@ -50,6 +50,11 @@ export interface ObjectNodeSchemaInternalData { * Lookup the property keys from the stored keys. */ readonly storedKeyToPropertyKey: ReadonlyMap; + + /** + * Stored keys which hold identifiers. + */ + readonly identifierFieldKeys: readonly FieldKey[]; } export const ObjectNodeSchema = { diff --git a/packages/dds/tree/src/simple-tree/treeNodeValid.ts b/packages/dds/tree/src/simple-tree/treeNodeValid.ts index 2ac529feabb1..f3e0fadb96c9 100644 --- a/packages/dds/tree/src/simple-tree/treeNodeValid.ts +++ b/packages/dds/tree/src/simple-tree/treeNodeValid.ts @@ -24,6 +24,7 @@ import { UsageError } from "@fluidframework/telemetry-utils/internal"; import { fail } from "../util/index.js"; import { getFlexSchema } from "./toFlexSchema.js"; +import { getSimpleNodeSchemaFromInnerNode } from "./core/index.js"; /** * Class which all {@link TreeNode}s must extend. @@ -164,7 +165,7 @@ export abstract class TreeNodeValid extends TreeNode { const node: InnerNode = isFlexTreeNode(input) ? input : schema.buildRawNode(this, input); assert( - tryGetSimpleNodeSchema(node.flexSchema) === schema, + getSimpleNodeSchemaFromInnerNode(node) === schema, 0x83b /* building node with wrong schema */, ); diff --git a/packages/dds/tree/src/test/simple-tree/core/types.spec.ts b/packages/dds/tree/src/test/simple-tree/core/types.spec.ts index 375703b13c7a..0ecfc646aaf3 100644 --- a/packages/dds/tree/src/test/simple-tree/core/types.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/core/types.spec.ts @@ -28,17 +28,14 @@ import { import type { FlexTreeNode } from "../../../feature-libraries/index.js"; // eslint-disable-next-line import/no-internal-modules import { numberSchema } from "../../../simple-tree/leafNodeSchema.js"; -// eslint-disable-next-line import/no-internal-modules -import { toFlexSchema } from "../../../simple-tree/toFlexSchema.js"; import { validateUsageError } from "../../utils.js"; import { brand } from "../../../util/index.js"; import { UnhydratedFlexTreeNode, - UnhydratedContext, // eslint-disable-next-line import/no-internal-modules } from "../../../simple-tree/core/unhydratedFlexTree.js"; // eslint-disable-next-line import/no-internal-modules -import { createUnhydratedContext } from "../../../simple-tree/createContext.js"; +import { getUnhydratedContext } from "../../../simple-tree/createContext.js"; // eslint-disable-next-line import/no-internal-modules import type { Context } from "../../../simple-tree/core/index.js"; @@ -121,7 +118,7 @@ describe("simple-tree types", () => { class MockFlexNode extends UnhydratedFlexTreeNode { public constructor(public readonly simpleSchema: TreeNodeSchema) { super( - new UnhydratedContext(toFlexSchema(simpleSchema)), + getUnhydratedContext(simpleSchema), { fields: new Map(), type: brand(simpleSchema.identifier) }, undefined, ); @@ -166,7 +163,7 @@ describe("simple-tree types", () => { protected static override oneTimeSetup(this: typeof TreeNodeValid): Context { log.push("oneTimeSetup"); - return createUnhydratedContext(Subclass); + return getUnhydratedContext(Subclass); } public static readonly childTypes: ReadonlySet = new Set(); @@ -263,7 +260,7 @@ describe("simple-tree types", () => { protected static override oneTimeSetup(this: typeof TreeNodeValid): Context { log.push("A"); - return createUnhydratedContext(A); + return getUnhydratedContext(A); } } @@ -272,7 +269,7 @@ describe("simple-tree types", () => { protected static override oneTimeSetup(this: typeof TreeNodeValid): Context { log.push("B"); - return createUnhydratedContext(A); + return getUnhydratedContext(A); } } @@ -316,7 +313,7 @@ describe("simple-tree types", () => { protected static override oneTimeSetup(this: typeof TreeNodeValid): Context { log.push(this.name); - return createUnhydratedContext(A); + return getUnhydratedContext(A); } } diff --git a/packages/dds/tree/src/test/simple-tree/core/unhydratedFlexTree.spec.ts b/packages/dds/tree/src/test/simple-tree/core/unhydratedFlexTree.spec.ts index b94ab02aa815..f937ab060286 100644 --- a/packages/dds/tree/src/test/simple-tree/core/unhydratedFlexTree.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/core/unhydratedFlexTree.spec.ts @@ -14,16 +14,12 @@ import { } from "../../../core/index.js"; import { brand } from "../../../util/index.js"; import { - UnhydratedContext, UnhydratedFlexTreeNode, // eslint-disable-next-line import/no-internal-modules } from "../../../simple-tree/core/unhydratedFlexTree.js"; -import { - getFlexSchema, - SchemaFactory, - stringSchema, - toFlexSchema, -} from "../../../simple-tree/index.js"; +import { getFlexSchema, SchemaFactory, stringSchema } from "../../../simple-tree/index.js"; +// eslint-disable-next-line import/no-internal-modules +import { getUnhydratedContext } from "../../../simple-tree/createContext.js"; describe("unhydratedFlexTree", () => { // #region The schema used in this test suite @@ -74,9 +70,11 @@ describe("unhydratedFlexTree", () => { // #endregion // The `MapTreeNode`s used in this test suite: - const context = new UnhydratedContext( - toFlexSchema([mapSchemaSimple, arrayNodeSchemaSimple, objectSchemaSimple]), - ); + const context = getUnhydratedContext([ + mapSchemaSimple, + arrayNodeSchemaSimple, + objectSchemaSimple, + ]); const map = UnhydratedFlexTreeNode.getOrCreate(context, mapMapTree); const arrayNode = UnhydratedFlexTreeNode.getOrCreate(context, fieldNodeMapTree); const object = UnhydratedFlexTreeNode.getOrCreate(context, objectMapTree); diff --git a/packages/dds/tree/src/test/simple-tree/objectNode.spec.ts b/packages/dds/tree/src/test/simple-tree/objectNode.spec.ts index 7533c0bff4ad..8c34dd2a9ecd 100644 --- a/packages/dds/tree/src/test/simple-tree/objectNode.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/objectNode.spec.ts @@ -9,6 +9,8 @@ import { validateAssertionError } from "@fluidframework/test-runtime-utils/inter import { SchemaFactory, + typeNameSymbol, + typeSchemaSymbol, type NodeBuilderData, type NodeKind, type TreeNodeSchema, @@ -453,5 +455,28 @@ describeHydration( assert.equal(newNode.id, "x"); assert.equal(Tree.shortId(newNode), "x"); }); + + it("custom identifier access works on POJO mode object", () => { + const HasId = schemaFactory.object("hasID", { id: schemaFactory.identifier }); + const newNode = new HasId({ id: "x" }); + assert.equal(newNode.id, "x"); + assert.equal(Tree.shortId(newNode), "x"); + }); + + it("schema access POJO", () => { + const Pojo = schemaFactory.object("A", {}); + const node = new Pojo({}); + assert.equal(Tree.schema(node), Pojo); + assert.equal(node[typeNameSymbol], Pojo.identifier); + assert.equal(node[typeSchemaSymbol], Pojo); + }); + + it("schema access Customizable", () => { + const Customizable = schemaFactory.object("A", {}); + const node = new Customizable({}); + assert.equal(Tree.schema(node), Customizable); + assert.equal(node[typeNameSymbol], Customizable.identifier); + assert.equal(node[typeSchemaSymbol], Customizable); + }); }, ); diff --git a/packages/dds/tree/src/test/simple-tree/utils.ts b/packages/dds/tree/src/test/simple-tree/utils.ts index e67d4bccb31a..012ccdc30673 100644 --- a/packages/dds/tree/src/test/simple-tree/utils.ts +++ b/packages/dds/tree/src/test/simple-tree/utils.ts @@ -12,9 +12,12 @@ import { MockNodeKeyManager, } from "../../feature-libraries/index.js"; import { + HydratedContext, isTreeNode, isTreeNodeSchemaClass, mapTreeFromNodeData, + normalizeFieldSchema, + SimpleContextSlot, type ImplicitFieldSchema, type InsertableContent, type InsertableTreeFieldFromImplicitField, @@ -119,8 +122,12 @@ export function hydrate( schema: new TreeStoredSchemaRepository(toStoredSchema(schema)), }); const manager = new MockNodeKeyManager(); - const field = new CheckoutFlexTreeView(branch, toFlexSchema(schema), manager).flexTree; - + const checkout = new CheckoutFlexTreeView(branch, toFlexSchema(schema), manager); + const field = checkout.flexTree; + branch.forest.anchors.slots.set( + SimpleContextSlot, + new HydratedContext(normalizeFieldSchema(schema).allowedTypeSet, checkout.context), + ); assert(field.context.isHydrated(), "Expected LazyField"); const mapTree = mapTreeFromNodeData( initialTree as InsertableContent,