diff --git a/packages/dds/tree/.vscode/settings.json b/packages/dds/tree/.vscode/settings.json index 48d879f4ffef..28312760378d 100644 --- a/packages/dds/tree/.vscode/settings.json +++ b/packages/dds/tree/.vscode/settings.json @@ -21,6 +21,7 @@ "covariantly", "deprioritized", "endregion", + "fluidframework", "insertable", "reentrantly", "typeparam", diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 33edcef07937..46e2b8b6826f 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -648,7 +648,7 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; - clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; + clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; }; // @alpha @sealed diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index 51222190eedb..3e99f9153305 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -449,7 +449,7 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; - clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; + clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; }; // @public @sealed diff --git a/packages/dds/tree/src/simple-tree/api/conciseTree.ts b/packages/dds/tree/src/simple-tree/api/conciseTree.ts index 512a082e2bb3..679690bf35db 100644 --- a/packages/dds/tree/src/simple-tree/api/conciseTree.ts +++ b/packages/dds/tree/src/simple-tree/api/conciseTree.ts @@ -16,7 +16,7 @@ import { getUnhydratedContext } from "../createContext.js"; * @remarks * This is "concise" meaning that explicit type information is omitted. * If the schema is compatible with {@link ITreeConfigurationOptions.preventAmbiguity}, - * types will be lossless and compatible with {@link TreeBeta.create} (unless the options are used to customize it). + * types will be lossless and compatible with {@link TreeAlpha.create} (unless the options are used to customize it). * * Every {@link TreeNode} is an array or object. * Any IFluidHandle values have been replaced by `THandle`. diff --git a/packages/dds/tree/src/simple-tree/api/create.ts b/packages/dds/tree/src/simple-tree/api/create.ts index 9c2548282eba..c0e4dc0ae4ea 100644 --- a/packages/dds/tree/src/simple-tree/api/create.ts +++ b/packages/dds/tree/src/simple-tree/api/create.ts @@ -19,6 +19,7 @@ import type { import { getOrCreateNodeFromInnerNode, UnhydratedFlexTreeNode, + type TreeNode, type Unhydrated, } from "../core/index.js"; import { @@ -50,18 +51,28 @@ import { getUnhydratedContext } from "../createContext.js"; * such as when `undefined` might be allowed (for an optional field), or when the type should be inferred from the data when more than one type is possible. * * Like with {@link TreeNodeSchemaClass}'s constructor, its an error to provide an existing node to this API. - * TODO: For that case, use we should provide `Tree.clone`. - * @privateRemarks - * This could be exposed as a public `Tree.create` function. + * For that case, use {@link TreeBeta.clone}. */ -export function createFromInsertable( - schema: TSchema, +export function createFromInsertable< + const TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema, +>( + schema: UnsafeUnknownSchema extends TSchema + ? ImplicitFieldSchema + : TSchema & ImplicitFieldSchema, data: InsertableField, context?: NodeKeyManager | undefined, -): Unhydrated> { +): Unhydrated< + TSchema extends ImplicitFieldSchema + ? TreeFieldFromImplicitField + : TreeNode | TreeLeafValue | undefined +> { const cursor = cursorFromInsertable(schema, data, context); const result = cursor === undefined ? undefined : createFromCursor(schema, cursor); - return result as Unhydrated>; + return result as Unhydrated< + TSchema extends ImplicitFieldSchema + ? TreeFieldFromImplicitField + : TreeNode | TreeLeafValue | undefined + >; } /** @@ -150,7 +161,7 @@ export function createFromVerbose( /** * Creates an unhydrated simple-tree field from a cursor in nodes mode. */ -export function createFromCursor( +export function createFromCursor( schema: TSchema, cursor: ITreeCursorSynchronous | undefined, ): Unhydrated> { diff --git a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts index 89d79614b453..c4cec14f6f6c 100644 --- a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts +++ b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts @@ -113,16 +113,19 @@ export const TreeBeta: { ): () => void; /** - * Clones the persisted data associated with a node. Some key things to note: + * Clones the persisted data associated with a node. + * + * @param node - The node to clone. + * @returns A new unhydrated node with the same persisted data as the original node. + * @remarks + * Some key things to note: + * * - Local state, such as properties added to customized schema classes, will not be cloned. However, they will be * initialized to their default state just as if the node had been created via its constructor. * - Value node types (i.e., numbers, strings, booleans, nulls and Fluid handles) will be returned as is. * - The identifiers in the node's subtree will be preserved, i.e., they are not replaced with new values. - * - * @param node - The node to clone. - * @returns A new unhydrated node with the same persisted data as the original node. */ - clone( + clone( node: TreeFieldFromImplicitField, ): TreeFieldFromImplicitField; } = { @@ -133,10 +136,10 @@ export const TreeBeta: { ): () => void { return treeNodeApi.on(node, eventName, listener); }, - clone( + clone( node: TreeFieldFromImplicitField, ): Unhydrated> { - /* The only non-TreeNode cases are {@link Value} (for an empty optional field) which can be returned as is. */ + /** The only non-TreeNode cases are {@link TreeLeafValue} and `undefined` (for an empty optional field) which can be returned as is. */ if (!isTreeNode(node)) { return node; } diff --git a/packages/dds/tree/src/test/simple-tree/object.spec.ts b/packages/dds/tree/src/test/simple-tree/object.spec.ts index 131f17dc28e9..d5017e391a90 100644 --- a/packages/dds/tree/src/test/simple-tree/object.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/object.spec.ts @@ -66,7 +66,7 @@ function testObjectLike(testCases: TestCaseErased[]) { describe("satisfies 'deepEqual'", () => { unsafeMapErased( testCases, - (item: TestCase) => { + (item: TestCase) => { it(item.name ?? pretty(item.initialTree).toString(), () => { const proxy = hydrate(item.schema, item.initialTree); assert.deepEqual(proxy, item.initialTree, "Proxy must satisfy 'deepEqual'."); @@ -86,7 +86,10 @@ function testObjectLike(testCases: TestCaseErased[]) { unsafeMapErased( testCases, - ({ initialTree, schema }: TestCase) => { + ({ + initialTree, + schema, + }: TestCase) => { describe("instanceof Object", () => { it(`${pretty(initialTree)} -> true`, () => { const root = hydrate(schema, initialTree); @@ -150,7 +153,7 @@ function testObjectLike(testCases: TestCaseErased[]) { function test1(fn: (subject: object) => unknown) { unsafeMapErased( testCases, - ({ + ({ initialTree, schema, name, diff --git a/packages/dds/tree/src/test/simple-tree/primitives.spec.ts b/packages/dds/tree/src/test/simple-tree/primitives.spec.ts index 416f6dfc7807..5649f2de9e95 100644 --- a/packages/dds/tree/src/test/simple-tree/primitives.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/primitives.spec.ts @@ -28,7 +28,7 @@ describe("Primitives", () => { * @param schema - Schema to use for the test (must include the type of 'value'.) * @param value - The value to be written/read/verified. */ - function checkExact( + function checkExact( schema: TSchema, value: InsertableTreeFieldFromImplicitField, ) { @@ -45,9 +45,9 @@ describe("Primitives", () => { }); // TODO: Consider improving coverage with more variations: - // - reading/writting an object field - // - reading/writting a list element - // - reading/writting a map entry + // - reading/writing an object field + // - reading/writing a list element + // - reading/writing a map entry // - optional } @@ -75,7 +75,7 @@ describe("Primitives", () => { * @param schema - Schema to use for the test (must include the coerced type of 'value'.) * @param value - The value to be written/read/verified. */ - function checkCoerced( + function checkCoerced( schema: TSchema, value: InsertableTreeFieldFromImplicitField, ) { @@ -93,9 +93,9 @@ describe("Primitives", () => { }); // TODO: Consider improving coverage with more variations: - // - reading/writting an object field - // - reading/writting a list element - // - reading/writting a map entry + // - reading/writing an object field + // - reading/writing a list element + // - reading/writing a map entry // - optional } @@ -106,7 +106,7 @@ describe("Primitives", () => { * @param schema - Schema to use for the test (must include the coerced type of 'value'.) * @param value - The value to be written/read/verified. */ - function checkThrows( + function checkThrows( schema: TSchema, value: InsertableTreeFieldFromImplicitField, ) { diff --git a/packages/dds/tree/src/test/simple-tree/utils.ts b/packages/dds/tree/src/test/simple-tree/utils.ts index 4fb8919a4f8c..9f573de59be5 100644 --- a/packages/dds/tree/src/test/simple-tree/utils.ts +++ b/packages/dds/tree/src/test/simple-tree/utils.ts @@ -182,7 +182,7 @@ export function pretty(arg: unknown): number | string { * @returns A new tree view for a branch of the input tree view, and an {@link TreeCheckoutFork} object that can be * used to merge the branch back into the original view. */ -export function getViewForForkedBranch( +export function getViewForForkedBranch( originalView: SchematizingSimpleTreeView, ): { forkView: SchematizingSimpleTreeView; forkCheckout: TreeCheckout } { const forkCheckout = originalView.checkout.branch(); diff --git a/packages/dds/tree/src/test/snapshots/chunked-forest-schema-compressed/hasAmbiguousField.json b/packages/dds/tree/src/test/snapshots/chunked-forest-schema-compressed/hasAmbiguousField.json new file mode 100644 index 000000000000..26b2546bf061 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/chunked-forest-schema-compressed/hasAmbiguousField.json @@ -0,0 +1,33 @@ +{ + "version": 1, + "identifiers": [], + "shapes": [ + { + "c": { + "type": "test.hasAmbiguousField", + "value": false, + "fields": [ + [ + "field", + 2 + ] + ] + } + }, + { + "c": { + "type": "test.minimal", + "value": false + } + }, + { + "d": 0 + } + ], + "data": [ + [ + 0, + 1 + ] + ] +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/chunked-forest-schema-compressed/hasRenamedField.json b/packages/dds/tree/src/test/snapshots/chunked-forest-schema-compressed/hasRenamedField.json new file mode 100644 index 000000000000..4cbef9a15d64 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/chunked-forest-schema-compressed/hasRenamedField.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "identifiers": [], + "shapes": [ + { + "c": { + "type": "test.hasRenamedField", + "value": false, + "fields": [ + [ + "stored-name", + 1 + ] + ] + } + }, + { + "c": { + "type": "test.minimal", + "value": false + } + } + ], + "data": [ + [ + 0 + ] + ] +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/hasAmbiguousField.json b/packages/dds/tree/src/test/snapshots/schema-files/hasAmbiguousField.json new file mode 100644 index 000000000000..92acc468da51 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/hasAmbiguousField.json @@ -0,0 +1,28 @@ +{ + "version": 1, + "nodes": { + "test.hasAmbiguousField": { + "object": { + "field": { + "kind": "Value", + "types": [ + "test.minimal", + "test.minimal2" + ] + } + } + }, + "test.minimal": { + "object": {} + }, + "test.minimal2": { + "object": {} + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasAmbiguousField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/hasRenamedField.json b/packages/dds/tree/src/test/snapshots/schema-files/hasRenamedField.json new file mode 100644 index 000000000000..87fcf584684a --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/hasRenamedField.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "nodes": { + "test.hasRenamedField": { + "object": { + "stored-name": { + "kind": "Value", + "types": [ + "test.minimal" + ] + } + } + }, + "test.minimal": { + "object": {} + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasRenamedField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasAmbiguousField.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasAmbiguousField.json new file mode 100644 index 000000000000..92acc468da51 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasAmbiguousField.json @@ -0,0 +1,28 @@ +{ + "version": 1, + "nodes": { + "test.hasAmbiguousField": { + "object": { + "field": { + "kind": "Value", + "types": [ + "test.minimal", + "test.minimal2" + ] + } + } + }, + "test.minimal": { + "object": {} + }, + "test.minimal2": { + "object": {} + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasAmbiguousField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasRenamedField.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasRenamedField.json new file mode 100644 index 000000000000..87fcf584684a --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasRenamedField.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "nodes": { + "test.hasRenamedField": { + "object": { + "stored-name": { + "kind": "Value", + "types": [ + "test.minimal" + ] + } + } + }, + "test.minimal": { + "object": {} + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasRenamedField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/testTrees.ts b/packages/dds/tree/src/test/testTrees.ts index c8abc0017d73..057f5a0090e0 100644 --- a/packages/dds/tree/src/test/testTrees.ts +++ b/packages/dds/tree/src/test/testTrees.ts @@ -46,6 +46,8 @@ import { jsonableTreesFromFieldCursor } from "./feature-libraries/chunked-forest import { fieldJsonCursor } from "./json/jsonCursor.js"; import { brand } from "../util/index.js"; import type { Partial } from "@sinclair/typebox"; +// eslint-disable-next-line import/no-internal-modules +import { isLazy, type LazyItem } from "../simple-tree/flexList.js"; interface TestSimpleTree { readonly name: string; @@ -53,7 +55,8 @@ interface TestSimpleTree { /** * InsertableTreeFieldFromImplicitField */ - readonly root: InsertableField; + root(): InsertableField; + readonly ambiguous: boolean; } interface TestTree { @@ -63,16 +66,23 @@ interface TestTree { readonly treeFactory: (idCompressor?: IIdCompressor) => JsonableTree[]; } -function testSimpleTree( +function testSimpleTree( name: string, schema: TSchema, - root: InsertableTreeFieldFromImplicitField, + root: LazyItem>, + ambiguous = false, ): TestSimpleTree { - return { name, schema, root: root as InsertableField }; + const normalizedLazy = isLazy(root) ? root : () => root; + return { + name, + schema, + root: normalizedLazy as () => InsertableField, + ambiguous, + }; } function convertSimpleTreeTest(data: TestSimpleTree): TestTree { - const cursor = cursorFromInsertable(data.schema, data.root); + const cursor = cursorFromInsertable(data.schema, data.root()); return test( data.name, toStoredSchema(data.schema), @@ -115,9 +125,16 @@ export function treeContentFromTestTree(testData: TestTree): TreeStoredContent { const factory = new SchemaFactory("test"); export class Minimal extends factory.object("minimal", {}) {} +export class Minimal2 extends factory.object("minimal2", {}) {} export class HasMinimalValueField extends factory.object("hasMinimalValueField", { field: Minimal, }) {} +export class HasRenamedField extends factory.object("hasRenamedField", { + field: factory.required(Minimal, { key: "stored-name" }), +}) {} +export class HasAmbiguousField extends factory.object("hasAmbiguousField", { + field: [Minimal, Minimal2], +}) {} export class HasNumericValueField extends factory.object("hasNumericValueField", { field: factory.number, }) {} @@ -186,6 +203,13 @@ export const testSimpleTrees: readonly TestSimpleTree[] = [ testSimpleTree("true boolean", factory.boolean, true), testSimpleTree("false boolean", factory.boolean, false), testSimpleTree("hasMinimalValueField", HasMinimalValueField, { field: {} }), + testSimpleTree("hasRenamedField", HasRenamedField, { field: {} }), + testSimpleTree( + "hasAmbiguousField", + HasAmbiguousField, + () => ({ field: new Minimal({}) }), + true, + ), testSimpleTree("hasNumericValueField", HasNumericValueField, { field: 5 }), testSimpleTree("hasPolymorphicValueField", HasPolymorphicValueField, { field: 5 }), testSimpleTree("hasOptionalField-empty", HasOptionalField, {}), diff --git a/packages/dds/tree/src/test/utils.ts b/packages/dds/tree/src/test/utils.ts index f6e61156a303..629d1cf67d42 100644 --- a/packages/dds/tree/src/test/utils.ts +++ b/packages/dds/tree/src/test/utils.ts @@ -1153,7 +1153,7 @@ export function treeTestFactory( * * Typically, users will want to initialize the returned view with some content (thereby setting its schema) using `TreeView.initialize`. */ -export function getView( +export function getView( config: TreeViewConfiguration, nodeKeyManager?: NodeKeyManager, logger?: ITelemetryLoggerExt, @@ -1178,7 +1178,7 @@ export function getView( /** * Views the supplied checkout with the given schema. */ -export function viewCheckout( +export function viewCheckout( checkout: TreeCheckout, config: TreeViewConfiguration, ): SchematizingSimpleTreeView { diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md index 42433c9c86fc..9f15695ab0c6 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md @@ -1023,7 +1023,7 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; - clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; + clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; }; // @alpha @sealed diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md index 8ffdee75166a..218504b783fd 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md @@ -821,7 +821,7 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; - clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; + clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; }; // @public @sealed