From 0598d7d914d8698906b97f0df68196ed1eb2076e Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:26:22 -0700 Subject: [PATCH 01/54] Add data input and output APIs and ensureUnambiguous --- packages/dds/tree/src/simple-tree/tree.ts | 146 ++++++++++++++++-- .../dds/tree/src/simple-tree/treeNodeApi.ts | 143 ++++++++++++++++- packages/dds/tree/src/util/utils.ts | 9 +- 3 files changed, 282 insertions(+), 16 deletions(-) diff --git a/packages/dds/tree/src/simple-tree/tree.ts b/packages/dds/tree/src/simple-tree/tree.ts index 6706562a2537..c0de143e9ec3 100644 --- a/packages/dds/tree/src/simple-tree/tree.ts +++ b/packages/dds/tree/src/simple-tree/tree.ts @@ -4,6 +4,7 @@ */ import type { IFluidLoadable, IDisposable } from "@fluidframework/core-interfaces"; +import { UsageError } from "@fluidframework/telemetry-utils/internal"; import type { CommitMetadata } from "../core/index.js"; import type { Listenable } from "../events/index.js"; @@ -17,12 +18,14 @@ import { type InsertableTreeFieldFromImplicitField, type TreeFieldFromImplicitField, type TreeNodeSchema, + FieldKind, } from "./schemaTypes.js"; import { toFlexSchema } from "./toFlexSchema.js"; import { LeafNodeSchema } from "./leafNodeSchema.js"; import { assert } from "@fluidframework/core-utils/internal"; -import { isObjectNodeSchema } from "./objectNodeTypes.js"; +import { isObjectNodeSchema, type ObjectNodeSchema } from "./objectNodeTypes.js"; import { markSchemaMostDerived } from "./schemaFactory.js"; +import { fail, getOrAddEmptyToMap } from "../util/index.js"; /** * Channel for a Fluid Tree DDS. @@ -84,11 +87,29 @@ export interface ITreeConfigurationOptions { * bit slower. */ enableSchemaValidation?: boolean; + + /** + * If `true`, constructing the configuration will error if any valid input data could ambiguously parse in multiple ways. + * + * @defaultValue `false`. + * + * @remarks + * When this is true, it ensures that the compile time type safety for data when constructing nodes is sufficient to ensure than the runtime behavior will node give data ambiguity errors. + * + * This ensures that the canonical JSON representation of the node (the default produce by JSON.stringify before any customization) of any union in the tree is lossless and unambiguous. + * + * This check is conservative: some complex cases may error if the current simple algorithm can not show no ambiguity is possible. + * This check may become more permissive over time. + * @privateRemarks + * In the future (mostly by making map node allow record like inputs), we can support lossless round tripping via tha canonical JSON representation mentioned above when unambiguous. + */ + readonly ensureUnambiguous?: boolean; } const defaultTreeConfigurationOptions: Required = { enableSchemaValidation: false, -}; + ensureUnambiguous: false, +} as const; /** * Property-bag configuration for {@link TreeViewConfiguration} construction. @@ -140,18 +161,117 @@ export class TreeViewConfiguration, errors: string[]): void { + const checked: Set = new Set(); + const maps: TreeNodeSchema[] = []; + const arrays: TreeNodeSchema[] = []; + + const objects: ObjectNodeSchema[] = []; + // Map from key to schema using that key + const allObjectKeys: Map = new Map(); + + for (const schema of union) { + if (checked.has(schema)) { + throw new UsageError(`Duplicate schema in allowed types: ${schema.identifier}`); + } + checked.add(schema); + + if (union instanceof LeafNodeSchema) { + // nothing to do + } else if (isObjectNodeSchema(schema)) { + objects.push(schema); + for (const key of schema.fields.keys()) { + getOrAddEmptyToMap(allObjectKeys, key).push(schema); + } + } else if (schema.kind === NodeKind.Array) { + arrays.push(schema); + } else { + assert(schema.kind === NodeKind.Map, "invalid schema"); + maps.push(schema); + } + } + + if (arrays.length > 1) { + errors.push( + `More than one kind of array allowed within union (${JSON.stringify(arrays.map((s) => s.identifier))}). This would require type disambiguation which is not supported by arrays during import or export to JSON.`, + ); + } + + if (maps.length > 1) { + errors.push( + `More than one kind of map allowed within union (${JSON.stringify(maps.map((s) => s.identifier))}). This would require type disambiguation which is not supported by maps during import or export to JSON.`, + ); + } + + if (maps.length > 0 && arrays.length > 0) { + errors.push( + `Both a map and an array allowed within union (${JSON.stringify([...arrays, ...maps].map((s) => s.identifier))}). Both can be implicitly constructed from arrays, which are ambiguous when the array is empty.`, + ); + } + + if (objects.length > 0 && maps.length > 0) { + errors.push( + `Both a object and a map allowed within union (${JSON.stringify([...objects, ...maps].map((s) => s.identifier))}). Support for constructing maps from objects is planned this will become ambiguous when that is supported.`, + ); + } + + // Check for objects which fully overlap: + for (const schema of objects) { + // Check that each object has at least one required field which is unique to it. + // This is a conservative uniqueness check that ensures that its possible to disambiguate objects, + // even if they have extra non-field members on them (that don't collide with any fields in the union), without even checking the types of fields. + let hasUniqueField = false; + for (const [key, field] of schema.fields) { + if (field.kind === FieldKind.Required) { + const withKey = allObjectKeys.get(key) ?? fail("missing schema"); + if (withKey.length === 0) { + hasUniqueField = true; + } + } + } + + if (hasUniqueField === false) { + // TODO: make this check more permissive. + // Allow using the type of the field to disambiguate, at least for leaf types. + // Add "constant" fields which can be used to disambiguate even more cases without adding persisted data: maybe make them optional in constructor? + // Consider separating unambiguous implicit construction format from constructor arguments at type level, allowing constructor to superset the implicit construction options (ex: optional constant fields). + // The policy here however must remain at least as conservative as shallowCompatibilityTest in src/simple-tree/toMapTree.ts. + + errors.push( + `Object ${JSON.stringify(schema.identifier)} does not have a unique required field within union (${JSON.stringify([...union].map((s) => s.identifier))}). Support for constructing maps from objects is planned this will become ambiguous when that is supported.`, + ); + } + } +} + export function walkNodeSchema( schema: TreeNodeSchema, - visitor: (schema: TreeNodeSchema) => void, + visitor: SchemaVisitor, visitedSet: Set, ): void { if (visitedSet.has(schema)) { @@ -176,12 +296,12 @@ export function walkNodeSchema( // This was picked since when fixing errors, // working from the inner types out to the types that use them will probably go better than the reverse. // This does not however ensure all types referenced by a type are visited before it, since in recursive cases thats impossible. - visitor(schema); + visitor.node?.(schema); } export function walkFieldSchema( schema: ImplicitFieldSchema, - visitor: (schema: TreeNodeSchema) => void, + visitor: SchemaVisitor, visitedSet: Set = new Set(), ): void { walkAllowedTypes(normalizeFieldSchema(schema).allowedTypeSet, visitor, visitedSet); @@ -189,12 +309,18 @@ export function walkFieldSchema( export function walkAllowedTypes( allowedTypes: Iterable, - visitor: (schema: TreeNodeSchema) => void, + visitor: SchemaVisitor, visitedSet: Set, ): void { for (const childType of allowedTypes) { walkNodeSchema(childType, visitor, visitedSet); } + visitor.allowedTypes?.(allowedTypes); +} + +export interface SchemaVisitor { + node?: (schema: TreeNodeSchema) => void; + allowedTypes?: (allowedTypes: Iterable) => void; } /** diff --git a/packages/dds/tree/src/simple-tree/treeNodeApi.ts b/packages/dds/tree/src/simple-tree/treeNodeApi.ts index 98ea637ddae4..c0233b1a8ddc 100644 --- a/packages/dds/tree/src/simple-tree/treeNodeApi.ts +++ b/packages/dds/tree/src/simple-tree/treeNodeApi.ts @@ -4,8 +4,9 @@ */ import { assert, oob } from "@fluidframework/core-utils/internal"; +import type { IFluidHandle } from "@fluidframework/core-interfaces"; -import { Multiplicity, rootFieldKey } from "../core/index.js"; +import { Multiplicity, rootFieldKey, type TreeNodeSchemaIdentifier } from "../core/index.js"; import { type LazyItem, type TreeStatus, @@ -13,7 +14,12 @@ import { isTreeValue, FlexObjectNodeSchema, } from "../feature-libraries/index.js"; -import { fail, extractFromOpaque, isReadonlyArray } from "../util/index.js"; +import { + fail, + extractFromOpaque, + isReadonlyArray, + type JsonCompatible, +} from "../util/index.js"; import { getOrCreateNodeProxy, isTreeNode } from "./proxies.js"; import { getFlexNode, getKernel } from "./proxyBinding.js"; @@ -26,6 +32,8 @@ import { FieldSchema, type ImplicitAllowedTypes, type TreeNodeFromImplicitAllowedTypes, + type InsertableTreeFieldFromImplicitField, + type TreeFieldFromImplicitField, } from "./schemaTypes.js"; import type { TreeNode, TreeChangeEvents } from "./types.js"; import { @@ -122,6 +130,137 @@ export interface TreeNodeApi { * The same node's identifier may, for example, be different across multiple sessions for the same client and document, or different across two clients in the same session. */ shortId(node: TreeNode): number | string | undefined; + + /** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. + * @remarks + * When providing a {@link TreeNodeSchemaClass}, this is the same as invoking its constructor except that an unhydrated node can also be provided. + * This function exists as a generalization that can be used in other cases as well, + * 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. + * For that case, use {@link Tree.clone}. + */ + create( + schema: TSchema, + data: InsertableTreeFieldFromImplicitField, + ): TreeFieldFromImplicitField; + + /** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + */ + createFromVerbose( + schema: TSchema, + data: VerboseTree | undefined, + options?: { + handleConverter>(data: T): T | IFluidHandle; + useStableFieldKeys?: boolean; + }, + ): TreeFieldFromImplicitField; + + /** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + */ + createFromVerbose( + schema: TSchema, + data: VerboseTree | undefined, + options?: { + handleConverter?: undefined; + useStableFieldKeys?: boolean; + }, + ): TreeFieldFromImplicitField; + + /** + * Like {@link Tree.create}, except deeply clones existing nodes. + * @remarks + * This only clones the persisted data associated with a node. + * Local state, such as properties added to customized schema classes, will not be cloned: + * they will be initialized however they end up after running the constructor, just like if a remote client had inserted the same nodes. + */ + clone( + original: TreeFieldFromImplicitField, + options?: { + /** + * If set, all identifier's in the cloned tree (See {@link SchemaFactory.identifier}) will be replaced with new ones allocated using the default identifier allocation schema. + * Otherwise any identifiers will be preserved as is. + */ + replaceIdentifiers?: true; + }, + ): TreeFieldFromImplicitField; + + /** + * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. + * + * @remarks + * If the schema is compatible with {@link ITreeConfigurationOptions.ensureUnambiguous}, then the returned object will be lossless and compatible with {@link Tree.create} (unless the options are used to customize it). + */ + cloneToJSON( + node: TreeNode, + options?: { handleConverter(handle: IFluidHandle): T; useStableFieldKeys?: boolean }, + ): JsonCompatible; + + /** + * + */ + cloneToJSON( + node: TreeNode, + options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, + ): JsonCompatible; + + /** + * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. + * Verbose tree format, with explicit type on every node. + * + * @remarks + * If the schema is compatible with {@link ITreeConfigurationOptions.ensureUnambiguous}, then the returned object will be lossless and compatible with {@link Tree.create} (unless the options are used to customize it). + */ + cloneToJSONVerbose( + node: TreeNode, + options?: { handleConverter(handle: IFluidHandle): T; useStableFieldKeys?: boolean }, + ): VerboseTree; + + /** + * Same as generic overload, except leaves handles as is. + */ + cloneToJSONVerbose( + node: TreeNode, + options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, + ): VerboseTree; +} + +export type VerboseTreeValue = + | VerboseTree + | Exclude + | THandle; + +/** + * The fields required by a node in a tree. + * + * @privateRemarks A forked version of this type is used in `persistedTreeTextFormat.ts`. + * Changes to this type might necessitate changes to `EncodedNodeData` or codecs. + * See persistedTreeTextFormat's module documentation for more details. + * + * @public + */ +export interface VerboseTree { + /** + * The meaning of this node. + * Provides contexts/semantics for this node and its content. + * Typically used to associate a node with metadata (including a schema) and source code (types, behaviors, etc). + */ + type: TreeNodeSchemaIdentifier; + + fields: + | VerboseTreeValue[] + | { + [key: string]: VerboseTreeValue; + }; } /** diff --git a/packages/dds/tree/src/util/utils.ts b/packages/dds/tree/src/util/utils.ts index 81a688d9a06e..e5ab0ee91b56 100644 --- a/packages/dds/tree/src/util/utils.ts +++ b/packages/dds/tree/src/util/utils.ts @@ -212,14 +212,15 @@ export function find(iterable: Iterable, predicate: (t: T) => boolean): T * but instead mostly restricts access to it. * @internal */ -export type JsonCompatible = +export type JsonCompatible = | string | number | boolean // eslint-disable-next-line @rushstack/no-new-null | null - | JsonCompatible[] - | JsonCompatibleObject; + | JsonCompatible[] + | JsonCompatibleObject + | TExtra; /** * Use for Json object compatible data. @@ -228,7 +229,7 @@ export type JsonCompatible = * but instead mostly restricts access to it. * @internal */ -export type JsonCompatibleObject = { [P in string]?: JsonCompatible }; +export type JsonCompatibleObject = { [P in string]?: JsonCompatible }; /** * Use for readonly view of Json compatible data. From 54f9fedc9adb3f8f47bace0576f0930934fc8309 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:51:30 -0700 Subject: [PATCH 02/54] Update packages/dds/tree/src/simple-tree/tree.ts Co-authored-by: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com> --- packages/dds/tree/src/simple-tree/tree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dds/tree/src/simple-tree/tree.ts b/packages/dds/tree/src/simple-tree/tree.ts index c0de143e9ec3..8e1bf36cc290 100644 --- a/packages/dds/tree/src/simple-tree/tree.ts +++ b/packages/dds/tree/src/simple-tree/tree.ts @@ -94,7 +94,7 @@ export interface ITreeConfigurationOptions { * @defaultValue `false`. * * @remarks - * When this is true, it ensures that the compile time type safety for data when constructing nodes is sufficient to ensure than the runtime behavior will node give data ambiguity errors. + * When this is true, it ensures that the compile time type safety for data when constructing nodes is sufficient to ensure than the runtime behavior will give node data ambiguity errors. * * This ensures that the canonical JSON representation of the node (the default produce by JSON.stringify before any customization) of any union in the tree is lossless and unambiguous. * From 7ade94ea420c84684f7ff9ff73718b7ff22cfe01 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:00:55 -0700 Subject: [PATCH 03/54] Update packages/dds/tree/src/simple-tree/tree.ts --- packages/dds/tree/src/simple-tree/tree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dds/tree/src/simple-tree/tree.ts b/packages/dds/tree/src/simple-tree/tree.ts index 8e1bf36cc290..36ffdac3c77f 100644 --- a/packages/dds/tree/src/simple-tree/tree.ts +++ b/packages/dds/tree/src/simple-tree/tree.ts @@ -201,7 +201,7 @@ function checkUnion(union: Iterable, errors: string[]): void { } checked.add(schema); - if (union instanceof LeafNodeSchema) { + if (schema instanceof LeafNodeSchema) { // nothing to do } else if (isObjectNodeSchema(schema)) { objects.push(schema); From b36056ecd5f591c3d057c566fe92cc438fde275e Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:16:02 -0700 Subject: [PATCH 04/54] Schema traversal cleanup and docs --- packages/dds/tree/src/simple-tree/tree.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/dds/tree/src/simple-tree/tree.ts b/packages/dds/tree/src/simple-tree/tree.ts index 36ffdac3c77f..228509effca6 100644 --- a/packages/dds/tree/src/simple-tree/tree.ts +++ b/packages/dds/tree/src/simple-tree/tree.ts @@ -19,6 +19,7 @@ import { type TreeFieldFromImplicitField, type TreeNodeSchema, FieldKind, + normalizeAllowedTypes, } from "./schemaTypes.js"; import { toFlexSchema } from "./toFlexSchema.js"; import { LeafNodeSchema } from "./leafNodeSchema.js"; @@ -282,7 +283,7 @@ export function walkNodeSchema( // nothing to do } else if (isObjectNodeSchema(schema)) { for (const field of schema.fields.values()) { - walkAllowedTypes(field.allowedTypeSet, visitor, visitedSet); + walkFieldSchema(field, visitor, visitedSet); } } else { assert( @@ -290,7 +291,7 @@ export function walkNodeSchema( 0x9b3 /* invalid schema */, ); const childTypes = schema.info as ImplicitAllowedTypes; - walkFieldSchema(childTypes, visitor, visitedSet); + walkAllowedTypes(normalizeAllowedTypes(childTypes), visitor, visitedSet); } // This visit is done at the end so the traversal order is most inner types first. // This was picked since when fixing errors, @@ -318,8 +319,20 @@ export function walkAllowedTypes( visitor.allowedTypes?.(allowedTypes); } +/** + * Callbacks for use in {@link walkFieldSchema} / {@link walkAllowedTypes} / {@link walkNodeSchema}. + */ export interface SchemaVisitor { + /** + * Called once for each node schema. + */ node?: (schema: TreeNodeSchema) => void; + /** + * Called once for set of allowed types. + * Includes implicit allowed types (when a single type was used instead of an array). + * + * This includes every field, but also the allowed types array for maps and arrays and the root if starting at {@link walkAllowedTypes}. + */ allowedTypes?: (allowedTypes: Iterable) => void; } From cceedc2c882e44f55e083458a03117dc613e214a Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:15:12 -0700 Subject: [PATCH 05/54] Cleanup and docs --- .../dds/tree/src/simple-tree/treeNodeApi.ts | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/dds/tree/src/simple-tree/treeNodeApi.ts b/packages/dds/tree/src/simple-tree/treeNodeApi.ts index c0233b1a8ddc..1ecbe6baec74 100644 --- a/packages/dds/tree/src/simple-tree/treeNodeApi.ts +++ b/packages/dds/tree/src/simple-tree/treeNodeApi.ts @@ -155,9 +155,9 @@ export interface TreeNodeApi { */ createFromVerbose( schema: TSchema, - data: VerboseTree | undefined, + data: VerboseTreeNode | undefined, options?: { - handleConverter>(data: T): T | IFluidHandle; + handleConverter>(data: T): T | IFluidHandle; useStableFieldKeys?: boolean; }, ): TreeFieldFromImplicitField; @@ -169,7 +169,7 @@ export interface TreeNodeApi { */ createFromVerbose( schema: TSchema, - data: VerboseTree | undefined, + data: VerboseTreeNode | undefined, options?: { handleConverter?: undefined; useStableFieldKeys?: boolean; @@ -223,7 +223,7 @@ export interface TreeNodeApi { cloneToJSONVerbose( node: TreeNode, options?: { handleConverter(handle: IFluidHandle): T; useStableFieldKeys?: boolean }, - ): VerboseTree; + ): VerboseTreeNode; /** * Same as generic overload, except leaves handles as is. @@ -231,24 +231,31 @@ export interface TreeNodeApi { cloneToJSONVerbose( node: TreeNode, options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, - ): VerboseTree; + ): VerboseTreeNode; } -export type VerboseTreeValue = - | VerboseTree +/** + * Verbose encoding of a {@link TreeNode} or {@link TreeValue}. + * @remarks + * This is verbose meaning that every {@link TreeNode} is a {@link VerboseTreeNode}. + * Any IFluidHandle values have been replaced by `THandle`. + * @public + */ +export type VerboseTree = + | VerboseTreeNode | Exclude | THandle; /** - * The fields required by a node in a tree. - * - * @privateRemarks A forked version of this type is used in `persistedTreeTextFormat.ts`. - * Changes to this type might necessitate changes to `EncodedNodeData` or codecs. - * See persistedTreeTextFormat's module documentation for more details. + * Verbose encoding of a {@link TreeNode}. + * @remarks + * This is verbose meaning that every {@link TreeNode} has an explicit `type` property, and `fields`. + * This allowed VerboseTreeNode to be unambiguous regarding which type each node is without relying on symbols or hidden state. * + * Any IFluidHandle values have been replaced by `THandle`. If the `THandle` is JSON compatible, then this type is JSON compatible as well. * @public */ -export interface VerboseTree { +export interface VerboseTreeNode { /** * The meaning of this node. * Provides contexts/semantics for this node and its content. @@ -256,10 +263,15 @@ export interface VerboseTree { */ type: TreeNodeSchemaIdentifier; + /** + * Content of this node. + * For array nodes, an array of children. + * For map and object nodes, an object which children under keys. + */ fields: - | VerboseTreeValue[] + | VerboseTree[] | { - [key: string]: VerboseTreeValue; + [key: string]: VerboseTree; }; } From d13b7c2c29ef39ad454337c665964c8f00f252fd Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:34:34 -0700 Subject: [PATCH 06/54] Tidy up API and comments --- .../dds/tree/src/simple-tree/treeNodeApi.ts | 53 ++++++++++++++----- packages/dds/tree/src/util/utils.ts | 4 +- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/packages/dds/tree/src/simple-tree/treeNodeApi.ts b/packages/dds/tree/src/simple-tree/treeNodeApi.ts index f03ee6b9bef4..1d171ce0f968 100644 --- a/packages/dds/tree/src/simple-tree/treeNodeApi.ts +++ b/packages/dds/tree/src/simple-tree/treeNodeApi.ts @@ -36,7 +36,7 @@ import { type InsertableTreeFieldFromImplicitField, type TreeFieldFromImplicitField, } from "./schemaTypes.js"; -import type { TreeNode, TreeChangeEvents } from "./types.js"; +import type { TreeNode, TreeChangeEvents, Unhydrated } from "./types.js"; import { booleanSchema, handleSchema, @@ -148,7 +148,7 @@ export interface TreeNodeApi { create( schema: TSchema, data: InsertableTreeFieldFromImplicitField, - ): TreeFieldFromImplicitField; + ): Unhydrated>; /** * Construct tree content compatible with a field defined by the provided `schema`. @@ -159,10 +159,13 @@ export interface TreeNodeApi { schema: TSchema, data: VerboseTreeNode | undefined, options?: { - handleConverter>(data: T): T | IFluidHandle; - useStableFieldKeys?: boolean; + handleConverter>( + data: T, + location: FieldSchema, + ): VerboseTreeNode; + readonly useStableFieldKeys?: boolean; }, - ): TreeFieldFromImplicitField; + ): Unhydrated>; /** * Construct tree content compatible with a field defined by the provided `schema`. @@ -173,10 +176,10 @@ export interface TreeNodeApi { schema: TSchema, data: VerboseTreeNode | undefined, options?: { - handleConverter?: undefined; - useStableFieldKeys?: boolean; + readonly handleConverter?: undefined; + readonly useStableFieldKeys?: boolean; }, - ): TreeFieldFromImplicitField; + ): Unhydrated>; /** * Like {@link Tree.create}, except deeply clones existing nodes. @@ -200,15 +203,18 @@ export interface TreeNodeApi { * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. * * @remarks - * If the schema is compatible with {@link ITreeConfigurationOptions.ensureUnambiguous}, then the returned object will be lossless and compatible with {@link Tree.create} (unless the options are used to customize it). + * If the schema is compatible with {@link ITreeConfigurationOptions.preventAmbiguity}, then the returned object will be lossless and compatible with {@link Tree.create} (unless the options are used to customize it). */ cloneToJSON( node: TreeNode, - options?: { handleConverter(handle: IFluidHandle): T; useStableFieldKeys?: boolean }, + options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; + }, ): JsonCompatible; /** - * + * Same as generic overload, except leaves handles as is. */ cloneToJSON( node: TreeNode, @@ -220,11 +226,21 @@ export interface TreeNodeApi { * Verbose tree format, with explicit type on every node. * * @remarks - * If the schema is compatible with {@link ITreeConfigurationOptions.ensureUnambiguous}, then the returned object will be lossless and compatible with {@link Tree.create} (unless the options are used to customize it). + * There are several cases this may be preferred to {@link Tree.clone}: + * + * 1. When not using {@link ITreeConfigurationOptions.preventAmbiguity} (or when using `useStableFieldKeys`), {@link Tree.clone} can produce ambiguous data (the type may be unclear on some nodes). + * This may be a good alternative to {@link Tree.clone} since it is lossless. + * + * 2. When the data might be interpreted without access to the exact same view schema. In such cases, the types may be unknowable if not included. + * + * 3. When easy access to the type is desired, or a more uniform simple to parse format is desired. */ cloneToJSONVerbose( node: TreeNode, - options?: { handleConverter(handle: IFluidHandle): T; useStableFieldKeys?: boolean }, + options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; + }, ): VerboseTreeNode; /** @@ -232,7 +248,7 @@ export interface TreeNodeApi { */ cloneToJSONVerbose( node: TreeNode, - options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, + options?: { readonly handleConverter?: undefined; readonly useStableFieldKeys?: boolean }, ): VerboseTreeNode; } @@ -255,6 +271,15 @@ export type VerboseTree = * This allowed VerboseTreeNode to be unambiguous regarding which type each node is without relying on symbols or hidden state. * * Any IFluidHandle values have been replaced by `THandle`. If the `THandle` is JSON compatible, then this type is JSON compatible as well. + * + * @privateRemarks + * This type is only used for data which is copied into and out of the tree. + * When being copied out, its fine to have the data be mutable since its a copy. + * + * When being copied in, we don't need to mutate, so we could use a readonly variant of this type. + * however the copy in case (createFromVerbose) probably isn't harmed much by just reusing this type as is, + * since if the caller has immutable data, TypeScript doesn't prevent assigning immutable data to a mutable type anyway. + * Also relaxing the input methods to take readonly data would be a non-breaking change so it can be done later if desired. * @public */ export interface VerboseTreeNode { diff --git a/packages/dds/tree/src/util/utils.ts b/packages/dds/tree/src/util/utils.ts index e5ab0ee91b56..43d828d1c3d0 100644 --- a/packages/dds/tree/src/util/utils.ts +++ b/packages/dds/tree/src/util/utils.ts @@ -210,7 +210,7 @@ export function find(iterable: Iterable, predicate: (t: T) => boolean): T * * Note that this does not robustly forbid non json comparable data via type checking, * but instead mostly restricts access to it. - * @internal + * @public */ export type JsonCompatible = | string @@ -227,7 +227,7 @@ export type JsonCompatible = * * Note that this does not robustly forbid non json comparable data via type checking, * but instead mostly restricts access to it. - * @internal + * @public */ export type JsonCompatibleObject = { [P in string]?: JsonCompatible }; From 2e5b97542c106f06e5a3e4f2e94e2e0117d3e5ca Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Fri, 9 Aug 2024 12:23:45 -0700 Subject: [PATCH 07/54] implement some json interop --- .../dds/tree/api-report/tree.alpha.api.md | 6 +- packages/dds/tree/api-report/tree.beta.api.md | 6 +- .../dds/tree/api-report/tree.public.api.md | 6 +- .../src/feature-libraries/mapTreeCursor.ts | 9 +- .../tree/src/simple-tree/api/jsonInterop.ts | 488 ++++++++++++++++++ .../tree/src/simple-tree/api/treeNodeApi.ts | 197 +------ .../dds/tree/src/simple-tree/arrayNode.ts | 2 +- .../src/simple-tree/core/treeNodeSchema.ts | 5 +- packages/dds/tree/src/simple-tree/mapNode.ts | 6 +- .../dds/tree/src/simple-tree/schemaTypes.ts | 3 +- .../dds/tree/src/simple-tree/toMapTree.ts | 44 +- packages/dds/tree/src/test/utils.ts | 18 +- 12 files changed, 554 insertions(+), 236 deletions(-) create mode 100644 packages/dds/tree/src/simple-tree/api/jsonInterop.ts diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 4252a7ae5d40..59b2ba9c018c 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -514,19 +514,19 @@ export interface TreeNodeApi { key(node: TreeNode): string | number; on(node: TreeNode, eventName: K, listener: TreeChangeEvents[K]): () => void; parent(node: TreeNode): TreeNode | undefined; - schema(node: T): TreeNodeSchema; + schema(node: T): TreeNodeSchema; shortId(node: TreeNode): number | string | undefined; status(node: TreeNode): TreeStatus; } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; +export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : TreeNode | TreeLeafValue; // @public type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public @sealed -export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; +export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; // @public @sealed export interface TreeNodeSchemaClass extends TreeNodeSchemaCore { diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index d07481673764..4ed0bbf11cdf 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -446,19 +446,19 @@ export interface TreeNodeApi { key(node: TreeNode): string | number; on(node: TreeNode, eventName: K, listener: TreeChangeEvents[K]): () => void; parent(node: TreeNode): TreeNode | undefined; - schema(node: T): TreeNodeSchema; + schema(node: T): TreeNodeSchema; shortId(node: TreeNode): number | string | undefined; status(node: TreeNode): TreeStatus; } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; +export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : TreeNode | TreeLeafValue; // @public type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public @sealed -export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; +export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; // @public @sealed export interface TreeNodeSchemaClass extends TreeNodeSchemaCore { diff --git a/packages/dds/tree/api-report/tree.public.api.md b/packages/dds/tree/api-report/tree.public.api.md index d665a97c9cee..49fe91708d2a 100644 --- a/packages/dds/tree/api-report/tree.public.api.md +++ b/packages/dds/tree/api-report/tree.public.api.md @@ -446,19 +446,19 @@ export interface TreeNodeApi { key(node: TreeNode): string | number; on(node: TreeNode, eventName: K, listener: TreeChangeEvents[K]): () => void; parent(node: TreeNode): TreeNode | undefined; - schema(node: T): TreeNodeSchema; + schema(node: T): TreeNodeSchema; shortId(node: TreeNode): number | string | undefined; status(node: TreeNode): TreeStatus; } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; +export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : TreeNode | TreeLeafValue; // @public type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public @sealed -export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; +export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; // @public @sealed export interface TreeNodeSchemaClass extends TreeNodeSchemaCore { diff --git a/packages/dds/tree/src/feature-libraries/mapTreeCursor.ts b/packages/dds/tree/src/feature-libraries/mapTreeCursor.ts index 24c980d77362..08fa8ad2b488 100644 --- a/packages/dds/tree/src/feature-libraries/mapTreeCursor.ts +++ b/packages/dds/tree/src/feature-libraries/mapTreeCursor.ts @@ -8,6 +8,7 @@ import { assert } from "@fluidframework/core-utils/internal"; import { CursorLocationType, type DetachedField, + type ExclusiveMapTree, type FieldKey, type ITreeCursor, type MapTree, @@ -59,15 +60,15 @@ const adapter: CursorAdapter = { /** * Extract a MapTree from the contents of the given ITreeCursor's current node. */ -export function mapTreeFromCursor(cursor: ITreeCursor): MapTree { +export function mapTreeFromCursor(cursor: ITreeCursor): ExclusiveMapTree { assert(cursor.mode === CursorLocationType.Nodes, 0x3b7 /* must start at node */); - const fields: Map = new Map(); + const fields: Map = new Map(); for (let inField = cursor.firstField(); inField; inField = cursor.nextField()) { - const field: MapTree[] = mapCursorField(cursor, mapTreeFromCursor); + const field: ExclusiveMapTree[] = mapCursorField(cursor, mapTreeFromCursor); fields.set(cursor.getFieldKey(), field); } - const node: MapTree = { + const node: ExclusiveMapTree = { type: cursor.type, value: cursor.value, fields, diff --git a/packages/dds/tree/src/simple-tree/api/jsonInterop.ts b/packages/dds/tree/src/simple-tree/api/jsonInterop.ts new file mode 100644 index 000000000000..84033c736e19 --- /dev/null +++ b/packages/dds/tree/src/simple-tree/api/jsonInterop.ts @@ -0,0 +1,488 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import { isFluidHandle } from "@fluidframework/runtime-utils/internal"; + +import { + aboveRootPlaceholder, + EmptyKey, + keyAsDetachedField, + type FieldKey, + type ITreeCursorSynchronous, + type SchemaAndPolicy, + type TreeNodeSchemaIdentifier, +} from "../../core/index.js"; +import { brand, fail } from "../../util/index.js"; +import type { + TreeLeafValue, + ImplicitFieldSchema, + InsertableTreeFieldFromImplicitField, + TreeFieldFromImplicitField, +} from "../schemaTypes.js"; +import { getSimpleNodeSchema, type Unhydrated } from "../core/index.js"; +import { + defaultSchemaPolicy, + intoStoredSchema, + isTreeValue, + mapTreeFromCursor, + stackTreeFieldCursor, + stackTreeNodeCursor, + type CursorAdapter, + type NodeKeyManager, +} from "../../feature-libraries/index.js"; +import { + booleanSchema, + handleSchema, + nullSchema, + numberSchema, + stringSchema, +} from "../leafNodeSchema.js"; +import { getOrCreateNodeFromFlexTreeNode } from "../proxies.js"; +import { getOrCreateMapTreeNode } from "../../feature-libraries/index.js"; +import { toFlexSchema } from "../toFlexSchema.js"; +import { cursorFromNodeData, inSchemaOrThrow } from "../toMapTree.js"; +import { isObjectNodeSchema } from "../objectNodeTypes.js"; + +/** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. + * @remarks + * When providing a {@link TreeNodeSchemaClass}, this is the same as invoking its constructor except that an unhydrated node can also be provided. + * This function exists as a generalization that can be used in other cases as well, + * 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. + * For that case, use {@link Tree.clone}. + */ +export function create( + schema: TSchema, + data: InsertableTreeFieldFromImplicitField, + context?: NodeKeyManager | undefined, +): Unhydrated> { + const flexSchema = toFlexSchema(schema); + 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), + }; + + const cursor = cursorFromNodeData(data, schema, context, schemaValidationPolicy); + const result = cursor === undefined ? undefined : createFromCursor(schema, cursor); + return result as Unhydrated>; +} + +/** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + */ +export function createFromVerbose( + schema: TSchema, + data: VerboseTreeNode | undefined, + options: ParseOptions, +): Unhydrated>; + +/** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + */ +export function createFromVerbose( + schema: TSchema, + data: VerboseTreeNode | undefined, + options?: Partial>, +): Unhydrated>; + +export function createFromVerbose( + schema: TSchema, + data: VerboseTreeNode | undefined, + options?: Partial>, +): Unhydrated> { + const config: ParseOptions = { + valueConverter: (input: VerboseTree) => { + return input as TreeLeafValue | VerboseTreeNode; + }, + ...options, + }; + const schemalessConfig = applySchemaToParserOptions(schema, config); + const cursor = cursorFromVerbose(data, schemalessConfig); + return createFromCursor(schema, cursor); +} + +export function createFromCursor( + schema: TSchema, + cursor: ITreeCursorSynchronous, +): Unhydrated> { + const mapTree = mapTreeFromCursor(cursor); + const flexSchema = toFlexSchema(schema); + + 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), + }; + + inSchemaOrThrow(schemaValidationPolicy, mapTree); + + const rootSchema = flexSchema.nodeSchema.get(cursor.type) ?? fail("missing schema"); + const mapTreeNode = getOrCreateMapTreeNode(rootSchema, mapTree); + + // TODO: ensure this works for InnerNodes to create unhydrated nodes + const result = getOrCreateNodeFromFlexTreeNode(mapTreeNode); + return result as Unhydrated>; +} + +interface ParseOptions { + /** + * Fixup custom input formats. + * @remarks + * Main usage is translate some JSON compatible handle format into actual IFluidHandles. + */ + valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; + /** + * If true, interpret the input keys of object nodes as stable keys. + * If false, interpret them as api keys. + * @defaultValue false. + */ + readonly useStoredKeys?: boolean; +} + +interface SchemalessParseOptions { + /** + * Fixup custom input formats. + * @remarks + * Main usage is translate some JSON compatible handle format into actual IFluidHandles. + */ + valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; + /** + * Converts to stable key names. + */ + keyConverter?(type: string, inputKey: string): string; +} + +function applySchemaToParserOptions( + schema: ImplicitFieldSchema, + options: ParseOptions, +): SchemalessParseOptions { + const config: Required> = { + useStoredKeys: false, + ...options, + }; + + // TODO: should provide a way to look up schema by name efficiently without converting to flex tree schema and back. + // Maybe cache identifier->schema map on simple tree schema lazily. + const flexSchema = toFlexSchema(schema); + + return { + valueConverter: config.valueConverter, + keyConverter: config.useStoredKeys + ? undefined + : (type, inputKey) => { + const flexNodeSchema = + flexSchema.nodeSchema.get(brand(type)) ?? fail("missing schema"); + const simpleNodeSchema = getSimpleNodeSchema(flexNodeSchema); + if (isObjectNodeSchema(simpleNodeSchema)) { + const info = + simpleNodeSchema.flexKeyMap.get(inputKey) ?? fail("missing field info"); + return info.storedKey; + } + return inputKey; + }, + }; +} + +/** + * Used to read a VerboseTree as a node cursor. + * + * @returns an {@link ITreeCursorSynchronous} for a single node in nodes mode. + */ +export function cursorFromVerbose( + data: VerboseTree, + options: SchemalessParseOptions, +): ITreeCursorSynchronous { + return stackTreeNodeCursor(verboseTreeAdapter(options), data); +} + +/** + * Used to read a VerboseTree[] as a field cursor. + * + * @returns an {@link ITreeCursorSynchronous} for a single field in fields mode. + */ +export function fieldCursorFromVerbose( + data: VerboseTree[], + options: SchemalessParseOptions, +): ITreeCursorSynchronous { + return stackTreeFieldCursor( + verboseTreeAdapter(options), + { type: aboveRootPlaceholder, fields: data }, + keyAsDetachedField(EmptyKey), + ); +} + +/** + * TODO: add ParserOptions + */ +function verboseTreeAdapter( + options: SchemalessParseOptions, +): CursorAdapter> { + return { + value: (input: VerboseTree) => { + const node = options.valueConverter(input); + return isTreeValue(node) ? node : undefined; + }, + type: (input: VerboseTree) => { + const node = options.valueConverter(input); + switch (typeof node) { + case "number": + return numberSchema.identifier as TreeNodeSchemaIdentifier; + case "string": + return stringSchema.identifier as TreeNodeSchemaIdentifier; + case "boolean": + return booleanSchema.identifier as TreeNodeSchemaIdentifier; + default: + if (node === null) { + return nullSchema.identifier as TreeNodeSchemaIdentifier; + } + if (isFluidHandle(node)) { + return handleSchema.identifier as TreeNodeSchemaIdentifier; + } + return node.type as TreeNodeSchemaIdentifier; + } + }, + keysFromNode: (input: VerboseTree): readonly FieldKey[] => { + const node = options.valueConverter(input); + switch (typeof node) { + case "object": { + if (node === null) { + return []; + } + if (isFluidHandle(node)) { + return []; + } + if (Array.isArray(node.fields)) { + return node.fields.length === 0 ? [] : [EmptyKey]; + } + const inputKeys = Object.keys(node.fields); + if (options.keyConverter === undefined) { + return inputKeys as FieldKey[]; + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return inputKeys.map((k) => brand(options.keyConverter!(node.type, k))); + } + default: + return []; + } + }, + getFieldFromNode: ( + input: VerboseTree, + key: FieldKey, + ): readonly VerboseTree[] => { + const node = options.valueConverter(input); + // Object.prototype.hasOwnProperty can return true for strings (ex: with key "0"), so we have to filter them out. + // Rather than just special casing strings, we can handle them with an early return for all primitives. + if (typeof node !== "object") { + return []; + } + + if (node === null) { + return []; + } + + if (isFluidHandle(node)) { + return []; + } + + if (Array.isArray(node.fields)) { + return key === EmptyKey ? node.fields : []; + } + + if (Object.prototype.hasOwnProperty.call(node, key)) { + const field = node.fields[key]; + return field === undefined ? [] : [field]; + } + + return []; + }, + }; +} + +// /** +// * Like {@link Tree.create}, except deeply clones existing nodes. +// * @remarks +// * This only clones the persisted data associated with a node. +// * Local state, such as properties added to customized schema classes, will not be cloned: +// * they will be initialized however they end up after running the constructor, just like if a remote client had inserted the same nodes. +// */ +// export function clone( +// original: TreeFieldFromImplicitField, +// options?: { +// /** +// * If set, all identifier's in the cloned tree (See {@link SchemaFactory.identifier}) will be replaced with new ones allocated using the default identifier allocation schema. +// * Otherwise any identifiers will be preserved as is. +// */ +// replaceIdentifiers?: true; +// }, +// ): TreeFieldFromImplicitField { +// throw new Error(); +// } + +// /** +// * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. +// * +// * @remarks +// * If the schema is compatible with {@link ITreeConfigurationOptions.preventAmbiguity}, then the returned object will be lossless and compatible with {@link Tree.create} (unless the options are used to customize it). +// */ +// export function cloneToJSON( +// node: TreeNode, +// options?: { +// handleConverter(handle: IFluidHandle): T; +// readonly useStableFieldKeys?: boolean; +// }, +// ): JsonCompatible; + +// /** +// * Same as generic overload, except leaves handles as is. +// */ +// export function cloneToJSON( +// node: TreeNode, +// options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, +// ): JsonCompatible; + +// export function cloneToJSON( +// node: TreeNode, +// options?: { +// handleConverter?(handle: IFluidHandle): T; +// readonly useStableFieldKeys?: boolean; +// }, +// ): JsonCompatible { +// throw new Error(); +// } + +// /** +// * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. +// * Verbose tree format, with explicit type on every node. +// * +// * @remarks +// * There are several cases this may be preferred to {@link Tree.clone}: +// * +// * 1. When not using {@link ITreeConfigurationOptions.preventAmbiguity} (or when using `useStableFieldKeys`), {@link Tree.clone} can produce ambiguous data (the type may be unclear on some nodes). +// * This may be a good alternative to {@link Tree.clone} since it is lossless. +// * +// * 2. When the data might be interpreted without access to the exact same view schema. In such cases, the types may be unknowable if not included. +// * +// * 3. When easy access to the type is desired, or a more uniform simple to parse format is desired. +// */ +// export function cloneToJSONVerbose( +// node: TreeNode, +// options?: { +// handleConverter(handle: IFluidHandle): T; +// readonly useStableFieldKeys?: boolean; +// }, +// ): VerboseTreeNode; + +// /** +// * Same as generic overload, except leaves handles as is. +// */ +// export function cloneToJSONVerbose( +// node: TreeNode, +// options?: { readonly handleConverter?: undefined; readonly useStableFieldKeys?: boolean }, +// ): VerboseTreeNode; + +// export function cloneToJSONVerbose( +// node: TreeNode, +// options?: { +// handleConverter?(handle: IFluidHandle): T; +// readonly useStableFieldKeys?: boolean; +// }, +// ): VerboseTreeNode { +// const config = { +// handleConverter(handle: IFluidHandle): T { +// return handle as T; +// }, +// useStableFieldKeys: false, +// ...options, +// }; + +// // TODO: this should probably just get a cursor to the underlying data and use that. + +// function convertNode(n: TreeNode): VerboseTreeNode { +// let fields: VerboseTreeNode["fields"]; + +// if (n instanceof CustomArrayNodeBase) { +// const x = n as CustomArrayNodeBase; +// fields = Array.from(x, convertNodeOrValue); +// } else if ((n as TreeNode) instanceof CustomMapNodeBase) { +// fields = {}; +// for (const [key, value] of n as CustomMapNodeBase) { +// fields[key] = convertNodeOrValue(value); +// } +// } else { +// fields = {}; +// for (const [key, value] of n as CustomMapNodeBase) { +// fields[key] = convertNodeOrValue(value); +// } +// } + +// return { type: n[typeNameSymbol], fields }; +// } + +// function convertNodeOrValue(n: TreeNode | TreeLeafValue): VerboseTree { +// return isTreeNode(n) ? convertNode(n) : isFluidHandle(n) ? config.handleConverter(n) : n; +// } + +// return convertNode(node); +// } + +/** + * Verbose encoding of a {@link TreeNode} or {@link TreeValue}. + * @remarks + * This is verbose meaning that every {@link TreeNode} is a {@link VerboseTreeNode}. + * Any IFluidHandle values have been replaced by `THandle`. + * @public + */ +export type VerboseTree = + | VerboseTreeNode + | Exclude + | THandle; + +/** + * Verbose encoding of a {@link TreeNode}. + * @remarks + * This is verbose meaning that every {@link TreeNode} has an explicit `type` property, and `fields`. + * This allowed VerboseTreeNode to be unambiguous regarding which type each node is without relying on symbols or hidden state. + * + * Any IFluidHandle values have been replaced by `THandle`. If the `THandle` is JSON compatible, then this type is JSON compatible as well. + * + * @privateRemarks + * This type is only used for data which is copied into and out of the tree. + * When being copied out, its fine to have the data be mutable since its a copy. + * + * When being copied in, we don't need to mutate, so we could use a readonly variant of this type. + * however the copy in case (createFromVerbose) probably isn't harmed much by just reusing this type as is, + * since if the caller has immutable data, TypeScript doesn't prevent assigning immutable data to a mutable type anyway. + * Also relaxing the input methods to take readonly data would be a non-breaking change so it can be done later if desired. + * @public + */ +export interface VerboseTreeNode { + /** + * The meaning of this node. + * Provides contexts/semantics for this node and its content. + * Typically used to associate a node with metadata (including a schema) and source code (types, behaviors, etc). + */ + type: string; + + /** + * Content of this node. + * For array nodes, an array of children. + * For map and object nodes, an object which children under keys. + */ + fields: + | VerboseTree[] + | { + [key: string]: VerboseTree; + }; +} diff --git a/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts b/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts index b429f5fcfd32..6aa14b04079e 100644 --- a/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts +++ b/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts @@ -4,13 +4,8 @@ */ import { assert, oob } from "@fluidframework/core-utils/internal"; -import type { IFluidHandle } from "@fluidframework/core-interfaces"; -import { - Multiplicity, - rootFieldKey, - type TreeNodeSchemaIdentifier, -} from "../../core/index.js"; +import { Multiplicity, rootFieldKey } from "../../core/index.js"; import { type LazyItem, type TreeStatus, @@ -19,12 +14,7 @@ import { FlexObjectNodeSchema, isMapTreeNode, } from "../../feature-libraries/index.js"; -import { - fail, - extractFromOpaque, - isReadonlyArray, - type JsonCompatible, -} from "../../util/index.js"; +import { fail, extractFromOpaque, isReadonlyArray } from "../../util/index.js"; import { getOrCreateNodeFromFlexTreeNode } from "../proxies.js"; import { getOrCreateInnerNode } from "../proxyBinding.js"; @@ -34,8 +24,6 @@ import { FieldSchema, type ImplicitAllowedTypes, type TreeNodeFromImplicitAllowedTypes, - type InsertableTreeFieldFromImplicitField, - type TreeFieldFromImplicitField, } from "../schemaTypes.js"; import { booleanSchema, @@ -55,7 +43,6 @@ import { type TreeNode, type TreeChangeEvents, tryGetTreeNodeSchema, - type Unhydrated, } from "../core/index.js"; /** @@ -74,9 +61,7 @@ export interface TreeNodeApi { /** * The schema information for this node. */ - schema( - node: T, - ): TreeNodeSchema; + schema(node: T): TreeNodeSchema; /** * Narrow the type of the given value if it satisfies the given schema. @@ -142,174 +127,6 @@ export interface TreeNodeApi { * The same node's identifier may, for example, be different across multiple sessions for the same client and document, or different across two clients in the same session. */ shortId(node: TreeNode): number | string | undefined; - - /** - * Construct tree content compatible with a field defined by the provided `schema`. - * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. - * @param data - The data used to construct the field content. - * @remarks - * When providing a {@link TreeNodeSchemaClass}, this is the same as invoking its constructor except that an unhydrated node can also be provided. - * This function exists as a generalization that can be used in other cases as well, - * 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. - * For that case, use {@link Tree.clone}. - */ - create( - schema: TSchema, - data: InsertableTreeFieldFromImplicitField, - ): Unhydrated>; - - /** - * Construct tree content compatible with a field defined by the provided `schema`. - * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. - * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. - */ - createFromVerbose( - schema: TSchema, - data: VerboseTreeNode | undefined, - options?: { - handleConverter>( - data: T, - location: FieldSchema, - ): VerboseTreeNode; - readonly useStableFieldKeys?: boolean; - }, - ): Unhydrated>; - - /** - * Construct tree content compatible with a field defined by the provided `schema`. - * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. - * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. - */ - createFromVerbose( - schema: TSchema, - data: VerboseTreeNode | undefined, - options?: { - readonly handleConverter?: undefined; - readonly useStableFieldKeys?: boolean; - }, - ): Unhydrated>; - - /** - * Like {@link Tree.create}, except deeply clones existing nodes. - * @remarks - * This only clones the persisted data associated with a node. - * Local state, such as properties added to customized schema classes, will not be cloned: - * they will be initialized however they end up after running the constructor, just like if a remote client had inserted the same nodes. - */ - clone( - original: TreeFieldFromImplicitField, - options?: { - /** - * If set, all identifier's in the cloned tree (See {@link SchemaFactory.identifier}) will be replaced with new ones allocated using the default identifier allocation schema. - * Otherwise any identifiers will be preserved as is. - */ - replaceIdentifiers?: true; - }, - ): TreeFieldFromImplicitField; - - /** - * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. - * - * @remarks - * If the schema is compatible with {@link ITreeConfigurationOptions.preventAmbiguity}, then the returned object will be lossless and compatible with {@link Tree.create} (unless the options are used to customize it). - */ - cloneToJSON( - node: TreeNode, - options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; - }, - ): JsonCompatible; - - /** - * Same as generic overload, except leaves handles as is. - */ - cloneToJSON( - node: TreeNode, - options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, - ): JsonCompatible; - - /** - * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. - * Verbose tree format, with explicit type on every node. - * - * @remarks - * There are several cases this may be preferred to {@link Tree.clone}: - * - * 1. When not using {@link ITreeConfigurationOptions.preventAmbiguity} (or when using `useStableFieldKeys`), {@link Tree.clone} can produce ambiguous data (the type may be unclear on some nodes). - * This may be a good alternative to {@link Tree.clone} since it is lossless. - * - * 2. When the data might be interpreted without access to the exact same view schema. In such cases, the types may be unknowable if not included. - * - * 3. When easy access to the type is desired, or a more uniform simple to parse format is desired. - */ - cloneToJSONVerbose( - node: TreeNode, - options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; - }, - ): VerboseTreeNode; - - /** - * Same as generic overload, except leaves handles as is. - */ - cloneToJSONVerbose( - node: TreeNode, - options?: { readonly handleConverter?: undefined; readonly useStableFieldKeys?: boolean }, - ): VerboseTreeNode; -} - -/** - * Verbose encoding of a {@link TreeNode} or {@link TreeValue}. - * @remarks - * This is verbose meaning that every {@link TreeNode} is a {@link VerboseTreeNode}. - * Any IFluidHandle values have been replaced by `THandle`. - * @public - */ -export type VerboseTree = - | VerboseTreeNode - | Exclude - | THandle; - -/** - * Verbose encoding of a {@link TreeNode}. - * @remarks - * This is verbose meaning that every {@link TreeNode} has an explicit `type` property, and `fields`. - * This allowed VerboseTreeNode to be unambiguous regarding which type each node is without relying on symbols or hidden state. - * - * Any IFluidHandle values have been replaced by `THandle`. If the `THandle` is JSON compatible, then this type is JSON compatible as well. - * - * @privateRemarks - * This type is only used for data which is copied into and out of the tree. - * When being copied out, its fine to have the data be mutable since its a copy. - * - * When being copied in, we don't need to mutate, so we could use a readonly variant of this type. - * however the copy in case (createFromVerbose) probably isn't harmed much by just reusing this type as is, - * since if the caller has immutable data, TypeScript doesn't prevent assigning immutable data to a mutable type anyway. - * Also relaxing the input methods to take readonly data would be a non-breaking change so it can be done later if desired. - * @public - */ -export interface VerboseTreeNode { - /** - * The meaning of this node. - * Provides contexts/semantics for this node and its content. - * Typically used to associate a node with metadata (including a schema) and source code (types, behaviors, etc). - */ - type: TreeNodeSchemaIdentifier; - - /** - * Content of this node. - * For array nodes, an array of children. - * For map and object nodes, an object which children under keys. - */ - fields: - | VerboseTree[] - | { - [key: string]: VerboseTree; - }; } /** @@ -377,10 +194,10 @@ export const treeNodeApi: TreeNodeApi = { return (schema as TreeNodeSchema) === actualSchema; } }, - schema( - node: T, - ): TreeNodeSchema { - return tryGetSchema(node) ?? fail("Not a tree node"); + schema(node: T): TreeNodeSchema { + return ( + (tryGetSchema(node) as TreeNodeSchema) ?? fail("Not a tree node") + ); }, shortId(node: TreeNode): number | string | undefined { const flexNode = getOrCreateInnerNode(node); diff --git a/packages/dds/tree/src/simple-tree/arrayNode.ts b/packages/dds/tree/src/simple-tree/arrayNode.ts index 977ae88694f1..2c2636a2cb2c 100644 --- a/packages/dds/tree/src/simple-tree/arrayNode.ts +++ b/packages/dds/tree/src/simple-tree/arrayNode.ts @@ -655,7 +655,7 @@ type Insertable = readonly ( | IterableTreeArrayContent> )[]; -abstract class CustomArrayNodeBase +export abstract class CustomArrayNodeBase extends TreeNodeWithArrayFeatures< Iterable>, TreeNodeFromImplicitAllowedTypes diff --git a/packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts b/packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts index d8783ee3e2a3..da5a768cdbd5 100644 --- a/packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts +++ b/packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. */ -import type { InternalTreeNode, Unhydrated } from "./types.js"; +import type { TreeLeafValue } from "../schemaTypes.js"; +import type { InternalTreeNode, TreeNode, Unhydrated } from "./types.js"; /** * Schema for a tree node. @@ -19,7 +20,7 @@ import type { InternalTreeNode, Unhydrated } from "./types.js"; export type TreeNodeSchema< Name extends string = string, Kind extends NodeKind = NodeKind, - TNode = unknown, + TNode = TreeNode | TreeLeafValue, TBuild = never, ImplicitlyConstructable extends boolean = boolean, Info = unknown, diff --git a/packages/dds/tree/src/simple-tree/mapNode.ts b/packages/dds/tree/src/simple-tree/mapNode.ts index d888062a66b9..1f14283c407f 100644 --- a/packages/dds/tree/src/simple-tree/mapNode.ts +++ b/packages/dds/tree/src/simple-tree/mapNode.ts @@ -129,9 +129,9 @@ const handler: ProxyHandler = { }, }; -abstract class CustomMapNodeBase extends TreeNodeValid< - MapNodeInsertableData -> { +export abstract class CustomMapNodeBase< + const T extends ImplicitAllowedTypes, +> extends TreeNodeValid> { public static readonly kind = NodeKind.Map; public [Symbol.iterator](): IterableIterator<[string, TreeNodeFromImplicitAllowedTypes]> { diff --git a/packages/dds/tree/src/simple-tree/schemaTypes.ts b/packages/dds/tree/src/simple-tree/schemaTypes.ts index 4a41e039d92f..da5a3aad9c79 100644 --- a/packages/dds/tree/src/simple-tree/schemaTypes.ts +++ b/packages/dds/tree/src/simple-tree/schemaTypes.ts @@ -19,6 +19,7 @@ import type { NodeKind, TreeNodeSchema, TreeNodeSchemaClass, + TreeNode, } from "./core/index.js"; import type { FieldKey } from "../core/index.js"; import type { InsertableContent } from "./proxies.js"; @@ -378,7 +379,7 @@ export type TreeNodeFromImplicitAllowedTypes< ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> - : unknown; + : TreeNode | TreeLeafValue; /** * Type of content that can be inserted into the tree for a node of the given schema. diff --git a/packages/dds/tree/src/simple-tree/toMapTree.ts b/packages/dds/tree/src/simple-tree/toMapTree.ts index e5d46109ca8a..b979abc72495 100644 --- a/packages/dds/tree/src/simple-tree/toMapTree.ts +++ b/packages/dds/tree/src/simple-tree/toMapTree.ts @@ -34,6 +34,9 @@ import { extractFieldProvider, isConstant, type FieldProvider, + type ImplicitFieldSchema, + normalizeFieldSchema, + FieldKind, } from "./schemaTypes.js"; import { NodeKind, tryGetSimpleNodeSchema, type TreeNodeSchema } from "./core/index.js"; import { SchemaValidationErrors, isNodeInSchema } from "../feature-libraries/index.js"; @@ -67,30 +70,36 @@ import { isObjectNodeSchema } from "./objectNodeTypes.js"; export function cursorFromNodeData( data: InsertableContent, allowedTypes: ImplicitAllowedTypes, - context: NodeKeyManager, - schemaValidationPolicy: SchemaAndPolicy, + context: NodeKeyManager | undefined, + schemaValidationPolicy: SchemaAndPolicy | undefined, ): CursorWithNode; export function cursorFromNodeData( data: InsertableContent | undefined, - allowedTypes: ImplicitAllowedTypes, - context: NodeKeyManager, + allowedTypes: ImplicitFieldSchema, + context: NodeKeyManager | undefined, schemaValidationPolicy: SchemaAndPolicy, -): CursorWithNode | undefined; +): CursorWithNode | undefined | undefined; export function cursorFromNodeData( data: InsertableContent | undefined, - allowedTypes: ImplicitAllowedTypes, - context: NodeKeyManager, - schemaValidationPolicy: SchemaAndPolicy, + allowedTypes: ImplicitFieldSchema, + context: NodeKeyManager | undefined, + schemaValidationPolicy: SchemaAndPolicy | undefined, ): CursorWithNode | undefined { + const normalizedFieldSchema = normalizeFieldSchema(allowedTypes); + if (data === undefined) { + // TODO: this code-path should support defaults + if (normalizedFieldSchema.kind !== FieldKind.Optional) { + throw new UsageError("Got undefined for non-optional field."); + } return undefined; } const mappedContent = nodeDataToMapTree( data, - normalizeAllowedTypes(allowedTypes), + normalizedFieldSchema.allowedTypeSet, schemaValidationPolicy, ); - addDefaultsToMapTree(mappedContent, allowedTypes, context); + addDefaultsToMapTree(mappedContent, normalizedFieldSchema.allowedTypes, context); return cursorForMapTreeNode(mappedContent); } @@ -226,15 +235,22 @@ function nodeDataToMapTree( } if (schemaValidationPolicy?.policy.validateSchema === true) { - const maybeError = isNodeInSchema(result, schemaValidationPolicy); - if (maybeError !== SchemaValidationErrors.NoError) { - throw new UsageError("Tree does not conform to schema."); - } + inSchemaOrThrow(schemaValidationPolicy, result); } return result; } +export function inSchemaOrThrow( + schemaValidationPolicy: SchemaAndPolicy, + mapTree: MapTree, +): void { + const maybeError = isNodeInSchema(mapTree, schemaValidationPolicy); + if (maybeError !== SchemaValidationErrors.NoError) { + throw new UsageError("Tree does not conform to schema."); + } +} + /** * Transforms data under a Leaf schema. * @param data - The tree data to be transformed. Must be a {@link TreeValue}. diff --git a/packages/dds/tree/src/test/utils.ts b/packages/dds/tree/src/test/utils.ts index aa2c5659a8a0..a09f4cf15c7b 100644 --- a/packages/dds/tree/src/test/utils.ts +++ b/packages/dds/tree/src/test/utils.ts @@ -150,11 +150,10 @@ import type { SharedTreeOptions } from "../shared-tree/sharedTree.js"; import { type ImplicitFieldSchema, type InsertableContent, - type InsertableTreeNodeFromImplicitAllowedTypes, TreeViewConfiguration, - normalizeFieldSchema, SchemaFactory, toFlexSchema, + type InsertableTreeFieldFromImplicitField, } from "../simple-tree/index.js"; import { type JsonCompatible, @@ -1391,25 +1390,20 @@ export function validateUsageError(expectedErrorMsg: string | RegExp): (error: E */ export function cursorFromUnhydratedRoot( schema: ImplicitFieldSchema, - tree: InsertableTreeNodeFromImplicitAllowedTypes, + tree: InsertableTreeFieldFromImplicitField, nodeKeyManager: NodeKeyManager, ): ITreeCursorSynchronous { - const data = tree as InsertableContent; - const normalizedFieldSchema = normalizeFieldSchema(schema); + const data = tree as InsertableContent | undefined; - const flexSchema = toFlexSchema(normalizedFieldSchema); + const flexSchema = toFlexSchema(schema); const storedSchema: SchemaAndPolicy = { policy: defaultSchemaPolicy, schema: intoStoredSchema(flexSchema), }; return ( - cursorFromNodeData( - data, - normalizedFieldSchema.allowedTypes, - nodeKeyManager, - storedSchema, - ) ?? assert.fail("failed to decode tree") + cursorFromNodeData(data, schema, nodeKeyManager, storedSchema) ?? + assert.fail("failed to decode tree") ); } From f81f23567683dd500c1897df679d643d34e27805 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:05:01 -0700 Subject: [PATCH 08/54] fix merge, cleanup unneeded diffs --- packages/dds/tree/api-report/tree.alpha.api.md | 8 ++------ packages/dds/tree/api-report/tree.beta.api.md | 8 ++------ packages/dds/tree/api-report/tree.public.api.md | 4 ++-- .../dds/tree/src/simple-tree/api/jsonInterop.ts | 17 +++++++++++++---- .../dds/tree/src/simple-tree/api/treeNodeApi.ts | 17 +++++++---------- packages/dds/tree/src/simple-tree/arrayNode.ts | 2 +- .../tree/src/simple-tree/core/treeNodeKernel.ts | 8 +++----- .../tree/src/simple-tree/core/treeNodeSchema.ts | 5 ++--- packages/dds/tree/src/simple-tree/mapNode.ts | 6 +++--- .../dds/tree/src/simple-tree/schemaTypes.ts | 3 +-- .../api-report/fluid-framework.beta.api.md | 2 +- .../fluid-framework.legacy.alpha.api.md | 2 +- .../fluid-framework.legacy.public.api.md | 2 +- .../api-report/fluid-framework.public.api.md | 2 +- 14 files changed, 40 insertions(+), 46 deletions(-) diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index e21815796a97..0d826ec5b76c 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -514,23 +514,19 @@ export interface TreeNodeApi { key(node: TreeNode): string | number; on(node: TreeNode, eventName: K, listener: TreeChangeEvents[K]): () => void; parent(node: TreeNode): TreeNode | undefined; -<<<<<<< HEAD - schema(node: T): TreeNodeSchema; -======= schema(node: TreeNode | TreeLeafValue): TreeNodeSchema; ->>>>>>> bfe8310a9406a8658c2fac8827c7114844c32234 shortId(node: TreeNode): number | string | undefined; status(node: TreeNode): TreeStatus; } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : TreeNode | TreeLeafValue; +export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public @sealed -export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; +export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; // @public @sealed export interface TreeNodeSchemaClass extends TreeNodeSchemaCore { diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index 37359aecda16..a263d7d8b16e 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -446,23 +446,19 @@ export interface TreeNodeApi { key(node: TreeNode): string | number; on(node: TreeNode, eventName: K, listener: TreeChangeEvents[K]): () => void; parent(node: TreeNode): TreeNode | undefined; -<<<<<<< HEAD - schema(node: T): TreeNodeSchema; -======= schema(node: TreeNode | TreeLeafValue): TreeNodeSchema; ->>>>>>> bfe8310a9406a8658c2fac8827c7114844c32234 shortId(node: TreeNode): number | string | undefined; status(node: TreeNode): TreeStatus; } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : TreeNode | TreeLeafValue; +export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public @sealed -export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; +export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; // @public @sealed export interface TreeNodeSchemaClass extends TreeNodeSchemaCore { diff --git a/packages/dds/tree/api-report/tree.public.api.md b/packages/dds/tree/api-report/tree.public.api.md index 874811978b07..c730a94d0dc2 100644 --- a/packages/dds/tree/api-report/tree.public.api.md +++ b/packages/dds/tree/api-report/tree.public.api.md @@ -452,13 +452,13 @@ export interface TreeNodeApi { } // @public -export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : TreeNode | TreeLeafValue; +export type TreeNodeFromImplicitAllowedTypes = TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public type TreeNodeFromImplicitAllowedTypesUnsafe> = TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public @sealed -export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; +export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; // @public @sealed export interface TreeNodeSchemaClass extends TreeNodeSchemaCore { diff --git a/packages/dds/tree/src/simple-tree/api/jsonInterop.ts b/packages/dds/tree/src/simple-tree/api/jsonInterop.ts index 84033c736e19..6ae2c35209e3 100644 --- a/packages/dds/tree/src/simple-tree/api/jsonInterop.ts +++ b/packages/dds/tree/src/simple-tree/api/jsonInterop.ts @@ -24,6 +24,7 @@ import type { } from "../schemaTypes.js"; import { getSimpleNodeSchema, type Unhydrated } from "../core/index.js"; import { + cursorForMapTreeNode, defaultSchemaPolicy, intoStoredSchema, isTreeValue, @@ -40,10 +41,10 @@ import { numberSchema, stringSchema, } from "../leafNodeSchema.js"; -import { getOrCreateNodeFromFlexTreeNode } from "../proxies.js"; +import { getOrCreateNodeFromFlexTreeNode, type InsertableContent } from "../proxies.js"; import { getOrCreateMapTreeNode } from "../../feature-libraries/index.js"; import { toFlexSchema } from "../toFlexSchema.js"; -import { cursorFromNodeData, inSchemaOrThrow } from "../toMapTree.js"; +import { inSchemaOrThrow, mapTreeFromNodeData } from "../toMapTree.js"; import { isObjectNodeSchema } from "../objectNodeTypes.js"; /** @@ -70,8 +71,16 @@ export function create( schema: intoStoredSchema(flexSchema), }; - const cursor = cursorFromNodeData(data, schema, context, schemaValidationPolicy); - const result = cursor === undefined ? undefined : createFromCursor(schema, cursor); + const mapTree = mapTreeFromNodeData( + data as InsertableContent | undefined, + schema, + context, + schemaValidationPolicy, + ); + const result = + mapTree === undefined + ? undefined + : createFromCursor(schema, cursorForMapTreeNode(mapTree)); return result as Unhydrated>; } diff --git a/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts b/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts index e3dffd283b35..00e80aacba0d 100644 --- a/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts +++ b/packages/dds/tree/src/simple-tree/api/treeNodeApi.ts @@ -235,27 +235,24 @@ export const treeNodeApi: TreeNodeApi = { * Returns a schema for a value if the value is a {@link TreeNode} or a {@link TreeLeafValue}. * Returns undefined for other values. */ -export function tryGetSchema( - value: T, -): undefined | TreeNodeSchema { - type TOut = TreeNodeSchema; +export function tryGetSchema(value: unknown): undefined | TreeNodeSchema { switch (typeof value) { case "string": - return stringSchema as TOut; + return stringSchema; case "number": - return numberSchema as TOut; + return numberSchema; case "boolean": - return booleanSchema as TOut; + return booleanSchema; case "object": { if (isTreeNode(value)) { // This case could be optimized, for example by placing the simple schema in a symbol on tree nodes. - return tryGetTreeNodeSchema(value) as TOut; + return tryGetTreeNodeSchema(value); } if (value === null) { - return nullSchema as TOut; + return nullSchema; } if (isFluidHandle(value)) { - return handleSchema as TOut; + return handleSchema; } } default: diff --git a/packages/dds/tree/src/simple-tree/arrayNode.ts b/packages/dds/tree/src/simple-tree/arrayNode.ts index db6143116304..cf15bb906586 100644 --- a/packages/dds/tree/src/simple-tree/arrayNode.ts +++ b/packages/dds/tree/src/simple-tree/arrayNode.ts @@ -653,7 +653,7 @@ type Insertable = readonly ( | IterableTreeArrayContent> )[]; -export abstract class CustomArrayNodeBase +abstract class CustomArrayNodeBase extends TreeNodeWithArrayFeatures< Iterable>, TreeNodeFromImplicitAllowedTypes diff --git a/packages/dds/tree/src/simple-tree/core/treeNodeKernel.ts b/packages/dds/tree/src/simple-tree/core/treeNodeKernel.ts index 629883d29426..2440191846d3 100644 --- a/packages/dds/tree/src/simple-tree/core/treeNodeKernel.ts +++ b/packages/dds/tree/src/simple-tree/core/treeNodeKernel.ts @@ -14,7 +14,7 @@ import { TreeStatus, treeStatusFromAnchorCache, } from "../../feature-libraries/index.js"; -import type { NodeKind, TreeNodeSchema } from "./treeNodeSchema.js"; +import type { TreeNodeSchema } from "./treeNodeSchema.js"; const treeNodeToKernel = new WeakMap(); @@ -49,11 +49,9 @@ export function isTreeNode(candidate: unknown): candidate is TreeNode | Unhydrat * @remarks * Does not give schema for a {@link TreeLeafValue}. */ -export function tryGetTreeNodeSchema( - value: T, -): undefined | TreeNodeSchema { +export function tryGetTreeNodeSchema(value: unknown): undefined | TreeNodeSchema { const kernel = treeNodeToKernel.get(value as TreeNode); - return kernel?.schema as undefined | TreeNodeSchema; + return kernel?.schema; } /** diff --git a/packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts b/packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts index da5a768cdbd5..d8783ee3e2a3 100644 --- a/packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts +++ b/packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. */ -import type { TreeLeafValue } from "../schemaTypes.js"; -import type { InternalTreeNode, TreeNode, Unhydrated } from "./types.js"; +import type { InternalTreeNode, Unhydrated } from "./types.js"; /** * Schema for a tree node. @@ -20,7 +19,7 @@ import type { InternalTreeNode, TreeNode, Unhydrated } from "./types.js"; export type TreeNodeSchema< Name extends string = string, Kind extends NodeKind = NodeKind, - TNode = TreeNode | TreeLeafValue, + TNode = unknown, TBuild = never, ImplicitlyConstructable extends boolean = boolean, Info = unknown, diff --git a/packages/dds/tree/src/simple-tree/mapNode.ts b/packages/dds/tree/src/simple-tree/mapNode.ts index caffcfa5b071..2f542b9119f8 100644 --- a/packages/dds/tree/src/simple-tree/mapNode.ts +++ b/packages/dds/tree/src/simple-tree/mapNode.ts @@ -130,9 +130,9 @@ const handler: ProxyHandler = { }, }; -export abstract class CustomMapNodeBase< - const T extends ImplicitAllowedTypes, -> extends TreeNodeValid> { +abstract class CustomMapNodeBase extends TreeNodeValid< + MapNodeInsertableData +> { public static readonly kind = NodeKind.Map; public [Symbol.iterator](): IterableIterator<[string, TreeNodeFromImplicitAllowedTypes]> { diff --git a/packages/dds/tree/src/simple-tree/schemaTypes.ts b/packages/dds/tree/src/simple-tree/schemaTypes.ts index da5a3aad9c79..4a41e039d92f 100644 --- a/packages/dds/tree/src/simple-tree/schemaTypes.ts +++ b/packages/dds/tree/src/simple-tree/schemaTypes.ts @@ -19,7 +19,6 @@ import type { NodeKind, TreeNodeSchema, TreeNodeSchemaClass, - TreeNode, } from "./core/index.js"; import type { FieldKey } from "../core/index.js"; import type { InsertableContent } from "./proxies.js"; @@ -379,7 +378,7 @@ export type TreeNodeFromImplicitAllowedTypes< ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> - : TreeNode | TreeLeafValue; + : unknown; /** * Type of content that can be inserted into the tree for a node of the given schema. 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 911eb9e7443f..f02adaa9ec04 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 @@ -830,7 +830,7 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public @sealed -export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; +export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; // @public @sealed export interface TreeNodeSchemaClass extends TreeNodeSchemaCore { diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md index fe3eea9df33c..e9fc58b033af 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md @@ -1227,7 +1227,7 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public @sealed -export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; +export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; // @public @sealed export interface TreeNodeSchemaClass extends TreeNodeSchemaCore { diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md index 72e32fe4448d..efd611cc6366 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md @@ -870,7 +870,7 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public @sealed -export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; +export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; // @public @sealed export interface TreeNodeSchemaClass extends TreeNodeSchemaCore { diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md index 6be3419bc324..5b31332e15df 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md @@ -830,7 +830,7 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public @sealed -export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; +export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; // @public @sealed export interface TreeNodeSchemaClass extends TreeNodeSchemaCore { From 48442d8b6faf2c3167ac0b8142bd4529e049ba14 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:08:10 -0700 Subject: [PATCH 09/54] fix old API diff --- .../fluid-framework/api-report/fluid-framework.beta.api.md | 2 +- .../api-report/fluid-framework.legacy.alpha.api.md | 2 +- .../api-report/fluid-framework.legacy.public.api.md | 2 +- .../fluid-framework/api-report/fluid-framework.public.api.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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 f02adaa9ec04..911eb9e7443f 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 @@ -830,7 +830,7 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public @sealed -export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; +export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; // @public @sealed export interface TreeNodeSchemaClass extends TreeNodeSchemaCore { diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md index e9fc58b033af..fe3eea9df33c 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md @@ -1227,7 +1227,7 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public @sealed -export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; +export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; // @public @sealed export interface TreeNodeSchemaClass extends TreeNodeSchemaCore { diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md index efd611cc6366..72e32fe4448d 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md @@ -870,7 +870,7 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public @sealed -export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; +export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; // @public @sealed export interface TreeNodeSchemaClass extends TreeNodeSchemaCore { diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md index 5b31332e15df..6be3419bc324 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md @@ -830,7 +830,7 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : TSchema extends TreeNodeSchema ? NodeFromSchema : TSchema extends AllowedTypes ? NodeFromSchema> : unknown; // @public @sealed -export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; +export type TreeNodeSchema = TreeNodeSchemaClass | TreeNodeSchemaNonClass; // @public @sealed export interface TreeNodeSchemaClass extends TreeNodeSchemaCore { From b0abad82f9f63e37e8e0ebad06a2407c76b4647f Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:33:41 -0700 Subject: [PATCH 10/54] split out and clean up verbose tree --- .../api/{jsonInterop.ts => create.ts} | 262 +---------------- .../tree/src/simple-tree/api/verboseTree.ts | 270 ++++++++++++++++++ 2 files changed, 283 insertions(+), 249 deletions(-) rename packages/dds/tree/src/simple-tree/api/{jsonInterop.ts => create.ts} (55%) create mode 100644 packages/dds/tree/src/simple-tree/api/verboseTree.ts diff --git a/packages/dds/tree/src/simple-tree/api/jsonInterop.ts b/packages/dds/tree/src/simple-tree/api/create.ts similarity index 55% rename from packages/dds/tree/src/simple-tree/api/jsonInterop.ts rename to packages/dds/tree/src/simple-tree/api/create.ts index 6ae2c35209e3..6b55eee06855 100644 --- a/packages/dds/tree/src/simple-tree/api/jsonInterop.ts +++ b/packages/dds/tree/src/simple-tree/api/create.ts @@ -4,48 +4,34 @@ */ import type { IFluidHandle } from "@fluidframework/core-interfaces"; -import { isFluidHandle } from "@fluidframework/runtime-utils/internal"; -import { - aboveRootPlaceholder, - EmptyKey, - keyAsDetachedField, - type FieldKey, - type ITreeCursorSynchronous, - type SchemaAndPolicy, - type TreeNodeSchemaIdentifier, -} from "../../core/index.js"; -import { brand, fail } from "../../util/index.js"; +import type { ITreeCursorSynchronous, SchemaAndPolicy } from "../../core/index.js"; +import { fail } from "../../util/index.js"; import type { TreeLeafValue, ImplicitFieldSchema, InsertableTreeFieldFromImplicitField, TreeFieldFromImplicitField, } from "../schemaTypes.js"; -import { getSimpleNodeSchema, type Unhydrated } from "../core/index.js"; +import type { Unhydrated } from "../core/index.js"; import { cursorForMapTreeNode, defaultSchemaPolicy, intoStoredSchema, - isTreeValue, mapTreeFromCursor, - stackTreeFieldCursor, - stackTreeNodeCursor, - type CursorAdapter, type NodeKeyManager, } from "../../feature-libraries/index.js"; -import { - booleanSchema, - handleSchema, - nullSchema, - numberSchema, - stringSchema, -} from "../leafNodeSchema.js"; import { getOrCreateNodeFromFlexTreeNode, type InsertableContent } from "../proxies.js"; import { getOrCreateMapTreeNode } from "../../feature-libraries/index.js"; import { toFlexSchema } from "../toFlexSchema.js"; import { inSchemaOrThrow, mapTreeFromNodeData } from "../toMapTree.js"; -import { isObjectNodeSchema } from "../objectNodeTypes.js"; +import { + applySchemaToParserOptions, + cursorFromVerbose, + type ParseOptions, + type VerboseTree, + type VerboseTreeNode, +} from "./verboseTree.js"; /** * Construct tree content compatible with a field defined by the provided `schema`. @@ -58,8 +44,10 @@ import { isObjectNodeSchema } from "../objectNodeTypes.js"; * * Like with {@link TreeNodeSchemaClass}'s constructor, its an error to provide an existing node to this API. * For that case, use {@link Tree.clone}. + * @privateRemarks + * This could be exposed as a public `Tree.create` function. */ -export function create( +export function createFromInsertable( schema: TSchema, data: InsertableTreeFieldFromImplicitField, context?: NodeKeyManager | undefined, @@ -145,180 +133,6 @@ export function createFromCursor( return result as Unhydrated>; } -interface ParseOptions { - /** - * Fixup custom input formats. - * @remarks - * Main usage is translate some JSON compatible handle format into actual IFluidHandles. - */ - valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; - /** - * If true, interpret the input keys of object nodes as stable keys. - * If false, interpret them as api keys. - * @defaultValue false. - */ - readonly useStoredKeys?: boolean; -} - -interface SchemalessParseOptions { - /** - * Fixup custom input formats. - * @remarks - * Main usage is translate some JSON compatible handle format into actual IFluidHandles. - */ - valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; - /** - * Converts to stable key names. - */ - keyConverter?(type: string, inputKey: string): string; -} - -function applySchemaToParserOptions( - schema: ImplicitFieldSchema, - options: ParseOptions, -): SchemalessParseOptions { - const config: Required> = { - useStoredKeys: false, - ...options, - }; - - // TODO: should provide a way to look up schema by name efficiently without converting to flex tree schema and back. - // Maybe cache identifier->schema map on simple tree schema lazily. - const flexSchema = toFlexSchema(schema); - - return { - valueConverter: config.valueConverter, - keyConverter: config.useStoredKeys - ? undefined - : (type, inputKey) => { - const flexNodeSchema = - flexSchema.nodeSchema.get(brand(type)) ?? fail("missing schema"); - const simpleNodeSchema = getSimpleNodeSchema(flexNodeSchema); - if (isObjectNodeSchema(simpleNodeSchema)) { - const info = - simpleNodeSchema.flexKeyMap.get(inputKey) ?? fail("missing field info"); - return info.storedKey; - } - return inputKey; - }, - }; -} - -/** - * Used to read a VerboseTree as a node cursor. - * - * @returns an {@link ITreeCursorSynchronous} for a single node in nodes mode. - */ -export function cursorFromVerbose( - data: VerboseTree, - options: SchemalessParseOptions, -): ITreeCursorSynchronous { - return stackTreeNodeCursor(verboseTreeAdapter(options), data); -} - -/** - * Used to read a VerboseTree[] as a field cursor. - * - * @returns an {@link ITreeCursorSynchronous} for a single field in fields mode. - */ -export function fieldCursorFromVerbose( - data: VerboseTree[], - options: SchemalessParseOptions, -): ITreeCursorSynchronous { - return stackTreeFieldCursor( - verboseTreeAdapter(options), - { type: aboveRootPlaceholder, fields: data }, - keyAsDetachedField(EmptyKey), - ); -} - -/** - * TODO: add ParserOptions - */ -function verboseTreeAdapter( - options: SchemalessParseOptions, -): CursorAdapter> { - return { - value: (input: VerboseTree) => { - const node = options.valueConverter(input); - return isTreeValue(node) ? node : undefined; - }, - type: (input: VerboseTree) => { - const node = options.valueConverter(input); - switch (typeof node) { - case "number": - return numberSchema.identifier as TreeNodeSchemaIdentifier; - case "string": - return stringSchema.identifier as TreeNodeSchemaIdentifier; - case "boolean": - return booleanSchema.identifier as TreeNodeSchemaIdentifier; - default: - if (node === null) { - return nullSchema.identifier as TreeNodeSchemaIdentifier; - } - if (isFluidHandle(node)) { - return handleSchema.identifier as TreeNodeSchemaIdentifier; - } - return node.type as TreeNodeSchemaIdentifier; - } - }, - keysFromNode: (input: VerboseTree): readonly FieldKey[] => { - const node = options.valueConverter(input); - switch (typeof node) { - case "object": { - if (node === null) { - return []; - } - if (isFluidHandle(node)) { - return []; - } - if (Array.isArray(node.fields)) { - return node.fields.length === 0 ? [] : [EmptyKey]; - } - const inputKeys = Object.keys(node.fields); - if (options.keyConverter === undefined) { - return inputKeys as FieldKey[]; - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return inputKeys.map((k) => brand(options.keyConverter!(node.type, k))); - } - default: - return []; - } - }, - getFieldFromNode: ( - input: VerboseTree, - key: FieldKey, - ): readonly VerboseTree[] => { - const node = options.valueConverter(input); - // Object.prototype.hasOwnProperty can return true for strings (ex: with key "0"), so we have to filter them out. - // Rather than just special casing strings, we can handle them with an early return for all primitives. - if (typeof node !== "object") { - return []; - } - - if (node === null) { - return []; - } - - if (isFluidHandle(node)) { - return []; - } - - if (Array.isArray(node.fields)) { - return key === EmptyKey ? node.fields : []; - } - - if (Object.prototype.hasOwnProperty.call(node, key)) { - const field = node.fields[key]; - return field === undefined ? [] : [field]; - } - - return []; - }, - }; -} - // /** // * Like {@link Tree.create}, except deeply clones existing nodes. // * @remarks @@ -445,53 +259,3 @@ function verboseTreeAdapter( // return convertNode(node); // } - -/** - * Verbose encoding of a {@link TreeNode} or {@link TreeValue}. - * @remarks - * This is verbose meaning that every {@link TreeNode} is a {@link VerboseTreeNode}. - * Any IFluidHandle values have been replaced by `THandle`. - * @public - */ -export type VerboseTree = - | VerboseTreeNode - | Exclude - | THandle; - -/** - * Verbose encoding of a {@link TreeNode}. - * @remarks - * This is verbose meaning that every {@link TreeNode} has an explicit `type` property, and `fields`. - * This allowed VerboseTreeNode to be unambiguous regarding which type each node is without relying on symbols or hidden state. - * - * Any IFluidHandle values have been replaced by `THandle`. If the `THandle` is JSON compatible, then this type is JSON compatible as well. - * - * @privateRemarks - * This type is only used for data which is copied into and out of the tree. - * When being copied out, its fine to have the data be mutable since its a copy. - * - * When being copied in, we don't need to mutate, so we could use a readonly variant of this type. - * however the copy in case (createFromVerbose) probably isn't harmed much by just reusing this type as is, - * since if the caller has immutable data, TypeScript doesn't prevent assigning immutable data to a mutable type anyway. - * Also relaxing the input methods to take readonly data would be a non-breaking change so it can be done later if desired. - * @public - */ -export interface VerboseTreeNode { - /** - * The meaning of this node. - * Provides contexts/semantics for this node and its content. - * Typically used to associate a node with metadata (including a schema) and source code (types, behaviors, etc). - */ - type: string; - - /** - * Content of this node. - * For array nodes, an array of children. - * For map and object nodes, an object which children under keys. - */ - fields: - | VerboseTree[] - | { - [key: string]: VerboseTree; - }; -} diff --git a/packages/dds/tree/src/simple-tree/api/verboseTree.ts b/packages/dds/tree/src/simple-tree/api/verboseTree.ts new file mode 100644 index 000000000000..0bff1029d1c0 --- /dev/null +++ b/packages/dds/tree/src/simple-tree/api/verboseTree.ts @@ -0,0 +1,270 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import { isFluidHandle } from "@fluidframework/runtime-utils/internal"; + +import { + aboveRootPlaceholder, + EmptyKey, + keyAsDetachedField, + type FieldKey, + type ITreeCursorSynchronous, + type TreeNodeSchemaIdentifier, +} from "../../core/index.js"; +import { brand, fail } from "../../util/index.js"; +import type { TreeLeafValue, ImplicitFieldSchema } from "../schemaTypes.js"; +import { getSimpleNodeSchema } from "../core/index.js"; +import { + isTreeValue, + stackTreeFieldCursor, + stackTreeNodeCursor, + type CursorAdapter, +} from "../../feature-libraries/index.js"; +import { + booleanSchema, + handleSchema, + nullSchema, + numberSchema, + stringSchema, +} from "../leafNodeSchema.js"; +import { toFlexSchema } from "../toFlexSchema.js"; +import { isObjectNodeSchema } from "../objectNodeTypes.js"; + +/** + * Verbose encoding of a {@link TreeNode} or {@link TreeValue}. + * @remarks + * This is verbose meaning that every {@link TreeNode} is a {@link VerboseTreeNode}. + * Any IFluidHandle values have been replaced by `THandle`. + */ +export type VerboseTree = + | VerboseTreeNode + | Exclude + | THandle; + +/** + * Verbose encoding of a {@link TreeNode}. + * @remarks + * This is verbose meaning that every {@link TreeNode} has an explicit `type` property, and `fields`. + * This allowed VerboseTreeNode to be unambiguous regarding which type each node is without relying on symbols or hidden state. + * + * Any IFluidHandle values have been replaced by `THandle`. If the `THandle` is JSON compatible, then this type is JSON compatible as well. + * + * @privateRemarks + * This type is only used for data which is copied into and out of the tree. + * When being copied out, its fine to have the data be mutable since its a copy. + * + * When being copied in, we don't need to mutate, so we could use a readonly variant of this type. + * however the copy in case (createFromVerbose) probably isn't harmed much by just reusing this type as is, + * since if the caller has immutable data, TypeScript doesn't prevent assigning immutable data to a mutable type anyway. + * Also relaxing the input methods to take readonly data would be a non-breaking change so it can be done later if desired. + * + * This format is simple-tree specialized alternative to {@link JsonableTree}. + * This format allows for all simple-tree compatible trees to be represented. + * + * Unlike `JsonableTree`, leaf nodes are not boxed into node objects, and instead have their schema inferred from the value. + * Additionally, sequence fields can only occur on a node that has a single sequence field (with the empty key) replicating the behavior of simple-tree ArrayNodes. + */ +export interface VerboseTreeNode { + /** + * The meaning of this node. + * Provides contexts/semantics for this node and its content. + * Typically used to associate a node with metadata (including a schema) and source code (types, behaviors, etc). + */ + type: string; + + /** + * Content of this node. + * For array nodes, an array of children. + * For map and object nodes, an object which children under keys. + * @remarks + * For object nodes, the keys could be either the stored keys, or the property keys depending on usage. + */ + fields: + | VerboseTree[] + | { + [key: string]: VerboseTree; + }; +} + +/** + * Options for how to interpret a `VerboseTree` when schema information is available. + */ +export interface ParseOptions { + /** + * Fixup custom input formats. + * @remarks + * Main usage is translate some JSON compatible handle format into actual IFluidHandles. + */ + valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; + /** + * If true, interpret the input keys of object nodes as stored keys. + * If false, interpret them as property keys. + * @defaultValue false. + */ + readonly useStoredKeys?: boolean; +} + +/** + * Options for how to interpret a `VerboseTree` without relying on schema. + */ +export interface SchemalessParseOptions { + /** + * Fixup custom input formats. + * @remarks + * Main usage is translate some JSON compatible handle format into actual IFluidHandles. + */ + valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; + /** + * Converts to stored keys. + */ + keyConverter?(type: string, inputKey: string): string; +} + +/** + * Use info from `schema` to convert `options` to {@link SchemalessParseOptions}. + */ +export function applySchemaToParserOptions( + schema: ImplicitFieldSchema, + options: ParseOptions, +): SchemalessParseOptions { + const config: Required> = { + useStoredKeys: false, + ...options, + }; + + // TODO: should provide a way to look up schema by name efficiently without converting to flex tree schema and back. + // Maybe cache identifier->schema map on simple tree schema lazily. + const flexSchema = toFlexSchema(schema); + + return { + valueConverter: config.valueConverter, + keyConverter: config.useStoredKeys + ? undefined + : (type, inputKey) => { + const flexNodeSchema = + flexSchema.nodeSchema.get(brand(type)) ?? fail("missing schema"); + const simpleNodeSchema = getSimpleNodeSchema(flexNodeSchema); + if (isObjectNodeSchema(simpleNodeSchema)) { + const info = + simpleNodeSchema.flexKeyMap.get(inputKey) ?? fail("missing field info"); + return info.storedKey; + } + return inputKey; + }, + }; +} + +/** + * Used to read a VerboseTree as a node cursor. + * + * @returns an {@link ITreeCursorSynchronous} for a single node in nodes mode. + */ +export function cursorFromVerbose( + data: VerboseTree, + options: SchemalessParseOptions, +): ITreeCursorSynchronous { + return stackTreeNodeCursor(verboseTreeAdapter(options), data); +} + +/** + * Used to read a VerboseTree[] as a field cursor. + * + * @returns an {@link ITreeCursorSynchronous} for a single field in fields mode. + */ +export function fieldCursorFromVerbose( + data: VerboseTree[], + options: SchemalessParseOptions, +): ITreeCursorSynchronous { + return stackTreeFieldCursor( + verboseTreeAdapter(options), + { type: aboveRootPlaceholder, fields: data }, + keyAsDetachedField(EmptyKey), + ); +} + +function verboseTreeAdapter( + options: SchemalessParseOptions, +): CursorAdapter> { + return { + value: (input: VerboseTree) => { + const node = options.valueConverter(input); + return isTreeValue(node) ? node : undefined; + }, + type: (input: VerboseTree) => { + const node = options.valueConverter(input); + switch (typeof node) { + case "number": + return numberSchema.identifier as TreeNodeSchemaIdentifier; + case "string": + return stringSchema.identifier as TreeNodeSchemaIdentifier; + case "boolean": + return booleanSchema.identifier as TreeNodeSchemaIdentifier; + default: + if (node === null) { + return nullSchema.identifier as TreeNodeSchemaIdentifier; + } + if (isFluidHandle(node)) { + return handleSchema.identifier as TreeNodeSchemaIdentifier; + } + return node.type as TreeNodeSchemaIdentifier; + } + }, + keysFromNode: (input: VerboseTree): readonly FieldKey[] => { + const node = options.valueConverter(input); + switch (typeof node) { + case "object": { + if (node === null) { + return []; + } + if (isFluidHandle(node)) { + return []; + } + if (Array.isArray(node.fields)) { + return node.fields.length === 0 ? [] : [EmptyKey]; + } + const inputKeys = Object.keys(node.fields); + if (options.keyConverter === undefined) { + return inputKeys as FieldKey[]; + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return inputKeys.map((k) => brand(options.keyConverter!(node.type, k))); + } + default: + return []; + } + }, + getFieldFromNode: ( + input: VerboseTree, + key: FieldKey, + ): readonly VerboseTree[] => { + const node = options.valueConverter(input); + // Object.prototype.hasOwnProperty can return true for strings (ex: with key "0"), so we have to filter them out. + // Rather than just special casing strings, we can handle them with an early return for all primitives. + if (typeof node !== "object") { + return []; + } + + if (node === null) { + return []; + } + + if (isFluidHandle(node)) { + return []; + } + + if (Array.isArray(node.fields)) { + return key === EmptyKey ? node.fields : []; + } + + if (Object.prototype.hasOwnProperty.call(node, key)) { + const field = node.fields[key]; + return field === undefined ? [] : [field]; + } + + return []; + }, + }; +} From b19530fba36b0f12e3b3f35f591689a77ef91432 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:43:51 -0700 Subject: [PATCH 11/54] Split out clone --- .../dds/tree/src/simple-tree/api/clone.ts | 142 ++++++++++++++++ .../dds/tree/src/simple-tree/api/create.ts | 157 +++--------------- 2 files changed, 166 insertions(+), 133 deletions(-) create mode 100644 packages/dds/tree/src/simple-tree/api/clone.ts diff --git a/packages/dds/tree/src/simple-tree/api/clone.ts b/packages/dds/tree/src/simple-tree/api/clone.ts new file mode 100644 index 000000000000..40e2a40ca188 --- /dev/null +++ b/packages/dds/tree/src/simple-tree/api/clone.ts @@ -0,0 +1,142 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +// import type { IFluidHandle } from "@fluidframework/core-interfaces"; + +// import type { JsonCompatible } from "../../util/index.js"; +// import type { +// TreeLeafValue, +// ImplicitFieldSchema, +// TreeFieldFromImplicitField, +// } from "../schemaTypes.js"; +// import type { TreeNode } from "../core/index.js"; +// import type { VerboseTree, VerboseTreeNode } from "./verboseTree.js"; + +// /** +// * Like {@link Tree.create}, except deeply clones existing nodes. +// * @remarks +// * This only clones the persisted data associated with a node. +// * Local state, such as properties added to customized schema classes, will not be cloned: +// * they will be initialized however they end up after running the constructor, just like if a remote client had inserted the same nodes. +// */ +// export function clone( +// original: TreeFieldFromImplicitField, +// options?: { +// /** +// * If set, all identifier's in the cloned tree (See {@link SchemaFactory.identifier}) will be replaced with new ones allocated using the default identifier allocation schema. +// * Otherwise any identifiers will be preserved as is. +// */ +// replaceIdentifiers?: true; +// }, +// ): TreeFieldFromImplicitField { +// throw new Error(); +// } + +// /** +// * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. +// * +// * @remarks +// * If the schema is compatible with {@link ITreeConfigurationOptions.preventAmbiguity}, then the returned object will be lossless and compatible with {@link Tree.create} (unless the options are used to customize it). +// */ +// export function cloneToJSON( +// node: TreeNode, +// options?: { +// handleConverter(handle: IFluidHandle): T; +// readonly useStableFieldKeys?: boolean; +// }, +// ): JsonCompatible; + +// /** +// * Same as generic overload, except leaves handles as is. +// */ +// export function cloneToJSON( +// node: TreeNode, +// options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, +// ): JsonCompatible; + +// export function cloneToJSON( +// node: TreeNode, +// options?: { +// handleConverter?(handle: IFluidHandle): T; +// readonly useStableFieldKeys?: boolean; +// }, +// ): JsonCompatible { +// throw new Error(); +// } + +// /** +// * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. +// * Verbose tree format, with explicit type on every node. +// * +// * @remarks +// * There are several cases this may be preferred to {@link Tree.clone}: +// * +// * 1. When not using {@link ITreeConfigurationOptions.preventAmbiguity} (or when using `useStableFieldKeys`), {@link Tree.clone} can produce ambiguous data (the type may be unclear on some nodes). +// * This may be a good alternative to {@link Tree.clone} since it is lossless. +// * +// * 2. When the data might be interpreted without access to the exact same view schema. In such cases, the types may be unknowable if not included. +// * +// * 3. When easy access to the type is desired, or a more uniform simple to parse format is desired. +// */ +// export function cloneToJSONVerbose( +// node: TreeNode, +// options?: { +// handleConverter(handle: IFluidHandle): T; +// readonly useStableFieldKeys?: boolean; +// }, +// ): VerboseTreeNode; + +// /** +// * Same as generic overload, except leaves handles as is. +// */ +// export function cloneToJSONVerbose( +// node: TreeNode, +// options?: { readonly handleConverter?: undefined; readonly useStableFieldKeys?: boolean }, +// ): VerboseTreeNode; + +// export function cloneToJSONVerbose( +// node: TreeNode, +// options?: { +// handleConverter?(handle: IFluidHandle): T; +// readonly useStableFieldKeys?: boolean; +// }, +// ): VerboseTreeNode { +// const config = { +// handleConverter(handle: IFluidHandle): T { +// return handle as T; +// }, +// useStableFieldKeys: false, +// ...options, +// }; + +// // TODO: this should probably just get a cursor to the underlying data and use that. + +// function convertNode(n: TreeNode): VerboseTreeNode { +// let fields: VerboseTreeNode["fields"]; + +// if (n instanceof CustomArrayNodeBase) { +// const x = n as CustomArrayNodeBase; +// fields = Array.from(x, convertNodeOrValue); +// } else if ((n as TreeNode) instanceof CustomMapNodeBase) { +// fields = {}; +// for (const [key, value] of n as CustomMapNodeBase) { +// fields[key] = convertNodeOrValue(value); +// } +// } else { +// fields = {}; +// for (const [key, value] of n as CustomMapNodeBase) { +// fields[key] = convertNodeOrValue(value); +// } +// } + +// return { type: n[typeNameSymbol], fields }; +// } + +// function convertNodeOrValue(n: TreeNode | TreeLeafValue): VerboseTree { +// return isTreeNode(n) ? convertNode(n) : isFluidHandle(n) ? config.handleConverter(n) : n; +// } + +// return convertNode(node); +// } diff --git a/packages/dds/tree/src/simple-tree/api/create.ts b/packages/dds/tree/src/simple-tree/api/create.ts index 6b55eee06855..c86678d9d33e 100644 --- a/packages/dds/tree/src/simple-tree/api/create.ts +++ b/packages/dds/tree/src/simple-tree/api/create.ts @@ -43,7 +43,7 @@ import { * 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. - * For that case, use {@link Tree.clone}. + * TODO: For that case, use we should provide `Tree.clone`. * @privateRemarks * This could be exposed as a public `Tree.create` function. */ @@ -52,6 +52,25 @@ export function createFromInsertable( data: InsertableTreeFieldFromImplicitField, context?: NodeKeyManager | undefined, ): Unhydrated> { + const cursor = cursorFromInsertable(schema, data, context); + const result = cursor === undefined ? undefined : createFromCursor(schema, cursor); + return result as Unhydrated>; +} + +/** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. + * @remarks + * When providing a {@link TreeNodeSchemaClass}, + * this is the same as invoking its constructor except that an unhydrated node can also be provided and the returned value is a cursor. + * When `undefined` is provided (for an optional field), `undefined` is returned. + */ +export function cursorFromInsertable( + schema: TSchema, + data: InsertableTreeFieldFromImplicitField, + context?: NodeKeyManager | undefined, +): ITreeCursorSynchronous | undefined { const flexSchema = toFlexSchema(schema); const schemaValidationPolicy: SchemaAndPolicy = { policy: defaultSchemaPolicy, @@ -65,17 +84,16 @@ export function createFromInsertable( context, schemaValidationPolicy, ); - const result = - mapTree === undefined - ? undefined - : createFromCursor(schema, cursorForMapTreeNode(mapTree)); - return result as Unhydrated>; + const result = mapTree === undefined ? undefined : cursorForMapTreeNode(mapTree); + return result; } /** * Construct tree content compatible with a field defined by the provided `schema`. * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + * @privateRemarks + * This could be exposed as a public `Tree.createFromVerbose` function. */ export function createFromVerbose( schema: TSchema, @@ -132,130 +150,3 @@ export function createFromCursor( const result = getOrCreateNodeFromFlexTreeNode(mapTreeNode); return result as Unhydrated>; } - -// /** -// * Like {@link Tree.create}, except deeply clones existing nodes. -// * @remarks -// * This only clones the persisted data associated with a node. -// * Local state, such as properties added to customized schema classes, will not be cloned: -// * they will be initialized however they end up after running the constructor, just like if a remote client had inserted the same nodes. -// */ -// export function clone( -// original: TreeFieldFromImplicitField, -// options?: { -// /** -// * If set, all identifier's in the cloned tree (See {@link SchemaFactory.identifier}) will be replaced with new ones allocated using the default identifier allocation schema. -// * Otherwise any identifiers will be preserved as is. -// */ -// replaceIdentifiers?: true; -// }, -// ): TreeFieldFromImplicitField { -// throw new Error(); -// } - -// /** -// * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. -// * -// * @remarks -// * If the schema is compatible with {@link ITreeConfigurationOptions.preventAmbiguity}, then the returned object will be lossless and compatible with {@link Tree.create} (unless the options are used to customize it). -// */ -// export function cloneToJSON( -// node: TreeNode, -// options?: { -// handleConverter(handle: IFluidHandle): T; -// readonly useStableFieldKeys?: boolean; -// }, -// ): JsonCompatible; - -// /** -// * Same as generic overload, except leaves handles as is. -// */ -// export function cloneToJSON( -// node: TreeNode, -// options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, -// ): JsonCompatible; - -// export function cloneToJSON( -// node: TreeNode, -// options?: { -// handleConverter?(handle: IFluidHandle): T; -// readonly useStableFieldKeys?: boolean; -// }, -// ): JsonCompatible { -// throw new Error(); -// } - -// /** -// * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. -// * Verbose tree format, with explicit type on every node. -// * -// * @remarks -// * There are several cases this may be preferred to {@link Tree.clone}: -// * -// * 1. When not using {@link ITreeConfigurationOptions.preventAmbiguity} (or when using `useStableFieldKeys`), {@link Tree.clone} can produce ambiguous data (the type may be unclear on some nodes). -// * This may be a good alternative to {@link Tree.clone} since it is lossless. -// * -// * 2. When the data might be interpreted without access to the exact same view schema. In such cases, the types may be unknowable if not included. -// * -// * 3. When easy access to the type is desired, or a more uniform simple to parse format is desired. -// */ -// export function cloneToJSONVerbose( -// node: TreeNode, -// options?: { -// handleConverter(handle: IFluidHandle): T; -// readonly useStableFieldKeys?: boolean; -// }, -// ): VerboseTreeNode; - -// /** -// * Same as generic overload, except leaves handles as is. -// */ -// export function cloneToJSONVerbose( -// node: TreeNode, -// options?: { readonly handleConverter?: undefined; readonly useStableFieldKeys?: boolean }, -// ): VerboseTreeNode; - -// export function cloneToJSONVerbose( -// node: TreeNode, -// options?: { -// handleConverter?(handle: IFluidHandle): T; -// readonly useStableFieldKeys?: boolean; -// }, -// ): VerboseTreeNode { -// const config = { -// handleConverter(handle: IFluidHandle): T { -// return handle as T; -// }, -// useStableFieldKeys: false, -// ...options, -// }; - -// // TODO: this should probably just get a cursor to the underlying data and use that. - -// function convertNode(n: TreeNode): VerboseTreeNode { -// let fields: VerboseTreeNode["fields"]; - -// if (n instanceof CustomArrayNodeBase) { -// const x = n as CustomArrayNodeBase; -// fields = Array.from(x, convertNodeOrValue); -// } else if ((n as TreeNode) instanceof CustomMapNodeBase) { -// fields = {}; -// for (const [key, value] of n as CustomMapNodeBase) { -// fields[key] = convertNodeOrValue(value); -// } -// } else { -// fields = {}; -// for (const [key, value] of n as CustomMapNodeBase) { -// fields[key] = convertNodeOrValue(value); -// } -// } - -// return { type: n[typeNameSymbol], fields }; -// } - -// function convertNodeOrValue(n: TreeNode | TreeLeafValue): VerboseTree { -// return isTreeNode(n) ? convertNode(n) : isFluidHandle(n) ? config.handleConverter(n) : n; -// } - -// return convertNode(node); -// } From 2ff695c0c620f43d350027f338a4fb3fcb0a8d45 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:53:48 -0700 Subject: [PATCH 12/54] Add independentView API --- packages/dds/tree/package.json | 8 --- packages/dds/tree/src/index.ts | 2 +- packages/dds/tree/src/shared-tree/index.ts | 4 ++ .../dds/tree/src/shared-tree/sharedTree.ts | 37 ++++++++---- .../feature-libraries/sequence-field/utils.ts | 2 +- .../test/snapshots/snapshotTestScenarios.ts | 2 +- packages/dds/tree/src/test/utils.ts | 7 ++- packages/dds/tree/src/treeFactory.ts | 58 ++++++++++++++++++- .../src/createAlwaysFinalizedIdCompressor.ts | 46 +++++++++++++++ packages/runtime/id-compressor/src/index.ts | 1 + .../src/test/idCompressorTestUtilities.ts | 32 ---------- .../runtime/id-compressor/src/test/index.ts | 2 +- 12 files changed, 142 insertions(+), 59 deletions(-) create mode 100644 packages/runtime/id-compressor/src/createAlwaysFinalizedIdCompressor.ts diff --git a/packages/dds/tree/package.json b/packages/dds/tree/package.json index 1a4a311492a2..40bff4f8c271 100644 --- a/packages/dds/tree/package.json +++ b/packages/dds/tree/package.json @@ -212,14 +212,6 @@ }, "fluidBuild": { "tasks": { - "build:test:cjs": [ - "...", - "@fluidframework/id-compressor#build:test:cjs" - ], - "build:test:esm": [ - "...", - "@fluidframework/id-compressor#build:test:esm" - ], "ci:build:docs": [ "build:esnext" ] diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index 33728f0a05c9..60afdf1a3ba2 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -144,7 +144,7 @@ export { type JsonLeafSchemaType, getJsonSchema, } from "./simple-tree/index.js"; -export { SharedTree, configuredSharedTree } from "./treeFactory.js"; +export { SharedTree, configuredSharedTree, independentView } from "./treeFactory.js"; export type { ICodecOptions, JsonValidator, SchemaValidationFunction } from "./codec/index.js"; export { noopValidator } from "./codec/index.js"; diff --git a/packages/dds/tree/src/shared-tree/index.ts b/packages/dds/tree/src/shared-tree/index.ts index ee8f0dab8f77..4bedc09e90c7 100644 --- a/packages/dds/tree/src/shared-tree/index.ts +++ b/packages/dds/tree/src/shared-tree/index.ts @@ -12,6 +12,8 @@ export { type SharedTreeContentSnapshot, type SharedTreeFormatOptions, SharedTreeFormatVersion, + buildConfiguredForest, + defaultSharedTreeOptions, } from "./sharedTree.js"; export { @@ -33,6 +35,8 @@ export { buildTreeConfiguration, } from "./schematizeTree.js"; +export { SchematizingSimpleTreeView } from "./schematizingTreeView.js"; + export { CheckoutFlexTreeView } from "./checkoutFlexTreeView.js"; export type { ISharedTreeEditor, ISchemaEditor } from "./sharedTreeEditBuilder.js"; diff --git a/packages/dds/tree/src/shared-tree/sharedTree.ts b/packages/dds/tree/src/shared-tree/sharedTree.ts index 009e2c0b8ba5..b6c7c6d94400 100644 --- a/packages/dds/tree/src/shared-tree/sharedTree.ts +++ b/packages/dds/tree/src/shared-tree/sharedTree.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { assert } from "@fluidframework/core-utils/internal"; +import { assert, unreachableCase } from "@fluidframework/core-utils/internal"; import type { IChannelAttributes, IChannelFactory, @@ -16,10 +16,12 @@ import { UsageError } from "@fluidframework/telemetry-utils/internal"; import { type ICodecOptions, noopValidator } from "../codec/index.js"; import { + type IEditableForest, type JsonableTree, RevisionTagCodec, type TreeStoredSchema, TreeStoredSchemaRepository, + type TreeStoredSchemaSubscription, makeDetachedFieldIndex, moveToDetachedField, } from "../core/index.js"; @@ -61,6 +63,7 @@ import type { SharedTreeChange } from "./sharedTreeChangeTypes.js"; import type { SharedTreeEditBuilder } from "./sharedTreeEditBuilder.js"; import { type CheckoutEvents, type TreeCheckout, createTreeCheckout } from "./treeCheckout.js"; import { breakingClass, throwIfBroken } from "../util/index.js"; +import type { IIdCompressor } from "@fluidframework/id-compressor"; /** * Copy of data from an {@link ISharedTree} at some point in time. @@ -144,6 +147,27 @@ function getCodecVersions(formatVersion: number): ExplicitCodecVersions { return versions; } +export function buildConfiguredForest( + type: ForestType, + schema: TreeStoredSchemaSubscription, + idCompressor: IIdCompressor, +): IEditableForest { + switch (type) { + case ForestType.Optimized: + return buildChunkedForest( + makeTreeChunker(schema, defaultSchemaPolicy), + undefined, + idCompressor, + ); + case ForestType.Reference: + return buildForest(); + case ForestType.Expensive: + return buildForest(undefined, true); + default: + unreachableCase(type); + } +} + /** * Shared tree, configured with a good set of indexes and field kinds which will maintain compatibility over time. * @@ -176,16 +200,7 @@ export class SharedTree const options = { ...defaultSharedTreeOptions, ...optionsParam }; const codecVersions = getCodecVersions(options.formatVersion); const schema = new TreeStoredSchemaRepository(); - const forest = - options.forest === ForestType.Optimized - ? buildChunkedForest( - makeTreeChunker(schema, defaultSchemaPolicy), - undefined, - runtime.idCompressor, - ) - : options.forest === ForestType.Reference - ? buildForest() - : buildForest(undefined, true); + const forest = buildConfiguredForest(options.forest, schema, runtime.idCompressor); const revisionTagCodec = new RevisionTagCodec(runtime.idCompressor); const removedRoots = makeDetachedFieldIndex( "repair", diff --git a/packages/dds/tree/src/test/feature-libraries/sequence-field/utils.ts b/packages/dds/tree/src/test/feature-libraries/sequence-field/utils.ts index cfe4373eb803..aa15ceea0501 100644 --- a/packages/dds/tree/src/test/feature-libraries/sequence-field/utils.ts +++ b/packages/dds/tree/src/test/feature-libraries/sequence-field/utils.ts @@ -6,7 +6,7 @@ import { strict } from "assert"; import { assert } from "@fluidframework/core-utils/internal"; -import { createAlwaysFinalizedIdCompressor } from "@fluidframework/id-compressor/internal/test-utils"; +import { createAlwaysFinalizedIdCompressor } from "@fluidframework/id-compressor/internal"; import { type ChangeAtomId, diff --git a/packages/dds/tree/src/test/snapshots/snapshotTestScenarios.ts b/packages/dds/tree/src/test/snapshots/snapshotTestScenarios.ts index 2a39683de34a..3101eea829e9 100644 --- a/packages/dds/tree/src/test/snapshots/snapshotTestScenarios.ts +++ b/packages/dds/tree/src/test/snapshots/snapshotTestScenarios.ts @@ -5,7 +5,7 @@ import { strict as assert } from "node:assert"; import type { SessionId } from "@fluidframework/id-compressor"; -import { createAlwaysFinalizedIdCompressor } from "@fluidframework/id-compressor/internal/test-utils"; +import { createAlwaysFinalizedIdCompressor } from "@fluidframework/id-compressor/internal"; import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils/internal"; import { typeboxValidator } from "../../external-utilities/index.js"; diff --git a/packages/dds/tree/src/test/utils.ts b/packages/dds/tree/src/test/utils.ts index 8b4c03346a7a..7b8291f80715 100644 --- a/packages/dds/tree/src/test/utils.ts +++ b/packages/dds/tree/src/test/utils.ts @@ -23,8 +23,11 @@ import type { IChannelServices, } from "@fluidframework/datastore-definitions/internal"; import type { SessionId } from "@fluidframework/id-compressor"; -import { assertIsStableId, createIdCompressor } from "@fluidframework/id-compressor/internal"; -import { createAlwaysFinalizedIdCompressor } from "@fluidframework/id-compressor/internal/test-utils"; +import { + assertIsStableId, + createIdCompressor, + createAlwaysFinalizedIdCompressor, +} from "@fluidframework/id-compressor/internal"; import { MockContainerRuntimeFactoryForReconnection, MockFluidDataStoreRuntime, diff --git a/packages/dds/tree/src/treeFactory.ts b/packages/dds/tree/src/treeFactory.ts index 5e67f76878b2..8d488d9ab0c2 100644 --- a/packages/dds/tree/src/treeFactory.ts +++ b/packages/dds/tree/src/treeFactory.ts @@ -9,6 +9,8 @@ import type { IFluidDataStoreRuntime, IChannelServices, } from "@fluidframework/datastore-definitions/internal"; +import { createAlwaysFinalizedIdCompressor } from "@fluidframework/id-compressor/internal"; + import type { SharedObjectKind } from "@fluidframework/shared-object-base"; import { type ISharedObjectKind, @@ -16,8 +18,26 @@ import { } from "@fluidframework/shared-object-base/internal"; import { pkgVersion } from "./packageVersion.js"; -import { SharedTree as SharedTreeImpl, type SharedTreeOptions } from "./shared-tree/index.js"; -import type { ITree } from "./simple-tree/index.js"; +import { + buildConfiguredForest, + createTreeCheckout, + SharedTree as SharedTreeImpl, + type SharedTreeOptions, +} from "./shared-tree/index.js"; +import type { + ImplicitFieldSchema, + ITree, + TreeView, + TreeViewConfiguration, +} from "./simple-tree/index.js"; +import { SchematizingSimpleTreeView, defaultSharedTreeOptions } from "./shared-tree/index.js"; +import type { IIdCompressor } from "@fluidframework/id-compressor"; +import { + RevisionTagCodec, + TreeStoredSchemaRepository, + type RevisionTag, +} from "./core/index.js"; +import { createNodeKeyManager } from "./feature-libraries/index.js"; /** * A channel factory that creates an {@link ITree}. @@ -96,3 +116,37 @@ export function configuredSharedTree( } return createSharedObjectKind(ConfiguredFactory); } + +/** + * Create a {@link TreeView} that is not tied to any {@link SharedTree} instance. + * + * @remarks + * Such a view can never experience collaboration or be persisted to to a Fluid Container. + * + * This can be useful for testing, as well as use-cases like working on local files instead of documents stored in some fluid service. + * @alpha + */ +export function independentView( + config: TreeViewConfiguration, + options: SharedTreeOptions & { idCompressor?: IIdCompressor | undefined }, +): TreeView { + const idCompressor: IIdCompressor = + options.idCompressor ?? createAlwaysFinalizedIdCompressor(); + const mintRevisionTag = (): RevisionTag => idCompressor.generateCompressedId(); + const revisionTagCodec = new RevisionTagCodec(idCompressor); + const schema = new TreeStoredSchemaRepository(); + const forest = buildConfiguredForest( + options.forest ?? defaultSharedTreeOptions.forest, + schema, + idCompressor, + ); + const checkout = createTreeCheckout(idCompressor, mintRevisionTag, revisionTagCodec, { + forest, + schema, + }); + return new SchematizingSimpleTreeView( + checkout, + config, + createNodeKeyManager(idCompressor), + ); +} diff --git a/packages/runtime/id-compressor/src/createAlwaysFinalizedIdCompressor.ts b/packages/runtime/id-compressor/src/createAlwaysFinalizedIdCompressor.ts new file mode 100644 index 000000000000..bdd7384da9be --- /dev/null +++ b/packages/runtime/id-compressor/src/createAlwaysFinalizedIdCompressor.ts @@ -0,0 +1,46 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import type { ITelemetryBaseLogger } from "@fluidframework/core-interfaces"; + +import { createIdCompressor, type IdCompressor } from "./idCompressor.js"; +import type { IIdCompressor, IIdCompressorCore, SessionId } from "./types/index.js"; +import { createSessionId } from "./utilities.js"; + +/** + * Creates a compressor that only produces final IDs. + * @remarks + * This should only be used for testing and synchronous (non-collaborative) purposes. + * @internal + */ +export function createAlwaysFinalizedIdCompressor( + logger?: ITelemetryBaseLogger, +): IIdCompressor & IIdCompressorCore; + +/** + * Creates a compressor that only produces final IDs. + * @remarks + * This should only be used for testing and synchronous (non-collaborative) purposes. + * @internal + */ +export function createAlwaysFinalizedIdCompressor( + sessionId: SessionId, + logger?: ITelemetryBaseLogger, +): IIdCompressor & IIdCompressorCore; + +export function createAlwaysFinalizedIdCompressor( + sessionIdOrLogger?: SessionId | ITelemetryBaseLogger, + loggerOrUndefined?: ITelemetryBaseLogger, +): IIdCompressor & IIdCompressorCore { + const compressor = + sessionIdOrLogger === undefined + ? createIdCompressor() + : typeof sessionIdOrLogger === "string" + ? createIdCompressor(sessionIdOrLogger, loggerOrUndefined) + : createIdCompressor(sessionIdOrLogger); + // Permanently put the compressor in a ghost session + (compressor as IdCompressor).startGhostSession(createSessionId()); + return compressor; +} diff --git a/packages/runtime/id-compressor/src/index.ts b/packages/runtime/id-compressor/src/index.ts index d1d9c40d816a..f255acdf4b70 100644 --- a/packages/runtime/id-compressor/src/index.ts +++ b/packages/runtime/id-compressor/src/index.ts @@ -26,3 +26,4 @@ export { StableId, IdCreationRange, } from "./types/index.js"; +export { createAlwaysFinalizedIdCompressor } from "./createAlwaysFinalizedIdCompressor.js"; diff --git a/packages/runtime/id-compressor/src/test/idCompressorTestUtilities.ts b/packages/runtime/id-compressor/src/test/idCompressorTestUtilities.ts index 700eeb9bfcdf..d046a3522806 100644 --- a/packages/runtime/id-compressor/src/test/idCompressorTestUtilities.ts +++ b/packages/runtime/id-compressor/src/test/idCompressorTestUtilities.ts @@ -20,8 +20,6 @@ import { ITelemetryBaseLogger } from "@fluidframework/core-interfaces"; import { IdCompressor } from "../idCompressor.js"; import { - type IIdCompressor, - type IIdCompressorCore, IdCreationRange, OpSpaceCompressedId, SerializedIdCompressorWithNoSession, @@ -996,33 +994,3 @@ export function generateCompressedIds( } return ids; } - -/** - * Creates a compressor that only produces final IDs. - * It should only be used for testing purposes. - */ -export function createAlwaysFinalizedIdCompressor( - logger?: ITelemetryBaseLogger, -): IIdCompressor & IIdCompressorCore; -/** - * Creates a compressor that only produces final IDs. - * It should only be used for testing purposes. - */ -export function createAlwaysFinalizedIdCompressor( - sessionId: SessionId, - logger?: ITelemetryBaseLogger, -): IIdCompressor & IIdCompressorCore; -export function createAlwaysFinalizedIdCompressor( - sessionIdOrLogger?: SessionId | ITelemetryBaseLogger, - loggerOrUndefined?: ITelemetryBaseLogger, -): IIdCompressor & IIdCompressorCore { - const compressor = - sessionIdOrLogger === undefined - ? createIdCompressor() - : typeof sessionIdOrLogger === "string" - ? createIdCompressor(sessionIdOrLogger, loggerOrUndefined) - : createIdCompressor(sessionIdOrLogger); - // Permanently put the compressor in a ghost session - (compressor as IdCompressor).startGhostSession(createSessionId()); - return compressor; -} diff --git a/packages/runtime/id-compressor/src/test/index.ts b/packages/runtime/id-compressor/src/test/index.ts index 8d8dd53612b2..dfaaa8e54caf 100644 --- a/packages/runtime/id-compressor/src/test/index.ts +++ b/packages/runtime/id-compressor/src/test/index.ts @@ -7,4 +7,4 @@ * Exports for test utilities for `id-compressor` */ -export { createAlwaysFinalizedIdCompressor } from "./idCompressorTestUtilities.js"; +export { createAlwaysFinalizedIdCompressor } from "../index.js"; From 86778130fe8e70d9fe85ce5c291817334496109e Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:24:19 -0700 Subject: [PATCH 13/54] Export via treeBeta --- .../dds/tree/api-report/tree.alpha.api.md | 71 +++++ packages/dds/tree/api-report/tree.beta.api.md | 71 +++++ packages/dds/tree/src/index.ts | 10 + .../dds/tree/src/simple-tree/api/clone.ts | 278 +++++++++--------- .../dds/tree/src/simple-tree/api/create.ts | 2 + .../dds/tree/src/simple-tree/api/index.ts | 10 +- .../tree/src/simple-tree/api/treeApiBeta.ts | 26 +- .../tree/src/simple-tree/api/verboseTree.ts | 12 +- packages/dds/tree/src/simple-tree/index.ts | 8 + packages/dds/tree/src/util/utils.ts | 2 + .../api-report/fluid-framework.alpha.api.md | 71 +++++ .../api-report/fluid-framework.beta.api.md | 71 +++++ 12 files changed, 490 insertions(+), 142 deletions(-) diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index e83ecf87014f..0cf38a9b8722 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -14,6 +14,35 @@ type ApplyKind = [FieldKind.Identifier]: DefaultsAreOptional extends true ? T | undefined : T; }[Kind]; +// @beta +export function clone(original: TreeFieldFromImplicitField, options?: { + replaceIdentifiers?: true; +}): TreeFieldFromImplicitField; + +// @beta +export function cloneToJSON(node: TreeNode, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; +}): JsonCompatible; + +// @beta +export function cloneToJSON(node: TreeNode, options?: { + handleConverter?: undefined; + useStableFieldKeys?: boolean; +}): JsonCompatible; + +// @beta +export function cloneToJSONVerbose(node: TreeNode, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; +}): VerboseTreeNode; + +// @beta +export function cloneToJSONVerbose(node: TreeNode, options?: { + readonly handleConverter?: undefined; + readonly useStableFieldKeys?: boolean; +}): VerboseTreeNode; + // @public export enum CommitKind { Default = 0, @@ -27,10 +56,22 @@ export interface CommitMetadata { readonly kind: CommitKind; } +// @beta +export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options: ParseOptions): Unhydrated>; + +// @beta +export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options?: Partial>): Unhydrated>; + // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @beta +export interface EncodeOptions { + readonly useStoredKeys?: boolean; + valueConverter(data: IFluidHandle): TCustom; +} + // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -211,6 +252,14 @@ export interface JsonArrayNodeSchema extends JsonNodeSchemaBase = string | number | boolean | null | JsonCompatible[] | JsonCompatibleObject | TExtra; + +// @beta +export type JsonCompatibleObject = { + [P in string]?: JsonCompatible; +}; + // @alpha @sealed export type JsonFieldSchema = { readonly description?: string | undefined; @@ -334,6 +383,12 @@ type ObjectFromSchemaRecordUnsafe void; +// @beta +export interface ParseOptions { + readonly useStoredKeys?: boolean; + valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; +} + // @public @sealed interface ReadonlyMapInlined> { [Symbol.iterator](): IterableIterator<[K, TreeNodeFromImplicitAllowedTypesUnsafe]>; @@ -485,6 +540,11 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>) => () => void; + readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; + readonly createFromVerbose: typeof createFromVerbose; + readonly clone: typeof clone; + readonly cloneToJSONVerbose: typeof cloneToJSONVerbose; + readonly cloneToJSON: typeof cloneToJSON; }; // @public @sealed @@ -638,6 +698,17 @@ export type ValidateRecursiveSchema> = true; +// @beta +export type VerboseTree = VerboseTreeNode | Exclude | THandle; + +// @beta +export interface VerboseTreeNode { + fields: VerboseTree[] | { + [key: string]: VerboseTree; + }; + type: string; +} + // @public @sealed export interface WithType { // @deprecated diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index 8909eb14b83c..8694795a9114 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -14,6 +14,35 @@ type ApplyKind = [FieldKind.Identifier]: DefaultsAreOptional extends true ? T | undefined : T; }[Kind]; +// @beta +export function clone(original: TreeFieldFromImplicitField, options?: { + replaceIdentifiers?: true; +}): TreeFieldFromImplicitField; + +// @beta +export function cloneToJSON(node: TreeNode, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; +}): JsonCompatible; + +// @beta +export function cloneToJSON(node: TreeNode, options?: { + handleConverter?: undefined; + useStableFieldKeys?: boolean; +}): JsonCompatible; + +// @beta +export function cloneToJSONVerbose(node: TreeNode, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; +}): VerboseTreeNode; + +// @beta +export function cloneToJSONVerbose(node: TreeNode, options?: { + readonly handleConverter?: undefined; + readonly useStableFieldKeys?: boolean; +}): VerboseTreeNode; + // @public export enum CommitKind { Default = 0, @@ -27,10 +56,22 @@ export interface CommitMetadata { readonly kind: CommitKind; } +// @beta +export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options: ParseOptions): Unhydrated>; + +// @beta +export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options?: Partial>): Unhydrated>; + // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @beta +export interface EncodeOptions { + readonly useStoredKeys?: boolean; + valueConverter(data: IFluidHandle): TCustom; +} + // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -200,6 +241,14 @@ export interface ITreeViewConfiguration = string | number | boolean | null | JsonCompatible[] | JsonCompatibleObject | TExtra; + +// @beta +export type JsonCompatibleObject = { + [P in string]?: JsonCompatible; +}; + // @public export type LazyItem = Item | (() => Item); @@ -266,6 +315,12 @@ type ObjectFromSchemaRecordUnsafe void; +// @beta +export interface ParseOptions { + readonly useStoredKeys?: boolean; + valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; +} + // @public @sealed interface ReadonlyMapInlined> { [Symbol.iterator](): IterableIterator<[K, TreeNodeFromImplicitAllowedTypesUnsafe]>; @@ -417,6 +472,11 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>) => () => void; + readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; + readonly createFromVerbose: typeof createFromVerbose; + readonly clone: typeof clone; + readonly cloneToJSONVerbose: typeof cloneToJSONVerbose; + readonly cloneToJSON: typeof cloneToJSON; }; // @public @sealed @@ -570,6 +630,17 @@ export type ValidateRecursiveSchema> = true; +// @beta +export type VerboseTree = VerboseTreeNode | Exclude | THandle; + +// @beta +export interface VerboseTreeNode { + fields: VerboseTree[] | { + [key: string]: VerboseTree; + }; + type: string; +} + // @public @sealed export interface WithType { // @deprecated diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index 8898e705e3ee..71343563beeb 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -129,6 +129,14 @@ export { // Beta APIs TreeBeta, type TreeChangeEventsBeta, + createFromVerbose, + clone, + cloneToJSON, + cloneToJSONVerbose, + type VerboseTreeNode, + type EncodeOptions, + type ParseOptions, + type VerboseTree, // Back to normal types type JsonTreeSchema, type JsonSchemaId, @@ -171,3 +179,5 @@ export { // These would be put in `internalTypes` except doing so tents to cause errors like: // The inferred type of 'NodeMap' cannot be named without a reference to '../../node_modules/@fluidframework/tree/lib/internalTypes.js'. This is likely not portable. A type annotation is necessary. export type { MapNodeInsertableData } from "./simple-tree/index.js"; + +export type { JsonCompatible, JsonCompatibleObject } from "./util/index.js"; diff --git a/packages/dds/tree/src/simple-tree/api/clone.ts b/packages/dds/tree/src/simple-tree/api/clone.ts index 40e2a40ca188..8321c171010c 100644 --- a/packages/dds/tree/src/simple-tree/api/clone.ts +++ b/packages/dds/tree/src/simple-tree/api/clone.ts @@ -3,140 +3,144 @@ * Licensed under the MIT License. */ -// import type { IFluidHandle } from "@fluidframework/core-interfaces"; - -// import type { JsonCompatible } from "../../util/index.js"; -// import type { -// TreeLeafValue, -// ImplicitFieldSchema, -// TreeFieldFromImplicitField, -// } from "../schemaTypes.js"; -// import type { TreeNode } from "../core/index.js"; -// import type { VerboseTree, VerboseTreeNode } from "./verboseTree.js"; - -// /** -// * Like {@link Tree.create}, except deeply clones existing nodes. -// * @remarks -// * This only clones the persisted data associated with a node. -// * Local state, such as properties added to customized schema classes, will not be cloned: -// * they will be initialized however they end up after running the constructor, just like if a remote client had inserted the same nodes. -// */ -// export function clone( -// original: TreeFieldFromImplicitField, -// options?: { -// /** -// * If set, all identifier's in the cloned tree (See {@link SchemaFactory.identifier}) will be replaced with new ones allocated using the default identifier allocation schema. -// * Otherwise any identifiers will be preserved as is. -// */ -// replaceIdentifiers?: true; -// }, -// ): TreeFieldFromImplicitField { -// throw new Error(); -// } - -// /** -// * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. -// * -// * @remarks -// * If the schema is compatible with {@link ITreeConfigurationOptions.preventAmbiguity}, then the returned object will be lossless and compatible with {@link Tree.create} (unless the options are used to customize it). -// */ -// export function cloneToJSON( -// node: TreeNode, -// options?: { -// handleConverter(handle: IFluidHandle): T; -// readonly useStableFieldKeys?: boolean; -// }, -// ): JsonCompatible; - -// /** -// * Same as generic overload, except leaves handles as is. -// */ -// export function cloneToJSON( -// node: TreeNode, -// options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, -// ): JsonCompatible; - -// export function cloneToJSON( -// node: TreeNode, -// options?: { -// handleConverter?(handle: IFluidHandle): T; -// readonly useStableFieldKeys?: boolean; -// }, -// ): JsonCompatible { -// throw new Error(); -// } - -// /** -// * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. -// * Verbose tree format, with explicit type on every node. -// * -// * @remarks -// * There are several cases this may be preferred to {@link Tree.clone}: -// * -// * 1. When not using {@link ITreeConfigurationOptions.preventAmbiguity} (or when using `useStableFieldKeys`), {@link Tree.clone} can produce ambiguous data (the type may be unclear on some nodes). -// * This may be a good alternative to {@link Tree.clone} since it is lossless. -// * -// * 2. When the data might be interpreted without access to the exact same view schema. In such cases, the types may be unknowable if not included. -// * -// * 3. When easy access to the type is desired, or a more uniform simple to parse format is desired. -// */ -// export function cloneToJSONVerbose( -// node: TreeNode, -// options?: { -// handleConverter(handle: IFluidHandle): T; -// readonly useStableFieldKeys?: boolean; -// }, -// ): VerboseTreeNode; - -// /** -// * Same as generic overload, except leaves handles as is. -// */ -// export function cloneToJSONVerbose( -// node: TreeNode, -// options?: { readonly handleConverter?: undefined; readonly useStableFieldKeys?: boolean }, -// ): VerboseTreeNode; - -// export function cloneToJSONVerbose( -// node: TreeNode, -// options?: { -// handleConverter?(handle: IFluidHandle): T; -// readonly useStableFieldKeys?: boolean; -// }, -// ): VerboseTreeNode { -// const config = { -// handleConverter(handle: IFluidHandle): T { -// return handle as T; -// }, -// useStableFieldKeys: false, -// ...options, -// }; - -// // TODO: this should probably just get a cursor to the underlying data and use that. - -// function convertNode(n: TreeNode): VerboseTreeNode { -// let fields: VerboseTreeNode["fields"]; - -// if (n instanceof CustomArrayNodeBase) { -// const x = n as CustomArrayNodeBase; -// fields = Array.from(x, convertNodeOrValue); -// } else if ((n as TreeNode) instanceof CustomMapNodeBase) { -// fields = {}; -// for (const [key, value] of n as CustomMapNodeBase) { -// fields[key] = convertNodeOrValue(value); -// } -// } else { -// fields = {}; -// for (const [key, value] of n as CustomMapNodeBase) { -// fields[key] = convertNodeOrValue(value); -// } -// } - -// return { type: n[typeNameSymbol], fields }; -// } - -// function convertNodeOrValue(n: TreeNode | TreeLeafValue): VerboseTree { -// return isTreeNode(n) ? convertNode(n) : isFluidHandle(n) ? config.handleConverter(n) : n; -// } - -// return convertNode(node); -// } +import type { IFluidHandle } from "@fluidframework/core-interfaces"; + +import type { JsonCompatible } from "../../util/index.js"; +import type { ImplicitFieldSchema, TreeFieldFromImplicitField } from "../schemaTypes.js"; +import type { TreeNode } from "../core/index.js"; +import type { VerboseTreeNode } from "./verboseTree.js"; + +/** + * Like {@link TreeBeta.create}, except deeply clones existing nodes. + * @remarks + * This only clones the persisted data associated with a node. + * Local state, such as properties added to customized schema classes, will not be cloned: + * they will be initialized however they end up after running the constructor, just like if a remote client had inserted the same nodes. + * @beta + */ +export function clone( + original: TreeFieldFromImplicitField, + options?: { + /** + * If set, all identifier's in the cloned tree (See {@link SchemaFactory.identifier}) will be replaced with new ones allocated using the default identifier allocation schema. + * Otherwise any identifiers will be preserved as is. + */ + replaceIdentifiers?: true; + }, +): TreeFieldFromImplicitField { + throw new Error(); +} + +/** + * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. + * + * @remarks + * If the schema is compatible with {@link ITreeConfigurationOptions.preventAmbiguity}, + * then the returned object will be lossless and compatible with {@link TreeBeta.create} (unless the options are used to customize it). + * @beta + */ +export function cloneToJSON( + node: TreeNode, + options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; + }, +): JsonCompatible; + +/** + * Same as generic overload, except leaves handles as is. + * @beta + */ +export function cloneToJSON( + node: TreeNode, + options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, +): JsonCompatible; + +export function cloneToJSON( + node: TreeNode, + options?: { + handleConverter?(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; + }, +): JsonCompatible { + throw new Error(); +} + +/** + * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. + * Verbose tree format, with explicit type on every node. + * + * @remarks + * There are several cases this may be preferred to {@link TreeBeta.clone}: + * + * 1. When not using {@link ITreeConfigurationOptions.preventAmbiguity} (or when using `useStableFieldKeys`), {@link TreeBeta.clone} can produce ambiguous data (the type may be unclear on some nodes). + * This may be a good alternative to {@link TreeBeta.clone} since it is lossless. + * + * 2. When the data might be interpreted without access to the exact same view schema. In such cases, the types may be unknowable if not included. + * + * 3. When easy access to the type is desired, or a more uniform simple to parse format is desired. + * @beta + */ +export function cloneToJSONVerbose( + node: TreeNode, + options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; + }, +): VerboseTreeNode; + +/** + * Same as generic overload, except leaves handles as is. + * @beta + */ +export function cloneToJSONVerbose( + node: TreeNode, + options?: { readonly handleConverter?: undefined; readonly useStableFieldKeys?: boolean }, +): VerboseTreeNode; + +export function cloneToJSONVerbose( + node: TreeNode, + options?: { + handleConverter?(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; + }, +): VerboseTreeNode { + // const config = { + // handleConverter(handle: IFluidHandle): T { + // return handle as T; + // }, + // useStableFieldKeys: false, + // ...options, + // }; + + // TODO: this should probably just get a cursor to the underlying data and use that. + + function convertNode(n: TreeNode): VerboseTreeNode { + // let fields: VerboseTreeNode["fields"]; + + // if (n instanceof CustomArrayNodeBase) { + // const x = n as CustomArrayNodeBase; + // fields = Array.from(x, convertNodeOrValue); + // } else if ((n as TreeNode) instanceof CustomMapNodeBase) { + // fields = {}; + // for (const [key, value] of n as CustomMapNodeBase) { + // fields[key] = convertNodeOrValue(value); + // } + // } else { + // fields = {}; + // for (const [key, value] of n as CustomMapNodeBase) { + // fields[key] = convertNodeOrValue(value); + // } + // } + + // return { type: n[typeNameSymbol], fields }; + + throw new Error(); + } + + // function convertNodeOrValue(n: TreeNode | TreeLeafValue): VerboseTree { + // return isTreeNode(n) ? convertNode(n) : isFluidHandle(n) ? config.handleConverter(n) : n; + // } + + return convertNode(node); +} diff --git a/packages/dds/tree/src/simple-tree/api/create.ts b/packages/dds/tree/src/simple-tree/api/create.ts index 1f842de007bb..4dad0dc2247d 100644 --- a/packages/dds/tree/src/simple-tree/api/create.ts +++ b/packages/dds/tree/src/simple-tree/api/create.ts @@ -107,6 +107,7 @@ export function cursorFromInsertable( * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. * @privateRemarks * This could be exposed as a public `Tree.createFromVerbose` function. + * @beta */ export function createFromVerbose( schema: TSchema, @@ -118,6 +119,7 @@ export function createFromVerbose( * Construct tree content compatible with a field defined by the provided `schema`. * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + * @beta */ export function createFromVerbose( schema: TSchema, diff --git a/packages/dds/tree/src/simple-tree/api/index.ts b/packages/dds/tree/src/simple-tree/api/index.ts index 83359ad2f4d4..65d902122f23 100644 --- a/packages/dds/tree/src/simple-tree/api/index.ts +++ b/packages/dds/tree/src/simple-tree/api/index.ts @@ -25,7 +25,8 @@ export { type EmptyObject, } from "./schemaCreationUtilities.js"; export { treeNodeApi, type TreeNodeApi } from "./treeNodeApi.js"; -export { createFromInsertable, cursorFromInsertable } from "./create.js"; +export { createFromInsertable, cursorFromInsertable, createFromVerbose } from "./create.js"; +export { clone, cloneToJSON, cloneToJSONVerbose } from "./clone.js"; export type { SimpleTreeSchema } from "./simpleSchema.js"; export { type JsonSchemaId, @@ -45,6 +46,13 @@ export { export { getJsonSchema } from "./getJsonSchema.js"; export { getSimpleSchema } from "./getSimpleSchema.js"; +export type { + VerboseTreeNode, + EncodeOptions, + ParseOptions, + VerboseTree, +} from "./verboseTree.js"; + export { TreeBeta, type NodeChangedData, type TreeChangeEventsBeta } from "./treeApiBeta.js"; // Exporting the schema (RecursiveObject) to test that recursive types are working correctly. diff --git a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts index 956bce801e7d..726cc25d156f 100644 --- a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts +++ b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts @@ -3,8 +3,21 @@ * Licensed under the MIT License. */ -import type { NodeKind, TreeChangeEvents, TreeNode, WithType } from "../core/index.js"; +import type { + NodeKind, + TreeChangeEvents, + TreeNode, + Unhydrated, + WithType, +} from "../core/index.js"; import { treeNodeApi } from "./treeNodeApi.js"; +import { createFromInsertable, createFromVerbose } from "./create.js"; +import { clone, cloneToJSON, cloneToJSONVerbose } from "./clone.js"; +import type { + ImplicitFieldSchema, + InsertableTreeFieldFromImplicitField, + TreeFieldFromImplicitField, +} from "../schemaTypes.js"; /** * Data included for {@link TreeChangeEventsBeta.nodeChanged}. @@ -98,4 +111,15 @@ export const TreeBeta = { ): () => void { return treeNodeApi.on(node, eventName, listener); }, + + create( + schema: TSchema, + data: InsertableTreeFieldFromImplicitField, + ): Unhydrated> { + return createFromInsertable(schema, data); + }, + createFromVerbose, + clone, + cloneToJSONVerbose, + cloneToJSON, } as const; diff --git a/packages/dds/tree/src/simple-tree/api/verboseTree.ts b/packages/dds/tree/src/simple-tree/api/verboseTree.ts index 69a6818b6b15..fa5cff753c95 100644 --- a/packages/dds/tree/src/simple-tree/api/verboseTree.ts +++ b/packages/dds/tree/src/simple-tree/api/verboseTree.ts @@ -44,12 +44,14 @@ import { isObjectNodeSchema } from "../objectNodeTypes.js"; import { walkFieldSchema } from "../walkSchema.js"; /** - * Verbose encoding of a {@link TreeNode} or {@link TreeValue}. + * Verbose encoding of a {@link TreeNode} or {@link TreeLeafValue}. * @remarks * This is verbose meaning that every {@link TreeNode} is a {@link VerboseTreeNode}. * Any IFluidHandle values have been replaced by `THandle`. * @privateRemarks - * This can store all possible simple trees, but it can not store all possible trees representable by our internal representations like FlexTree and JsonableTree. + * This can store all possible simple trees, + * but it can not store all possible trees representable by our internal representations like FlexTree and JsonableTree. + * @beta */ export type VerboseTree = | VerboseTreeNode @@ -77,7 +79,9 @@ export type VerboseTree = * This format allows for all simple-tree compatible trees to be represented. * * Unlike `JsonableTree`, leaf nodes are not boxed into node objects, and instead have their schema inferred from the value. - * Additionally, sequence fields can only occur on a node that has a single sequence field (with the empty key) replicating the behavior of simple-tree ArrayNodes. + * Additionally, sequence fields can only occur on a node that has a single sequence field (with the empty key) + * replicating the behavior of simple-tree ArrayNodes. + * @beta */ export interface VerboseTreeNode { /** @@ -105,6 +109,7 @@ export interface VerboseTreeNode { /** * Options for how to interpret a `VerboseTree` when schema information is available. + * @beta */ export interface ParseOptions { /** @@ -146,6 +151,7 @@ export interface SchemalessParseOptions { /** * Options for how to interpret a `VerboseTree` without relying on schema. + * @beta */ export interface EncodeOptions { /** diff --git a/packages/dds/tree/src/simple-tree/index.ts b/packages/dds/tree/src/simple-tree/index.ts index 0cf1b6bfcdc7..7d3765b9980a 100644 --- a/packages/dds/tree/src/simple-tree/index.ts +++ b/packages/dds/tree/src/simple-tree/index.ts @@ -64,6 +64,14 @@ export { type JsonLeafSchemaType, getJsonSchema, getSimpleSchema, + type createFromVerbose, + type clone, + type cloneToJSON, + type cloneToJSONVerbose, + type VerboseTreeNode, + type EncodeOptions, + type ParseOptions, + type VerboseTree, } from "./api/index.js"; export { type NodeFromSchema, diff --git a/packages/dds/tree/src/util/utils.ts b/packages/dds/tree/src/util/utils.ts index 90f6368ce630..01439191942f 100644 --- a/packages/dds/tree/src/util/utils.ts +++ b/packages/dds/tree/src/util/utils.ts @@ -222,6 +222,7 @@ export function count(iterable: Iterable): number { * * Note that this does not robustly forbid non json comparable data via type checking, * but instead mostly restricts access to it. + * @beta */ export type JsonCompatible = | string @@ -238,6 +239,7 @@ export type JsonCompatible = * * Note that this does not robustly forbid non json comparable data via type checking, * but instead mostly restricts access to it. + * @beta */ export type JsonCompatibleObject = { [P in string]?: JsonCompatible }; 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 dcdbdb4ab467..a98e17fcada1 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 @@ -21,6 +21,35 @@ export enum AttachState { Detached = "Detached" } +// @beta +export function clone(original: TreeFieldFromImplicitField, options?: { + replaceIdentifiers?: true; +}): TreeFieldFromImplicitField; + +// @beta +export function cloneToJSON(node: TreeNode, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; +}): JsonCompatible; + +// @beta +export function cloneToJSON(node: TreeNode, options?: { + handleConverter?: undefined; + useStableFieldKeys?: boolean; +}): JsonCompatible; + +// @beta +export function cloneToJSONVerbose(node: TreeNode, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; +}): VerboseTreeNode; + +// @beta +export function cloneToJSONVerbose(node: TreeNode, options?: { + readonly handleConverter?: undefined; + readonly useStableFieldKeys?: boolean; +}): VerboseTreeNode; + // @public export enum CommitKind { Default = 0, @@ -62,10 +91,22 @@ export interface ContainerSchema { readonly initialObjects: Record; } +// @beta +export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options: ParseOptions): Unhydrated>; + +// @beta +export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options?: Partial>): Unhydrated>; + // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @beta +export interface EncodeOptions { + readonly useStoredKeys?: boolean; + valueConverter(data: IFluidHandle): TCustom; +} + // @public @sealed export abstract class ErasedType { static [Symbol.hasInstance](value: never): value is never; @@ -548,6 +589,14 @@ export interface JsonArrayNodeSchema extends JsonNodeSchemaBase = string | number | boolean | null | JsonCompatible[] | JsonCompatibleObject | TExtra; + +// @beta +export type JsonCompatibleObject = { + [P in string]?: JsonCompatible; +}; + // @alpha @sealed export type JsonFieldSchema = { readonly description?: string | undefined; @@ -679,6 +728,12 @@ type ObjectFromSchemaRecordUnsafe void; +// @beta +export interface ParseOptions { + readonly useStoredKeys?: boolean; + valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; +} + // @public @sealed interface ReadonlyMapInlined> { [Symbol.iterator](): IterableIterator<[K, TreeNodeFromImplicitAllowedTypesUnsafe]>; @@ -857,6 +912,11 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>) => () => void; + readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; + readonly createFromVerbose: typeof createFromVerbose; + readonly clone: typeof clone; + readonly cloneToJSONVerbose: typeof cloneToJSONVerbose; + readonly cloneToJSON: typeof cloneToJSON; }; // @public @sealed @@ -1010,6 +1070,17 @@ export type ValidateRecursiveSchema> = true; +// @beta +export type VerboseTree = VerboseTreeNode | Exclude | THandle; + +// @beta +export interface VerboseTreeNode { + fields: VerboseTree[] | { + [key: string]: VerboseTree; + }; + type: string; +} + // @public @sealed export interface WithType { // @deprecated 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 cce1df261615..4812f3d88c60 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 @@ -21,6 +21,35 @@ export enum AttachState { Detached = "Detached" } +// @beta +export function clone(original: TreeFieldFromImplicitField, options?: { + replaceIdentifiers?: true; +}): TreeFieldFromImplicitField; + +// @beta +export function cloneToJSON(node: TreeNode, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; +}): JsonCompatible; + +// @beta +export function cloneToJSON(node: TreeNode, options?: { + handleConverter?: undefined; + useStableFieldKeys?: boolean; +}): JsonCompatible; + +// @beta +export function cloneToJSONVerbose(node: TreeNode, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; +}): VerboseTreeNode; + +// @beta +export function cloneToJSONVerbose(node: TreeNode, options?: { + readonly handleConverter?: undefined; + readonly useStableFieldKeys?: boolean; +}): VerboseTreeNode; + // @public export enum CommitKind { Default = 0, @@ -62,10 +91,22 @@ export interface ContainerSchema { readonly initialObjects: Record; } +// @beta +export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options: ParseOptions): Unhydrated>; + +// @beta +export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options?: Partial>): Unhydrated>; + // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @beta +export interface EncodeOptions { + readonly useStoredKeys?: boolean; + valueConverter(data: IFluidHandle): TCustom; +} + // @public @sealed export abstract class ErasedType { static [Symbol.hasInstance](value: never): value is never; @@ -537,6 +578,14 @@ export interface ITreeViewConfiguration = string | number | boolean | null | JsonCompatible[] | JsonCompatibleObject | TExtra; + +// @beta +export type JsonCompatibleObject = { + [P in string]?: JsonCompatible; +}; + // @public export type LazyItem = Item | (() => Item); @@ -611,6 +660,12 @@ type ObjectFromSchemaRecordUnsafe void; +// @beta +export interface ParseOptions { + readonly useStoredKeys?: boolean; + valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; +} + // @public @sealed interface ReadonlyMapInlined> { [Symbol.iterator](): IterableIterator<[K, TreeNodeFromImplicitAllowedTypesUnsafe]>; @@ -789,6 +844,11 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>) => () => void; + readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; + readonly createFromVerbose: typeof createFromVerbose; + readonly clone: typeof clone; + readonly cloneToJSONVerbose: typeof cloneToJSONVerbose; + readonly cloneToJSON: typeof cloneToJSON; }; // @public @sealed @@ -942,6 +1002,17 @@ export type ValidateRecursiveSchema> = true; +// @beta +export type VerboseTree = VerboseTreeNode | Exclude | THandle; + +// @beta +export interface VerboseTreeNode { + fields: VerboseTree[] | { + [key: string]: VerboseTree; + }; + type: string; +} + // @public @sealed export interface WithType { // @deprecated From 20443bf0c0d6877d67a14c9dc9fe2283b996cde8 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:13:36 -0700 Subject: [PATCH 14/54] Fix API, add compressed apis --- .../dds/tree/api-report/tree.alpha.api.md | 12 ++-- packages/dds/tree/api-report/tree.beta.api.md | 12 ++-- .../dds/tree/src/simple-tree/api/clone.ts | 65 ++++++++++++------- .../dds/tree/src/simple-tree/api/create.ts | 15 ++++- .../api-report/fluid-framework.alpha.api.md | 12 ++-- .../api-report/fluid-framework.beta.api.md | 12 ++-- 6 files changed, 79 insertions(+), 49 deletions(-) diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 0cf38a9b8722..b61ca0b9bc60 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -20,28 +20,28 @@ export function clone(original: TreeFieldFr }): TreeFieldFromImplicitField; // @beta -export function cloneToJSON(node: TreeNode, options?: { +export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSON(node: TreeNode, options?: { +export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { handleConverter?: undefined; useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSONVerbose(node: TreeNode, options?: { +export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; -}): VerboseTreeNode; +}): VerboseTree; // @beta -export function cloneToJSONVerbose(node: TreeNode, options?: { +export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { readonly handleConverter?: undefined; readonly useStableFieldKeys?: boolean; -}): VerboseTreeNode; +}): VerboseTree; // @public export enum CommitKind { diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index 8694795a9114..632b0315fc9d 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -20,28 +20,28 @@ export function clone(original: TreeFieldFr }): TreeFieldFromImplicitField; // @beta -export function cloneToJSON(node: TreeNode, options?: { +export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSON(node: TreeNode, options?: { +export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { handleConverter?: undefined; useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSONVerbose(node: TreeNode, options?: { +export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; -}): VerboseTreeNode; +}): VerboseTree; // @beta -export function cloneToJSONVerbose(node: TreeNode, options?: { +export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { readonly handleConverter?: undefined; readonly useStableFieldKeys?: boolean; -}): VerboseTreeNode; +}): VerboseTree; // @public export enum CommitKind { diff --git a/packages/dds/tree/src/simple-tree/api/clone.ts b/packages/dds/tree/src/simple-tree/api/clone.ts index 8321c171010c..a63b7e86aae1 100644 --- a/packages/dds/tree/src/simple-tree/api/clone.ts +++ b/packages/dds/tree/src/simple-tree/api/clone.ts @@ -5,10 +5,15 @@ import type { IFluidHandle } from "@fluidframework/core-interfaces"; -import type { JsonCompatible } from "../../util/index.js"; -import type { ImplicitFieldSchema, TreeFieldFromImplicitField } from "../schemaTypes.js"; -import type { TreeNode } from "../core/index.js"; -import type { VerboseTreeNode } from "./verboseTree.js"; +import { fail, type JsonCompatible } from "../../util/index.js"; +import type { + ImplicitFieldSchema, + TreeFieldFromImplicitField, + TreeLeafValue, +} from "../schemaTypes.js"; +import { isTreeNode, type TreeNode } from "../core/index.js"; +import type { VerboseTree, VerboseTreeNode } from "./verboseTree.js"; +import { isFluidHandle } from "@fluidframework/runtime-utils"; /** * Like {@link TreeBeta.create}, except deeply clones existing nodes. @@ -40,7 +45,7 @@ export function clone( * @beta */ export function cloneToJSON( - node: TreeNode, + node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; @@ -52,12 +57,12 @@ export function cloneToJSON( * @beta */ export function cloneToJSON( - node: TreeNode, + node: TreeNode | TreeLeafValue, options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, ): JsonCompatible; export function cloneToJSON( - node: TreeNode, + node: TreeNode | TreeLeafValue, options?: { handleConverter?(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; @@ -82,36 +87,36 @@ export function cloneToJSON( * @beta */ export function cloneToJSONVerbose( - node: TreeNode, + node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; }, -): VerboseTreeNode; +): VerboseTree; /** * Same as generic overload, except leaves handles as is. * @beta */ export function cloneToJSONVerbose( - node: TreeNode, + node: TreeNode | TreeLeafValue, options?: { readonly handleConverter?: undefined; readonly useStableFieldKeys?: boolean }, -): VerboseTreeNode; +): VerboseTree; export function cloneToJSONVerbose( - node: TreeNode, + node: TreeNode | TreeLeafValue, options?: { handleConverter?(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; }, -): VerboseTreeNode { - // const config = { - // handleConverter(handle: IFluidHandle): T { - // return handle as T; - // }, - // useStableFieldKeys: false, - // ...options, - // }; +): VerboseTree { + const config = { + handleConverter(handle: IFluidHandle): T { + return handle as T; + }, + useStableFieldKeys: false, + ...options, + }; // TODO: this should probably just get a cursor to the underlying data and use that. @@ -138,9 +143,21 @@ export function cloneToJSONVerbose( throw new Error(); } - // function convertNodeOrValue(n: TreeNode | TreeLeafValue): VerboseTree { - // return isTreeNode(n) ? convertNode(n) : isFluidHandle(n) ? config.handleConverter(n) : n; - // } + function convertNodeOrValue(n: TreeNode | TreeLeafValue): VerboseTree { + return isTreeNode(n) ? convertNode(n) : isFluidHandle(n) ? config.handleConverter(n) : n; + } + + return convertNodeOrValue(node); +} - return convertNode(node); +/** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + * @beta + */ +export function cloneToCompressed( + node: TreeNode | TreeLeafValue, +): JsonCompatible { + return fail("TODO"); } diff --git a/packages/dds/tree/src/simple-tree/api/create.ts b/packages/dds/tree/src/simple-tree/api/create.ts index 4dad0dc2247d..82ec1a2d30e6 100644 --- a/packages/dds/tree/src/simple-tree/api/create.ts +++ b/packages/dds/tree/src/simple-tree/api/create.ts @@ -7,7 +7,7 @@ import type { IFluidHandle } from "@fluidframework/core-interfaces"; import { assert } from "@fluidframework/core-utils/internal"; import type { ITreeCursorSynchronous, SchemaAndPolicy } from "../../core/index.js"; -import { fail } from "../../util/index.js"; +import { fail, type JsonCompatible } from "../../util/index.js"; import type { TreeLeafValue, ImplicitFieldSchema, @@ -143,6 +143,19 @@ export function createFromVerbose( return createFromCursor(schema, cursor); } +/** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + * @beta + */ +export function createFromCompressed( + schema: TSchema, + data: JsonCompatible, +): Unhydrated> { + fail("TODO"); +} + /** * Creates an unhydrated simple-tree field from a cursor in nodes mode. */ 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 a98e17fcada1..cf57ae3bddb1 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 @@ -27,28 +27,28 @@ export function clone(original: TreeFieldFr }): TreeFieldFromImplicitField; // @beta -export function cloneToJSON(node: TreeNode, options?: { +export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSON(node: TreeNode, options?: { +export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { handleConverter?: undefined; useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSONVerbose(node: TreeNode, options?: { +export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; -}): VerboseTreeNode; +}): VerboseTree; // @beta -export function cloneToJSONVerbose(node: TreeNode, options?: { +export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { readonly handleConverter?: undefined; readonly useStableFieldKeys?: boolean; -}): VerboseTreeNode; +}): VerboseTree; // @public export enum CommitKind { 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 4812f3d88c60..78c1c273e83f 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 @@ -27,28 +27,28 @@ export function clone(original: TreeFieldFr }): TreeFieldFromImplicitField; // @beta -export function cloneToJSON(node: TreeNode, options?: { +export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSON(node: TreeNode, options?: { +export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { handleConverter?: undefined; useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSONVerbose(node: TreeNode, options?: { +export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; -}): VerboseTreeNode; +}): VerboseTree; // @beta -export function cloneToJSONVerbose(node: TreeNode, options?: { +export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { readonly handleConverter?: undefined; readonly useStableFieldKeys?: boolean; -}): VerboseTreeNode; +}): VerboseTree; // @public export enum CommitKind { From f580a20fd41dd8d1750e2077796ef9192abcd2dc Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:43:16 -0700 Subject: [PATCH 15/54] Clone Progress --- .../flex-tree/flexTreeTypes.ts | 9 ++ packages/dds/tree/src/index.ts | 4 +- .../dds/tree/src/simple-tree/api/clone.ts | 87 ++++++++----------- .../dds/tree/src/simple-tree/api/index.ts | 2 +- .../tree/src/simple-tree/api/treeApiBeta.ts | 6 +- packages/dds/tree/src/simple-tree/index.ts | 4 +- 6 files changed, 51 insertions(+), 61 deletions(-) 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..918efc2677fe 100644 --- a/packages/dds/tree/src/feature-libraries/flex-tree/flexTreeTypes.ts +++ b/packages/dds/tree/src/feature-libraries/flex-tree/flexTreeTypes.ts @@ -8,6 +8,7 @@ import { type ExclusiveMapTree, type FieldKey, type FieldUpPath, + type ITreeCursorSynchronous, type TreeFieldStoredSchema, type TreeNodeSchemaIdentifier, type TreeValue, @@ -200,6 +201,14 @@ export interface FlexTreeNode extends FlexTreeEntity { * If well-formed, it must follow this schema. */ readonly flexSchema: FlexTreeNodeSchema; + + /** + * Get a cursor for the underlying data. + * @remarks + * This cursor might be one the node uses in its implementation, and thus must be returned to its original location before using any other APIs to interact with the tree. + * Must not be held onto across edits or any other tree API use. + */ + borrowCursor(): ITreeCursorSynchronous; } /** diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index 71343563beeb..947364617deb 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -131,8 +131,8 @@ export { type TreeChangeEventsBeta, createFromVerbose, clone, - cloneToJSON, - cloneToJSONVerbose, + cloneToJson, + cloneToVerbose, type VerboseTreeNode, type EncodeOptions, type ParseOptions, diff --git a/packages/dds/tree/src/simple-tree/api/clone.ts b/packages/dds/tree/src/simple-tree/api/clone.ts index a63b7e86aae1..e933ef51a6a4 100644 --- a/packages/dds/tree/src/simple-tree/api/clone.ts +++ b/packages/dds/tree/src/simple-tree/api/clone.ts @@ -11,9 +11,12 @@ import type { TreeFieldFromImplicitField, TreeLeafValue, } from "../schemaTypes.js"; -import { isTreeNode, type TreeNode } from "../core/index.js"; -import type { VerboseTree, VerboseTreeNode } from "./verboseTree.js"; -import { isFluidHandle } from "@fluidframework/runtime-utils"; +import { getKernel, type TreeNode } from "../core/index.js"; +import { verboseFromCursor, type EncodeOptions, type VerboseTree } from "./verboseTree.js"; +import type { ITreeCursorSynchronous } from "../../core/index.js"; +import { cursorFromInsertable } from "./create.js"; +import { tryGetSchema } from "./treeNodeApi.js"; +import { isTreeValue } from "../../feature-libraries/index.js"; /** * Like {@link TreeBeta.create}, except deeply clones existing nodes. @@ -44,7 +47,7 @@ export function clone( * then the returned object will be lossless and compatible with {@link TreeBeta.create} (unless the options are used to customize it). * @beta */ -export function cloneToJSON( +export function cloneToJson( node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; @@ -56,19 +59,32 @@ export function cloneToJSON( * Same as generic overload, except leaves handles as is. * @beta */ -export function cloneToJSON( +export function cloneToJson( node: TreeNode | TreeLeafValue, options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, ): JsonCompatible; -export function cloneToJSON( +export function cloneToJson( node: TreeNode | TreeLeafValue, options?: { handleConverter?(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; }, ): JsonCompatible { - throw new Error(); + const _schema = tryGetSchema(node) ?? fail("invalid input"); + const _cursor = borrowCursorFromTreeNodeOrValue(node); + fail("TODO"); +} + +function borrowCursorFromTreeNodeOrValue( + node: TreeNode | TreeLeafValue, +): ITreeCursorSynchronous { + if (isTreeValue(node)) { + return cursorFromInsertable(tryGetSchema(node) ?? fail("missing schema"), node); + } + const kernel = getKernel(node); + const cursor = kernel.getOrCreateInnerNode().borrowCursor(); + return cursor; } /** @@ -76,7 +92,7 @@ export function cloneToJSON( * Verbose tree format, with explicit type on every node. * * @remarks - * There are several cases this may be preferred to {@link TreeBeta.clone}: + * There are several cases this may be preferred to {@link TreeBeta.cloneToJSON}: * * 1. When not using {@link ITreeConfigurationOptions.preventAmbiguity} (or when using `useStableFieldKeys`), {@link TreeBeta.clone} can produce ambiguous data (the type may be unclear on some nodes). * This may be a good alternative to {@link TreeBeta.clone} since it is lossless. @@ -86,68 +102,33 @@ export function cloneToJSON( * 3. When easy access to the type is desired, or a more uniform simple to parse format is desired. * @beta */ -export function cloneToJSONVerbose( +export function cloneToVerbose( node: TreeNode | TreeLeafValue, - options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; - }, + options: EncodeOptions, ): VerboseTree; /** * Same as generic overload, except leaves handles as is. * @beta */ -export function cloneToJSONVerbose( +export function cloneToVerbose( node: TreeNode | TreeLeafValue, - options?: { readonly handleConverter?: undefined; readonly useStableFieldKeys?: boolean }, + options?: Partial>, ): VerboseTree; -export function cloneToJSONVerbose( +export function cloneToVerbose( node: TreeNode | TreeLeafValue, - options?: { - handleConverter?(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; - }, + options?: Partial>, ): VerboseTree { - const config = { - handleConverter(handle: IFluidHandle): T { + const config: EncodeOptions = { + valueConverter(handle: IFluidHandle): T { return handle as T; }, - useStableFieldKeys: false, ...options, }; - // TODO: this should probably just get a cursor to the underlying data and use that. - - function convertNode(n: TreeNode): VerboseTreeNode { - // let fields: VerboseTreeNode["fields"]; - - // if (n instanceof CustomArrayNodeBase) { - // const x = n as CustomArrayNodeBase; - // fields = Array.from(x, convertNodeOrValue); - // } else if ((n as TreeNode) instanceof CustomMapNodeBase) { - // fields = {}; - // for (const [key, value] of n as CustomMapNodeBase) { - // fields[key] = convertNodeOrValue(value); - // } - // } else { - // fields = {}; - // for (const [key, value] of n as CustomMapNodeBase) { - // fields[key] = convertNodeOrValue(value); - // } - // } - - // return { type: n[typeNameSymbol], fields }; - - throw new Error(); - } - - function convertNodeOrValue(n: TreeNode | TreeLeafValue): VerboseTree { - return isTreeNode(n) ? convertNode(n) : isFluidHandle(n) ? config.handleConverter(n) : n; - } - - return convertNodeOrValue(node); + const cursor = borrowCursorFromTreeNodeOrValue(node); + return verboseFromCursor(cursor, tryGetSchema(node) ?? fail("invalid input"), config); } /** diff --git a/packages/dds/tree/src/simple-tree/api/index.ts b/packages/dds/tree/src/simple-tree/api/index.ts index 65d902122f23..8db89c8851af 100644 --- a/packages/dds/tree/src/simple-tree/api/index.ts +++ b/packages/dds/tree/src/simple-tree/api/index.ts @@ -26,7 +26,7 @@ export { } from "./schemaCreationUtilities.js"; export { treeNodeApi, type TreeNodeApi } from "./treeNodeApi.js"; export { createFromInsertable, cursorFromInsertable, createFromVerbose } from "./create.js"; -export { clone, cloneToJSON, cloneToJSONVerbose } from "./clone.js"; +export { clone, cloneToJson, cloneToVerbose } from "./clone.js"; export type { SimpleTreeSchema } from "./simpleSchema.js"; export { type JsonSchemaId, diff --git a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts index 726cc25d156f..70f1f42ae6b4 100644 --- a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts +++ b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts @@ -12,7 +12,7 @@ import type { } from "../core/index.js"; import { treeNodeApi } from "./treeNodeApi.js"; import { createFromInsertable, createFromVerbose } from "./create.js"; -import { clone, cloneToJSON, cloneToJSONVerbose } from "./clone.js"; +import { clone, cloneToJson, cloneToVerbose } from "./clone.js"; import type { ImplicitFieldSchema, InsertableTreeFieldFromImplicitField, @@ -120,6 +120,6 @@ export const TreeBeta = { }, createFromVerbose, clone, - cloneToJSONVerbose, - cloneToJSON, + cloneToVerbose, + cloneToJson, } as const; diff --git a/packages/dds/tree/src/simple-tree/index.ts b/packages/dds/tree/src/simple-tree/index.ts index 7d3765b9980a..9b5e9c6aacde 100644 --- a/packages/dds/tree/src/simple-tree/index.ts +++ b/packages/dds/tree/src/simple-tree/index.ts @@ -66,8 +66,8 @@ export { getSimpleSchema, type createFromVerbose, type clone, - type cloneToJSON, - type cloneToJSONVerbose, + type cloneToJson, + type cloneToVerbose, type VerboseTreeNode, type EncodeOptions, type ParseOptions, From 543a7f244bdf913465f06fa88b21df1e2006945e Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:53:34 -0700 Subject: [PATCH 16/54] CloneProgress --- .../dds/tree/api-report/tree.alpha.api.md | 26 ++++++------- packages/dds/tree/api-report/tree.beta.api.md | 26 ++++++------- .../flex-map-tree/mapTreeNode.ts | 6 +++ .../feature-libraries/flex-tree/lazyNode.ts | 5 +++ packages/dds/tree/src/index.ts | 2 + .../dds/tree/src/simple-tree/api/clone.ts | 37 +++++++++++++++++-- .../dds/tree/src/simple-tree/api/create.ts | 6 +-- .../dds/tree/src/simple-tree/api/index.ts | 2 +- .../tree/src/simple-tree/api/treeApiBeta.ts | 3 +- packages/dds/tree/src/simple-tree/index.ts | 1 + 10 files changed, 78 insertions(+), 36 deletions(-) diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index b61ca0b9bc60..803159a6a30c 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -20,28 +20,25 @@ export function clone(original: TreeFieldFr }): TreeFieldFromImplicitField; // @beta -export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { +export function cloneToCompressed(node: TreeNode | TreeLeafValue): JsonCompatible; + +// @beta +export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { +export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { handleConverter?: undefined; useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; -}): VerboseTree; +export function cloneToVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; // @beta -export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { - readonly handleConverter?: undefined; - readonly useStableFieldKeys?: boolean; -}): VerboseTree; +export function cloneToVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; // @public export enum CommitKind { @@ -57,10 +54,10 @@ export interface CommitMetadata { } // @beta -export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options: ParseOptions): Unhydrated>; +export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; // @beta -export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options?: Partial>): Unhydrated>; +export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { @@ -543,8 +540,9 @@ export const TreeBeta: { readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; readonly createFromVerbose: typeof createFromVerbose; readonly clone: typeof clone; - readonly cloneToJSONVerbose: typeof cloneToJSONVerbose; - readonly cloneToJSON: typeof cloneToJSON; + readonly cloneToVerbose: typeof cloneToVerbose; + readonly cloneToJson: typeof cloneToJson; + readonly cloneToCompressed: typeof cloneToCompressed; }; // @public @sealed diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index 632b0315fc9d..b837b46a4dd6 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -20,28 +20,25 @@ export function clone(original: TreeFieldFr }): TreeFieldFromImplicitField; // @beta -export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { +export function cloneToCompressed(node: TreeNode | TreeLeafValue): JsonCompatible; + +// @beta +export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { +export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { handleConverter?: undefined; useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; -}): VerboseTree; +export function cloneToVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; // @beta -export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { - readonly handleConverter?: undefined; - readonly useStableFieldKeys?: boolean; -}): VerboseTree; +export function cloneToVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; // @public export enum CommitKind { @@ -57,10 +54,10 @@ export interface CommitMetadata { } // @beta -export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options: ParseOptions): Unhydrated>; +export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; // @beta -export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options?: Partial>): Unhydrated>; +export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { @@ -475,8 +472,9 @@ export const TreeBeta: { readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; readonly createFromVerbose: typeof createFromVerbose; readonly clone: typeof clone; - readonly cloneToJSONVerbose: typeof cloneToJSONVerbose; - readonly cloneToJSON: typeof cloneToJSON; + readonly cloneToVerbose: typeof cloneToVerbose; + readonly cloneToJson: typeof cloneToJson; + readonly cloneToCompressed: typeof cloneToCompressed; }; // @public @sealed diff --git a/packages/dds/tree/src/feature-libraries/flex-map-tree/mapTreeNode.ts b/packages/dds/tree/src/feature-libraries/flex-map-tree/mapTreeNode.ts index 4853db9ae12b..1e2e913715db 100644 --- a/packages/dds/tree/src/feature-libraries/flex-map-tree/mapTreeNode.ts +++ b/packages/dds/tree/src/feature-libraries/flex-map-tree/mapTreeNode.ts @@ -12,6 +12,7 @@ import { type ExclusiveMapTree, type FieldKey, type FieldUpPath, + type ITreeCursorSynchronous, type MapTree, type TreeFieldStoredSchema, type TreeNodeSchemaIdentifier, @@ -47,6 +48,7 @@ import { FieldKinds, type SequenceFieldEditBuilder, } from "../default-schema/index.js"; +import { cursorForMapTreeNode } from "../mapTreeCursor.js"; // #region Nodes @@ -184,6 +186,10 @@ export class EagerMapTreeNode implements MapTreeNode { return (schema as unknown) === this.flexSchema; } + public borrowCursor(): ITreeCursorSynchronous { + return cursorForMapTreeNode(this.mapTree); + } + public tryGetField(key: FieldKey): EagerMapTreeField | undefined { const field = this.mapTree.fields.get(key); // Only return the field if it is not empty, in order to fulfill the contract of `tryGetField`. 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..3351baa255bc 100644 --- a/packages/dds/tree/src/feature-libraries/flex-tree/lazyNode.ts +++ b/packages/dds/tree/src/feature-libraries/flex-tree/lazyNode.ts @@ -10,6 +10,7 @@ import { type AnchorNode, CursorLocationType, type FieldKey, + type ITreeCursorSynchronous, type ITreeSubscriptionCursor, type TreeNavigationResult, type TreeNodeSchemaIdentifier, @@ -100,6 +101,10 @@ export class LazyTreeNode( /** * Construct tree content compatible with a field defined by the provided `schema`. * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. - * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + * @param data - The data used to construct the field content. See `Tree.cloneToVerbose`. * @beta */ export function cloneToCompressed( node: TreeNode | TreeLeafValue, + options: { oldestCompatibleClient: FluidClientVersion }, ): JsonCompatible { + const format = versionToFormat[options.oldestCompatibleClient]; + const codec = makeFieldBatchCodec({ jsonValidator: noopValidator }, format); + const cursor = borrowCursorFromTreeNodeOrValue(node); + const batch: FieldBatch = [cursor]; + const context: FieldBatchEncodingContext = { + encodeType: TreeCompressionStrategy.Compressed, + idCompressor: undefined, + }; + codec.encode(batch, context); return fail("TODO"); } + +export enum FluidClientVersion { + v2_0 = "v2_0", + v2_1 = "v2_1", + v2_2 = "v2_2", + v2_3 = "v2_3", +} + +const versionToFormat = { + v2_0: 1, + v2_1: 1, + v2_2: 1, + v2_3: 1, +}; diff --git a/packages/dds/tree/src/simple-tree/api/create.ts b/packages/dds/tree/src/simple-tree/api/create.ts index 82ec1a2d30e6..755ba7aaaa04 100644 --- a/packages/dds/tree/src/simple-tree/api/create.ts +++ b/packages/dds/tree/src/simple-tree/api/create.ts @@ -111,7 +111,7 @@ export function cursorFromInsertable( */ export function createFromVerbose( schema: TSchema, - data: VerboseTreeNode | undefined, + data: VerboseTree | undefined, options: ParseOptions, ): Unhydrated>; @@ -123,13 +123,13 @@ export function createFromVerbose( */ export function createFromVerbose( schema: TSchema, - data: VerboseTreeNode | undefined, + data: VerboseTree | undefined, options?: Partial>, ): Unhydrated>; export function createFromVerbose( schema: TSchema, - data: VerboseTreeNode | undefined, + data: VerboseTree | undefined, options?: Partial>, ): Unhydrated> { const config: ParseOptions = { diff --git a/packages/dds/tree/src/simple-tree/api/index.ts b/packages/dds/tree/src/simple-tree/api/index.ts index 8db89c8851af..8c36f5b75b91 100644 --- a/packages/dds/tree/src/simple-tree/api/index.ts +++ b/packages/dds/tree/src/simple-tree/api/index.ts @@ -26,7 +26,7 @@ export { } from "./schemaCreationUtilities.js"; export { treeNodeApi, type TreeNodeApi } from "./treeNodeApi.js"; export { createFromInsertable, cursorFromInsertable, createFromVerbose } from "./create.js"; -export { clone, cloneToJson, cloneToVerbose } from "./clone.js"; +export { clone, cloneToJson, cloneToVerbose, cloneToCompressed } from "./clone.js"; export type { SimpleTreeSchema } from "./simpleSchema.js"; export { type JsonSchemaId, diff --git a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts index 70f1f42ae6b4..311d6d10e20c 100644 --- a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts +++ b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts @@ -12,7 +12,7 @@ import type { } from "../core/index.js"; import { treeNodeApi } from "./treeNodeApi.js"; import { createFromInsertable, createFromVerbose } from "./create.js"; -import { clone, cloneToJson, cloneToVerbose } from "./clone.js"; +import { clone, cloneToCompressed, cloneToJson, cloneToVerbose } from "./clone.js"; import type { ImplicitFieldSchema, InsertableTreeFieldFromImplicitField, @@ -122,4 +122,5 @@ export const TreeBeta = { clone, cloneToVerbose, cloneToJson, + cloneToCompressed, } as const; diff --git a/packages/dds/tree/src/simple-tree/index.ts b/packages/dds/tree/src/simple-tree/index.ts index 9b5e9c6aacde..3dacbf186b5a 100644 --- a/packages/dds/tree/src/simple-tree/index.ts +++ b/packages/dds/tree/src/simple-tree/index.ts @@ -68,6 +68,7 @@ export { type clone, type cloneToJson, type cloneToVerbose, + type cloneToCompressed, type VerboseTreeNode, type EncodeOptions, type ParseOptions, From 7444e75951c8d4d12b52c6ab3feef51a70394656 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:43:20 -0700 Subject: [PATCH 17/54] schema export --- .../dds/tree/api-report/tree.alpha.api.md | 20 +++++++++- packages/dds/tree/api-report/tree.beta.api.md | 17 +++++++- packages/dds/tree/src/codec/codec.ts | 10 +++++ packages/dds/tree/src/codec/index.ts | 1 + .../schema-index/schemaSummarizer.ts | 12 +----- packages/dds/tree/src/index.ts | 10 +++-- .../dds/tree/src/simple-tree/api/clone.ts | 39 ++++++++++++------- .../src/simple-tree/api/getStoredSchema.ts | 35 +++++++++++++++++ .../dds/tree/src/simple-tree/api/index.ts | 2 + packages/dds/tree/src/simple-tree/index.ts | 1 + 10 files changed, 119 insertions(+), 28 deletions(-) create mode 100644 packages/dds/tree/src/simple-tree/api/getStoredSchema.ts diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 803159a6a30c..03e1c2d7ce73 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -20,7 +20,10 @@ export function clone(original: TreeFieldFr }): TreeFieldFromImplicitField; // @beta -export function cloneToCompressed(node: TreeNode | TreeLeafValue): JsonCompatible; +export function cloneToCompressed(node: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; +}): JsonCompatible; // @beta export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { @@ -72,6 +75,9 @@ export interface EncodeOptions { // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; +// @alpha +export function extractPersistedSchema(schema: ImplicitFieldSchema): JsonCompatible; + // @public type FieldHasDefault = T extends FieldSchema ? true : false; @@ -130,6 +136,18 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; +// @beta (undocumented) +export enum FluidClientVersion { + // (undocumented) + v2_0 = "v2_0", + // (undocumented) + v2_1 = "v2_1", + // (undocumented) + v2_2 = "v2_2", + // (undocumented) + v2_3 = "v2_3" +} + // @alpha export function getJsonSchema(schema: ImplicitAllowedTypes): JsonTreeSchema; diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index b837b46a4dd6..03be0df2fcdd 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -20,7 +20,10 @@ export function clone(original: TreeFieldFr }): TreeFieldFromImplicitField; // @beta -export function cloneToCompressed(node: TreeNode | TreeLeafValue): JsonCompatible; +export function cloneToCompressed(node: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; +}): JsonCompatible; // @beta export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { @@ -127,6 +130,18 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; +// @beta (undocumented) +export enum FluidClientVersion { + // (undocumented) + v2_0 = "v2_0", + // (undocumented) + v2_1 = "v2_1", + // (undocumented) + v2_2 = "v2_2", + // (undocumented) + v2_3 = "v2_3" +} + // @public export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; diff --git a/packages/dds/tree/src/codec/codec.ts b/packages/dds/tree/src/codec/codec.ts index 9cfc25d34913..c4270a77b175 100644 --- a/packages/dds/tree/src/codec/codec.ts +++ b/packages/dds/tree/src/codec/codec.ts @@ -328,3 +328,13 @@ export function withSchemaValidation< }, }; } + +/** + * @beta + */ +export enum FluidClientVersion { + v2_0 = "v2_0", + v2_1 = "v2_1", + v2_2 = "v2_2", + v2_3 = "v2_3", +} diff --git a/packages/dds/tree/src/codec/index.ts b/packages/dds/tree/src/codec/index.ts index 348cc6f204e4..8a9a81ca0ef6 100644 --- a/packages/dds/tree/src/codec/index.ts +++ b/packages/dds/tree/src/codec/index.ts @@ -18,6 +18,7 @@ export { unitCodec, withDefaultBinaryEncoding, withSchemaValidation, + FluidClientVersion, } from "./codec.js"; export { DiscriminatedUnionDispatcher, diff --git a/packages/dds/tree/src/feature-libraries/schema-index/schemaSummarizer.ts b/packages/dds/tree/src/feature-libraries/schema-index/schemaSummarizer.ts index 192aafdfb2ad..b0f92028a0ea 100644 --- a/packages/dds/tree/src/feature-libraries/schema-index/schemaSummarizer.ts +++ b/packages/dds/tree/src/feature-libraries/schema-index/schemaSummarizer.ts @@ -29,7 +29,6 @@ import type { SummaryElementParser, SummaryElementStringifier, } from "../../shared-tree-core/index.js"; -import type { JsonCompatible } from "../../util/index.js"; import type { CollabWindow } from "../incrementalSummarizationUtils.js"; import { encodeRepo, makeSchemaCodec } from "./codec.js"; @@ -127,18 +126,11 @@ export class SchemaSummarizer implements Summarizable { } /** - * Dumps schema into a deterministic JSON compatible semi-human readable but unspecified format. + * Dumps schema into a deterministic JSON compatible semi-human readable format. * * @remarks * This can be used to help inspect schema for debugging, and to save a snapshot of schema to help detect and review changes to an applications schema. - * - * This format may change across major versions of this package: such changes are considered breaking. - * Beyond that, no compatibility guarantee is provided for this format: it should never be relied upon to load data, it should only be used for comparing outputs from this function. - * @privateRemarks - * This currently uses the schema summary format, but that could be changed to something more human readable (particularly if the encoded format becomes less human readable). - * This intentionally does not leak the format types in the API. - * @internal */ -export function encodeTreeSchema(schema: TreeStoredSchema): JsonCompatible { +export function encodeTreeSchema(schema: TreeStoredSchema): Format { return encodeRepo(schema); } diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index a1daf051ab45..231aee3fc5a8 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -138,7 +138,7 @@ export { type EncodeOptions, type ParseOptions, type VerboseTree, - encodeSchema, + extractPersistedSchema, // Back to normal types type JsonTreeSchema, type JsonSchemaId, @@ -157,8 +157,12 @@ export { } from "./simple-tree/index.js"; export { SharedTree, configuredSharedTree } from "./treeFactory.js"; -export type { ICodecOptions, JsonValidator, SchemaValidationFunction } from "./codec/index.js"; -export { noopValidator } from "./codec/index.js"; +export type { + ICodecOptions, + JsonValidator, + SchemaValidationFunction, +} from "./codec/index.js"; +export { noopValidator, FluidClientVersion } from "./codec/index.js"; export { typeboxValidator } from "./external-utilities/index.js"; export { diff --git a/packages/dds/tree/src/simple-tree/api/clone.ts b/packages/dds/tree/src/simple-tree/api/clone.ts index eedc63bd491c..888e7d140ed0 100644 --- a/packages/dds/tree/src/simple-tree/api/clone.ts +++ b/packages/dds/tree/src/simple-tree/api/clone.ts @@ -17,13 +17,19 @@ import type { ITreeCursorSynchronous } from "../../core/index.js"; import { cursorFromInsertable } from "./create.js"; import { tryGetSchema } from "./treeNodeApi.js"; import { + cursorForMapTreeField, + defaultSchemaPolicy, isTreeValue, makeFieldBatchCodec, + mapTreeFromCursor, TreeCompressionStrategy, type FieldBatch, type FieldBatchEncodingContext, } from "../../feature-libraries/index.js"; -import { noopValidator } from "../../codec/index.js"; +import { noopValidator, type FluidClientVersion } from "../../codec/index.js"; +import type { IIdCompressor } from "@fluidframework/id-compressor"; +import { createIdCompressor } from "@fluidframework/id-compressor/internal"; // TODO: no alpha exports? +import { toStoredSchema } from "../toFlexSchema.js"; /** * Like {@link TreeBeta.create}, except deeply clones existing nodes. @@ -94,6 +100,15 @@ function borrowCursorFromTreeNodeOrValue( return cursor; } +function borrowFieldCursorFromTreeNodeOrValue( + node: TreeNode | TreeLeafValue, +): ITreeCursorSynchronous { + const cursor = borrowCursorFromTreeNodeOrValue(node); + // TODO: avoid copy + const mapTree = mapTreeFromCursor(cursor); + return cursorForMapTreeField([mapTree]); +} + /** * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. * Verbose tree format, with explicit type on every node. @@ -146,25 +161,23 @@ export function cloneToVerbose( */ export function cloneToCompressed( node: TreeNode | TreeLeafValue, - options: { oldestCompatibleClient: FluidClientVersion }, + options: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor }, ): JsonCompatible { + const schema = tryGetSchema(node) ?? fail("invalid input"); const format = versionToFormat[options.oldestCompatibleClient]; const codec = makeFieldBatchCodec({ jsonValidator: noopValidator }, format); - const cursor = borrowCursorFromTreeNodeOrValue(node); + const cursor = borrowFieldCursorFromTreeNodeOrValue(node); const batch: FieldBatch = [cursor]; + // If none provided, create a compressor which will not compress anything (TODO: is this the right way to do that?). + const idCompressor = options.idCompressor ?? createIdCompressor(); const context: FieldBatchEncodingContext = { encodeType: TreeCompressionStrategy.Compressed, - idCompressor: undefined, + idCompressor, + originatorId: idCompressor.localSessionId, // Is this right? If so, why is is needed? + schema: { schema: toStoredSchema(schema), policy: defaultSchemaPolicy }, }; - codec.encode(batch, context); - return fail("TODO"); -} - -export enum FluidClientVersion { - v2_0 = "v2_0", - v2_1 = "v2_1", - v2_2 = "v2_2", - v2_3 = "v2_3", + const result = codec.encode(batch, context); + return result; } const versionToFormat = { diff --git a/packages/dds/tree/src/simple-tree/api/getStoredSchema.ts b/packages/dds/tree/src/simple-tree/api/getStoredSchema.ts new file mode 100644 index 000000000000..c75908cb58cd --- /dev/null +++ b/packages/dds/tree/src/simple-tree/api/getStoredSchema.ts @@ -0,0 +1,35 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { encodeTreeSchema } from "../../feature-libraries/index.js"; +import type { JsonCompatible } from "../../util/index.js"; +import type { ImplicitFieldSchema } from "../schemaTypes.js"; +import { toStoredSchema } from "../toFlexSchema.js"; + +/** + * Dumps the "persisted" schema subset of `schema` into a deterministic JSON compatible semi-human readable but unspecified format. + * + * @remarks + * This can be used to help inspect schema for debugging, and to save a snapshot of schema to help detect and review changes to an applications schema. + * + * This format may change across major versions of this package: such changes are considered breaking. + * Beyond that, no compatibility guarantee is provided for this format: it should never be relied upon to load data, it should only be used for comparing outputs from this function. + * + * This only includes the "persisted" subset of schema information, which means the portion which gets included in documents. + * It thus uses "persisted" keys, see {@link FieldProps.key}. + * + * If two schema have identical "persisted" schema, then they are considered {@link SchemaCompatibilityStatus.isEquivalent|equivalent}. + * + * @privateRemarks + * This currently uses the schema summary format, but that could be changed to something more human readable (particularly if the encoded format becomes less human readable). + * This intentionally does not leak the format types in the API. + * + * Public API surface uses "persisted" terminology while internally we use "stored". + * @alpha + */ +export function extractPersistedSchema(schema: ImplicitFieldSchema): JsonCompatible { + const stored = toStoredSchema(schema); + return encodeTreeSchema(stored); +} diff --git a/packages/dds/tree/src/simple-tree/api/index.ts b/packages/dds/tree/src/simple-tree/api/index.ts index 8c36f5b75b91..9c1e0a3f2511 100644 --- a/packages/dds/tree/src/simple-tree/api/index.ts +++ b/packages/dds/tree/src/simple-tree/api/index.ts @@ -55,6 +55,8 @@ export type { export { TreeBeta, type NodeChangedData, type TreeChangeEventsBeta } from "./treeApiBeta.js"; +export { extractPersistedSchema } from "./getStoredSchema.js"; + // Exporting the schema (RecursiveObject) to test that recursive types are working correctly. // These are `@internal` so they can't be included in the `InternalClassTreeTypes` due to https://github.com/microsoft/rushstack/issues/3639 export { diff --git a/packages/dds/tree/src/simple-tree/index.ts b/packages/dds/tree/src/simple-tree/index.ts index 3dacbf186b5a..7ffcdbd35768 100644 --- a/packages/dds/tree/src/simple-tree/index.ts +++ b/packages/dds/tree/src/simple-tree/index.ts @@ -73,6 +73,7 @@ export { type EncodeOptions, type ParseOptions, type VerboseTree, + extractPersistedSchema, } from "./api/index.js"; export { type NodeFromSchema, From 694e069d1d96a305e1d49513a49a144f6566c2cf Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:47:29 -0700 Subject: [PATCH 18/54] Use createIdCompressor --- packages/dds/tree/src/treeFactory.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/dds/tree/src/treeFactory.ts b/packages/dds/tree/src/treeFactory.ts index 8d488d9ab0c2..5baebcf20f1d 100644 --- a/packages/dds/tree/src/treeFactory.ts +++ b/packages/dds/tree/src/treeFactory.ts @@ -9,7 +9,7 @@ import type { IFluidDataStoreRuntime, IChannelServices, } from "@fluidframework/datastore-definitions/internal"; -import { createAlwaysFinalizedIdCompressor } from "@fluidframework/id-compressor/internal"; +import { createIdCompressor } from "@fluidframework/id-compressor/internal"; import type { SharedObjectKind } from "@fluidframework/shared-object-base"; import { @@ -130,8 +130,7 @@ export function independentView( config: TreeViewConfiguration, options: SharedTreeOptions & { idCompressor?: IIdCompressor | undefined }, ): TreeView { - const idCompressor: IIdCompressor = - options.idCompressor ?? createAlwaysFinalizedIdCompressor(); + const idCompressor: IIdCompressor = options.idCompressor ?? createIdCompressor(); const mintRevisionTag = (): RevisionTag => idCompressor.generateCompressedId(); const revisionTagCodec = new RevisionTagCodec(idCompressor); const schema = new TreeStoredSchemaRepository(); From 6ed4f211ddcbd54d61876c97008d9225b611ba77 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:59:37 -0700 Subject: [PATCH 19/54] revert unneeded id compressor changes --- packages/dds/tree/package.json | 8 +++++ .../feature-libraries/sequence-field/utils.ts | 2 +- .../test/snapshots/snapshotTestScenarios.ts | 2 +- packages/dds/tree/src/test/utils.ts | 7 ++-- packages/runtime/id-compressor/src/index.ts | 1 - .../src/test/idCompressorTestUtilities.ts | 32 +++++++++++++++++++ .../runtime/id-compressor/src/test/index.ts | 2 +- 7 files changed, 45 insertions(+), 9 deletions(-) diff --git a/packages/dds/tree/package.json b/packages/dds/tree/package.json index f00b6f9f7250..49f0b2fd222e 100644 --- a/packages/dds/tree/package.json +++ b/packages/dds/tree/package.json @@ -212,6 +212,14 @@ }, "fluidBuild": { "tasks": { + "build:test:cjs": [ + "...", + "@fluidframework/id-compressor#build:test:cjs" + ], + "build:test:esm": [ + "...", + "@fluidframework/id-compressor#build:test:esm" + ], "ci:build:docs": [ "build:esnext" ] diff --git a/packages/dds/tree/src/test/feature-libraries/sequence-field/utils.ts b/packages/dds/tree/src/test/feature-libraries/sequence-field/utils.ts index aa15ceea0501..cfe4373eb803 100644 --- a/packages/dds/tree/src/test/feature-libraries/sequence-field/utils.ts +++ b/packages/dds/tree/src/test/feature-libraries/sequence-field/utils.ts @@ -6,7 +6,7 @@ import { strict } from "assert"; import { assert } from "@fluidframework/core-utils/internal"; -import { createAlwaysFinalizedIdCompressor } from "@fluidframework/id-compressor/internal"; +import { createAlwaysFinalizedIdCompressor } from "@fluidframework/id-compressor/internal/test-utils"; import { type ChangeAtomId, diff --git a/packages/dds/tree/src/test/snapshots/snapshotTestScenarios.ts b/packages/dds/tree/src/test/snapshots/snapshotTestScenarios.ts index 3101eea829e9..2a39683de34a 100644 --- a/packages/dds/tree/src/test/snapshots/snapshotTestScenarios.ts +++ b/packages/dds/tree/src/test/snapshots/snapshotTestScenarios.ts @@ -5,7 +5,7 @@ import { strict as assert } from "node:assert"; import type { SessionId } from "@fluidframework/id-compressor"; -import { createAlwaysFinalizedIdCompressor } from "@fluidframework/id-compressor/internal"; +import { createAlwaysFinalizedIdCompressor } from "@fluidframework/id-compressor/internal/test-utils"; import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils/internal"; import { typeboxValidator } from "../../external-utilities/index.js"; diff --git a/packages/dds/tree/src/test/utils.ts b/packages/dds/tree/src/test/utils.ts index 7b8291f80715..8b4c03346a7a 100644 --- a/packages/dds/tree/src/test/utils.ts +++ b/packages/dds/tree/src/test/utils.ts @@ -23,11 +23,8 @@ import type { IChannelServices, } from "@fluidframework/datastore-definitions/internal"; import type { SessionId } from "@fluidframework/id-compressor"; -import { - assertIsStableId, - createIdCompressor, - createAlwaysFinalizedIdCompressor, -} from "@fluidframework/id-compressor/internal"; +import { assertIsStableId, createIdCompressor } from "@fluidframework/id-compressor/internal"; +import { createAlwaysFinalizedIdCompressor } from "@fluidframework/id-compressor/internal/test-utils"; import { MockContainerRuntimeFactoryForReconnection, MockFluidDataStoreRuntime, diff --git a/packages/runtime/id-compressor/src/index.ts b/packages/runtime/id-compressor/src/index.ts index f255acdf4b70..d1d9c40d816a 100644 --- a/packages/runtime/id-compressor/src/index.ts +++ b/packages/runtime/id-compressor/src/index.ts @@ -26,4 +26,3 @@ export { StableId, IdCreationRange, } from "./types/index.js"; -export { createAlwaysFinalizedIdCompressor } from "./createAlwaysFinalizedIdCompressor.js"; diff --git a/packages/runtime/id-compressor/src/test/idCompressorTestUtilities.ts b/packages/runtime/id-compressor/src/test/idCompressorTestUtilities.ts index d046a3522806..700eeb9bfcdf 100644 --- a/packages/runtime/id-compressor/src/test/idCompressorTestUtilities.ts +++ b/packages/runtime/id-compressor/src/test/idCompressorTestUtilities.ts @@ -20,6 +20,8 @@ import { ITelemetryBaseLogger } from "@fluidframework/core-interfaces"; import { IdCompressor } from "../idCompressor.js"; import { + type IIdCompressor, + type IIdCompressorCore, IdCreationRange, OpSpaceCompressedId, SerializedIdCompressorWithNoSession, @@ -994,3 +996,33 @@ export function generateCompressedIds( } return ids; } + +/** + * Creates a compressor that only produces final IDs. + * It should only be used for testing purposes. + */ +export function createAlwaysFinalizedIdCompressor( + logger?: ITelemetryBaseLogger, +): IIdCompressor & IIdCompressorCore; +/** + * Creates a compressor that only produces final IDs. + * It should only be used for testing purposes. + */ +export function createAlwaysFinalizedIdCompressor( + sessionId: SessionId, + logger?: ITelemetryBaseLogger, +): IIdCompressor & IIdCompressorCore; +export function createAlwaysFinalizedIdCompressor( + sessionIdOrLogger?: SessionId | ITelemetryBaseLogger, + loggerOrUndefined?: ITelemetryBaseLogger, +): IIdCompressor & IIdCompressorCore { + const compressor = + sessionIdOrLogger === undefined + ? createIdCompressor() + : typeof sessionIdOrLogger === "string" + ? createIdCompressor(sessionIdOrLogger, loggerOrUndefined) + : createIdCompressor(sessionIdOrLogger); + // Permanently put the compressor in a ghost session + (compressor as IdCompressor).startGhostSession(createSessionId()); + return compressor; +} diff --git a/packages/runtime/id-compressor/src/test/index.ts b/packages/runtime/id-compressor/src/test/index.ts index dfaaa8e54caf..8d8dd53612b2 100644 --- a/packages/runtime/id-compressor/src/test/index.ts +++ b/packages/runtime/id-compressor/src/test/index.ts @@ -7,4 +7,4 @@ * Exports for test utilities for `id-compressor` */ -export { createAlwaysFinalizedIdCompressor } from "../index.js"; +export { createAlwaysFinalizedIdCompressor } from "./idCompressorTestUtilities.js"; From a962ad13dc97b142ff5e0d5dc874e14e6e2692cb Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:33:09 -0700 Subject: [PATCH 20/54] Fixup API --- .../dds/tree/api-report/tree.alpha.api.md | 17 ++++++++++++++++ packages/dds/tree/src/index.ts | 1 + packages/dds/tree/src/shared-tree/index.ts | 1 + .../dds/tree/src/shared-tree/sharedTree.ts | 20 ++++++++++++------- packages/dds/tree/src/treeFactory.ts | 7 ++++--- .../api-report/fluid-framework.alpha.api.md | 17 ++++++++++++++++ 6 files changed, 53 insertions(+), 10 deletions(-) diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 1651e1b3125b..fe1da08957d5 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -92,6 +92,18 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; +// @alpha (undocumented) +export interface ForestOptions { + readonly forest?: ForestType; +} + +// @alpha +export enum ForestType { + Expensive = 2, + Optimized = 1, + Reference = 0 +} + // @alpha export function getJsonSchema(schema: ImplicitAllowedTypes): JsonTreeSchema; @@ -101,6 +113,11 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; // @public export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; +// @alpha +export function independentView(config: TreeViewConfiguration, options: ForestOptions & { + idCompressor?: IIdCompressor | undefined; +}): TreeView; + // @public type _InlineTrick = 0; diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index 46e90b44cec7..12dcf0f8fa2b 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -58,6 +58,7 @@ export { type NodeInDocumentConstraint, type RunTransaction, rollback, + type ForestOptions, } from "./shared-tree/index.js"; export { diff --git a/packages/dds/tree/src/shared-tree/index.ts b/packages/dds/tree/src/shared-tree/index.ts index 4bedc09e90c7..37b108085fa4 100644 --- a/packages/dds/tree/src/shared-tree/index.ts +++ b/packages/dds/tree/src/shared-tree/index.ts @@ -14,6 +14,7 @@ export { SharedTreeFormatVersion, buildConfiguredForest, defaultSharedTreeOptions, + type ForestOptions, } from "./sharedTree.js"; export { diff --git a/packages/dds/tree/src/shared-tree/sharedTree.ts b/packages/dds/tree/src/shared-tree/sharedTree.ts index b6c7c6d94400..6f9ebf0695b6 100644 --- a/packages/dds/tree/src/shared-tree/sharedTree.ts +++ b/packages/dds/tree/src/shared-tree/sharedTree.ts @@ -371,12 +371,18 @@ export type SharedTreeFormatVersion = typeof SharedTreeFormatVersion; * @internal */ export type SharedTreeOptions = Partial & - Partial & { - /** - * The {@link ForestType} indicating which forest type should be created for the SharedTree. - */ - forest?: ForestType; - }; + Partial & + ForestOptions; + +/** + * @alpha + */ +export interface ForestOptions { + /** + * The {@link ForestType} indicating which forest type should be created for the SharedTree. + */ + readonly forest?: ForestType; +} /** * Options for configuring the persisted format SharedTree uses. @@ -404,7 +410,7 @@ export interface SharedTreeFormatOptions { /** * Used to distinguish between different forest types. - * @internal + * @alpha */ export enum ForestType { /** diff --git a/packages/dds/tree/src/treeFactory.ts b/packages/dds/tree/src/treeFactory.ts index 5baebcf20f1d..21f00c029714 100644 --- a/packages/dds/tree/src/treeFactory.ts +++ b/packages/dds/tree/src/treeFactory.ts @@ -22,6 +22,7 @@ import { buildConfiguredForest, createTreeCheckout, SharedTree as SharedTreeImpl, + type ForestOptions, type SharedTreeOptions, } from "./shared-tree/index.js"; import type { @@ -118,17 +119,17 @@ export function configuredSharedTree( } /** - * Create a {@link TreeView} that is not tied to any {@link SharedTree} instance. + * Create a {@link TreeView} that is not tied to any {@link ITree} instance. * * @remarks * Such a view can never experience collaboration or be persisted to to a Fluid Container. * - * This can be useful for testing, as well as use-cases like working on local files instead of documents stored in some fluid service. + * This can be useful for testing, as well as use-cases like working on local files instead of documents stored in some Fluid service. * @alpha */ export function independentView( config: TreeViewConfiguration, - options: SharedTreeOptions & { idCompressor?: IIdCompressor | undefined }, + options: ForestOptions & { idCompressor?: IIdCompressor | undefined }, ): TreeView { const idCompressor: IIdCompressor = options.idCompressor ?? createIdCompressor(); const mintRevisionTag = (): RevisionTag => idCompressor.generateCompressedId(); 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 76dd878c1a94..2655d9729cd7 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 @@ -141,6 +141,18 @@ export type FluidObject = { // @public export type FluidObjectProviderKeys = string extends TProp ? never : number extends TProp ? never : TProp extends keyof Required[TProp] ? Required[TProp] extends Required[TProp]>[TProp] ? TProp : never : never; +// @alpha (undocumented) +export interface ForestOptions { + readonly forest?: ForestType; +} + +// @alpha +export enum ForestType { + Expensive = 2, + Optimized = 1, + Reference = 0 +} + // @alpha export function getJsonSchema(schema: ImplicitAllowedTypes): JsonTreeSchema; @@ -403,6 +415,11 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; // @public export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; +// @alpha +export function independentView(config: TreeViewConfiguration, options: ForestOptions & { + idCompressor?: IIdCompressor | undefined; +}): TreeView; + // @public export type InitialObjects = { [K in keyof T["initialObjects"]]: T["initialObjects"][K] extends SharedObjectKind ? TChannel : never; From 2365179301739bb5d4c9f7b6dfebd1bc4b7fefd9 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:34:15 -0700 Subject: [PATCH 21/54] Cleanup missed file --- .../src/createAlwaysFinalizedIdCompressor.ts | 46 ------------------- 1 file changed, 46 deletions(-) delete mode 100644 packages/runtime/id-compressor/src/createAlwaysFinalizedIdCompressor.ts diff --git a/packages/runtime/id-compressor/src/createAlwaysFinalizedIdCompressor.ts b/packages/runtime/id-compressor/src/createAlwaysFinalizedIdCompressor.ts deleted file mode 100644 index bdd7384da9be..000000000000 --- a/packages/runtime/id-compressor/src/createAlwaysFinalizedIdCompressor.ts +++ /dev/null @@ -1,46 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -import type { ITelemetryBaseLogger } from "@fluidframework/core-interfaces"; - -import { createIdCompressor, type IdCompressor } from "./idCompressor.js"; -import type { IIdCompressor, IIdCompressorCore, SessionId } from "./types/index.js"; -import { createSessionId } from "./utilities.js"; - -/** - * Creates a compressor that only produces final IDs. - * @remarks - * This should only be used for testing and synchronous (non-collaborative) purposes. - * @internal - */ -export function createAlwaysFinalizedIdCompressor( - logger?: ITelemetryBaseLogger, -): IIdCompressor & IIdCompressorCore; - -/** - * Creates a compressor that only produces final IDs. - * @remarks - * This should only be used for testing and synchronous (non-collaborative) purposes. - * @internal - */ -export function createAlwaysFinalizedIdCompressor( - sessionId: SessionId, - logger?: ITelemetryBaseLogger, -): IIdCompressor & IIdCompressorCore; - -export function createAlwaysFinalizedIdCompressor( - sessionIdOrLogger?: SessionId | ITelemetryBaseLogger, - loggerOrUndefined?: ITelemetryBaseLogger, -): IIdCompressor & IIdCompressorCore { - const compressor = - sessionIdOrLogger === undefined - ? createIdCompressor() - : typeof sessionIdOrLogger === "string" - ? createIdCompressor(sessionIdOrLogger, loggerOrUndefined) - : createIdCompressor(sessionIdOrLogger); - // Permanently put the compressor in a ghost session - (compressor as IdCompressor).startGhostSession(createSessionId()); - return compressor; -} From 6835ecaa680b695ce8cfe08173219584d72f4d1f Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:37:02 -0700 Subject: [PATCH 22/54] app --- packages/dds/tree/list.combo.json | 29 ++++++++++ packages/dds/tree/list.compressed.json | 16 ++++++ packages/dds/tree/list.json | 4 ++ packages/dds/tree/list.verbose.json | 4 ++ packages/dds/tree/src/test/app.ts | 76 ++++++++++++++++++++++++++ 5 files changed, 129 insertions(+) create mode 100644 packages/dds/tree/list.combo.json create mode 100644 packages/dds/tree/list.compressed.json create mode 100644 packages/dds/tree/list.json create mode 100644 packages/dds/tree/list.verbose.json create mode 100644 packages/dds/tree/src/test/app.ts diff --git a/packages/dds/tree/list.combo.json b/packages/dds/tree/list.combo.json new file mode 100644 index 000000000000..6014c37a0686 --- /dev/null +++ b/packages/dds/tree/list.combo.json @@ -0,0 +1,29 @@ +{ + "tree": { + "version": 1, + "identifiers": [], + "shapes": [ + { + "c": { + "type": "com.fluidframework.example.cli.List", + "value": false, + "fields": [["", 1]] + } + }, + { "a": 2 }, + { "c": { "type": "com.fluidframework.leaf.string", "value": true } } + ], + "data": [[0, ["x", "x", "x", "x", "x", "x", "x"]]] + }, + "schema": { + "version": 1, + "nodes": { + "com.fluidframework.example.cli.List": { + "object": { "": { "kind": "Sequence", "types": ["com.fluidframework.leaf.string"] } } + }, + "com.fluidframework.leaf.string": { "leaf": 1 } + }, + "root": { "kind": "Value", "types": ["com.fluidframework.example.cli.List"] } + }, + "idCompressor": "AAAAAAAAAEAAAAAAAADwPwAAAAAAAPA/AAAAAAAAAACWepTcM1z7sTigkGhCs9QAAAAAAAAAAAAAAAAAAADwPwAAAAAAAAAA" +} diff --git a/packages/dds/tree/list.compressed.json b/packages/dds/tree/list.compressed.json new file mode 100644 index 000000000000..0135f79ea447 --- /dev/null +++ b/packages/dds/tree/list.compressed.json @@ -0,0 +1,16 @@ +{ + "version": 1, + "identifiers": [], + "shapes": [ + { + "c": { + "type": "com.fluidframework.example.cli.List", + "value": false, + "fields": [["", 1]] + } + }, + { "a": 2 }, + { "c": { "type": "com.fluidframework.leaf.string", "value": true } } + ], + "data": [[0, ["x", "x", "x", "x", "x", "x", "x"]]] +} diff --git a/packages/dds/tree/list.json b/packages/dds/tree/list.json new file mode 100644 index 000000000000..7039c2850422 --- /dev/null +++ b/packages/dds/tree/list.json @@ -0,0 +1,4 @@ +{ + "type": "com.fluidframework.example.cli.List", + "fields": ["x", "x", "x", "x", "x", "x", "x"] +} diff --git a/packages/dds/tree/list.verbose.json b/packages/dds/tree/list.verbose.json new file mode 100644 index 000000000000..7039c2850422 --- /dev/null +++ b/packages/dds/tree/list.verbose.json @@ -0,0 +1,4 @@ +{ + "type": "com.fluidframework.example.cli.List", + "fields": ["x", "x", "x", "x", "x", "x", "x"] +} diff --git a/packages/dds/tree/src/test/app.ts b/packages/dds/tree/src/test/app.ts new file mode 100644 index 000000000000..93c60b08789e --- /dev/null +++ b/packages/dds/tree/src/test/app.ts @@ -0,0 +1,76 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { type Static, Type } from "@sinclair/typebox"; + +import { + SchemaFactory, + TreeBeta, + // type InsertableTypedNode, + type JsonCompatible, + type VerboseTree, + extractPersistedSchema, + FluidClientVersion, +} from "../index.js"; +import { readFileSync, writeFileSync } from "node:fs"; +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import { createIdCompressor } from "@fluidframework/id-compressor/internal"; +import { isFluidHandle } from "@fluidframework/runtime-utils"; + +console.log("App"); + +const schemaBuilder = new SchemaFactory("com.fluidframework.example.cli"); +class List extends schemaBuilder.array("List", schemaBuilder.string) {} + +const path = "list.json"; + +const data: JsonCompatible = JSON.parse(readFileSync(path).toString()); + +// const node = TreeBeta.create(List, data as InsertableTypedNode); +const node = TreeBeta.createFromVerbose(List, data as VerboseTree); + +node.insertAtEnd("x"); + +writeFileSync(path, JSON.stringify(TreeBeta.cloneToVerbose(node))); + +// Demo all formats: +writeFileSync("list.verbose.json", JSON.stringify(TreeBeta.cloneToVerbose(node))); +// writeFileSync("list.simple.json", JSON.stringify(TreeBeta.cloneToJson(node))); +writeFileSync( + "list.compressed.json", + JSON.stringify( + TreeBeta.cloneToCompressed(node, { oldestCompatibleClient: FluidClientVersion.v2_3 }), + ), +); + +// Combo + +const File = Type.Object({ + tree: Type.Unsafe>(), + schema: Type.Unsafe(), + idCompressor: Type.Unsafe(), +}); +type File = Static; + +const idCompressor = createIdCompressor(); +// idCompressor.finalizeCreationRange(idCompressor.takeUnfinalizedCreationRange()); + +const file: File = { + tree: TreeBeta.cloneToCompressed(node, { + oldestCompatibleClient: FluidClientVersion.v2_3, + idCompressor, + }), + schema: extractPersistedSchema(List), + idCompressor: idCompressor.serialize(true), +}; + +function rejectHandles(key: string, value: unknown): unknown { + if (isFluidHandle(value)) { + throw new Error("Fluid handles are not supported"); + } + return value; +} + +writeFileSync("list.combo.json", JSON.stringify(file, rejectHandles)); From c138bc4cd1e532596229357ae9089405a9c4189e Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:00:09 -0700 Subject: [PATCH 23/54] end to end --- .../dds/tree/api-report/tree.alpha.api.md | 32 +++++ packages/dds/tree/src/codec/codec.ts | 6 +- packages/dds/tree/src/codec/noopValidator.ts | 2 +- .../external-utilities/typeboxValidator.ts | 2 +- packages/dds/tree/src/index.ts | 8 +- packages/dds/tree/src/test/app.ts | 37 +++++- packages/dds/tree/src/treeFactory.ts | 120 +++++++++++++++++- .../api-report/fluid-framework.alpha.api.md | 76 +++++++++-- .../api-report/fluid-framework.beta.api.md | 41 ++++-- 9 files changed, 284 insertions(+), 40 deletions(-) diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 0f7fec7a4a6a..2bd4255a6435 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -163,12 +163,20 @@ export enum ForestType { // @alpha export function getJsonSchema(schema: ImplicitAllowedTypes): JsonTreeSchema; +// @alpha +export interface ICodecOptions { + readonly jsonValidator: JsonValidator; +} + // @public export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; // @public export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; +// @alpha +export function independentInitializedView(config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent): TreeView; + // @alpha export function independentView(config: TreeViewConfiguration, options: ForestOptions & { idCompressor?: IIdCompressor | undefined; @@ -349,6 +357,11 @@ export type JsonTreeSchema = JsonFieldSchema & { readonly $defs: Record; }; +// @alpha +export interface JsonValidator { + compile(schema: Schema): SchemaValidationFunction; +} + // @public export type LazyItem = Item | (() => Item); @@ -402,6 +415,9 @@ export enum NodeKind { Object = 2 } +// @alpha +export const noopValidator: JsonValidator; + // @public type ObjectFromSchemaRecord> = { -readonly [Property in keyof T]: TreeFieldFromImplicitField; @@ -519,6 +535,12 @@ export class SchemaFactory; } +// @alpha +export interface SchemaValidationFunction { + // (undocumented) + check(data: unknown): data is Static; +} + // @public type ScopedSchemaName = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`; @@ -709,6 +731,9 @@ export interface TreeViewEvents { schemaChanged(): void; } +// @alpha +export const typeboxValidator: JsonValidator; + // @public @deprecated const typeNameSymbol: unique symbol; @@ -743,6 +768,13 @@ export interface VerboseTreeNode { type: string; } +// @alpha (undocumented) +export interface ViewContent { + readonly idCompressor: IIdCompressor; + readonly schema: JsonCompatible; + readonly tree: JsonCompatible; +} + // @public @sealed export interface WithType { // @deprecated diff --git a/packages/dds/tree/src/codec/codec.ts b/packages/dds/tree/src/codec/codec.ts index c4270a77b175..6881e96545db 100644 --- a/packages/dds/tree/src/codec/codec.ts +++ b/packages/dds/tree/src/codec/codec.ts @@ -35,7 +35,7 @@ export interface IDecoder { /** * Validates data complies with some particular schema. * Implementations are typically created by a {@link JsonValidator}. - * @internal + * @alpha */ export interface SchemaValidationFunction { /** @@ -46,7 +46,7 @@ export interface SchemaValidationFunction { /** * JSON schema validator compliant with draft 6 schema. See https://json-schema.org. - * @internal + * @alpha */ export interface JsonValidator { /** @@ -63,7 +63,7 @@ export interface JsonValidator { /** * Options relating to handling of persisted data. - * @internal + * @alpha */ export interface ICodecOptions { /** diff --git a/packages/dds/tree/src/codec/noopValidator.ts b/packages/dds/tree/src/codec/noopValidator.ts index 19df25db9750..7beaaaa4bc16 100644 --- a/packages/dds/tree/src/codec/noopValidator.ts +++ b/packages/dds/tree/src/codec/noopValidator.ts @@ -11,7 +11,7 @@ import type { JsonValidator } from "./codec.js"; * A {@link JsonValidator} implementation which performs no validation and accepts all data as valid. * @privateRemarks Having this as an option unifies opting out of validation with selection of * validators, simplifying code performing validation. - * @internal + * @alpha */ export const noopValidator: JsonValidator = { compile: () => ({ check: (data): data is Static => true }), diff --git a/packages/dds/tree/src/external-utilities/typeboxValidator.ts b/packages/dds/tree/src/external-utilities/typeboxValidator.ts index 1f3676d46338..7a85198baa86 100644 --- a/packages/dds/tree/src/external-utilities/typeboxValidator.ts +++ b/packages/dds/tree/src/external-utilities/typeboxValidator.ts @@ -19,7 +19,7 @@ import type { JsonValidator } from "../codec/index.js"; * * Defining this validator in its own file also helps to ensure it is tree-shakeable. * - * @internal + * @alpha */ export const typeboxValidator: JsonValidator = { compile: (schema: Schema) => { diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index 50de2568916f..a77478bf5258 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -156,7 +156,13 @@ export { type JsonLeafSchemaType, getJsonSchema, } from "./simple-tree/index.js"; -export { SharedTree, configuredSharedTree, independentView } from "./treeFactory.js"; +export { + SharedTree, + configuredSharedTree, + independentView, + independentInitializedView, + type ViewContent, +} from "./treeFactory.js"; export type { ICodecOptions, diff --git a/packages/dds/tree/src/test/app.ts b/packages/dds/tree/src/test/app.ts index 93c60b08789e..a00f3738678b 100644 --- a/packages/dds/tree/src/test/app.ts +++ b/packages/dds/tree/src/test/app.ts @@ -13,10 +13,20 @@ import { type VerboseTree, extractPersistedSchema, FluidClientVersion, + type ViewContent, + independentInitializedView, + TreeViewConfiguration, + type ForestOptions, + type ICodecOptions, + typeboxValidator, } from "../index.js"; import { readFileSync, writeFileSync } from "node:fs"; import type { IFluidHandle } from "@fluidframework/core-interfaces"; -import { createIdCompressor } from "@fluidframework/id-compressor/internal"; +import { + createIdCompressor, + deserializeIdCompressor, + type SerializedIdCompressorWithOngoingSession, +} from "@fluidframework/id-compressor/internal"; import { isFluidHandle } from "@fluidframework/runtime-utils"; console.log("App"); @@ -50,7 +60,7 @@ writeFileSync( const File = Type.Object({ tree: Type.Unsafe>(), schema: Type.Unsafe(), - idCompressor: Type.Unsafe(), + idCompressor: Type.Unsafe(), }); type File = Static; @@ -74,3 +84,26 @@ function rejectHandles(key: string, value: unknown): unknown { } writeFileSync("list.combo.json", JSON.stringify(file, rejectHandles)); + +// deserializeIdCompressor + +const combo: File = JSON.parse(readFileSync("list.combo.json").toString()); + +const content: ViewContent = { + schema: combo.schema, + tree: combo.tree, + idCompressor: deserializeIdCompressor(combo.idCompressor), +}; + +const config = new TreeViewConfiguration({ + schema: List, + enableSchemaValidation: true, + preventAmbiguity: true, +}); + +const options: ForestOptions & ICodecOptions = { jsonValidator: typeboxValidator }; + +const view = independentInitializedView(config, options, content); +const compat = view.compatibility; +const root = view.root; +console.log("Done"); diff --git a/packages/dds/tree/src/treeFactory.ts b/packages/dds/tree/src/treeFactory.ts index 21f00c029714..823b364ca586 100644 --- a/packages/dds/tree/src/treeFactory.ts +++ b/packages/dds/tree/src/treeFactory.ts @@ -10,7 +10,7 @@ import type { IChannelServices, } from "@fluidframework/datastore-definitions/internal"; import { createIdCompressor } from "@fluidframework/id-compressor/internal"; - +import { assert } from "@fluidframework/core-utils/internal"; import type { SharedObjectKind } from "@fluidframework/shared-object-base"; import { type ISharedObjectKind, @@ -34,11 +34,28 @@ import type { import { SchematizingSimpleTreeView, defaultSharedTreeOptions } from "./shared-tree/index.js"; import type { IIdCompressor } from "@fluidframework/id-compressor"; import { + initializeForest, + mapCursorField, RevisionTagCodec, TreeStoredSchemaRepository, + type ITreeCursorSynchronous, type RevisionTag, } from "./core/index.js"; -import { createNodeKeyManager } from "./feature-libraries/index.js"; +import { + chunkTree, + createNodeKeyManager, + defaultChunkPolicy, + defaultSchemaPolicy, + makeFieldBatchCodec, + makeSchemaCodec, + TreeCompressionStrategy, + type FieldBatchEncodingContext, +} from "./feature-libraries/index.js"; +import type { JsonCompatible, JsonCompatibleReadOnly } from "./util/index.js"; +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import type { ICodecOptions } from "./codec/index.js"; +// eslint-disable-next-line import/no-internal-modules +import type { Format } from "./feature-libraries/schema-index/index.js"; /** * A channel factory that creates an {@link ITree}. @@ -119,7 +136,7 @@ export function configuredSharedTree( } /** - * Create a {@link TreeView} that is not tied to any {@link ITree} instance. + * Create an uninitialized {@link TreeView} that is not tied to any {@link ITree} instance. * * @remarks * Such a view can never experience collaboration or be persisted to to a Fluid Container. @@ -144,9 +161,104 @@ export function independentView( forest, schema, }); - return new SchematizingSimpleTreeView( + const out: TreeView = new SchematizingSimpleTreeView( + checkout, + config, + createNodeKeyManager(idCompressor), + ); + return out; +} + +/** + * Create an uninitialized {@link TreeView} that is not tied to any {@link ITree} instance. + * + * @remarks + * Such a view can never experience collaboration or be persisted to to a Fluid Container. + * + * This can be useful for testing, as well as use-cases like working on local files instead of documents stored in some Fluid service. + * @alpha + */ +export function independentInitializedView( + config: TreeViewConfiguration, + options: ForestOptions & ICodecOptions, + content: ViewContent, +): TreeView { + const idCompressor: IIdCompressor = content.idCompressor; + const mintRevisionTag = (): RevisionTag => idCompressor.generateCompressedId(); + const revisionTagCodec = new RevisionTagCodec(idCompressor); + + const schemaCodec = makeSchemaCodec(options); + const fieldBatchCodec = makeFieldBatchCodec(options, 1); + + const schema = new TreeStoredSchemaRepository(schemaCodec.decode(content.schema as Format)); + const forest = buildConfiguredForest( + options.forest ?? defaultSharedTreeOptions.forest, + schema, + idCompressor, + ); + + const context: FieldBatchEncodingContext = { + encodeType: TreeCompressionStrategy.Compressed, + idCompressor, + originatorId: idCompressor.localSessionId, // Is this right? If so, why is is needed? + schema: { schema, policy: defaultSchemaPolicy }, + }; + + const fieldCursors = fieldBatchCodec.decode(content.tree as JsonCompatibleReadOnly, context); + assert(fieldCursors.length === 1, "must have exactly 1 field in batch"); + // Checked above. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const cursors = fieldCursorToNodesCursors(fieldCursors[0]!); + + initializeForest(forest, cursors, revisionTagCodec, idCompressor, false); + + const checkout = createTreeCheckout(idCompressor, mintRevisionTag, revisionTagCodec, { + forest, + schema, + }); + const out: TreeView = new SchematizingSimpleTreeView( checkout, config, createNodeKeyManager(idCompressor), ); + return out; +} + +function fieldCursorToNodesCursors( + fieldCursor: ITreeCursorSynchronous, +): ITreeCursorSynchronous[] { + return mapCursorField(fieldCursor, copyNodeCursor); +} + +/** + * TODO: avoid needing this, or optimize it. + */ +function copyNodeCursor(cursor: ITreeCursorSynchronous): ITreeCursorSynchronous { + const copy = chunkTree(cursor, { + policy: defaultChunkPolicy, + idCompressor: undefined, + }).cursor(); + copy.enterNode(0); + return copy; +} + +/** + * @alpha + */ +export interface ViewContent { + /** + * Compressed tree from {@link TreeBeta.cloneToCompressed}. + * @remarks + * This is an owning reference: + * consumers of this content might modify this data in place (for example when applying edits) to avoid copying. + */ + readonly tree: JsonCompatible; + /** + * Persisted schema from {@link extractPersistedSchema}. + */ + readonly schema: JsonCompatible; + /** + * idCompressor + */ + readonly idCompressor: IIdCompressor; } 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 874e3da24e16..f51243d19195 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 @@ -27,28 +27,28 @@ export function clone(original: TreeFieldFr }): TreeFieldFromImplicitField; // @beta -export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { +export function cloneToCompressed(node: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; +}): JsonCompatible; + +// @beta +export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { +export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { handleConverter?: undefined; useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; -}): VerboseTree; +export function cloneToVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; // @beta -export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { - readonly handleConverter?: undefined; - readonly useStableFieldKeys?: boolean; -}): VerboseTree; +export function cloneToVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; // @public export enum CommitKind { @@ -92,10 +92,10 @@ export interface ContainerSchema { } // @beta -export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options: ParseOptions): Unhydrated>; +export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; // @beta -export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options?: Partial>): Unhydrated>; +export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { @@ -116,6 +116,9 @@ export abstract class ErasedType { // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; +// @alpha +export function extractPersistedSchema(schema: ImplicitFieldSchema): JsonCompatible; + // @public type FieldHasDefault = T extends FieldSchema ? true : false; @@ -174,6 +177,18 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; +// @beta (undocumented) +export enum FluidClientVersion { + // (undocumented) + v2_0 = "v2_0", + // (undocumented) + v2_1 = "v2_1", + // (undocumented) + v2_2 = "v2_2", + // (undocumented) + v2_3 = "v2_3" +} + // @public export type FluidObject = { [P in FluidObjectProviderKeys]?: T[P]; @@ -197,6 +212,11 @@ export enum ForestType { // @alpha export function getJsonSchema(schema: ImplicitAllowedTypes): JsonTreeSchema; +// @alpha +export interface ICodecOptions { + readonly jsonValidator: JsonValidator; +} + // @public export interface IConnection { readonly id: string; @@ -456,6 +476,9 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; // @public export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; +// @alpha +export function independentInitializedView(config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent): TreeView; + // @alpha export function independentView(config: TreeViewConfiguration, options: ForestOptions & { idCompressor?: IIdCompressor | undefined; @@ -671,6 +694,11 @@ export type JsonTreeSchema = JsonFieldSchema & { readonly $defs: Record; }; +// @alpha +export interface JsonValidator { + compile(schema: Schema): SchemaValidationFunction; +} + // @public export type LazyItem = Item | (() => Item); @@ -732,6 +760,9 @@ export enum NodeKind { Object = 2 } +// @alpha +export const noopValidator: JsonValidator; + // @public type ObjectFromSchemaRecord> = { -readonly [Property in keyof T]: TreeFieldFromImplicitField; @@ -854,6 +885,12 @@ export class SchemaFactory; } +// @alpha +export interface SchemaValidationFunction { + // (undocumented) + check(data: unknown): data is Static; +} + // @public type ScopedSchemaName = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`; @@ -932,8 +969,9 @@ export const TreeBeta: { readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; readonly createFromVerbose: typeof createFromVerbose; readonly clone: typeof clone; - readonly cloneToJSONVerbose: typeof cloneToJSONVerbose; - readonly cloneToJSON: typeof cloneToJSON; + readonly cloneToVerbose: typeof cloneToVerbose; + readonly cloneToJson: typeof cloneToJson; + readonly cloneToCompressed: typeof cloneToCompressed; }; // @public @sealed @@ -1065,6 +1103,9 @@ export interface TreeViewEvents { schemaChanged(): void; } +// @alpha +export const typeboxValidator: JsonValidator; + // @public @deprecated const typeNameSymbol: unique symbol; @@ -1099,6 +1140,13 @@ export interface VerboseTreeNode { type: string; } +// @alpha (undocumented) +export interface ViewContent { + readonly idCompressor: IIdCompressor; + readonly schema: JsonCompatible; + readonly tree: JsonCompatible; +} + // @public @sealed export interface WithType { // @deprecated 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 820a827773aa..ebf2551105c2 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 @@ -27,28 +27,28 @@ export function clone(original: TreeFieldFr }): TreeFieldFromImplicitField; // @beta -export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { +export function cloneToCompressed(node: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; +}): JsonCompatible; + +// @beta +export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSON(node: TreeNode | TreeLeafValue, options?: { +export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { handleConverter?: undefined; useStableFieldKeys?: boolean; }): JsonCompatible; // @beta -export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; -}): VerboseTree; +export function cloneToVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; // @beta -export function cloneToJSONVerbose(node: TreeNode | TreeLeafValue, options?: { - readonly handleConverter?: undefined; - readonly useStableFieldKeys?: boolean; -}): VerboseTree; +export function cloneToVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; // @public export enum CommitKind { @@ -92,10 +92,10 @@ export interface ContainerSchema { } // @beta -export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options: ParseOptions): Unhydrated>; +export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; // @beta -export function createFromVerbose(schema: TSchema, data: VerboseTreeNode | undefined, options?: Partial>): Unhydrated>; +export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { @@ -171,6 +171,18 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; +// @beta (undocumented) +export enum FluidClientVersion { + // (undocumented) + v2_0 = "v2_0", + // (undocumented) + v2_1 = "v2_1", + // (undocumented) + v2_2 = "v2_2", + // (undocumented) + v2_3 = "v2_3" +} + // @public export type FluidObject = { [P in FluidObjectProviderKeys]?: T[P]; @@ -847,8 +859,9 @@ export const TreeBeta: { readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; readonly createFromVerbose: typeof createFromVerbose; readonly clone: typeof clone; - readonly cloneToJSONVerbose: typeof cloneToJSONVerbose; - readonly cloneToJSON: typeof cloneToJSON; + readonly cloneToVerbose: typeof cloneToVerbose; + readonly cloneToJson: typeof cloneToJson; + readonly cloneToCompressed: typeof cloneToCompressed; }; // @public @sealed From bd7967d30231963a790021e5c34bb457f612a2b4 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:11:40 -0700 Subject: [PATCH 24/54] exportConcise --- .../dds/tree/api-report/tree.alpha.api.md | 70 ++++++------ packages/dds/tree/api-report/tree.beta.api.md | 70 ++++++------ packages/dds/tree/src/index.ts | 9 +- .../dds/tree/src/simple-tree/api/clone.ts | 37 +++--- .../tree/src/simple-tree/api/conciseTree.ts | 65 +++++++++++ .../dds/tree/src/simple-tree/api/create.ts | 6 +- .../tree/src/simple-tree/api/customTree.ts | 106 ++++++++++++++++++ .../dds/tree/src/simple-tree/api/index.ts | 6 +- .../tree/src/simple-tree/api/treeApiBeta.ts | 45 +++++++- .../tree/src/simple-tree/api/verboseTree.ts | 62 +++------- packages/dds/tree/src/simple-tree/index.ts | 9 +- packages/dds/tree/src/test/app.ts | 12 +- .../src/test/simple-tree/api/create.spec.ts | 4 +- packages/dds/tree/src/treeFactory.ts | 2 +- 14 files changed, 346 insertions(+), 157 deletions(-) create mode 100644 packages/dds/tree/src/simple-tree/api/conciseTree.ts create mode 100644 packages/dds/tree/src/simple-tree/api/customTree.ts diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 2bd4255a6435..783dccbbeb0b 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -19,30 +19,6 @@ export function clone(original: TreeFieldFr replaceIdentifiers?: true; }): TreeFieldFromImplicitField; -// @beta -export function cloneToCompressed(node: TreeNode | TreeLeafValue, options: { - oldestCompatibleClient: FluidClientVersion; - idCompressor?: IIdCompressor; -}): JsonCompatible; - -// @beta -export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; -}): JsonCompatible; - -// @beta -export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { - handleConverter?: undefined; - useStableFieldKeys?: boolean; -}): JsonCompatible; - -// @beta -export function cloneToVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; - -// @beta -export function cloneToVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; - // @public export enum CommitKind { Default = 0, @@ -57,10 +33,9 @@ export interface CommitMetadata { } // @beta -export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; - -// @beta -export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; +export type ConciseTree = Exclude | THandle | ConciseTree[] | { + [key: string]: ConciseTree; +}; // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { @@ -72,6 +47,30 @@ export interface EncodeOptions { valueConverter(data: IFluidHandle): TCustom; } +// @beta +export function exportCompressed(node: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; +}): JsonCompatible; + +// @beta +export function exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; +}): ConciseTree; + +// @beta +export function exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter?: undefined; + useStableFieldKeys?: boolean; +}): JsonCompatible; + +// @beta +export function exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + +// @beta +export function exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -174,6 +173,12 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; // @public export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; +// @beta +export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + +// @beta +export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; + // @alpha export function independentInitializedView(config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent): TreeView; @@ -595,11 +600,12 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>) => () => void; readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; - readonly createFromVerbose: typeof createFromVerbose; + readonly importConcise: (schema: TSchema_1, data: InsertableTreeFieldFromImplicitField | ConciseTree) => TreeFieldFromImplicitField; + readonly importVerbose: typeof importVerbose; readonly clone: typeof clone; - readonly cloneToVerbose: typeof cloneToVerbose; - readonly cloneToJson: typeof cloneToJson; - readonly cloneToCompressed: typeof cloneToCompressed; + readonly exportVerbose: typeof exportVerbose; + readonly exportConcise: typeof exportConcise; + readonly exportCompressed: typeof exportCompressed; }; // @public @sealed diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index 621b4ad4743e..829b1e1e79ac 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -19,30 +19,6 @@ export function clone(original: TreeFieldFr replaceIdentifiers?: true; }): TreeFieldFromImplicitField; -// @beta -export function cloneToCompressed(node: TreeNode | TreeLeafValue, options: { - oldestCompatibleClient: FluidClientVersion; - idCompressor?: IIdCompressor; -}): JsonCompatible; - -// @beta -export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; -}): JsonCompatible; - -// @beta -export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { - handleConverter?: undefined; - useStableFieldKeys?: boolean; -}): JsonCompatible; - -// @beta -export function cloneToVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; - -// @beta -export function cloneToVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; - // @public export enum CommitKind { Default = 0, @@ -57,10 +33,9 @@ export interface CommitMetadata { } // @beta -export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; - -// @beta -export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; +export type ConciseTree = Exclude | THandle | ConciseTree[] | { + [key: string]: ConciseTree; +}; // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { @@ -72,6 +47,30 @@ export interface EncodeOptions { valueConverter(data: IFluidHandle): TCustom; } +// @beta +export function exportCompressed(node: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; +}): JsonCompatible; + +// @beta +export function exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; +}): ConciseTree; + +// @beta +export function exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter?: undefined; + useStableFieldKeys?: boolean; +}): JsonCompatible; + +// @beta +export function exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + +// @beta +export function exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -148,6 +147,12 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; // @public export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; +// @beta +export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + +// @beta +export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; + // @public type _InlineTrick = 0; @@ -485,11 +490,12 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>) => () => void; readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; - readonly createFromVerbose: typeof createFromVerbose; + readonly importConcise: (schema: TSchema_1, data: InsertableTreeFieldFromImplicitField | ConciseTree) => TreeFieldFromImplicitField; + readonly importVerbose: typeof importVerbose; readonly clone: typeof clone; - readonly cloneToVerbose: typeof cloneToVerbose; - readonly cloneToJson: typeof cloneToJson; - readonly cloneToCompressed: typeof cloneToCompressed; + readonly exportVerbose: typeof exportVerbose; + readonly exportConcise: typeof exportConcise; + readonly exportCompressed: typeof exportCompressed; }; // @public @sealed diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index a77478bf5258..30d6ca56c79a 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -130,11 +130,11 @@ export { // Beta APIs TreeBeta, type TreeChangeEventsBeta, - createFromVerbose, + importVerbose, clone, - cloneToJson, - cloneToVerbose, - cloneToCompressed, + exportConcise, + exportVerbose, + exportCompressed, type VerboseTreeNode, type EncodeOptions, type ParseOptions, @@ -155,6 +155,7 @@ export { type JsonSchemaType, type JsonLeafSchemaType, getJsonSchema, + type ConciseTree, } from "./simple-tree/index.js"; export { SharedTree, diff --git a/packages/dds/tree/src/simple-tree/api/clone.ts b/packages/dds/tree/src/simple-tree/api/clone.ts index 888e7d140ed0..2b5bac0198f9 100644 --- a/packages/dds/tree/src/simple-tree/api/clone.ts +++ b/packages/dds/tree/src/simple-tree/api/clone.ts @@ -30,6 +30,7 @@ import { noopValidator, type FluidClientVersion } from "../../codec/index.js"; import type { IIdCompressor } from "@fluidframework/id-compressor"; import { createIdCompressor } from "@fluidframework/id-compressor/internal"; // TODO: no alpha exports? import { toStoredSchema } from "../toFlexSchema.js"; +import { conciseFromCursor, type ConciseTree } from "./conciseTree.js"; /** * Like {@link TreeBeta.create}, except deeply clones existing nodes. @@ -53,40 +54,42 @@ export function clone( } /** - * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. - * - * @remarks - * If the schema is compatible with {@link ITreeConfigurationOptions.preventAmbiguity}, - * then the returned object will be lossless and compatible with {@link TreeBeta.create} (unless the options are used to customize it). + * Copy a snapshot of the current version of a TreeNode into a {@link ConciseTree}. * @beta */ -export function cloneToJson( +export function exportConcise( node: TreeNode | TreeLeafValue, options?: { handleConverter(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; }, -): JsonCompatible; +): ConciseTree; /** * Same as generic overload, except leaves handles as is. * @beta */ -export function cloneToJson( +export function exportConcise( node: TreeNode | TreeLeafValue, options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, ): JsonCompatible; -export function cloneToJson( +export function exportConcise( node: TreeNode | TreeLeafValue, options?: { handleConverter?(handle: IFluidHandle): T; readonly useStableFieldKeys?: boolean; }, ): JsonCompatible { - const _schema = tryGetSchema(node) ?? fail("invalid input"); - const _cursor = borrowCursorFromTreeNodeOrValue(node); - fail("TODO"); + const config: EncodeOptions = { + valueConverter(handle: IFluidHandle): T { + return handle as T; + }, + ...options, + }; + + const cursor = borrowCursorFromTreeNodeOrValue(node); + return conciseFromCursor(cursor, tryGetSchema(node) ?? fail("invalid input"), config); } function borrowCursorFromTreeNodeOrValue( @@ -114,7 +117,7 @@ function borrowFieldCursorFromTreeNodeOrValue( * Verbose tree format, with explicit type on every node. * * @remarks - * There are several cases this may be preferred to {@link TreeBeta.cloneToJson}: + * There are several cases this may be preferred to {@link TreeBeta.exportConcise}: * * 1. When not using {@link ITreeConfigurationOptions.preventAmbiguity} (or when using `useStableFieldKeys`), {@link TreeBeta.clone} can produce ambiguous data (the type may be unclear on some nodes). * This may be a good alternative to {@link TreeBeta.clone} since it is lossless. @@ -124,7 +127,7 @@ function borrowFieldCursorFromTreeNodeOrValue( * 3. When easy access to the type is desired, or a more uniform simple to parse format is desired. * @beta */ -export function cloneToVerbose( +export function exportVerbose( node: TreeNode | TreeLeafValue, options: EncodeOptions, ): VerboseTree; @@ -133,12 +136,12 @@ export function cloneToVerbose( * Same as generic overload, except leaves handles as is. * @beta */ -export function cloneToVerbose( +export function exportVerbose( node: TreeNode | TreeLeafValue, options?: Partial>, ): VerboseTree; -export function cloneToVerbose( +export function exportVerbose( node: TreeNode | TreeLeafValue, options?: Partial>, ): VerboseTree { @@ -159,7 +162,7 @@ export function cloneToVerbose( * @param data - The data used to construct the field content. See `Tree.cloneToVerbose`. * @beta */ -export function cloneToCompressed( +export function exportCompressed( node: TreeNode | TreeLeafValue, options: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor }, ): JsonCompatible { diff --git a/packages/dds/tree/src/simple-tree/api/conciseTree.ts b/packages/dds/tree/src/simple-tree/api/conciseTree.ts new file mode 100644 index 000000000000..af52d8a05f2d --- /dev/null +++ b/packages/dds/tree/src/simple-tree/api/conciseTree.ts @@ -0,0 +1,65 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import type { IFluidHandle } from "@fluidframework/core-interfaces"; + +import type { ITreeCursor } from "../../core/index.js"; +import type { TreeLeafValue, ImplicitAllowedTypes } from "../schemaTypes.js"; +import type { TreeNodeSchema } from "../core/index.js"; +import { walkFieldSchema } from "../walkSchema.js"; +import { customFromCursorInner, type EncodeOptions } from "./customTree.js"; + +/** + * Concise encoding of a {@link TreeNode} or {@link TreeLeafValue}. + * @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). + * + * Every {@link TreeNode} is an array or object. + * Any IFluidHandle values have been replaced by `THandle`. + * @privateRemarks + * This can store all possible simple trees, + * but it can not store all possible trees representable by our internal representations like FlexTree and JsonableTree. + * @beta + */ +export type ConciseTree = + | Exclude + | THandle + | ConciseTree[] + | { + [key: string]: ConciseTree; + }; + +/** + * Used to read a node cursor as a ConciseTree. + */ +export function conciseFromCursor( + reader: ITreeCursor, + rootSchema: ImplicitAllowedTypes, + options: EncodeOptions, +): ConciseTree { + const config: Required> = { + useStoredKeys: false, + ...options, + }; + + const schemaMap = new Map(); + walkFieldSchema(rootSchema, { + node(schema) { + schemaMap.set(schema.identifier, schema); + }, + }); + + return conciseFromCursorInner(reader, config, schemaMap); +} + +function conciseFromCursorInner( + reader: ITreeCursor, + options: Required>, + schema: ReadonlyMap, +): ConciseTree { + return customFromCursorInner(reader, options, schema, conciseFromCursorInner); +} diff --git a/packages/dds/tree/src/simple-tree/api/create.ts b/packages/dds/tree/src/simple-tree/api/create.ts index 755ba7aaaa04..9fcf5a221a13 100644 --- a/packages/dds/tree/src/simple-tree/api/create.ts +++ b/packages/dds/tree/src/simple-tree/api/create.ts @@ -109,7 +109,7 @@ export function cursorFromInsertable( * This could be exposed as a public `Tree.createFromVerbose` function. * @beta */ -export function createFromVerbose( +export function importVerbose( schema: TSchema, data: VerboseTree | undefined, options: ParseOptions, @@ -121,13 +121,13 @@ export function createFromVerbose( * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. * @beta */ -export function createFromVerbose( +export function importVerbose( schema: TSchema, data: VerboseTree | undefined, options?: Partial>, ): Unhydrated>; -export function createFromVerbose( +export function importVerbose( schema: TSchema, data: VerboseTree | undefined, options?: Partial>, diff --git a/packages/dds/tree/src/simple-tree/api/customTree.ts b/packages/dds/tree/src/simple-tree/api/customTree.ts new file mode 100644 index 000000000000..acfd5d332ab2 --- /dev/null +++ b/packages/dds/tree/src/simple-tree/api/customTree.ts @@ -0,0 +1,106 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import { isFluidHandle } from "@fluidframework/runtime-utils/internal"; +import { assert } from "@fluidframework/core-utils/internal"; + +import { + EmptyKey, + forEachField, + inCursorField, + mapCursorField, + type ITreeCursor, +} from "../../core/index.js"; +import { fail } from "../../util/index.js"; +import type { TreeLeafValue } from "../schemaTypes.js"; +import { NodeKind, type TreeNodeSchema } from "../core/index.js"; +import { + booleanSchema, + handleSchema, + nullSchema, + numberSchema, + stringSchema, +} from "../leafNodeSchema.js"; +import { isObjectNodeSchema } from "../objectNodeTypes.js"; + +/** + * Options for how to interpret a `ConciseTree` without relying on schema. + * @beta + */ +export interface EncodeOptions { + /** + * Fixup custom input formats. + * @remarks + * See note on {@link ParseOptions.valueConverter}. + */ + valueConverter(data: IFluidHandle): TCustom; + /** + * If true, interpret the input keys of object nodes as stored keys. + * If false, interpret them as property keys. + * @defaultValue false. + */ + readonly useStoredKeys?: boolean; +} + +export type CustomTree = CustomTreeNode | CustomTreeValue; +export type CustomTreeValue = Exclude | THandle; +export type CustomTreeNode = TChild[] | { [key: string]: TChild }; + +export function customFromCursorInner( + reader: ITreeCursor, + options: Required>, + schema: ReadonlyMap, + childHandler: ( + reader: ITreeCursor, + options: Required>, + schema: ReadonlyMap, + ) => TChild, +): CustomTree { + const type = reader.type; + const nodeSchema = schema.get(type) ?? fail("missing schema for type in cursor"); + + switch (type) { + case numberSchema.identifier: + case booleanSchema.identifier: + case nullSchema.identifier: + case stringSchema.identifier: + assert(reader.value !== undefined, "out of schema: missing value"); + assert(!isFluidHandle(reader.value), "out of schema: unexpected FluidHandle"); + return reader.value; + case handleSchema.identifier: + assert(reader.value !== undefined, "out of schema: missing value"); + assert(isFluidHandle(reader.value), "out of schema: unexpected FluidHandle"); + return options.valueConverter(reader.value); + default: { + assert(reader.value === undefined, "out of schema: unexpected value"); + if (nodeSchema.kind === NodeKind.Array) { + const fields = inCursorField(reader, EmptyKey, () => + mapCursorField(reader, () => childHandler(reader, options, schema)), + ); + return fields; + } else { + const fields: Record = {}; + forEachField(reader, () => { + const children = mapCursorField(reader, () => childHandler(reader, options, schema)); + if (children.length === 1) { + const storedKey = reader.getFieldKey(); + const key = + isObjectNodeSchema(nodeSchema) && !options.useStoredKeys + ? nodeSchema.storedKeyToPropertyKey.get(storedKey) ?? + fail("missing property key") + : storedKey; + // Length is checked above. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + fields[key] = children[0]!; + } else { + assert(children.length === 0, 0xa19 /* invalid children number */); + } + }); + return fields; + } + } + } +} diff --git a/packages/dds/tree/src/simple-tree/api/index.ts b/packages/dds/tree/src/simple-tree/api/index.ts index 9c1e0a3f2511..366efa3fd43b 100644 --- a/packages/dds/tree/src/simple-tree/api/index.ts +++ b/packages/dds/tree/src/simple-tree/api/index.ts @@ -25,8 +25,8 @@ export { type EmptyObject, } from "./schemaCreationUtilities.js"; export { treeNodeApi, type TreeNodeApi } from "./treeNodeApi.js"; -export { createFromInsertable, cursorFromInsertable, createFromVerbose } from "./create.js"; -export { clone, cloneToJson, cloneToVerbose, cloneToCompressed } from "./clone.js"; +export { createFromInsertable, cursorFromInsertable, importVerbose } from "./create.js"; +export { clone, exportConcise, exportVerbose, exportCompressed } from "./clone.js"; export type { SimpleTreeSchema } from "./simpleSchema.js"; export { type JsonSchemaId, @@ -53,6 +53,8 @@ export type { VerboseTree, } from "./verboseTree.js"; +export type { ConciseTree } from "./conciseTree.js"; + export { TreeBeta, type NodeChangedData, type TreeChangeEventsBeta } from "./treeApiBeta.js"; export { extractPersistedSchema } from "./getStoredSchema.js"; diff --git a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts index 311d6d10e20c..1a8e80996757 100644 --- a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts +++ b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts @@ -11,13 +11,14 @@ import type { WithType, } from "../core/index.js"; import { treeNodeApi } from "./treeNodeApi.js"; -import { createFromInsertable, createFromVerbose } from "./create.js"; -import { clone, cloneToCompressed, cloneToJson, cloneToVerbose } from "./clone.js"; +import { createFromInsertable, importVerbose } from "./create.js"; +import { clone, exportCompressed, exportConcise, exportVerbose } from "./clone.js"; import type { ImplicitFieldSchema, InsertableTreeFieldFromImplicitField, TreeFieldFromImplicitField, } from "../schemaTypes.js"; +import type { ConciseTree } from "./conciseTree.js"; /** * Data included for {@link TreeChangeEventsBeta.nodeChanged}. @@ -112,15 +113,47 @@ export const TreeBeta = { return treeNodeApi.on(node, eventName, listener); }, + /** + * Generic tree constructor. + * @remarks + * This is equivalent to calling {@link TreeNodeSchemaClass}'s constructor or `TreeNodeSchemaNonClass.create`, + * except that this also handles the case where the root is polymorphic, or optional. + * + * Documented (and thus recoverable) error handling/reporting for this is not yet implemented, + * but for now most invalid inputs will throw a recoverable error. + */ create( schema: TSchema, data: InsertableTreeFieldFromImplicitField, ): Unhydrated> { return createFromInsertable(schema, data); }, - createFromVerbose, + + /** + * Less type safe version of {@link TreeBeta.create}, suitable for importing data. + * @remarks + * Due to {@link ConciseTree} relying on type inference from the data, its use is somewhat limited. + * This does not support {@link ConciseTree}'s with customized handle encodings or using persisted keys. + * Use "compressed" or "verbose" formats to for more flexibility. + * + * When using this function, + * it is recommend to ensure you schema is unambiguous with {@link ITreeConfigurationOptions.preventAmbiguity}. + * If the schema is ambiguous, consider using {@link TreeBeta.create} and {@link Unhydrated} nodes where needed, + * or using {@link TreeBeta.importVerbose} and specify all types. + * + * Documented (and thus recoverable) error handling/reporting for this is not yet implemented, + * but for now most invalid inputs will throw a recoverable error. + */ + importConcise( + schema: TSchema, + data: InsertableTreeFieldFromImplicitField | ConciseTree, + ): Unhydrated> { + return createFromInsertable(schema, data as InsertableTreeFieldFromImplicitField); + }, + + importVerbose, clone, - cloneToVerbose, - cloneToJson, - cloneToCompressed, + exportVerbose, + exportConcise, + exportCompressed, } as const; diff --git a/packages/dds/tree/src/simple-tree/api/verboseTree.ts b/packages/dds/tree/src/simple-tree/api/verboseTree.ts index fa5cff753c95..88b3c10f0228 100644 --- a/packages/dds/tree/src/simple-tree/api/verboseTree.ts +++ b/packages/dds/tree/src/simple-tree/api/verboseTree.ts @@ -10,10 +10,7 @@ import { assert } from "@fluidframework/core-utils/internal"; import { aboveRootPlaceholder, EmptyKey, - forEachField, - inCursorField, keyAsDetachedField, - mapCursorField, type FieldKey, type ITreeCursor, type ITreeCursorSynchronous, @@ -42,6 +39,11 @@ import { import { toFlexSchema } from "../toFlexSchema.js"; import { isObjectNodeSchema } from "../objectNodeTypes.js"; import { walkFieldSchema } from "../walkSchema.js"; +import { + customFromCursorInner, + type CustomTreeNode, + type CustomTreeValue, +} from "./customTree.js"; /** * Verbose encoding of a {@link TreeNode} or {@link TreeLeafValue}. @@ -372,50 +374,14 @@ function verboseFromCursorInner( options: Required>, schema: ReadonlyMap, ): VerboseTree { - const type = reader.type; - const nodeSchema = schema.get(type) ?? fail("missing schema for type in cursor"); - - switch (type) { - case numberSchema.identifier: - case booleanSchema.identifier: - case nullSchema.identifier: - case stringSchema.identifier: - assert(reader.value !== undefined, 0xa14 /* out of schema: missing value */); - assert(!isFluidHandle(reader.value), 0xa15 /* out of schema: unexpected FluidHandle */); - return reader.value; - case handleSchema.identifier: - assert(reader.value !== undefined, 0xa16 /* out of schema: missing value */); - assert(isFluidHandle(reader.value), 0xa17 /* out of schema: unexpected FluidHandle */); - return options.valueConverter(reader.value); - default: { - assert(reader.value === undefined, 0xa18 /* out of schema: unexpected value */); - if (nodeSchema.kind === NodeKind.Array) { - const fields = inCursorField(reader, EmptyKey, () => - mapCursorField(reader, () => verboseFromCursorInner(reader, options, schema)), - ); - return { type, fields }; - } else { - const fields: Record> = {}; - forEachField(reader, () => { - const children = mapCursorField(reader, () => - verboseFromCursorInner(reader, options, schema), - ); - if (children.length === 1) { - const storedKey = reader.getFieldKey(); - const key = - isObjectNodeSchema(nodeSchema) && !options.useStoredKeys - ? nodeSchema.storedKeyToPropertyKey.get(storedKey) ?? - fail("missing property key") - : storedKey; - // Length is checked above. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - fields[key] = children[0]!; - } else { - assert(children.length === 0, 0xa19 /* invalid children number */); - } - }); - return { type, fields }; - } - } + const fields = customFromCursorInner(reader, options, schema, verboseFromCursorInner); + const nodeSchema = schema.get(reader.type) ?? fail("missing schema for type in cursor"); + if (nodeSchema.kind === NodeKind.Leaf) { + return fields as CustomTreeValue; } + + return { + type: reader.type, + fields: fields as CustomTreeNode, + }; } diff --git a/packages/dds/tree/src/simple-tree/index.ts b/packages/dds/tree/src/simple-tree/index.ts index 7ffcdbd35768..1d82920ad465 100644 --- a/packages/dds/tree/src/simple-tree/index.ts +++ b/packages/dds/tree/src/simple-tree/index.ts @@ -64,16 +64,17 @@ export { type JsonLeafSchemaType, getJsonSchema, getSimpleSchema, - type createFromVerbose, + type importVerbose, type clone, - type cloneToJson, - type cloneToVerbose, - type cloneToCompressed, + type exportConcise, + type exportVerbose, + type exportCompressed, type VerboseTreeNode, type EncodeOptions, type ParseOptions, type VerboseTree, extractPersistedSchema, + type ConciseTree, } from "./api/index.js"; export { type NodeFromSchema, diff --git a/packages/dds/tree/src/test/app.ts b/packages/dds/tree/src/test/app.ts index a00f3738678b..c2d195534153 100644 --- a/packages/dds/tree/src/test/app.ts +++ b/packages/dds/tree/src/test/app.ts @@ -39,19 +39,19 @@ const path = "list.json"; const data: JsonCompatible = JSON.parse(readFileSync(path).toString()); // const node = TreeBeta.create(List, data as InsertableTypedNode); -const node = TreeBeta.createFromVerbose(List, data as VerboseTree); +const node = TreeBeta.importVerbose(List, data as VerboseTree); node.insertAtEnd("x"); -writeFileSync(path, JSON.stringify(TreeBeta.cloneToVerbose(node))); +writeFileSync(path, JSON.stringify(TreeBeta.exportVerbose(node))); // Demo all formats: -writeFileSync("list.verbose.json", JSON.stringify(TreeBeta.cloneToVerbose(node))); -// writeFileSync("list.simple.json", JSON.stringify(TreeBeta.cloneToJson(node))); +writeFileSync("list.verbose.json", JSON.stringify(TreeBeta.exportVerbose(node))); +// writeFileSync("list.simple.json", JSON.stringify(TreeBeta.exportConcise(node))); writeFileSync( "list.compressed.json", JSON.stringify( - TreeBeta.cloneToCompressed(node, { oldestCompatibleClient: FluidClientVersion.v2_3 }), + TreeBeta.exportCompressed(node, { oldestCompatibleClient: FluidClientVersion.v2_3 }), ), ); @@ -68,7 +68,7 @@ const idCompressor = createIdCompressor(); // idCompressor.finalizeCreationRange(idCompressor.takeUnfinalizedCreationRange()); const file: File = { - tree: TreeBeta.cloneToCompressed(node, { + tree: TreeBeta.exportCompressed(node, { oldestCompatibleClient: FluidClientVersion.v2_3, idCompressor, }), diff --git a/packages/dds/tree/src/test/simple-tree/api/create.spec.ts b/packages/dds/tree/src/test/simple-tree/api/create.spec.ts index d18262c94418..7ff990445b3e 100644 --- a/packages/dds/tree/src/test/simple-tree/api/create.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/create.spec.ts @@ -7,7 +7,7 @@ import { strict as assert } from "node:assert"; import { createFromInsertable, SchemaFactory } from "../../../simple-tree/index.js"; // eslint-disable-next-line import/no-internal-modules -import { createFromVerbose } from "../../../simple-tree/api/create.js"; +import { importVerbose } from "../../../simple-tree/api/create.js"; const schema = new SchemaFactory("com.example"); @@ -25,7 +25,7 @@ describe("simple-tree create", () => { }); it("createFromVerbose", () => { - const canvas1 = createFromVerbose(Canvas, { + const canvas1 = importVerbose(Canvas, { type: Canvas.identifier, fields: { stuff: { type: NodeList.identifier, fields: [] } }, }); diff --git a/packages/dds/tree/src/treeFactory.ts b/packages/dds/tree/src/treeFactory.ts index 823b364ca586..2d9fd3c5b1ae 100644 --- a/packages/dds/tree/src/treeFactory.ts +++ b/packages/dds/tree/src/treeFactory.ts @@ -247,7 +247,7 @@ function copyNodeCursor(cursor: ITreeCursorSynchronous): ITreeCursorSynchronous */ export interface ViewContent { /** - * Compressed tree from {@link TreeBeta.cloneToCompressed}. + * Compressed tree from {@link TreeBeta.exportCompressed}. * @remarks * This is an owning reference: * consumers of this content might modify this data in place (for example when applying edits) to avoid copying. From aa27b6f383654c30030cbfe9bb88f3bff01bdc76 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:17:53 -0700 Subject: [PATCH 25/54] update API reports --- .../api-report/fluid-framework.alpha.api.md | 74 ++++++++++--------- .../api-report/fluid-framework.beta.api.md | 74 ++++++++++--------- 2 files changed, 80 insertions(+), 68 deletions(-) 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 f51243d19195..307c9a553bcd 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 @@ -26,30 +26,6 @@ export function clone(original: TreeFieldFr replaceIdentifiers?: true; }): TreeFieldFromImplicitField; -// @beta -export function cloneToCompressed(node: TreeNode | TreeLeafValue, options: { - oldestCompatibleClient: FluidClientVersion; - idCompressor?: IIdCompressor; -}): JsonCompatible; - -// @beta -export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; -}): JsonCompatible; - -// @beta -export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { - handleConverter?: undefined; - useStableFieldKeys?: boolean; -}): JsonCompatible; - -// @beta -export function cloneToVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; - -// @beta -export function cloneToVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; - // @public export enum CommitKind { Default = 0, @@ -63,6 +39,11 @@ export interface CommitMetadata { readonly kind: CommitKind; } +// @beta +export type ConciseTree = Exclude | THandle | ConciseTree[] | { + [key: string]: ConciseTree; +}; + // @public export enum ConnectionState { CatchingUp = 1, @@ -91,12 +72,6 @@ export interface ContainerSchema { readonly initialObjects: Record; } -// @beta -export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; - -// @beta -export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } @@ -113,6 +88,30 @@ export abstract class ErasedType { protected abstract brand(dummy: never): Name; } +// @beta +export function exportCompressed(node: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; +}): JsonCompatible; + +// @beta +export function exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; +}): ConciseTree; + +// @beta +export function exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter?: undefined; + useStableFieldKeys?: boolean; +}): JsonCompatible; + +// @beta +export function exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + +// @beta +export function exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -476,6 +475,12 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; // @public export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; +// @beta +export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + +// @beta +export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; + // @alpha export function independentInitializedView(config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent): TreeView; @@ -967,11 +972,12 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>) => () => void; readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; - readonly createFromVerbose: typeof createFromVerbose; + readonly importConcise: (schema: TSchema_1, data: InsertableTreeFieldFromImplicitField | ConciseTree) => TreeFieldFromImplicitField; + readonly importVerbose: typeof importVerbose; readonly clone: typeof clone; - readonly cloneToVerbose: typeof cloneToVerbose; - readonly cloneToJson: typeof cloneToJson; - readonly cloneToCompressed: typeof cloneToCompressed; + readonly exportVerbose: typeof exportVerbose; + readonly exportConcise: typeof exportConcise; + readonly exportCompressed: typeof exportCompressed; }; // @public @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 ebf2551105c2..78e6e3f74509 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 @@ -26,30 +26,6 @@ export function clone(original: TreeFieldFr replaceIdentifiers?: true; }): TreeFieldFromImplicitField; -// @beta -export function cloneToCompressed(node: TreeNode | TreeLeafValue, options: { - oldestCompatibleClient: FluidClientVersion; - idCompressor?: IIdCompressor; -}): JsonCompatible; - -// @beta -export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; -}): JsonCompatible; - -// @beta -export function cloneToJson(node: TreeNode | TreeLeafValue, options?: { - handleConverter?: undefined; - useStableFieldKeys?: boolean; -}): JsonCompatible; - -// @beta -export function cloneToVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; - -// @beta -export function cloneToVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; - // @public export enum CommitKind { Default = 0, @@ -63,6 +39,11 @@ export interface CommitMetadata { readonly kind: CommitKind; } +// @beta +export type ConciseTree = Exclude | THandle | ConciseTree[] | { + [key: string]: ConciseTree; +}; + // @public export enum ConnectionState { CatchingUp = 1, @@ -91,12 +72,6 @@ export interface ContainerSchema { readonly initialObjects: Record; } -// @beta -export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; - -// @beta -export function createFromVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } @@ -113,6 +88,30 @@ export abstract class ErasedType { protected abstract brand(dummy: never): Name; } +// @beta +export function exportCompressed(node: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; +}): JsonCompatible; + +// @beta +export function exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; +}): ConciseTree; + +// @beta +export function exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter?: undefined; + useStableFieldKeys?: boolean; +}): JsonCompatible; + +// @beta +export function exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + +// @beta +export function exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -450,6 +449,12 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; // @public export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; +// @beta +export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + +// @beta +export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; + // @public export type InitialObjects = { [K in keyof T["initialObjects"]]: T["initialObjects"][K] extends SharedObjectKind ? TChannel : never; @@ -857,11 +862,12 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>) => () => void; readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; - readonly createFromVerbose: typeof createFromVerbose; + readonly importConcise: (schema: TSchema_1, data: InsertableTreeFieldFromImplicitField | ConciseTree) => TreeFieldFromImplicitField; + readonly importVerbose: typeof importVerbose; readonly clone: typeof clone; - readonly cloneToVerbose: typeof cloneToVerbose; - readonly cloneToJson: typeof cloneToJson; - readonly cloneToCompressed: typeof cloneToCompressed; + readonly exportVerbose: typeof exportVerbose; + readonly exportConcise: typeof exportConcise; + readonly exportCompressed: typeof exportCompressed; }; // @public @sealed From 7cfe3031827d602b7d1bc6f2106c3deae23fa1c0 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:56:21 -0700 Subject: [PATCH 26/54] Cleanup TreeBeta typing approach to work better with overloads --- .../dds/tree/api-report/tree.alpha.api.md | 62 ++-- packages/dds/tree/api-report/tree.beta.api.md | 62 ++-- packages/dds/tree/list.combo.json | 30 +- packages/dds/tree/list.compressed.json | 17 +- packages/dds/tree/list.json | 5 +- packages/dds/tree/list.verbose.json | 5 +- packages/dds/tree/src/index.ts | 5 - .../dds/tree/src/simple-tree/api/clone.ts | 191 ------------ .../dds/tree/src/simple-tree/api/create.ts | 50 --- .../dds/tree/src/simple-tree/api/index.ts | 3 +- .../tree/src/simple-tree/api/treeApiBeta.ts | 285 ++++++++++++++++-- packages/dds/tree/src/simple-tree/index.ts | 5 - .../src/test/simple-tree/api/create.spec.ts | 6 +- 13 files changed, 306 insertions(+), 420 deletions(-) delete mode 100644 packages/dds/tree/src/simple-tree/api/clone.ts diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 783dccbbeb0b..9b5f06c2d888 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -14,11 +14,6 @@ type ApplyKind = [FieldKind.Identifier]: DefaultsAreOptional extends true ? T | undefined : T; }[Kind]; -// @beta -export function clone(original: TreeFieldFromImplicitField, options?: { - replaceIdentifiers?: true; -}): TreeFieldFromImplicitField; - // @public export enum CommitKind { Default = 0, @@ -47,30 +42,6 @@ export interface EncodeOptions { valueConverter(data: IFluidHandle): TCustom; } -// @beta -export function exportCompressed(node: TreeNode | TreeLeafValue, options: { - oldestCompatibleClient: FluidClientVersion; - idCompressor?: IIdCompressor; -}): JsonCompatible; - -// @beta -export function exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; -}): ConciseTree; - -// @beta -export function exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter?: undefined; - useStableFieldKeys?: boolean; -}): JsonCompatible; - -// @beta -export function exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; - -// @beta -export function exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; - // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -173,12 +144,6 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; // @public export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; -// @beta -export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; - -// @beta -export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - // @alpha export function independentInitializedView(config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent): TreeView; @@ -598,14 +563,25 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>) => () => void; - readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; - readonly importConcise: (schema: TSchema_1, data: InsertableTreeFieldFromImplicitField | ConciseTree) => TreeFieldFromImplicitField; - readonly importVerbose: typeof importVerbose; - readonly clone: typeof clone; - readonly exportVerbose: typeof exportVerbose; - readonly exportConcise: typeof exportConcise; - readonly exportCompressed: typeof exportCompressed; + on, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; + create(schema: TSchema, data: InsertableTreeFieldFromImplicitField): Unhydrated>; + importConcise(schema: TSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; + exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; + }): ConciseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter?: undefined; + useStableFieldKeys?: boolean; + }): JsonCompatible; + exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + exportCompressed(tree: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; + }): JsonCompatible; }; // @public @sealed diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index 829b1e1e79ac..3b44992d7029 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -14,11 +14,6 @@ type ApplyKind = [FieldKind.Identifier]: DefaultsAreOptional extends true ? T | undefined : T; }[Kind]; -// @beta -export function clone(original: TreeFieldFromImplicitField, options?: { - replaceIdentifiers?: true; -}): TreeFieldFromImplicitField; - // @public export enum CommitKind { Default = 0, @@ -47,30 +42,6 @@ export interface EncodeOptions { valueConverter(data: IFluidHandle): TCustom; } -// @beta -export function exportCompressed(node: TreeNode | TreeLeafValue, options: { - oldestCompatibleClient: FluidClientVersion; - idCompressor?: IIdCompressor; -}): JsonCompatible; - -// @beta -export function exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; -}): ConciseTree; - -// @beta -export function exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter?: undefined; - useStableFieldKeys?: boolean; -}): JsonCompatible; - -// @beta -export function exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; - -// @beta -export function exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; - // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -147,12 +118,6 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; // @public export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; -// @beta -export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; - -// @beta -export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - // @public type _InlineTrick = 0; @@ -488,14 +453,25 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>) => () => void; - readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; - readonly importConcise: (schema: TSchema_1, data: InsertableTreeFieldFromImplicitField | ConciseTree) => TreeFieldFromImplicitField; - readonly importVerbose: typeof importVerbose; - readonly clone: typeof clone; - readonly exportVerbose: typeof exportVerbose; - readonly exportConcise: typeof exportConcise; - readonly exportCompressed: typeof exportCompressed; + on, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; + create(schema: TSchema, data: InsertableTreeFieldFromImplicitField): Unhydrated>; + importConcise(schema: TSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; + exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; + }): ConciseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter?: undefined; + useStableFieldKeys?: boolean; + }): JsonCompatible; + exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + exportCompressed(tree: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; + }): JsonCompatible; }; // @public @sealed diff --git a/packages/dds/tree/list.combo.json b/packages/dds/tree/list.combo.json index 6014c37a0686..11e195c4c843 100644 --- a/packages/dds/tree/list.combo.json +++ b/packages/dds/tree/list.combo.json @@ -1,29 +1 @@ -{ - "tree": { - "version": 1, - "identifiers": [], - "shapes": [ - { - "c": { - "type": "com.fluidframework.example.cli.List", - "value": false, - "fields": [["", 1]] - } - }, - { "a": 2 }, - { "c": { "type": "com.fluidframework.leaf.string", "value": true } } - ], - "data": [[0, ["x", "x", "x", "x", "x", "x", "x"]]] - }, - "schema": { - "version": 1, - "nodes": { - "com.fluidframework.example.cli.List": { - "object": { "": { "kind": "Sequence", "types": ["com.fluidframework.leaf.string"] } } - }, - "com.fluidframework.leaf.string": { "leaf": 1 } - }, - "root": { "kind": "Value", "types": ["com.fluidframework.example.cli.List"] } - }, - "idCompressor": "AAAAAAAAAEAAAAAAAADwPwAAAAAAAPA/AAAAAAAAAACWepTcM1z7sTigkGhCs9QAAAAAAAAAAAAAAAAAAADwPwAAAAAAAAAA" -} +{"tree":{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"c":{"type":"com.fluidframework.leaf.string","value":true}}],"data":[[0,["x","x","x","x","x","x","x","x","x","x","x","x","x","x"]]]},"schema":{"version":1,"nodes":{"com.fluidframework.example.cli.List":{"object":{"":{"kind":"Sequence","types":["com.fluidframework.leaf.string"]}}},"com.fluidframework.leaf.string":{"leaf":1}},"root":{"kind":"Value","types":["com.fluidframework.example.cli.List"]}},"idCompressor":"AAAAAAAAAEAAAAAAAADwPwAAAAAAAPA/AAAAAAAAAAALBQHNgtUdL+5qUjl7BwYAAAAAAAAAAAAAAAAAAADwPwAAAAAAAAAA"} \ No newline at end of file diff --git a/packages/dds/tree/list.compressed.json b/packages/dds/tree/list.compressed.json index 0135f79ea447..5455df4d3487 100644 --- a/packages/dds/tree/list.compressed.json +++ b/packages/dds/tree/list.compressed.json @@ -1,16 +1 @@ -{ - "version": 1, - "identifiers": [], - "shapes": [ - { - "c": { - "type": "com.fluidframework.example.cli.List", - "value": false, - "fields": [["", 1]] - } - }, - { "a": 2 }, - { "c": { "type": "com.fluidframework.leaf.string", "value": true } } - ], - "data": [[0, ["x", "x", "x", "x", "x", "x", "x"]]] -} +{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"c":{"type":"com.fluidframework.leaf.string","value":true}}],"data":[[0,["x","x","x","x","x","x","x","x","x","x","x","x","x","x"]]]} \ No newline at end of file diff --git a/packages/dds/tree/list.json b/packages/dds/tree/list.json index 7039c2850422..b8d3d1fc3d78 100644 --- a/packages/dds/tree/list.json +++ b/packages/dds/tree/list.json @@ -1,4 +1 @@ -{ - "type": "com.fluidframework.example.cli.List", - "fields": ["x", "x", "x", "x", "x", "x", "x"] -} +{"type":"com.fluidframework.example.cli.List","fields":["x","x","x","x","x","x","x","x","x","x","x","x","x","x"]} \ No newline at end of file diff --git a/packages/dds/tree/list.verbose.json b/packages/dds/tree/list.verbose.json index 7039c2850422..b8d3d1fc3d78 100644 --- a/packages/dds/tree/list.verbose.json +++ b/packages/dds/tree/list.verbose.json @@ -1,4 +1 @@ -{ - "type": "com.fluidframework.example.cli.List", - "fields": ["x", "x", "x", "x", "x", "x", "x"] -} +{"type":"com.fluidframework.example.cli.List","fields":["x","x","x","x","x","x","x","x","x","x","x","x","x","x"]} \ No newline at end of file diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index 30d6ca56c79a..199717a2d5d0 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -130,11 +130,6 @@ export { // Beta APIs TreeBeta, type TreeChangeEventsBeta, - importVerbose, - clone, - exportConcise, - exportVerbose, - exportCompressed, type VerboseTreeNode, type EncodeOptions, type ParseOptions, diff --git a/packages/dds/tree/src/simple-tree/api/clone.ts b/packages/dds/tree/src/simple-tree/api/clone.ts deleted file mode 100644 index 2b5bac0198f9..000000000000 --- a/packages/dds/tree/src/simple-tree/api/clone.ts +++ /dev/null @@ -1,191 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -import type { IFluidHandle } from "@fluidframework/core-interfaces"; - -import { fail, type JsonCompatible } from "../../util/index.js"; -import type { - ImplicitFieldSchema, - TreeFieldFromImplicitField, - TreeLeafValue, -} from "../schemaTypes.js"; -import { getKernel, type TreeNode } from "../core/index.js"; -import { verboseFromCursor, type EncodeOptions, type VerboseTree } from "./verboseTree.js"; -import type { ITreeCursorSynchronous } from "../../core/index.js"; -import { cursorFromInsertable } from "./create.js"; -import { tryGetSchema } from "./treeNodeApi.js"; -import { - cursorForMapTreeField, - defaultSchemaPolicy, - isTreeValue, - makeFieldBatchCodec, - mapTreeFromCursor, - TreeCompressionStrategy, - type FieldBatch, - type FieldBatchEncodingContext, -} from "../../feature-libraries/index.js"; -import { noopValidator, type FluidClientVersion } from "../../codec/index.js"; -import type { IIdCompressor } from "@fluidframework/id-compressor"; -import { createIdCompressor } from "@fluidframework/id-compressor/internal"; // TODO: no alpha exports? -import { toStoredSchema } from "../toFlexSchema.js"; -import { conciseFromCursor, type ConciseTree } from "./conciseTree.js"; - -/** - * Like {@link TreeBeta.create}, except deeply clones existing nodes. - * @remarks - * This only clones the persisted data associated with a node. - * Local state, such as properties added to customized schema classes, will not be cloned: - * they will be initialized however they end up after running the constructor, just like if a remote client had inserted the same nodes. - * @beta - */ -export function clone( - original: TreeFieldFromImplicitField, - options?: { - /** - * If set, all identifier's in the cloned tree (See {@link SchemaFactory.identifier}) will be replaced with new ones allocated using the default identifier allocation schema. - * Otherwise any identifiers will be preserved as is. - */ - replaceIdentifiers?: true; - }, -): TreeFieldFromImplicitField { - throw new Error(); -} - -/** - * Copy a snapshot of the current version of a TreeNode into a {@link ConciseTree}. - * @beta - */ -export function exportConcise( - node: TreeNode | TreeLeafValue, - options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; - }, -): ConciseTree; - -/** - * Same as generic overload, except leaves handles as is. - * @beta - */ -export function exportConcise( - node: TreeNode | TreeLeafValue, - options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, -): JsonCompatible; - -export function exportConcise( - node: TreeNode | TreeLeafValue, - options?: { - handleConverter?(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; - }, -): JsonCompatible { - const config: EncodeOptions = { - valueConverter(handle: IFluidHandle): T { - return handle as T; - }, - ...options, - }; - - const cursor = borrowCursorFromTreeNodeOrValue(node); - return conciseFromCursor(cursor, tryGetSchema(node) ?? fail("invalid input"), config); -} - -function borrowCursorFromTreeNodeOrValue( - node: TreeNode | TreeLeafValue, -): ITreeCursorSynchronous { - if (isTreeValue(node)) { - return cursorFromInsertable(tryGetSchema(node) ?? fail("missing schema"), node); - } - const kernel = getKernel(node); - const cursor = kernel.getOrCreateInnerNode().borrowCursor(); - return cursor; -} - -function borrowFieldCursorFromTreeNodeOrValue( - node: TreeNode | TreeLeafValue, -): ITreeCursorSynchronous { - const cursor = borrowCursorFromTreeNodeOrValue(node); - // TODO: avoid copy - const mapTree = mapTreeFromCursor(cursor); - return cursorForMapTreeField([mapTree]); -} - -/** - * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. - * Verbose tree format, with explicit type on every node. - * - * @remarks - * There are several cases this may be preferred to {@link TreeBeta.exportConcise}: - * - * 1. When not using {@link ITreeConfigurationOptions.preventAmbiguity} (or when using `useStableFieldKeys`), {@link TreeBeta.clone} can produce ambiguous data (the type may be unclear on some nodes). - * This may be a good alternative to {@link TreeBeta.clone} since it is lossless. - * - * 2. When the data might be interpreted without access to the exact same view schema. In such cases, the types may be unknowable if not included. - * - * 3. When easy access to the type is desired, or a more uniform simple to parse format is desired. - * @beta - */ -export function exportVerbose( - node: TreeNode | TreeLeafValue, - options: EncodeOptions, -): VerboseTree; - -/** - * Same as generic overload, except leaves handles as is. - * @beta - */ -export function exportVerbose( - node: TreeNode | TreeLeafValue, - options?: Partial>, -): VerboseTree; - -export function exportVerbose( - node: TreeNode | TreeLeafValue, - options?: Partial>, -): VerboseTree { - const config: EncodeOptions = { - valueConverter(handle: IFluidHandle): T { - return handle as T; - }, - ...options, - }; - - const cursor = borrowCursorFromTreeNodeOrValue(node); - return verboseFromCursor(cursor, tryGetSchema(node) ?? fail("invalid input"), config); -} - -/** - * Construct tree content compatible with a field defined by the provided `schema`. - * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. - * @param data - The data used to construct the field content. See `Tree.cloneToVerbose`. - * @beta - */ -export function exportCompressed( - node: TreeNode | TreeLeafValue, - options: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor }, -): JsonCompatible { - const schema = tryGetSchema(node) ?? fail("invalid input"); - const format = versionToFormat[options.oldestCompatibleClient]; - const codec = makeFieldBatchCodec({ jsonValidator: noopValidator }, format); - const cursor = borrowFieldCursorFromTreeNodeOrValue(node); - const batch: FieldBatch = [cursor]; - // If none provided, create a compressor which will not compress anything (TODO: is this the right way to do that?). - const idCompressor = options.idCompressor ?? createIdCompressor(); - const context: FieldBatchEncodingContext = { - encodeType: TreeCompressionStrategy.Compressed, - idCompressor, - originatorId: idCompressor.localSessionId, // Is this right? If so, why is is needed? - schema: { schema: toStoredSchema(schema), policy: defaultSchemaPolicy }, - }; - const result = codec.encode(batch, context); - return result; -} - -const versionToFormat = { - v2_0: 1, - v2_1: 1, - v2_2: 1, - v2_3: 1, -}; diff --git a/packages/dds/tree/src/simple-tree/api/create.ts b/packages/dds/tree/src/simple-tree/api/create.ts index 9fcf5a221a13..460fd475735c 100644 --- a/packages/dds/tree/src/simple-tree/api/create.ts +++ b/packages/dds/tree/src/simple-tree/api/create.ts @@ -9,7 +9,6 @@ import { assert } from "@fluidframework/core-utils/internal"; import type { ITreeCursorSynchronous, SchemaAndPolicy } from "../../core/index.js"; import { fail, type JsonCompatible } from "../../util/index.js"; import type { - TreeLeafValue, ImplicitFieldSchema, InsertableTreeFieldFromImplicitField, TreeFieldFromImplicitField, @@ -30,13 +29,6 @@ import { getOrCreateNodeFromFlexTreeNode } from "../proxies.js"; import { getOrCreateMapTreeNode, isFieldInSchema } from "../../feature-libraries/index.js"; import { toFlexSchema } from "../toFlexSchema.js"; import { inSchemaOrThrow, mapTreeFromNodeData, type InsertableContent } from "../toMapTree.js"; -import { - applySchemaToParserOptions, - cursorFromVerbose, - type ParseOptions, - type VerboseTree, - type VerboseTreeNode, -} from "./verboseTree.js"; /** * Construct tree content that is compatible with the field defined by the provided `schema`. @@ -101,48 +93,6 @@ export function cursorFromInsertable( return cursorForMapTreeNode(mapTree); } -/** - * Construct tree content compatible with a field defined by the provided `schema`. - * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. - * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. - * @privateRemarks - * This could be exposed as a public `Tree.createFromVerbose` function. - * @beta - */ -export function importVerbose( - schema: TSchema, - data: VerboseTree | undefined, - options: ParseOptions, -): Unhydrated>; - -/** - * Construct tree content compatible with a field defined by the provided `schema`. - * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. - * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. - * @beta - */ -export function importVerbose( - schema: TSchema, - data: VerboseTree | undefined, - options?: Partial>, -): Unhydrated>; - -export function importVerbose( - schema: TSchema, - data: VerboseTree | undefined, - options?: Partial>, -): Unhydrated> { - const config: ParseOptions = { - valueConverter: (input: VerboseTree) => { - return input as TreeLeafValue | VerboseTreeNode; - }, - ...options, - }; - const schemalessConfig = applySchemaToParserOptions(schema, config); - const cursor = cursorFromVerbose(data, schemalessConfig); - return createFromCursor(schema, cursor); -} - /** * Construct tree content compatible with a field defined by the provided `schema`. * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. diff --git a/packages/dds/tree/src/simple-tree/api/index.ts b/packages/dds/tree/src/simple-tree/api/index.ts index 366efa3fd43b..aabbab1d3e9b 100644 --- a/packages/dds/tree/src/simple-tree/api/index.ts +++ b/packages/dds/tree/src/simple-tree/api/index.ts @@ -25,8 +25,7 @@ export { type EmptyObject, } from "./schemaCreationUtilities.js"; export { treeNodeApi, type TreeNodeApi } from "./treeNodeApi.js"; -export { createFromInsertable, cursorFromInsertable, importVerbose } from "./create.js"; -export { clone, exportConcise, exportVerbose, exportCompressed } from "./clone.js"; +export { createFromInsertable, cursorFromInsertable } from "./create.js"; export type { SimpleTreeSchema } from "./simpleSchema.js"; export { type JsonSchemaId, diff --git a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts index 1a8e80996757..832e7d0d4f5a 100644 --- a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts +++ b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts @@ -3,22 +3,49 @@ * Licensed under the MIT License. */ -import type { - NodeKind, - TreeChangeEvents, - TreeNode, - Unhydrated, - WithType, +import { + getKernel, + type NodeKind, + type TreeChangeEvents, + type TreeNode, + type Unhydrated, + type WithType, } from "../core/index.js"; -import { treeNodeApi } from "./treeNodeApi.js"; -import { createFromInsertable, importVerbose } from "./create.js"; -import { clone, exportCompressed, exportConcise, exportVerbose } from "./clone.js"; +import { treeNodeApi, tryGetSchema } from "./treeNodeApi.js"; +import { createFromCursor, createFromInsertable, cursorFromInsertable } from "./create.js"; import type { ImplicitFieldSchema, InsertableTreeFieldFromImplicitField, TreeFieldFromImplicitField, + TreeLeafValue, } from "../schemaTypes.js"; -import type { ConciseTree } from "./conciseTree.js"; +import { conciseFromCursor, type ConciseTree } from "./conciseTree.js"; +import { + applySchemaToParserOptions, + cursorFromVerbose, + verboseFromCursor, + type EncodeOptions, + type ParseOptions, + type VerboseTree, + type VerboseTreeNode, +} from "./verboseTree.js"; +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import { fail, type JsonCompatible } from "../../util/index.js"; +import { noopValidator, type FluidClientVersion } from "../../codec/index.js"; +import type { IIdCompressor } from "@fluidframework/id-compressor"; +import type { ITreeCursorSynchronous } from "../../core/index.js"; +import { + cursorForMapTreeField, + defaultSchemaPolicy, + isTreeValue, + makeFieldBatchCodec, + mapTreeFromCursor, + TreeCompressionStrategy, + type FieldBatch, + type FieldBatchEncodingContext, +} from "../../feature-libraries/index.js"; +import { createIdCompressor } from "@fluidframework/id-compressor/internal"; +import { toStoredSchema } from "../toFlexSchema.js"; /** * Data included for {@link TreeChangeEventsBeta.nodeChanged}. @@ -96,7 +123,7 @@ export interface TreeChangeEventsBeta * Extensions to {@link Tree} which are not yet stable. * @sealed @beta */ -export const TreeBeta = { +export const TreeBeta: { /** * Register an event listener on the given node. * @param node - The node whose events should be subscribed to. @@ -109,9 +136,26 @@ export const TreeBeta = { node: TNode, eventName: K, listener: NoInfer[K]>, - ): () => void { - return treeNodeApi.on(node, eventName, listener); - }, + ): () => void; + + // TODO: support clone + // /** + // * Like {@link TreeBeta.create}, except deeply clones existing nodes. + // * @remarks + // * This only clones the persisted data associated with a node. + // * Local state, such as properties added to customized schema classes, will not be cloned: + // * they will be initialized however they end up after running the constructor, just like if a remote client had inserted the same nodes. + // */ + // clone( + // original: TreeFieldFromImplicitField, + // options?: { + // /** + // * If set, all identifier's in the cloned tree (See {@link SchemaFactory.identifier}) will be replaced with new ones allocated using the default identifier allocation schema. + // * Otherwise any identifiers will be preserved as is. + // */ + // replaceIdentifiers?: true; + // }, + // ): TreeFieldFromImplicitField; /** * Generic tree constructor. @@ -125,9 +169,7 @@ export const TreeBeta = { create( schema: TSchema, data: InsertableTreeFieldFromImplicitField, - ): Unhydrated> { - return createFromInsertable(schema, data); - }, + ): Unhydrated>; /** * Less type safe version of {@link TreeBeta.create}, suitable for importing data. @@ -139,11 +181,110 @@ export const TreeBeta = { * When using this function, * it is recommend to ensure you schema is unambiguous with {@link ITreeConfigurationOptions.preventAmbiguity}. * If the schema is ambiguous, consider using {@link TreeBeta.create} and {@link Unhydrated} nodes where needed, - * or using {@link TreeBeta.importVerbose} and specify all types. + * or using {@link TreeBeta.(importVerbose:1)} and specify all types. * * Documented (and thus recoverable) error handling/reporting for this is not yet implemented, * but for now most invalid inputs will throw a recoverable error. */ + importConcise( + schema: TSchema, + data: InsertableTreeFieldFromImplicitField | ConciseTree, + ): Unhydrated>; + + /** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + * @privateRemarks + * This could be exposed as a public `Tree.createFromVerbose` function. + */ + importVerbose( + schema: TSchema, + data: VerboseTree | undefined, + options: ParseOptions, + ): Unhydrated>; + + /** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + */ + importVerbose( + schema: TSchema, + data: VerboseTree | undefined, + options?: Partial>, + ): Unhydrated>; + + /** + * Copy a snapshot of the current version of a TreeNode into a {@link ConciseTree}. + */ + exportConcise( + node: TreeNode | TreeLeafValue, + options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; + }, + ): ConciseTree; + + /** + * Same as generic overload, except leaves handles as is. + */ + exportConcise( + node: TreeNode | TreeLeafValue, + options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, + ): JsonCompatible; + + /** + * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. + * Verbose tree format, with explicit type on every node. + * + * @remarks + * There are several cases this may be preferred to {@link TreeBeta.(exportConcise:1)}: + * + * 1. When not using {@link ITreeConfigurationOptions.preventAmbiguity} (or when using `useStableFieldKeys`), `exportConcise` can produce ambiguous data (the type may be unclear on some nodes). + * `exportVerbose` will always be unambiguous and thus lossless. + * + * 2. When the data might be interpreted without access to the exact same view schema. In such cases, the types may be unknowable if not included. + * + * 3. When easy access to the type is desired. + */ + exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + + /** + * Same {@link TreeBeta.(exportVerbose:1)} except leaves handles as is. + */ + exportVerbose( + node: TreeNode | TreeLeafValue, + options?: Partial>, + ): VerboseTree; + + /** + * Export the content of the provided `tree` in a compressed JSON compatible format. + * @remarks + * If an `idCompressor` is provided, it will be used to compress identifiers and thus will be needed to decompress the data. + */ + exportCompressed( + tree: TreeNode | TreeLeafValue, + options: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor }, + ): JsonCompatible; +} = { + on, TNode extends TreeNode>( + node: TNode, + eventName: K, + listener: NoInfer[K]>, + ): () => void { + return treeNodeApi.on(node, eventName, listener); + }, + + // clone, + + create( + schema: TSchema, + data: InsertableTreeFieldFromImplicitField, + ): Unhydrated> { + return createFromInsertable(schema, data); + }, + importConcise( schema: TSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree, @@ -151,9 +292,105 @@ export const TreeBeta = { return createFromInsertable(schema, data as InsertableTreeFieldFromImplicitField); }, - importVerbose, - clone, - exportVerbose, - exportConcise, - exportCompressed, -} as const; + importVerbose( + schema: TSchema, + data: VerboseTree | undefined, + options?: Partial>, + ): Unhydrated> { + const config: ParseOptions = { + valueConverter: (input: VerboseTree) => { + return input as TreeLeafValue | VerboseTreeNode; + }, + ...options, + }; + const schemalessConfig = applySchemaToParserOptions(schema, config); + const cursor = cursorFromVerbose(data, schemalessConfig); + return createFromCursor(schema, cursor); + }, + + exportConcise( + node: TreeNode | TreeLeafValue, + options?: { + handleConverter?(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; + }, + ): JsonCompatible { + const config: EncodeOptions = { + valueConverter(handle: IFluidHandle): T { + return handle as T; + }, + ...options, + }; + + const cursor = borrowCursorFromTreeNodeOrValue(node); + return conciseFromCursor(cursor, tryGetSchema(node) ?? fail("invalid input"), config); + }, + + exportVerbose( + node: TreeNode | TreeLeafValue, + options?: Partial>, + ): VerboseTree { + const config: EncodeOptions = { + valueConverter(handle: IFluidHandle): T { + return handle as T; + }, + ...options, + }; + + const cursor = borrowCursorFromTreeNodeOrValue(node); + return verboseFromCursor(cursor, tryGetSchema(node) ?? fail("invalid input"), config); + }, + + /** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. See `Tree.cloneToVerbose`. + */ + exportCompressed( + node: TreeNode | TreeLeafValue, + options: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor }, + ): JsonCompatible { + const schema = tryGetSchema(node) ?? fail("invalid input"); + const format = versionToFormat[options.oldestCompatibleClient]; + const codec = makeFieldBatchCodec({ jsonValidator: noopValidator }, format); + const cursor = borrowFieldCursorFromTreeNodeOrValue(node); + const batch: FieldBatch = [cursor]; + // If none provided, create a compressor which will not compress anything (TODO: is this the right way to do that?). + const idCompressor = options.idCompressor ?? createIdCompressor(); + const context: FieldBatchEncodingContext = { + encodeType: TreeCompressionStrategy.Compressed, + idCompressor, + originatorId: idCompressor.localSessionId, // Is this right? If so, why is is needed? + schema: { schema: toStoredSchema(schema), policy: defaultSchemaPolicy }, + }; + const result = codec.encode(batch, context); + return result; + }, +}; + +function borrowCursorFromTreeNodeOrValue( + node: TreeNode | TreeLeafValue, +): ITreeCursorSynchronous { + if (isTreeValue(node)) { + return cursorFromInsertable(tryGetSchema(node) ?? fail("missing schema"), node); + } + const kernel = getKernel(node); + const cursor = kernel.getOrCreateInnerNode().borrowCursor(); + return cursor; +} + +function borrowFieldCursorFromTreeNodeOrValue( + node: TreeNode | TreeLeafValue, +): ITreeCursorSynchronous { + const cursor = borrowCursorFromTreeNodeOrValue(node); + // TODO: avoid copy + const mapTree = mapTreeFromCursor(cursor); + return cursorForMapTreeField([mapTree]); +} + +const versionToFormat = { + v2_0: 1, + v2_1: 1, + v2_2: 1, + v2_3: 1, +}; diff --git a/packages/dds/tree/src/simple-tree/index.ts b/packages/dds/tree/src/simple-tree/index.ts index 1d82920ad465..1ce7a8a02dc5 100644 --- a/packages/dds/tree/src/simple-tree/index.ts +++ b/packages/dds/tree/src/simple-tree/index.ts @@ -64,11 +64,6 @@ export { type JsonLeafSchemaType, getJsonSchema, getSimpleSchema, - type importVerbose, - type clone, - type exportConcise, - type exportVerbose, - type exportCompressed, type VerboseTreeNode, type EncodeOptions, type ParseOptions, diff --git a/packages/dds/tree/src/test/simple-tree/api/create.spec.ts b/packages/dds/tree/src/test/simple-tree/api/create.spec.ts index 7ff990445b3e..0134a80d4252 100644 --- a/packages/dds/tree/src/test/simple-tree/api/create.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/create.spec.ts @@ -5,9 +5,7 @@ import { strict as assert } from "node:assert"; -import { createFromInsertable, SchemaFactory } from "../../../simple-tree/index.js"; -// eslint-disable-next-line import/no-internal-modules -import { importVerbose } from "../../../simple-tree/api/create.js"; +import { createFromInsertable, SchemaFactory, TreeBeta } from "../../../simple-tree/index.js"; const schema = new SchemaFactory("com.example"); @@ -25,7 +23,7 @@ describe("simple-tree create", () => { }); it("createFromVerbose", () => { - const canvas1 = importVerbose(Canvas, { + const canvas1 = TreeBeta.importVerbose(Canvas, { type: Canvas.identifier, fields: { stuff: { type: NodeList.identifier, fields: [] } }, }); From 2ccfe08fd3667e79d951276c8e9f5b562144f975 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:21:27 -0700 Subject: [PATCH 27/54] Add cli app --- examples/apps/tree-cli-app/.eslintrc.cjs | 11 ++ examples/apps/tree-cli-app/.mocharc.cjs | 17 +++ examples/apps/tree-cli-app/.npmignore | 6 + examples/apps/tree-cli-app/LICENSE | 21 ++++ examples/apps/tree-cli-app/README.md | 5 + examples/apps/tree-cli-app/biome.jsonc | 4 + examples/apps/tree-cli-app/package.json | 76 +++++++++++++ .../apps/tree-cli-app/prettier.config.cjs | 8 ++ examples/apps/tree-cli-app/src/index.ts | 106 ++++++++++++++++++ examples/apps/tree-cli-app/src/schema.ts | 22 ++++ .../apps/tree-cli-app/src/test/schema.spec.ts | 92 +++++++++++++++ .../tree-cli-app/src/test/tsconfig.cjs.json | 12 ++ .../apps/tree-cli-app/src/test/tsconfig.json | 23 ++++ examples/apps/tree-cli-app/tsconfig.cjs.json | 7 ++ examples/apps/tree-cli-app/tsconfig.json | 12 ++ examples/apps/tree-cli-app/tsdoc.json | 4 + feeds/internal-test.txt | 1 + .../dds/tree/api-report/tree.alpha.api.md | 3 + packages/dds/tree/list.combo.json | 2 +- packages/dds/tree/list.compressed.json | 2 +- packages/dds/tree/list.json | 2 +- packages/dds/tree/list.verbose.json | 2 +- packages/dds/tree/src/index.ts | 1 + .../src/shared-tree/schematizingTreeView.ts | 23 ++-- .../src/simple-tree/api/getStoredSchema.ts | 71 +++++++++++- .../dds/tree/src/simple-tree/api/index.ts | 6 +- packages/dds/tree/src/simple-tree/index.ts | 2 + packages/dds/tree/src/treeFactory.ts | 2 +- .../api-report/fluid-framework.alpha.api.md | 62 ++++------ .../api-report/fluid-framework.beta.api.md | 62 ++++------ pnpm-lock.yaml | 101 ++++++++++++++++- 31 files changed, 657 insertions(+), 111 deletions(-) create mode 100644 examples/apps/tree-cli-app/.eslintrc.cjs create mode 100644 examples/apps/tree-cli-app/.mocharc.cjs create mode 100644 examples/apps/tree-cli-app/.npmignore create mode 100644 examples/apps/tree-cli-app/LICENSE create mode 100644 examples/apps/tree-cli-app/README.md create mode 100644 examples/apps/tree-cli-app/biome.jsonc create mode 100644 examples/apps/tree-cli-app/package.json create mode 100644 examples/apps/tree-cli-app/prettier.config.cjs create mode 100644 examples/apps/tree-cli-app/src/index.ts create mode 100644 examples/apps/tree-cli-app/src/schema.ts create mode 100644 examples/apps/tree-cli-app/src/test/schema.spec.ts create mode 100644 examples/apps/tree-cli-app/src/test/tsconfig.cjs.json create mode 100644 examples/apps/tree-cli-app/src/test/tsconfig.json create mode 100644 examples/apps/tree-cli-app/tsconfig.cjs.json create mode 100644 examples/apps/tree-cli-app/tsconfig.json create mode 100644 examples/apps/tree-cli-app/tsdoc.json diff --git a/examples/apps/tree-cli-app/.eslintrc.cjs b/examples/apps/tree-cli-app/.eslintrc.cjs new file mode 100644 index 000000000000..f1aaefc67a58 --- /dev/null +++ b/examples/apps/tree-cli-app/.eslintrc.cjs @@ -0,0 +1,11 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +module.exports = { + extends: [require.resolve("@fluidframework/eslint-config-fluid/strict"), "prettier"], + parserOptions: { + project: ["./tsconfig.json", "./src/test/tsconfig.json"], + }, +}; diff --git a/examples/apps/tree-cli-app/.mocharc.cjs b/examples/apps/tree-cli-app/.mocharc.cjs new file mode 100644 index 000000000000..adc57b4616e9 --- /dev/null +++ b/examples/apps/tree-cli-app/.mocharc.cjs @@ -0,0 +1,17 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +"use strict"; + +const getFluidTestMochaConfig = require("@fluid-internal/mocha-test-setup/mocharc-common"); + +const packageDir = __dirname; +const config = getFluidTestMochaConfig(packageDir); +config.spec = process.env.MOCHA_SPEC ?? "lib/test"; +// TODO: figure out why this package needs the --exit flag, tests might not be cleaning up correctly after themselves. +// AB#7856 +config.exit = true; + +module.exports = config; diff --git a/examples/apps/tree-cli-app/.npmignore b/examples/apps/tree-cli-app/.npmignore new file mode 100644 index 000000000000..a40f882cf599 --- /dev/null +++ b/examples/apps/tree-cli-app/.npmignore @@ -0,0 +1,6 @@ +nyc +*.log +**/*.tsbuildinfo +src/test +dist/test +**/_api-extractor-temp/** diff --git a/examples/apps/tree-cli-app/LICENSE b/examples/apps/tree-cli-app/LICENSE new file mode 100644 index 000000000000..60af0a6a40e9 --- /dev/null +++ b/examples/apps/tree-cli-app/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation and contributors. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/apps/tree-cli-app/README.md b/examples/apps/tree-cli-app/README.md new file mode 100644 index 000000000000..2727ede5bcc3 --- /dev/null +++ b/examples/apps/tree-cli-app/README.md @@ -0,0 +1,5 @@ +# @fluid-example/tree-cli-app + +Example application using Shared-Tree to create a non-collaborative file editing CLI application. + +Note that its perfectly possible to write a collaborative online CLI app using tree as well: this simply is not an example of that. diff --git a/examples/apps/tree-cli-app/biome.jsonc b/examples/apps/tree-cli-app/biome.jsonc new file mode 100644 index 000000000000..4b65e1c0aea2 --- /dev/null +++ b/examples/apps/tree-cli-app/biome.jsonc @@ -0,0 +1,4 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["../../../biome.jsonc"] +} diff --git a/examples/apps/tree-cli-app/package.json b/examples/apps/tree-cli-app/package.json new file mode 100644 index 000000000000..c4d4cd1aff24 --- /dev/null +++ b/examples/apps/tree-cli-app/package.json @@ -0,0 +1,76 @@ +{ + "name": "@fluid-example/tree-cli-app", + "version": "2.4.0", + "description": "SharedTree CLI app demo", + "homepage": "https://fluidframework.com", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/FluidFramework.git", + "directory": "examples/apps/tree-cli-app" + }, + "license": "MIT", + "author": "Microsoft and contributors", + "type": "module", + "scripts": { + "build": "fluid-build . --task build", + "build:compile": "fluid-build . --task compile", + "build:esnext": "tsc --project ./tsconfig.json", + "build:test": "npm run build:test:esm", + "build:test:esm": "tsc --project ./src/test/tsconfig.json", + "check:biome": "biome check .", + "check:format": "npm run check:biome", + "check:prettier": "prettier --check . --cache --ignore-path ../../../.prettierignore", + "clean": "rimraf --glob dist lib \"**/*.tsbuildinfo\" \"**/*.build.log\" nyc", + "eslint": "eslint --format stylish src", + "eslint:fix": "eslint --format stylish src --fix --fix-type problem,suggestion,layout", + "format": "npm run format:biome", + "format:biome": "biome check . --write", + "format:prettier": "prettier --write . --cache --ignore-path ../../../.prettierignore", + "lint": "fluid-build . --task lint", + "lint:fix": "fluid-build . --task eslint:fix --task format", + "test": "npm run test:mocha", + "test:mocha": "npm run test:mocha:esm", + "test:mocha:esm": "mocha", + "test:mocha:verbose": "cross-env FLUID_TEST_VERBOSE=1 npm run test:mocha" + }, + "dependencies": { + "@fluidframework/aqueduct": "workspace:~", + "@fluidframework/core-interfaces": "workspace:~", + "@fluidframework/datastore-definitions": "workspace:~", + "@fluidframework/fluid-static": "workspace:~", + "@fluidframework/id-compressor": "workspace:~", + "@fluidframework/runtime-definitions": "workspace:~", + "@fluidframework/runtime-utils": "workspace:~", + "@fluidframework/shared-object-base": "workspace:~", + "@fluidframework/tree": "workspace:~", + "@sinclair/typebox": "^0.32.29" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.15.2", + "@biomejs/biome": "~1.8.3", + "@fluid-internal/mocha-test-setup": "workspace:~", + "@fluid-tools/build-cli": "^0.46.0", + "@fluidframework/build-common": "^2.0.3", + "@fluidframework/build-tools": "^0.46.0", + "@fluidframework/eslint-config-fluid": "^5.4.0", + "@types/mocha": "^9.1.1", + "@types/node": "^18.19.0", + "concurrently": "^8.2.1", + "copyfiles": "^2.4.1", + "cross-env": "^7.0.3", + "eslint": "~8.55.0", + "eslint-config-prettier": "~9.0.0", + "mocha": "^10.2.0", + "mocha-json-output-reporter": "^2.0.1", + "mocha-multi-reporters": "^1.5.1", + "moment": "^2.21.0", + "prettier": "~3.0.3", + "rimraf": "^4.4.0", + "typescript": "~5.4.5" + }, + "typeValidation": { + "disabled": true, + "broken": {}, + "entrypoint": "internal" + } +} diff --git a/examples/apps/tree-cli-app/prettier.config.cjs b/examples/apps/tree-cli-app/prettier.config.cjs new file mode 100644 index 000000000000..d4870022599f --- /dev/null +++ b/examples/apps/tree-cli-app/prettier.config.cjs @@ -0,0 +1,8 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +module.exports = { + ...require("@fluidframework/build-common/prettier.config.cjs"), +}; diff --git a/examples/apps/tree-cli-app/src/index.ts b/examples/apps/tree-cli-app/src/index.ts new file mode 100644 index 000000000000..c145530e916d --- /dev/null +++ b/examples/apps/tree-cli-app/src/index.ts @@ -0,0 +1,106 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +// This is a node powered CLI application, so using node makes sense: +// eslint-disable-next-line import/no-nodejs-modules +import { readFileSync, writeFileSync } from "node:fs"; + +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import { + createIdCompressor, + deserializeIdCompressor, + type SerializedIdCompressorWithOngoingSession, +} from "@fluidframework/id-compressor/internal"; +import { isFluidHandle } from "@fluidframework/runtime-utils"; +import { + TreeBeta, + type JsonCompatible, + type VerboseTree, + extractPersistedSchema, + FluidClientVersion, + type ViewContent, + independentInitializedView, + type ForestOptions, + type ICodecOptions, + typeboxValidator, + // eslint-disable-next-line import/no-internal-modules +} from "@fluidframework/tree/alpha"; +import { type Static, Type } from "@sinclair/typebox"; + +import { config, List } from "./schema.js"; + +console.log("App"); + +const path = "list.json"; + +// Data parsed from JSON is safe to consider JsonCompatible. +// If file is invalid JSON, that will throw and is fine for this app. +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const data: JsonCompatible = JSON.parse(readFileSync(path).toString()); + +// const node = TreeBeta.create(List, data as InsertableTypedNode); +const node = TreeBeta.importVerbose(List, data as VerboseTree); + +node.insertAtEnd("x"); + +writeFileSync(path, JSON.stringify(TreeBeta.exportVerbose(node))); + +// Demo all formats: +writeFileSync("list.verbose.json", JSON.stringify(TreeBeta.exportVerbose(node))); +// writeFileSync("list.simple.json", JSON.stringify(TreeBeta.exportConcise(node))); +writeFileSync( + "list.compressed.json", + JSON.stringify( + TreeBeta.exportCompressed(node, { oldestCompatibleClient: FluidClientVersion.v2_3 }), + ), +); + +// Combo + +const File = Type.Object({ + tree: Type.Unsafe>(), + schema: Type.Unsafe(), + idCompressor: Type.Unsafe(), +}); +type File = Static; + +const idCompressor = createIdCompressor(); +// idCompressor.finalizeCreationRange(idCompressor.takeUnfinalizedCreationRange()); + +const file: File = { + tree: TreeBeta.exportCompressed(node, { + oldestCompatibleClient: FluidClientVersion.v2_3, + idCompressor, + }), + schema: extractPersistedSchema(List), + idCompressor: idCompressor.serialize(true), +}; + +function rejectHandles(key: string, value: unknown): unknown { + if (isFluidHandle(value)) { + throw new Error("Fluid handles are not supported"); + } + return value; +} + +writeFileSync("list.combo.json", JSON.stringify(file, rejectHandles)); + +// Data parsed from JSON is safe to consider JsonCompatible. +// If file is invalid JSON, that will throw and is fine for this app. +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const combo: File = JSON.parse(readFileSync("list.combo.json").toString()); + +const content: ViewContent = { + schema: combo.schema, + tree: combo.tree, + idCompressor: deserializeIdCompressor(combo.idCompressor), +}; + +const options: ForestOptions & ICodecOptions = { jsonValidator: typeboxValidator }; + +const view = independentInitializedView(config, options, content); +const compat = view.compatibility; +const root = view.root; +console.log("Done"); diff --git a/examples/apps/tree-cli-app/src/schema.ts b/examples/apps/tree-cli-app/src/schema.ts new file mode 100644 index 000000000000..e6ddae0fb48c --- /dev/null +++ b/examples/apps/tree-cli-app/src/schema.ts @@ -0,0 +1,22 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { SchemaFactory, TreeViewConfiguration } from "@fluidframework/tree"; + +const schemaBuilder = new SchemaFactory("com.fluidframework.example.cli"); + +/** + * List node. + */ +export class List extends schemaBuilder.array("List", schemaBuilder.string) {} + +/** + * Tree configuration. + */ +export const config = new TreeViewConfiguration({ + schema: List, + enableSchemaValidation: true, + preventAmbiguity: true, +}); diff --git a/examples/apps/tree-cli-app/src/test/schema.spec.ts b/examples/apps/tree-cli-app/src/test/schema.spec.ts new file mode 100644 index 000000000000..50743b965150 --- /dev/null +++ b/examples/apps/tree-cli-app/src/test/schema.spec.ts @@ -0,0 +1,92 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { strict as assert } from "node:assert"; + +import { + comparePersistedSchema, + extractPersistedSchema, + typeboxValidator, + type ForestOptions, + type ICodecOptions, + type JsonCompatible, + // eslint-disable-next-line import/no-internal-modules +} from "@fluidframework/tree/alpha"; + +import { List } from "../schema.js"; + +/** + * List of schema from previous versions of this application. + * Storing these as .json files ina folder may make more sense for more complex applications. + * + * The `schema` field is generated by passing the schema to `extractPersistedSchema`. + */ +const historicalSchema: { version: string; schema: JsonCompatible }[] = [ + { + version: "2.3", + schema: { + version: 1, + nodes: { + "com.fluidframework.example.cli.List": { + object: { + "": { + kind: "Sequence", + types: ["com.fluidframework.leaf.string"], + }, + }, + }, + "com.fluidframework.leaf.string": { + leaf: 1, + }, + }, + root: { + kind: "Value", + types: ["com.fluidframework.example.cli.List"], + }, + }, + }, +]; + +describe("schema", () => { + it("current schema matches latest historical schema", () => { + const current = extractPersistedSchema(List); + + // For compatibility with deep equality and simply objects, round trip via JSON to erase prototypes. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const currentRoundTripped: JsonCompatible = JSON.parse(JSON.stringify(current)); + + const previous = historicalSchema.at(-1); + assert(previous !== undefined); + // This ensures that historicalSchema's last entry is up to date with the current application code. + // This can catch: + // 1. Forgetting to update historicalSchema when intentionally making schema changes. + // 2. Accidentally changing schema in a way that impacts document compatibility. + assert.deepEqual(currentRoundTripped, previous.schema); + }); + + it("historical schema can be upgraded to current schema", () => { + const options: ForestOptions & ICodecOptions = { jsonValidator: typeboxValidator }; + + for (let documentIndex = 0; documentIndex < historicalSchema.length; documentIndex++) { + for (let viewIndex = 0; viewIndex < historicalSchema.length; viewIndex++) { + const compat = comparePersistedSchema( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + historicalSchema[documentIndex]!.schema, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + historicalSchema[viewIndex]!.schema, + options, + false, + ); + + // We do not expect duplicates in historicalSchema. + assert.equal(compat.isEquivalent, documentIndex === viewIndex); + // Currently collaboration is only allowed between identical versions + assert.equal(compat.canView, documentIndex === viewIndex); + // Older versions should be upgradable to newer versions, but not the reverse. + assert.equal(compat.canUpgrade, documentIndex <= viewIndex); + } + } + }); +}); diff --git a/examples/apps/tree-cli-app/src/test/tsconfig.cjs.json b/examples/apps/tree-cli-app/src/test/tsconfig.cjs.json new file mode 100644 index 000000000000..fdd3a3c1c9fc --- /dev/null +++ b/examples/apps/tree-cli-app/src/test/tsconfig.cjs.json @@ -0,0 +1,12 @@ +{ + // This config must be used in a "type": "commonjs" environment. (Use `fluid-tsc commonjs`.) + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/test", + }, + "references": [ + { + "path": "../../tsconfig.cjs.json", + }, + ], +} diff --git a/examples/apps/tree-cli-app/src/test/tsconfig.json b/examples/apps/tree-cli-app/src/test/tsconfig.json new file mode 100644 index 000000000000..acbd99a2893e --- /dev/null +++ b/examples/apps/tree-cli-app/src/test/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../../../common/build/build-common/tsconfig.test.node16.json", + "compilerOptions": { + "rootDir": "./", + "outDir": "../../lib/test", + "types": ["mocha", "node"], + // Allows writing type checking expression without having to use the results. + "noUnusedLocals": false, + // Allow testing that declarations work properly + "declaration": true, + // Needed to ensure testExport's produce a valid d.ts + "skipLibCheck": false, + // Due to several of our own packages' exports failing to build with "exactOptionalPropertyTypes", + // disable it to prevent that from erroring when combined with "skipLibCheck". + "exactOptionalPropertyTypes": false, + }, + "include": ["./**/*"], + "references": [ + { + "path": "../..", + }, + ], +} diff --git a/examples/apps/tree-cli-app/tsconfig.cjs.json b/examples/apps/tree-cli-app/tsconfig.cjs.json new file mode 100644 index 000000000000..6ec35ad731a3 --- /dev/null +++ b/examples/apps/tree-cli-app/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + // This config must be used in a "type": "commonjs" environment. (Use `fluid-tsc commonjs`.) + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + }, +} diff --git a/examples/apps/tree-cli-app/tsconfig.json b/examples/apps/tree-cli-app/tsconfig.json new file mode 100644 index 000000000000..55e7ad09857c --- /dev/null +++ b/examples/apps/tree-cli-app/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../common/build/build-common/tsconfig.node16.json", + "include": ["src/**/*"], + "exclude": ["src/test/**/*"], + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "exactOptionalPropertyTypes": false, + "noUnusedLocals": false, + "types": ["node"], + }, +} diff --git a/examples/apps/tree-cli-app/tsdoc.json b/examples/apps/tree-cli-app/tsdoc.json new file mode 100644 index 000000000000..ecb918da5cb8 --- /dev/null +++ b/examples/apps/tree-cli-app/tsdoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": ["../../../common/build/build-common/tsdoc-base.json"] +} diff --git a/feeds/internal-test.txt b/feeds/internal-test.txt index 3b3a8bbb5506..9efd0b6f67a5 100644 --- a/feeds/internal-test.txt +++ b/feeds/internal-test.txt @@ -89,6 +89,7 @@ fluid-framework @fluid-experimental/property-common @fluid-experimental/property-changeset @fluid-example/table-document +@fluid-example/tree-cli-app @fluid-experimental/azure-scenario-runner @fluidframework/azure-service-utils @fluidframework/azure-local-service \ No newline at end of file diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 9b5f06c2d888..e068783908b6 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -27,6 +27,9 @@ export interface CommitMetadata { readonly kind: CommitKind; } +// @alpha +export function comparePersistedSchema(persisted: JsonCompatible, view: JsonCompatible, options: ICodecOptions, canInitialize: boolean): SchemaCompatibilityStatus; + // @beta export type ConciseTree = Exclude | THandle | ConciseTree[] | { [key: string]: ConciseTree; diff --git a/packages/dds/tree/list.combo.json b/packages/dds/tree/list.combo.json index 11e195c4c843..289dbc1d65f0 100644 --- a/packages/dds/tree/list.combo.json +++ b/packages/dds/tree/list.combo.json @@ -1 +1 @@ -{"tree":{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"c":{"type":"com.fluidframework.leaf.string","value":true}}],"data":[[0,["x","x","x","x","x","x","x","x","x","x","x","x","x","x"]]]},"schema":{"version":1,"nodes":{"com.fluidframework.example.cli.List":{"object":{"":{"kind":"Sequence","types":["com.fluidframework.leaf.string"]}}},"com.fluidframework.leaf.string":{"leaf":1}},"root":{"kind":"Value","types":["com.fluidframework.example.cli.List"]}},"idCompressor":"AAAAAAAAAEAAAAAAAADwPwAAAAAAAPA/AAAAAAAAAAALBQHNgtUdL+5qUjl7BwYAAAAAAAAAAAAAAAAAAADwPwAAAAAAAAAA"} \ No newline at end of file +{"tree":{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"c":{"type":"com.fluidframework.leaf.string","value":true}}],"data":[[0,["x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x"]]]},"schema":{"version":1,"nodes":{"com.fluidframework.example.cli.List":{"object":{"":{"kind":"Sequence","types":["com.fluidframework.leaf.string"]}}},"com.fluidframework.leaf.string":{"leaf":1}},"root":{"kind":"Value","types":["com.fluidframework.example.cli.List"]}},"idCompressor":"AAAAAAAAAEAAAAAAAADwPwAAAAAAAPA/AAAAAAAAAADBZW3W6gY69ueaU3IZGwcDAAAAAAAAAAAAAAAAAADwPwAAAAAAAAAA"} \ No newline at end of file diff --git a/packages/dds/tree/list.compressed.json b/packages/dds/tree/list.compressed.json index 5455df4d3487..57dc0ed0640e 100644 --- a/packages/dds/tree/list.compressed.json +++ b/packages/dds/tree/list.compressed.json @@ -1 +1 @@ -{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"c":{"type":"com.fluidframework.leaf.string","value":true}}],"data":[[0,["x","x","x","x","x","x","x","x","x","x","x","x","x","x"]]]} \ No newline at end of file +{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"c":{"type":"com.fluidframework.leaf.string","value":true}}],"data":[[0,["x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x"]]]} \ No newline at end of file diff --git a/packages/dds/tree/list.json b/packages/dds/tree/list.json index b8d3d1fc3d78..41b8cb1241ee 100644 --- a/packages/dds/tree/list.json +++ b/packages/dds/tree/list.json @@ -1 +1 @@ -{"type":"com.fluidframework.example.cli.List","fields":["x","x","x","x","x","x","x","x","x","x","x","x","x","x"]} \ No newline at end of file +{"type":"com.fluidframework.example.cli.List","fields":["x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x"]} \ No newline at end of file diff --git a/packages/dds/tree/list.verbose.json b/packages/dds/tree/list.verbose.json index b8d3d1fc3d78..41b8cb1241ee 100644 --- a/packages/dds/tree/list.verbose.json +++ b/packages/dds/tree/list.verbose.json @@ -1 +1 @@ -{"type":"com.fluidframework.example.cli.List","fields":["x","x","x","x","x","x","x","x","x","x","x","x","x","x"]} \ No newline at end of file +{"type":"com.fluidframework.example.cli.List","fields":["x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x"]} \ No newline at end of file diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index 199717a2d5d0..79baadfd9e4a 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -135,6 +135,7 @@ export { type ParseOptions, type VerboseTree, extractPersistedSchema, + comparePersistedSchema, // Back to normal types type JsonTreeSchema, type JsonSchemaId, diff --git a/packages/dds/tree/src/shared-tree/schematizingTreeView.ts b/packages/dds/tree/src/shared-tree/schematizingTreeView.ts index 483d21b9ad99..3903deb95b77 100644 --- a/packages/dds/tree/src/shared-tree/schematizingTreeView.ts +++ b/packages/dds/tree/src/shared-tree/schematizingTreeView.ts @@ -38,6 +38,7 @@ import { type TreeViewConfiguration, mapTreeFromNodeData, prepareContentForHydration, + comparePersistedSchemaInternal, } from "../simple-tree/index.js"; import { Breakable, breakingClass, disposeSymbol, type WithBreakable } from "../util/index.js"; @@ -219,22 +220,12 @@ export class SchematizingSimpleTreeView( const mintRevisionTag = (): RevisionTag => idCompressor.generateCompressedId(); const revisionTagCodec = new RevisionTagCodec(idCompressor); - const schemaCodec = makeSchemaCodec(options); const fieldBatchCodec = makeFieldBatchCodec(options, 1); + const schemaCodec = makeSchemaCodec(options); const schema = new TreeStoredSchemaRepository(schemaCodec.decode(content.schema as Format)); const forest = buildConfiguredForest( 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 307c9a553bcd..ed0739096e1d 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 @@ -21,11 +21,6 @@ export enum AttachState { Detached = "Detached" } -// @beta -export function clone(original: TreeFieldFromImplicitField, options?: { - replaceIdentifiers?: true; -}): TreeFieldFromImplicitField; - // @public export enum CommitKind { Default = 0, @@ -88,30 +83,6 @@ export abstract class ErasedType { protected abstract brand(dummy: never): Name; } -// @beta -export function exportCompressed(node: TreeNode | TreeLeafValue, options: { - oldestCompatibleClient: FluidClientVersion; - idCompressor?: IIdCompressor; -}): JsonCompatible; - -// @beta -export function exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; -}): ConciseTree; - -// @beta -export function exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter?: undefined; - useStableFieldKeys?: boolean; -}): JsonCompatible; - -// @beta -export function exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; - -// @beta -export function exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; - // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -475,12 +446,6 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; // @public export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; -// @beta -export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; - -// @beta -export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - // @alpha export function independentInitializedView(config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent): TreeView; @@ -970,14 +935,25 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>) => () => void; - readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; - readonly importConcise: (schema: TSchema_1, data: InsertableTreeFieldFromImplicitField | ConciseTree) => TreeFieldFromImplicitField; - readonly importVerbose: typeof importVerbose; - readonly clone: typeof clone; - readonly exportVerbose: typeof exportVerbose; - readonly exportConcise: typeof exportConcise; - readonly exportCompressed: typeof exportCompressed; + on, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; + create(schema: TSchema, data: InsertableTreeFieldFromImplicitField): Unhydrated>; + importConcise(schema: TSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; + exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; + }): ConciseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter?: undefined; + useStableFieldKeys?: boolean; + }): JsonCompatible; + exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + exportCompressed(tree: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; + }): JsonCompatible; }; // @public @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 78e6e3f74509..ffd6c4d42ec0 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 @@ -21,11 +21,6 @@ export enum AttachState { Detached = "Detached" } -// @beta -export function clone(original: TreeFieldFromImplicitField, options?: { - replaceIdentifiers?: true; -}): TreeFieldFromImplicitField; - // @public export enum CommitKind { Default = 0, @@ -88,30 +83,6 @@ export abstract class ErasedType { protected abstract brand(dummy: never): Name; } -// @beta -export function exportCompressed(node: TreeNode | TreeLeafValue, options: { - oldestCompatibleClient: FluidClientVersion; - idCompressor?: IIdCompressor; -}): JsonCompatible; - -// @beta -export function exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; -}): ConciseTree; - -// @beta -export function exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter?: undefined; - useStableFieldKeys?: boolean; -}): JsonCompatible; - -// @beta -export function exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; - -// @beta -export function exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; - // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -449,12 +420,6 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; // @public export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; -// @beta -export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; - -// @beta -export function importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - // @public export type InitialObjects = { [K in keyof T["initialObjects"]]: T["initialObjects"][K] extends SharedObjectKind ? TChannel : never; @@ -860,14 +825,25 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>) => () => void; - readonly create: (schema: TSchema, data: InsertableTreeFieldFromImplicitField) => Unhydrated>; - readonly importConcise: (schema: TSchema_1, data: InsertableTreeFieldFromImplicitField | ConciseTree) => TreeFieldFromImplicitField; - readonly importVerbose: typeof importVerbose; - readonly clone: typeof clone; - readonly exportVerbose: typeof exportVerbose; - readonly exportConcise: typeof exportConcise; - readonly exportCompressed: typeof exportCompressed; + on, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; + create(schema: TSchema, data: InsertableTreeFieldFromImplicitField): Unhydrated>; + importConcise(schema: TSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; + exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter(handle: IFluidHandle): T; + readonly useStableFieldKeys?: boolean; + }): ConciseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: { + handleConverter?: undefined; + useStableFieldKeys?: boolean; + }): JsonCompatible; + exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + exportCompressed(tree: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; + }): JsonCompatible; }; // @public @sealed diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef70e292fa92..c0aac251b643 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1122,6 +1122,103 @@ importers: specifier: ^6.0.1 version: 6.0.1 + examples/apps/tree-cli-app: + dependencies: + '@fluidframework/aqueduct': + specifier: workspace:~ + version: link:../../../packages/framework/aqueduct + '@fluidframework/core-interfaces': + specifier: workspace:~ + version: link:../../../packages/common/core-interfaces + '@fluidframework/datastore-definitions': + specifier: workspace:~ + version: link:../../../packages/runtime/datastore-definitions + '@fluidframework/fluid-static': + specifier: workspace:~ + version: link:../../../packages/framework/fluid-static + '@fluidframework/id-compressor': + specifier: workspace:~ + version: link:../../../packages/runtime/id-compressor + '@fluidframework/runtime-definitions': + specifier: workspace:~ + version: link:../../../packages/runtime/runtime-definitions + '@fluidframework/runtime-utils': + specifier: workspace:~ + version: link:../../../packages/runtime/runtime-utils + '@fluidframework/shared-object-base': + specifier: workspace:~ + version: link:../../../packages/dds/shared-object-base + '@fluidframework/tree': + specifier: workspace:~ + version: link:../../../packages/dds/tree + '@sinclair/typebox': + specifier: ^0.32.29 + version: 0.32.29 + devDependencies: + '@arethetypeswrong/cli': + specifier: ^0.15.2 + version: 0.15.2 + '@biomejs/biome': + specifier: ~1.8.3 + version: 1.8.3 + '@fluid-internal/mocha-test-setup': + specifier: workspace:~ + version: link:../../../packages/test/mocha-test-setup + '@fluid-tools/build-cli': + specifier: ^0.46.0 + version: 0.46.0(@types/node@18.19.1)(typescript@5.4.5)(webpack-cli@5.1.4) + '@fluidframework/build-common': + specifier: ^2.0.3 + version: 2.0.3 + '@fluidframework/build-tools': + specifier: ^0.46.0 + version: 0.46.0 + '@fluidframework/eslint-config-fluid': + specifier: ^5.4.0 + version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) + '@types/mocha': + specifier: ^9.1.1 + version: 9.1.1 + '@types/node': + specifier: ^18.19.0 + version: 18.19.1 + concurrently: + specifier: ^8.2.1 + version: 8.2.2 + copyfiles: + specifier: ^2.4.1 + version: 2.4.1 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + eslint: + specifier: ~8.55.0 + version: 8.55.0 + eslint-config-prettier: + specifier: ~9.0.0 + version: 9.0.0(eslint@8.55.0) + mocha: + specifier: ^10.2.0 + version: 10.2.0 + mocha-json-output-reporter: + specifier: ^2.0.1 + version: 2.1.0(mocha@10.2.0)(moment@2.29.4) + mocha-multi-reporters: + specifier: ^1.5.1 + version: 1.5.1(mocha@10.2.0) + moment: + specifier: ^2.21.0 + version: 2.29.4 + prettier: + specifier: ~3.0.3 + version: 3.0.3 + rimraf: + specifier: ^4.4.0 + version: 4.4.1 + typescript: + specifier: ~5.4.5 + version: 5.4.5 + examples/apps/tree-comparison: dependencies: '@fluid-example/example-utils': @@ -22844,7 +22941,7 @@ packages: resolution: {integrity: sha512-9iVaCrRPkyE/n5v+S+gIAtvlNWMe3KQgGufkn8IVuKyvy7J48gpf0y7VKyL9hySjdwwCl+63vFdttkl6Yb7Ffg==} dependencies: assert-never: 1.2.1 - chokidar: 3.5.3 + chokidar: 3.6.0 fs-extra: 11.2.0 dev: true @@ -33232,7 +33329,7 @@ packages: peerDependencies: mocha: '>=3.1.2' dependencies: - debug: 4.3.5 + debug: 4.3.6(supports-color@8.1.1) lodash: 4.17.21 mocha: 10.2.0 transitivePeerDependencies: From 2d0f5bec97344f211c15548f2aef3eb9cc8cfe6a Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:05:52 -0700 Subject: [PATCH 28/54] Real app --- biome.jsonc | 2 + examples/apps/tree-cli-app/.mocharc.cjs | 5 +- .../apps/tree-cli-app/data/list.combo.json | 1 + .../tree-cli-app/data/list.compressed.json | 1 + .../apps/tree-cli-app/data/list.verbose.json | 1 + .../apps/tree-cli-app/data/test.verbose.json | 1 + .../apps/tree-cli-app/data/test2.concise.json | 1 + examples/apps/tree-cli-app/package.json | 1 + .../apps/tree-cli-app/prettier.config.cjs | 8 - examples/apps/tree-cli-app/src/index.ts | 101 ++--------- .../tree-cli-app/src/test/tsconfig.cjs.json | 12 -- examples/apps/tree-cli-app/src/utils.ts | 168 ++++++++++++++++++ examples/apps/tree-cli-app/tsconfig.cjs.json | 7 - packages/dds/tree/src/test/app.ts | 109 ------------ 14 files changed, 188 insertions(+), 230 deletions(-) create mode 100644 examples/apps/tree-cli-app/data/list.combo.json create mode 100644 examples/apps/tree-cli-app/data/list.compressed.json create mode 100644 examples/apps/tree-cli-app/data/list.verbose.json create mode 100644 examples/apps/tree-cli-app/data/test.verbose.json create mode 100644 examples/apps/tree-cli-app/data/test2.concise.json delete mode 100644 examples/apps/tree-cli-app/prettier.config.cjs delete mode 100644 examples/apps/tree-cli-app/src/test/tsconfig.cjs.json create mode 100644 examples/apps/tree-cli-app/src/utils.ts delete mode 100644 examples/apps/tree-cli-app/tsconfig.cjs.json delete mode 100644 packages/dds/tree/src/test/app.ts diff --git a/biome.jsonc b/biome.jsonc index 6dc671dfdfbd..c277009c1d80 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -57,6 +57,8 @@ "packages/tools/fluid-runner/src/test/localOdspSnapshots/**", "packages/tools/fluid-runner/src/test/telemetryExpectedOutputs/**", "tools/api-markdown-documenter/src/test/snapshots/**", + // TODO: why does examples/apps/tree-cli-app/*.json not work? + "**/data/*.json", // Generated type-tests "**/*.generated.ts", diff --git a/examples/apps/tree-cli-app/.mocharc.cjs b/examples/apps/tree-cli-app/.mocharc.cjs index adc57b4616e9..a2bcb0e04316 100644 --- a/examples/apps/tree-cli-app/.mocharc.cjs +++ b/examples/apps/tree-cli-app/.mocharc.cjs @@ -9,9 +9,6 @@ const getFluidTestMochaConfig = require("@fluid-internal/mocha-test-setup/mochar const packageDir = __dirname; const config = getFluidTestMochaConfig(packageDir); -config.spec = process.env.MOCHA_SPEC ?? "lib/test"; -// TODO: figure out why this package needs the --exit flag, tests might not be cleaning up correctly after themselves. -// AB#7856 -config.exit = true; +config.spec = "lib/test"; module.exports = config; diff --git a/examples/apps/tree-cli-app/data/list.combo.json b/examples/apps/tree-cli-app/data/list.combo.json new file mode 100644 index 000000000000..60398a8de9b3 --- /dev/null +++ b/examples/apps/tree-cli-app/data/list.combo.json @@ -0,0 +1 @@ +{"tree":{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"c":{"type":"com.fluidframework.leaf.string","value":true}}],"data":[[0,["x"]]]},"schema":{"version":1,"nodes":{"com.fluidframework.example.cli.List":{"object":{"":{"kind":"Sequence","types":["com.fluidframework.leaf.string"]}}},"com.fluidframework.leaf.string":{"leaf":1}},"root":{"kind":"Value","types":["com.fluidframework.example.cli.List"]}},"idCompressor":"AAAAAAAAAEAAAAAAAADwPwAAAAAAAPA/AAAAAAAAAAC3byk/I3d3gUqlkUUy4+IAAAAAAAAAAAAAAAAAAADwPwAAAAAAAAAA"} \ No newline at end of file diff --git a/examples/apps/tree-cli-app/data/list.compressed.json b/examples/apps/tree-cli-app/data/list.compressed.json new file mode 100644 index 000000000000..d54ac8d87e32 --- /dev/null +++ b/examples/apps/tree-cli-app/data/list.compressed.json @@ -0,0 +1 @@ +{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"c":{"type":"com.fluidframework.leaf.string","value":true}}],"data":[[0,["x"]]]} \ No newline at end of file diff --git a/examples/apps/tree-cli-app/data/list.verbose.json b/examples/apps/tree-cli-app/data/list.verbose.json new file mode 100644 index 000000000000..bddf3e4640b6 --- /dev/null +++ b/examples/apps/tree-cli-app/data/list.verbose.json @@ -0,0 +1 @@ +{"type":"com.fluidframework.example.cli.List","fields":["x"]} \ No newline at end of file diff --git a/examples/apps/tree-cli-app/data/test.verbose.json b/examples/apps/tree-cli-app/data/test.verbose.json new file mode 100644 index 000000000000..5e552cdf566d --- /dev/null +++ b/examples/apps/tree-cli-app/data/test.verbose.json @@ -0,0 +1 @@ +{ "type": "com.fluidframework.example.cli.List", "fields": ["x"] } diff --git a/examples/apps/tree-cli-app/data/test2.concise.json b/examples/apps/tree-cli-app/data/test2.concise.json new file mode 100644 index 000000000000..172d152c1c5c --- /dev/null +++ b/examples/apps/tree-cli-app/data/test2.concise.json @@ -0,0 +1 @@ +["x","x"] \ No newline at end of file diff --git a/examples/apps/tree-cli-app/package.json b/examples/apps/tree-cli-app/package.json index c4d4cd1aff24..38b7f97bde1e 100644 --- a/examples/apps/tree-cli-app/package.json +++ b/examples/apps/tree-cli-app/package.json @@ -12,6 +12,7 @@ "author": "Microsoft and contributors", "type": "module", "scripts": { + "app": "node ./lib/index.js", "build": "fluid-build . --task build", "build:compile": "fluid-build . --task compile", "build:esnext": "tsc --project ./tsconfig.json", diff --git a/examples/apps/tree-cli-app/prettier.config.cjs b/examples/apps/tree-cli-app/prettier.config.cjs deleted file mode 100644 index d4870022599f..000000000000 --- a/examples/apps/tree-cli-app/prettier.config.cjs +++ /dev/null @@ -1,8 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -module.exports = { - ...require("@fluidframework/build-common/prettier.config.cjs"), -}; diff --git a/examples/apps/tree-cli-app/src/index.ts b/examples/apps/tree-cli-app/src/index.ts index c145530e916d..ba367d69cd9c 100644 --- a/examples/apps/tree-cli-app/src/index.ts +++ b/examples/apps/tree-cli-app/src/index.ts @@ -4,103 +4,24 @@ */ // This is a node powered CLI application, so using node makes sense: -// eslint-disable-next-line import/no-nodejs-modules -import { readFileSync, writeFileSync } from "node:fs"; +/* eslint-disable unicorn/no-process-exit */ -import type { IFluidHandle } from "@fluidframework/core-interfaces"; -import { - createIdCompressor, - deserializeIdCompressor, - type SerializedIdCompressorWithOngoingSession, -} from "@fluidframework/id-compressor/internal"; -import { isFluidHandle } from "@fluidframework/runtime-utils"; -import { - TreeBeta, - type JsonCompatible, - type VerboseTree, - extractPersistedSchema, - FluidClientVersion, - type ViewContent, - independentInitializedView, - type ForestOptions, - type ICodecOptions, - typeboxValidator, - // eslint-disable-next-line import/no-internal-modules -} from "@fluidframework/tree/alpha"; -import { type Static, Type } from "@sinclair/typebox"; +import { loadDocument, saveDocument } from "./utils.js"; -import { config, List } from "./schema.js"; +const args = process.argv.slice(2); -console.log("App"); +console.log(`Running with augments: ${args}`); -const path = "list.json"; +console.log(`Requires arguments: `); -// Data parsed from JSON is safe to consider JsonCompatible. -// If file is invalid JSON, that will throw and is fine for this app. -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -const data: JsonCompatible = JSON.parse(readFileSync(path).toString()); - -// const node = TreeBeta.create(List, data as InsertableTypedNode); -const node = TreeBeta.importVerbose(List, data as VerboseTree); - -node.insertAtEnd("x"); - -writeFileSync(path, JSON.stringify(TreeBeta.exportVerbose(node))); - -// Demo all formats: -writeFileSync("list.verbose.json", JSON.stringify(TreeBeta.exportVerbose(node))); -// writeFileSync("list.simple.json", JSON.stringify(TreeBeta.exportConcise(node))); -writeFileSync( - "list.compressed.json", - JSON.stringify( - TreeBeta.exportCompressed(node, { oldestCompatibleClient: FluidClientVersion.v2_3 }), - ), -); - -// Combo - -const File = Type.Object({ - tree: Type.Unsafe>(), - schema: Type.Unsafe(), - idCompressor: Type.Unsafe(), -}); -type File = Static; - -const idCompressor = createIdCompressor(); -// idCompressor.finalizeCreationRange(idCompressor.takeUnfinalizedCreationRange()); - -const file: File = { - tree: TreeBeta.exportCompressed(node, { - oldestCompatibleClient: FluidClientVersion.v2_3, - idCompressor, - }), - schema: extractPersistedSchema(List), - idCompressor: idCompressor.serialize(true), -}; - -function rejectHandles(key: string, value: unknown): unknown { - if (isFluidHandle(value)) { - throw new Error("Fluid handles are not supported"); - } - return value; +if (args.length > 3) { + process.exit(1); } -writeFileSync("list.combo.json", JSON.stringify(file, rejectHandles)); - -// Data parsed from JSON is safe to consider JsonCompatible. -// If file is invalid JSON, that will throw and is fine for this app. -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -const combo: File = JSON.parse(readFileSync("list.combo.json").toString()); +const [sourceArg, destinationArg, editArg] = args; -const content: ViewContent = { - schema: combo.schema, - tree: combo.tree, - idCompressor: deserializeIdCompressor(combo.idCompressor), -}; +const node = loadDocument(sourceArg); -const options: ForestOptions & ICodecOptions = { jsonValidator: typeboxValidator }; +node.insertAtEnd("x"); -const view = independentInitializedView(config, options, content); -const compat = view.compatibility; -const root = view.root; -console.log("Done"); +saveDocument(destinationArg, node); diff --git a/examples/apps/tree-cli-app/src/test/tsconfig.cjs.json b/examples/apps/tree-cli-app/src/test/tsconfig.cjs.json deleted file mode 100644 index fdd3a3c1c9fc..000000000000 --- a/examples/apps/tree-cli-app/src/test/tsconfig.cjs.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - // This config must be used in a "type": "commonjs" environment. (Use `fluid-tsc commonjs`.) - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/test", - }, - "references": [ - { - "path": "../../tsconfig.cjs.json", - }, - ], -} diff --git a/examples/apps/tree-cli-app/src/utils.ts b/examples/apps/tree-cli-app/src/utils.ts new file mode 100644 index 000000000000..8902ec0e3bde --- /dev/null +++ b/examples/apps/tree-cli-app/src/utils.ts @@ -0,0 +1,168 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +// This is a node powered CLI application, so using node makes sense: +/* eslint-disable unicorn/no-process-exit */ +/* eslint-disable import/no-nodejs-modules */ + +import { readFileSync, writeFileSync } from "node:fs"; + +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import type { SerializedIdCompressorWithOngoingSession } from "@fluidframework/id-compressor/internal"; +import { + createIdCompressor, + deserializeIdCompressor, +} from "@fluidframework/id-compressor/internal"; +import { isFluidHandle } from "@fluidframework/runtime-utils"; +import { + extractPersistedSchema, + FluidClientVersion, + independentInitializedView, + TreeBeta, + typeboxValidator, + type ForestOptions, + type ICodecOptions, + type JsonCompatible, + type VerboseTree, + type ViewContent, + // eslint-disable-next-line import/no-internal-modules +} from "@fluidframework/tree/alpha"; +import { type Static, Type } from "@sinclair/typebox"; + +import { config, List } from "./schema.js"; + +/** + * Load from file. + */ +export function loadDocument(source: string | undefined): List { + if (source === undefined || source === "default") { + return new List([]); + } + const parts = source.split("."); + if (parts.length < 3 || parts.at(-1) !== "json") { + console.log(`Invalid source: ${source}`); + process.exit(1); + } + + // Data parsed from JSON is safe to consider JsonCompatible. + // If file is invalid JSON, that will throw and is fine for this app. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const fileData: JsonCompatible = JSON.parse(readFileSync(source).toString()); + + switch (parts.at(-2)) { + case "concise": { + return TreeBeta.importConcise(List, fileData); + } + case "verbose": { + return TreeBeta.importVerbose(List, fileData as VerboseTree); + } + case "compressed": { + const content: ViewContent = { + schema: extractPersistedSchema(List), + tree: fileData, + idCompressor: createIdCompressor(), + }; + const view = independentInitializedView(config, options, content); + return view.root; + } + case "snapshot": { + // TODO: validate + const combo: File = fileData as File; + + const content: ViewContent = { + schema: combo.schema, + tree: combo.tree, + idCompressor: deserializeIdCompressor(combo.idCompressor), + }; + const view = independentInitializedView(config, options, content); + return view.root; + } + default: { + console.log(`Invalid source format: ${parts.at(-2)}`); + process.exit(1); + } + } +} + +/** + * Save to file. + */ +export function saveDocument(destination: string | undefined, tree: List): void { + if (destination === undefined || destination === "default") { + console.log("Tree Content:"); + console.log(tree); + return; + } + const parts = destination.split("."); + if (parts.length < 3 || parts.at(-1) !== "json") { + console.log(`Invalid destination: ${destination}`); + process.exit(1); + } + + const fileData: JsonCompatible = exportContent(destination, tree); + console.log(`Writing: ${destination}`); + writeFileSync(destination, JSON.stringify(fileData, rejectHandles)); +} + +/** + * Encode to format based on file name. + */ +export function exportContent(destination: string, tree: List): JsonCompatible { + const parts = destination.split("."); + if (parts.length < 3 || parts.at(-1) !== "json") { + console.log(`Invalid destination: ${destination}`); + process.exit(1); + } + + switch (parts.at(-2)) { + case "concise": { + return TreeBeta.exportConcise(tree) as JsonCompatible; + } + case "verbose": { + return TreeBeta.exportVerbose(tree) as JsonCompatible; + } + case "compressed": { + return TreeBeta.exportCompressed(tree, { + ...options, + oldestCompatibleClient: FluidClientVersion.v2_3, + }) as JsonCompatible; + } + case "snapshot": { + const idCompressor = createIdCompressor(); // TODO: get from tree? + const file: File = { + tree: TreeBeta.exportCompressed(tree, { + oldestCompatibleClient: FluidClientVersion.v2_3, + idCompressor, + }), + schema: extractPersistedSchema(List), + idCompressor: idCompressor.serialize(true), + }; + return file as JsonCompatible; + } + default: { + console.log(`Invalid source format: ${parts.at(-2)}`); + process.exit(1); + } + } +} + +/** + * Throw if handle. + */ +export function rejectHandles(key: string, value: unknown): unknown { + if (isFluidHandle(value)) { + throw new Error("Fluid handles are not supported"); + } + return value; +} + +const options: ForestOptions & ICodecOptions = { jsonValidator: typeboxValidator }; + +const File = Type.Object({ + tree: Type.Unsafe>(), + schema: Type.Unsafe(), + idCompressor: Type.Unsafe(), +}); +type File = Static; diff --git a/examples/apps/tree-cli-app/tsconfig.cjs.json b/examples/apps/tree-cli-app/tsconfig.cjs.json deleted file mode 100644 index 6ec35ad731a3..000000000000 --- a/examples/apps/tree-cli-app/tsconfig.cjs.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - // This config must be used in a "type": "commonjs" environment. (Use `fluid-tsc commonjs`.) - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - }, -} diff --git a/packages/dds/tree/src/test/app.ts b/packages/dds/tree/src/test/app.ts deleted file mode 100644 index c2d195534153..000000000000 --- a/packages/dds/tree/src/test/app.ts +++ /dev/null @@ -1,109 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -import { type Static, Type } from "@sinclair/typebox"; - -import { - SchemaFactory, - TreeBeta, - // type InsertableTypedNode, - type JsonCompatible, - type VerboseTree, - extractPersistedSchema, - FluidClientVersion, - type ViewContent, - independentInitializedView, - TreeViewConfiguration, - type ForestOptions, - type ICodecOptions, - typeboxValidator, -} from "../index.js"; -import { readFileSync, writeFileSync } from "node:fs"; -import type { IFluidHandle } from "@fluidframework/core-interfaces"; -import { - createIdCompressor, - deserializeIdCompressor, - type SerializedIdCompressorWithOngoingSession, -} from "@fluidframework/id-compressor/internal"; -import { isFluidHandle } from "@fluidframework/runtime-utils"; - -console.log("App"); - -const schemaBuilder = new SchemaFactory("com.fluidframework.example.cli"); -class List extends schemaBuilder.array("List", schemaBuilder.string) {} - -const path = "list.json"; - -const data: JsonCompatible = JSON.parse(readFileSync(path).toString()); - -// const node = TreeBeta.create(List, data as InsertableTypedNode); -const node = TreeBeta.importVerbose(List, data as VerboseTree); - -node.insertAtEnd("x"); - -writeFileSync(path, JSON.stringify(TreeBeta.exportVerbose(node))); - -// Demo all formats: -writeFileSync("list.verbose.json", JSON.stringify(TreeBeta.exportVerbose(node))); -// writeFileSync("list.simple.json", JSON.stringify(TreeBeta.exportConcise(node))); -writeFileSync( - "list.compressed.json", - JSON.stringify( - TreeBeta.exportCompressed(node, { oldestCompatibleClient: FluidClientVersion.v2_3 }), - ), -); - -// Combo - -const File = Type.Object({ - tree: Type.Unsafe>(), - schema: Type.Unsafe(), - idCompressor: Type.Unsafe(), -}); -type File = Static; - -const idCompressor = createIdCompressor(); -// idCompressor.finalizeCreationRange(idCompressor.takeUnfinalizedCreationRange()); - -const file: File = { - tree: TreeBeta.exportCompressed(node, { - oldestCompatibleClient: FluidClientVersion.v2_3, - idCompressor, - }), - schema: extractPersistedSchema(List), - idCompressor: idCompressor.serialize(true), -}; - -function rejectHandles(key: string, value: unknown): unknown { - if (isFluidHandle(value)) { - throw new Error("Fluid handles are not supported"); - } - return value; -} - -writeFileSync("list.combo.json", JSON.stringify(file, rejectHandles)); - -// deserializeIdCompressor - -const combo: File = JSON.parse(readFileSync("list.combo.json").toString()); - -const content: ViewContent = { - schema: combo.schema, - tree: combo.tree, - idCompressor: deserializeIdCompressor(combo.idCompressor), -}; - -const config = new TreeViewConfiguration({ - schema: List, - enableSchemaValidation: true, - preventAmbiguity: true, -}); - -const options: ForestOptions & ICodecOptions = { jsonValidator: typeboxValidator }; - -const view = independentInitializedView(config, options, content); -const compat = view.compatibility; -const root = view.root; -console.log("Done"); From 93ad5407cc3e77f95b18f8e1ea3a4b5900f364f5 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:41:53 -0700 Subject: [PATCH 29/54] Bigger schema, support editing --- examples/apps/tree-cli-app/src/index.ts | 10 +- examples/apps/tree-cli-app/src/schema.ts | 20 ++- .../apps/tree-cli-app/src/test/schema.spec.ts | 114 +++++++++++++----- examples/apps/tree-cli-app/src/utils.ts | 53 ++++++++ 4 files changed, 159 insertions(+), 38 deletions(-) diff --git a/examples/apps/tree-cli-app/src/index.ts b/examples/apps/tree-cli-app/src/index.ts index ba367d69cd9c..9b02780fcbe3 100644 --- a/examples/apps/tree-cli-app/src/index.ts +++ b/examples/apps/tree-cli-app/src/index.ts @@ -6,14 +6,14 @@ // This is a node powered CLI application, so using node makes sense: /* eslint-disable unicorn/no-process-exit */ -import { loadDocument, saveDocument } from "./utils.js"; +import { applyEdit, loadDocument, saveDocument } from "./utils.js"; const args = process.argv.slice(2); +console.log(`Requires arguments: [] [] []`); +console.log(`Example arguments: default data/large.concise.json string:10,item:100`); console.log(`Running with augments: ${args}`); -console.log(`Requires arguments: `); - if (args.length > 3) { process.exit(1); } @@ -22,6 +22,8 @@ const [sourceArg, destinationArg, editArg] = args; const node = loadDocument(sourceArg); -node.insertAtEnd("x"); +if (editArg !== undefined) { + applyEdit(editArg, node); +} saveDocument(destinationArg, node); diff --git a/examples/apps/tree-cli-app/src/schema.ts b/examples/apps/tree-cli-app/src/schema.ts index e6ddae0fb48c..30cdac82082e 100644 --- a/examples/apps/tree-cli-app/src/schema.ts +++ b/examples/apps/tree-cli-app/src/schema.ts @@ -5,12 +5,28 @@ import { SchemaFactory, TreeViewConfiguration } from "@fluidframework/tree"; -const schemaBuilder = new SchemaFactory("com.fluidframework.example.cli"); +/** + * + */ +export const schemaBuilder = new SchemaFactory("com.fluidframework.example.cli"); + +class Point extends schemaBuilder.object("Point", { + x: schemaBuilder.number, + y: schemaBuilder.number, +}) {} + +/** + * Complex list item. + */ +export class Item extends schemaBuilder.object("Item", { + location: Point, + name: schemaBuilder.string, +}) {} /** * List node. */ -export class List extends schemaBuilder.array("List", schemaBuilder.string) {} +export class List extends schemaBuilder.array("List", [schemaBuilder.string, Item]) {} /** * Tree configuration. diff --git a/examples/apps/tree-cli-app/src/test/schema.spec.ts b/examples/apps/tree-cli-app/src/test/schema.spec.ts index 50743b965150..04eb7f874232 100644 --- a/examples/apps/tree-cli-app/src/test/schema.spec.ts +++ b/examples/apps/tree-cli-app/src/test/schema.spec.ts @@ -17,38 +17,6 @@ import { import { List } from "../schema.js"; -/** - * List of schema from previous versions of this application. - * Storing these as .json files ina folder may make more sense for more complex applications. - * - * The `schema` field is generated by passing the schema to `extractPersistedSchema`. - */ -const historicalSchema: { version: string; schema: JsonCompatible }[] = [ - { - version: "2.3", - schema: { - version: 1, - nodes: { - "com.fluidframework.example.cli.List": { - object: { - "": { - kind: "Sequence", - types: ["com.fluidframework.leaf.string"], - }, - }, - }, - "com.fluidframework.leaf.string": { - leaf: 1, - }, - }, - root: { - kind: "Value", - types: ["com.fluidframework.example.cli.List"], - }, - }, - }, -]; - describe("schema", () => { it("current schema matches latest historical schema", () => { const current = extractPersistedSchema(List); @@ -90,3 +58,85 @@ describe("schema", () => { } }); }); + +/** + * List of schema from previous versions of this application. + * Storing these as .json files ina folder may make more sense for more complex applications. + * + * The `schema` field is generated by passing the schema to `extractPersistedSchema`. + */ +const historicalSchema: { version: string; schema: JsonCompatible }[] = [ + { + version: "1.0", + schema: { + version: 1, + nodes: { + "com.fluidframework.example.cli.List": { + object: { + "": { + kind: "Sequence", + types: ["com.fluidframework.leaf.string"], + }, + }, + }, + "com.fluidframework.leaf.string": { + leaf: 1, + }, + }, + root: { + kind: "Value", + types: ["com.fluidframework.example.cli.List"], + }, + }, + }, + { + version: "2.0", + schema: { + version: 1, + nodes: { + "com.fluidframework.example.cli.Item": { + object: { + location: { + kind: "Value", + types: ["com.fluidframework.example.cli.Point"], + }, + name: { + kind: "Value", + types: ["com.fluidframework.leaf.string"], + }, + }, + }, + "com.fluidframework.example.cli.List": { + object: { + "": { + kind: "Sequence", + types: ["com.fluidframework.example.cli.Item", "com.fluidframework.leaf.string"], + }, + }, + }, + "com.fluidframework.example.cli.Point": { + object: { + x: { + kind: "Value", + types: ["com.fluidframework.leaf.number"], + }, + y: { + kind: "Value", + types: ["com.fluidframework.leaf.number"], + }, + }, + }, + "com.fluidframework.leaf.number": { + leaf: 0, + }, + "com.fluidframework.leaf.string": { + leaf: 1, + }, + }, + root: { + kind: "Value", + types: ["com.fluidframework.example.cli.List"], + }, + }, + }, +]; diff --git a/examples/apps/tree-cli-app/src/utils.ts b/examples/apps/tree-cli-app/src/utils.ts index 8902ec0e3bde..a680869c54c4 100644 --- a/examples/apps/tree-cli-app/src/utils.ts +++ b/examples/apps/tree-cli-app/src/utils.ts @@ -20,10 +20,12 @@ import { extractPersistedSchema, FluidClientVersion, independentInitializedView, + TreeArrayNode, TreeBeta, typeboxValidator, type ForestOptions, type ICodecOptions, + type InsertableTypedNode, type JsonCompatible, type VerboseTree, type ViewContent, @@ -31,6 +33,7 @@ import { } from "@fluidframework/tree/alpha"; import { type Static, Type } from "@sinclair/typebox"; +import type { Item } from "./schema.js"; import { config, List } from "./schema.js"; /** @@ -148,6 +151,56 @@ export function exportContent(destination: string, tree: List): JsonCompatible { } } +/** + * Encode to format based on file name. + */ +export function applyEdit(edits: string, tree: List): void { + for (const edit of edits.split(",")) { + console.log(`Applying edit ${edit}`); + const parts = edit.split(":"); + if (parts.length !== 2) { + throw new Error(`Invalid edit ${edit}`); + } + const [kind, countString] = parts; + const count = Number(countString); + if (count === 0 || !Number.isInteger(count)) { + throw new TypeError(`Invalid count in edit ${edit}`); + } + if (count > 0) { + let data: InsertableTypedNode | string; + switch (kind) { + case "string": { + data = "x"; + break; + } + case "item": { + data = { location: { x: 0, y: 0 }, name: "item" }; + break; + } + default: { + throw new TypeError(`Invalid kind in insert edit ${edit}`); + } + } + // eslint-disable-next-line unicorn/no-new-array + tree.insertAtEnd(TreeArrayNode.spread(new Array(count).fill(data))); + } else { + switch (kind) { + case "start": { + tree.removeRange(0, -count); + break; + } + case "end": { + tree.removeRange(tree.length + count, -count); + break; + } + default: { + throw new TypeError(`Invalid end in remove edit ${edit}`); + } + } + } + } +} + /** * Throw if handle. */ From 84f4c49dbe3a16f3fa2049a6fbcb32070a15e0b2 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:44:35 -0700 Subject: [PATCH 30/54] Cleanup imports, add larger example files --- .../tree-cli-app/data/large.compressed.json | 458 ++++++++ .../apps/tree-cli-app/data/large.concise.json | 112 ++ .../apps/tree-cli-app/data/large.verbose.json | 1015 +++++++++++++++++ examples/apps/tree-cli-app/src/utils.ts | 3 +- 4 files changed, 1586 insertions(+), 2 deletions(-) create mode 100644 examples/apps/tree-cli-app/data/large.compressed.json create mode 100644 examples/apps/tree-cli-app/data/large.concise.json create mode 100644 examples/apps/tree-cli-app/data/large.verbose.json diff --git a/examples/apps/tree-cli-app/data/large.compressed.json b/examples/apps/tree-cli-app/data/large.compressed.json new file mode 100644 index 000000000000..db6f91902972 --- /dev/null +++ b/examples/apps/tree-cli-app/data/large.compressed.json @@ -0,0 +1,458 @@ +{ + "version": 1, + "identifiers": [], + "shapes": [ + { + "c": { + "type": "com.fluidframework.example.cli.Item", + "value": false, + "fields": [["location", 4], ["name", 1]] + } + }, + { "c": { "type": "com.fluidframework.leaf.string", "value": true } }, + { "c": { "type": "com.fluidframework.leaf.number", "value": true } }, + { + "c": { + "type": "com.fluidframework.example.cli.List", + "value": false, + "fields": [["", 5]] + } + }, + { + "c": { + "type": "com.fluidframework.example.cli.Point", + "value": false, + "fields": [["x", 2], ["y", 2]] + } + }, + { "a": 6 }, + { "d": 0 } + ], + "data": [ + [ + 3, + [ + 1, + "x", + 1, + "x", + 1, + "x", + 1, + "x", + 1, + "x", + 1, + "x", + 1, + "x", + 1, + "x", + 1, + "x", + 1, + "x", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item", + 0, + 0, + 0, + "item" + ] + ] + ] +} diff --git a/examples/apps/tree-cli-app/data/large.concise.json b/examples/apps/tree-cli-app/data/large.concise.json new file mode 100644 index 000000000000..8219b2264520 --- /dev/null +++ b/examples/apps/tree-cli-app/data/large.concise.json @@ -0,0 +1,112 @@ +[ + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" } +] diff --git a/examples/apps/tree-cli-app/data/large.verbose.json b/examples/apps/tree-cli-app/data/large.verbose.json new file mode 100644 index 000000000000..7d8e47264644 --- /dev/null +++ b/examples/apps/tree-cli-app/data/large.verbose.json @@ -0,0 +1,1015 @@ +{ + "type": "com.fluidframework.example.cli.List", + "fields": [ + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + } + ] +} diff --git a/examples/apps/tree-cli-app/src/utils.ts b/examples/apps/tree-cli-app/src/utils.ts index a680869c54c4..603bdd278a84 100644 --- a/examples/apps/tree-cli-app/src/utils.ts +++ b/examples/apps/tree-cli-app/src/utils.ts @@ -16,16 +16,15 @@ import { deserializeIdCompressor, } from "@fluidframework/id-compressor/internal"; import { isFluidHandle } from "@fluidframework/runtime-utils"; +import { TreeArrayNode, type InsertableTypedNode } from "@fluidframework/tree"; import { extractPersistedSchema, FluidClientVersion, independentInitializedView, - TreeArrayNode, TreeBeta, typeboxValidator, type ForestOptions, type ICodecOptions, - type InsertableTypedNode, type JsonCompatible, type VerboseTree, type ViewContent, From 81e9bf221006f04ccf594fed7cc6b310daae3927 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:13:44 -0700 Subject: [PATCH 31/54] Support stored key formats --- .../data/large.concise-stored.json | 112 ++ .../apps/tree-cli-app/data/large.concise.json | 200 ++-- .../data/large.verbose-stored.json | 1015 +++++++++++++++++ .../apps/tree-cli-app/data/large.verbose.json | 200 ++-- examples/apps/tree-cli-app/src/schema.ts | 2 +- examples/apps/tree-cli-app/src/utils.ts | 13 +- .../dds/tree/api-report/tree.alpha.api.md | 10 +- packages/dds/tree/api-report/tree.beta.api.md | 10 +- packages/dds/tree/list.combo.json | 1 - packages/dds/tree/list.compressed.json | 1 - packages/dds/tree/list.json | 1 - packages/dds/tree/list.verbose.json | 1 - .../tree/src/simple-tree/api/treeApiBeta.ts | 19 +- .../api-report/fluid-framework.alpha.api.md | 13 +- .../api-report/fluid-framework.beta.api.md | 10 +- 15 files changed, 1359 insertions(+), 249 deletions(-) create mode 100644 examples/apps/tree-cli-app/data/large.concise-stored.json create mode 100644 examples/apps/tree-cli-app/data/large.verbose-stored.json delete mode 100644 packages/dds/tree/list.combo.json delete mode 100644 packages/dds/tree/list.compressed.json delete mode 100644 packages/dds/tree/list.json delete mode 100644 packages/dds/tree/list.verbose.json diff --git a/examples/apps/tree-cli-app/data/large.concise-stored.json b/examples/apps/tree-cli-app/data/large.concise-stored.json new file mode 100644 index 000000000000..8219b2264520 --- /dev/null +++ b/examples/apps/tree-cli-app/data/large.concise-stored.json @@ -0,0 +1,112 @@ +[ + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" }, + { "location": { "x": 0, "y": 0 }, "name": "item" } +] diff --git a/examples/apps/tree-cli-app/data/large.concise.json b/examples/apps/tree-cli-app/data/large.concise.json index 8219b2264520..c04c275b687a 100644 --- a/examples/apps/tree-cli-app/data/large.concise.json +++ b/examples/apps/tree-cli-app/data/large.concise.json @@ -9,104 +9,104 @@ "x", "x", "x", - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" } + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" }, + { "position": { "x": 0, "y": 0 }, "name": "item" } ] diff --git a/examples/apps/tree-cli-app/data/large.verbose-stored.json b/examples/apps/tree-cli-app/data/large.verbose-stored.json new file mode 100644 index 000000000000..7d8e47264644 --- /dev/null +++ b/examples/apps/tree-cli-app/data/large.verbose-stored.json @@ -0,0 +1,1015 @@ +{ + "type": "com.fluidframework.example.cli.List", + "fields": [ + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + }, + { + "type": "com.fluidframework.example.cli.Item", + "fields": { + "location": { + "type": "com.fluidframework.example.cli.Point", + "fields": { "x": 0, "y": 0 } + }, + "name": "item" + } + } + ] +} diff --git a/examples/apps/tree-cli-app/data/large.verbose.json b/examples/apps/tree-cli-app/data/large.verbose.json index 7d8e47264644..67460df52834 100644 --- a/examples/apps/tree-cli-app/data/large.verbose.json +++ b/examples/apps/tree-cli-app/data/large.verbose.json @@ -14,7 +14,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -24,7 +24,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -34,7 +34,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -44,7 +44,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -54,7 +54,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -64,7 +64,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -74,7 +74,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -84,7 +84,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -94,7 +94,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -104,7 +104,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -114,7 +114,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -124,7 +124,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -134,7 +134,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -144,7 +144,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -154,7 +154,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -164,7 +164,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -174,7 +174,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -184,7 +184,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -194,7 +194,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -204,7 +204,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -214,7 +214,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -224,7 +224,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -234,7 +234,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -244,7 +244,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -254,7 +254,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -264,7 +264,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -274,7 +274,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -284,7 +284,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -294,7 +294,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -304,7 +304,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -314,7 +314,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -324,7 +324,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -334,7 +334,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -344,7 +344,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -354,7 +354,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -364,7 +364,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -374,7 +374,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -384,7 +384,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -394,7 +394,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -404,7 +404,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -414,7 +414,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -424,7 +424,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -434,7 +434,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -444,7 +444,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -454,7 +454,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -464,7 +464,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -474,7 +474,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -484,7 +484,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -494,7 +494,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -504,7 +504,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -514,7 +514,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -524,7 +524,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -534,7 +534,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -544,7 +544,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -554,7 +554,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -564,7 +564,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -574,7 +574,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -584,7 +584,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -594,7 +594,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -604,7 +604,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -614,7 +614,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -624,7 +624,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -634,7 +634,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -644,7 +644,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -654,7 +654,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -664,7 +664,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -674,7 +674,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -684,7 +684,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -694,7 +694,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -704,7 +704,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -714,7 +714,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -724,7 +724,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -734,7 +734,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -744,7 +744,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -754,7 +754,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -764,7 +764,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -774,7 +774,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -784,7 +784,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -794,7 +794,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -804,7 +804,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -814,7 +814,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -824,7 +824,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -834,7 +834,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -844,7 +844,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -854,7 +854,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -864,7 +864,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -874,7 +874,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -884,7 +884,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -894,7 +894,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -904,7 +904,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -914,7 +914,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -924,7 +924,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -934,7 +934,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -944,7 +944,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -954,7 +954,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -964,7 +964,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -974,7 +974,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -984,7 +984,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -994,7 +994,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, @@ -1004,7 +1004,7 @@ { "type": "com.fluidframework.example.cli.Item", "fields": { - "location": { + "position": { "type": "com.fluidframework.example.cli.Point", "fields": { "x": 0, "y": 0 } }, diff --git a/examples/apps/tree-cli-app/src/schema.ts b/examples/apps/tree-cli-app/src/schema.ts index 30cdac82082e..c4bee3252559 100644 --- a/examples/apps/tree-cli-app/src/schema.ts +++ b/examples/apps/tree-cli-app/src/schema.ts @@ -19,7 +19,7 @@ class Point extends schemaBuilder.object("Point", { * Complex list item. */ export class Item extends schemaBuilder.object("Item", { - location: Point, + position: schemaBuilder.required(Point, { key: "location" }), name: schemaBuilder.string, }) {} diff --git a/examples/apps/tree-cli-app/src/utils.ts b/examples/apps/tree-cli-app/src/utils.ts index 603bdd278a84..26705bf18bd9 100644 --- a/examples/apps/tree-cli-app/src/utils.ts +++ b/examples/apps/tree-cli-app/src/utils.ts @@ -60,6 +60,11 @@ export function loadDocument(source: string | undefined): List { case "verbose": { return TreeBeta.importVerbose(List, fileData as VerboseTree); } + case "verbose-stored": { + return TreeBeta.importVerbose(List, fileData as VerboseTree, { + useStoredKeys: true, + }); + } case "compressed": { const content: ViewContent = { schema: extractPersistedSchema(List), @@ -125,6 +130,12 @@ export function exportContent(destination: string, tree: List): JsonCompatible { case "verbose": { return TreeBeta.exportVerbose(tree) as JsonCompatible; } + case "concise-stored": { + return TreeBeta.exportConcise(tree, { useStoredKeys: true }) as JsonCompatible; + } + case "verbose-stored": { + return TreeBeta.exportVerbose(tree, { useStoredKeys: true }) as JsonCompatible; + } case "compressed": { return TreeBeta.exportCompressed(tree, { ...options, @@ -173,7 +184,7 @@ export function applyEdit(edits: string, tree: List): void { break; } case "item": { - data = { location: { x: 0, y: 0 }, name: "item" }; + data = { position: { x: 0, y: 0 }, name: "item" }; break; } default: { diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index e068783908b6..9e65f60b0880 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -571,14 +571,8 @@ export const TreeBeta: { importConcise(schema: TSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; - }): ConciseTree; - exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter?: undefined; - useStableFieldKeys?: boolean; - }): JsonCompatible; + exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): JsonCompatible; exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; exportCompressed(tree: TreeNode | TreeLeafValue, options: { diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index 3b44992d7029..74ea0351cdd8 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -458,14 +458,8 @@ export const TreeBeta: { importConcise(schema: TSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; - }): ConciseTree; - exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter?: undefined; - useStableFieldKeys?: boolean; - }): JsonCompatible; + exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): JsonCompatible; exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; exportCompressed(tree: TreeNode | TreeLeafValue, options: { diff --git a/packages/dds/tree/list.combo.json b/packages/dds/tree/list.combo.json deleted file mode 100644 index 289dbc1d65f0..000000000000 --- a/packages/dds/tree/list.combo.json +++ /dev/null @@ -1 +0,0 @@ -{"tree":{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"c":{"type":"com.fluidframework.leaf.string","value":true}}],"data":[[0,["x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x"]]]},"schema":{"version":1,"nodes":{"com.fluidframework.example.cli.List":{"object":{"":{"kind":"Sequence","types":["com.fluidframework.leaf.string"]}}},"com.fluidframework.leaf.string":{"leaf":1}},"root":{"kind":"Value","types":["com.fluidframework.example.cli.List"]}},"idCompressor":"AAAAAAAAAEAAAAAAAADwPwAAAAAAAPA/AAAAAAAAAADBZW3W6gY69ueaU3IZGwcDAAAAAAAAAAAAAAAAAADwPwAAAAAAAAAA"} \ No newline at end of file diff --git a/packages/dds/tree/list.compressed.json b/packages/dds/tree/list.compressed.json deleted file mode 100644 index 57dc0ed0640e..000000000000 --- a/packages/dds/tree/list.compressed.json +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"c":{"type":"com.fluidframework.leaf.string","value":true}}],"data":[[0,["x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x"]]]} \ No newline at end of file diff --git a/packages/dds/tree/list.json b/packages/dds/tree/list.json deleted file mode 100644 index 41b8cb1241ee..000000000000 --- a/packages/dds/tree/list.json +++ /dev/null @@ -1 +0,0 @@ -{"type":"com.fluidframework.example.cli.List","fields":["x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x"]} \ No newline at end of file diff --git a/packages/dds/tree/list.verbose.json b/packages/dds/tree/list.verbose.json deleted file mode 100644 index 41b8cb1241ee..000000000000 --- a/packages/dds/tree/list.verbose.json +++ /dev/null @@ -1 +0,0 @@ -{"type":"com.fluidframework.example.cli.List","fields":["x","x","x","x","x","x","x","x","x","x","x","x","x","x","x","x"]} \ No newline at end of file diff --git a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts index 832e7d0d4f5a..3c65461f6b55 100644 --- a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts +++ b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts @@ -218,20 +218,17 @@ export const TreeBeta: { /** * Copy a snapshot of the current version of a TreeNode into a {@link ConciseTree}. */ - exportConcise( + exportConcise( node: TreeNode | TreeLeafValue, - options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; - }, - ): ConciseTree; + options?: EncodeOptions, + ): ConciseTree; /** * Same as generic overload, except leaves handles as is. */ exportConcise( node: TreeNode | TreeLeafValue, - options?: { handleConverter?: undefined; useStableFieldKeys?: boolean }, + options?: Partial>, ): JsonCompatible; /** @@ -262,6 +259,9 @@ export const TreeBeta: { * Export the content of the provided `tree` in a compressed JSON compatible format. * @remarks * If an `idCompressor` is provided, it will be used to compress identifiers and thus will be needed to decompress the data. + * + * Always uses "stored" keys. + * See {@link EncodeOptions.useStoredKeys} for details. */ exportCompressed( tree: TreeNode | TreeLeafValue, @@ -310,10 +310,7 @@ export const TreeBeta: { exportConcise( node: TreeNode | TreeLeafValue, - options?: { - handleConverter?(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; - }, + options?: Partial>, ): JsonCompatible { const config: EncodeOptions = { valueConverter(handle: IFluidHandle): T { 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 ed0739096e1d..1edb54693790 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 @@ -34,6 +34,9 @@ export interface CommitMetadata { readonly kind: CommitKind; } +// @alpha +export function comparePersistedSchema(persisted: JsonCompatible, view: JsonCompatible, options: ICodecOptions, canInitialize: boolean): SchemaCompatibilityStatus; + // @beta export type ConciseTree = Exclude | THandle | ConciseTree[] | { [key: string]: ConciseTree; @@ -940,14 +943,8 @@ export const TreeBeta: { importConcise(schema: TSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; - }): ConciseTree; - exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter?: undefined; - useStableFieldKeys?: boolean; - }): JsonCompatible; + exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): JsonCompatible; exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; exportCompressed(tree: TreeNode | TreeLeafValue, options: { 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 ffd6c4d42ec0..6a2bf207d396 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 @@ -830,14 +830,8 @@ export const TreeBeta: { importConcise(schema: TSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter(handle: IFluidHandle): T; - readonly useStableFieldKeys?: boolean; - }): ConciseTree; - exportConcise(node: TreeNode | TreeLeafValue, options?: { - handleConverter?: undefined; - useStableFieldKeys?: boolean; - }): JsonCompatible; + exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): JsonCompatible; exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; exportCompressed(tree: TreeNode | TreeLeafValue, options: { From 7ec74bf9ae40a015ef764e0f0259555b463182ae Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:24:56 -0700 Subject: [PATCH 32/54] misc cleanup --- examples/apps/tree-cli-app/package.json | 1 + feeds/internal-test.txt | 1 - .../tree/src/simple-tree/api/conciseTree.ts | 1 + .../tree/src/simple-tree/api/customTree.ts | 2 +- .../dds/tree/src/simple-tree/api/index.ts | 3 ++- .../tree/src/simple-tree/api/treeApiBeta.ts | 2 +- .../tree/src/simple-tree/api/verboseTree.ts | 21 ++----------------- .../test/simple-tree/api/verboseTree.spec.ts | 7 +++++-- 8 files changed, 13 insertions(+), 25 deletions(-) diff --git a/examples/apps/tree-cli-app/package.json b/examples/apps/tree-cli-app/package.json index 38b7f97bde1e..a82414c62b0b 100644 --- a/examples/apps/tree-cli-app/package.json +++ b/examples/apps/tree-cli-app/package.json @@ -1,6 +1,7 @@ { "name": "@fluid-example/tree-cli-app", "version": "2.4.0", + "private": true, "description": "SharedTree CLI app demo", "homepage": "https://fluidframework.com", "repository": { diff --git a/feeds/internal-test.txt b/feeds/internal-test.txt index 9efd0b6f67a5..3b3a8bbb5506 100644 --- a/feeds/internal-test.txt +++ b/feeds/internal-test.txt @@ -89,7 +89,6 @@ fluid-framework @fluid-experimental/property-common @fluid-experimental/property-changeset @fluid-example/table-document -@fluid-example/tree-cli-app @fluid-experimental/azure-scenario-runner @fluidframework/azure-service-utils @fluidframework/azure-local-service \ No newline at end of file diff --git a/packages/dds/tree/src/simple-tree/api/conciseTree.ts b/packages/dds/tree/src/simple-tree/api/conciseTree.ts index af52d8a05f2d..6bca25302812 100644 --- a/packages/dds/tree/src/simple-tree/api/conciseTree.ts +++ b/packages/dds/tree/src/simple-tree/api/conciseTree.ts @@ -46,6 +46,7 @@ export function conciseFromCursor( ...options, }; + // TODO: get schema map from context when available const schemaMap = new Map(); walkFieldSchema(rootSchema, { node(schema) { diff --git a/packages/dds/tree/src/simple-tree/api/customTree.ts b/packages/dds/tree/src/simple-tree/api/customTree.ts index acfd5d332ab2..dd009b1f7418 100644 --- a/packages/dds/tree/src/simple-tree/api/customTree.ts +++ b/packages/dds/tree/src/simple-tree/api/customTree.ts @@ -27,7 +27,7 @@ import { import { isObjectNodeSchema } from "../objectNodeTypes.js"; /** - * Options for how to interpret a `ConciseTree` without relying on schema. + * Options for how to interpret a `CustomTree` without relying on schema. * @beta */ export interface EncodeOptions { diff --git a/packages/dds/tree/src/simple-tree/api/index.ts b/packages/dds/tree/src/simple-tree/api/index.ts index f671ce6eee5b..61b510e0702a 100644 --- a/packages/dds/tree/src/simple-tree/api/index.ts +++ b/packages/dds/tree/src/simple-tree/api/index.ts @@ -47,11 +47,12 @@ export { getSimpleSchema } from "./getSimpleSchema.js"; export type { VerboseTreeNode, - EncodeOptions, ParseOptions, VerboseTree, } from "./verboseTree.js"; +export type { EncodeOptions } from "./customTree.js"; + export type { ConciseTree } from "./conciseTree.js"; export { TreeBeta, type NodeChangedData, type TreeChangeEventsBeta } from "./treeApiBeta.js"; diff --git a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts index 3c65461f6b55..a4abece48eaf 100644 --- a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts +++ b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts @@ -24,7 +24,6 @@ import { applySchemaToParserOptions, cursorFromVerbose, verboseFromCursor, - type EncodeOptions, type ParseOptions, type VerboseTree, type VerboseTreeNode, @@ -46,6 +45,7 @@ import { } from "../../feature-libraries/index.js"; import { createIdCompressor } from "@fluidframework/id-compressor/internal"; import { toStoredSchema } from "../toFlexSchema.js"; +import type { EncodeOptions } from "./customTree.js"; /** * Data included for {@link TreeChangeEventsBeta.nodeChanged}. diff --git a/packages/dds/tree/src/simple-tree/api/verboseTree.ts b/packages/dds/tree/src/simple-tree/api/verboseTree.ts index 88b3c10f0228..66a0906383aa 100644 --- a/packages/dds/tree/src/simple-tree/api/verboseTree.ts +++ b/packages/dds/tree/src/simple-tree/api/verboseTree.ts @@ -43,6 +43,7 @@ import { customFromCursorInner, type CustomTreeNode, type CustomTreeValue, + type EncodeOptions, } from "./customTree.js"; /** @@ -151,25 +152,6 @@ export interface SchemalessParseOptions { }; } -/** - * Options for how to interpret a `VerboseTree` without relying on schema. - * @beta - */ -export interface EncodeOptions { - /** - * Fixup custom input formats. - * @remarks - * See note on {@link ParseOptions.valueConverter}. - */ - valueConverter(data: IFluidHandle): TCustom; - /** - * If true, interpret the input keys of object nodes as stored keys. - * If false, interpret them as property keys. - * @defaultValue false. - */ - readonly useStoredKeys?: boolean; -} - /** * Use info from `schema` to convert `options` to {@link SchemalessParseOptions}. */ @@ -359,6 +341,7 @@ export function verboseFromCursor( ...options, }; + // TODO: get schema map from context when available const schemaMap = new Map(); walkFieldSchema(rootSchema, { node(schema) { diff --git a/packages/dds/tree/src/test/simple-tree/api/verboseTree.spec.ts b/packages/dds/tree/src/test/simple-tree/api/verboseTree.spec.ts index 0411cfedec71..acfaee6962b9 100644 --- a/packages/dds/tree/src/test/simple-tree/api/verboseTree.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/verboseTree.spec.ts @@ -9,13 +9,16 @@ import type { IFluidHandle } from "@fluidframework/core-interfaces"; import { testSpecializedCursor, type TestTree } from "../../cursorTestSuite.js"; -import { SchemaFactory, type TreeLeafValue } from "../../../simple-tree/index.js"; +import { + SchemaFactory, + type EncodeOptions, + type TreeLeafValue, +} from "../../../simple-tree/index.js"; import { applySchemaToParserOptions, cursorFromVerbose, verboseFromCursor, - type EncodeOptions, type ParseOptions, type VerboseTree, type VerboseTreeNode, From 2bd4a645216e9da85a24510cf57b68ead6978493 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:48:55 -0700 Subject: [PATCH 33/54] cleanup --- examples/apps/tree-cli-app/.npmignore | 6 --- examples/apps/tree-cli-app/LICENSE | 21 ---------- examples/apps/tree-cli-app/package.json | 15 ------- examples/apps/tree-cli-app/tsdoc.json | 4 -- .../dds/tree/api-report/tree.alpha.api.md | 2 +- packages/dds/tree/api-report/tree.beta.api.md | 2 +- .../tree/src/simple-tree/api/treeApiBeta.ts | 6 +-- .../api-report/fluid-framework.alpha.api.md | 2 +- .../api-report/fluid-framework.beta.api.md | 2 +- pnpm-lock.yaml | 39 ------------------- 10 files changed, 6 insertions(+), 93 deletions(-) delete mode 100644 examples/apps/tree-cli-app/.npmignore delete mode 100644 examples/apps/tree-cli-app/LICENSE delete mode 100644 examples/apps/tree-cli-app/tsdoc.json diff --git a/examples/apps/tree-cli-app/.npmignore b/examples/apps/tree-cli-app/.npmignore deleted file mode 100644 index a40f882cf599..000000000000 --- a/examples/apps/tree-cli-app/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -nyc -*.log -**/*.tsbuildinfo -src/test -dist/test -**/_api-extractor-temp/** diff --git a/examples/apps/tree-cli-app/LICENSE b/examples/apps/tree-cli-app/LICENSE deleted file mode 100644 index 60af0a6a40e9..000000000000 --- a/examples/apps/tree-cli-app/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -Copyright (c) Microsoft Corporation and contributors. All rights reserved. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/examples/apps/tree-cli-app/package.json b/examples/apps/tree-cli-app/package.json index a82414c62b0b..0a62826cabaf 100644 --- a/examples/apps/tree-cli-app/package.json +++ b/examples/apps/tree-cli-app/package.json @@ -21,13 +21,11 @@ "build:test:esm": "tsc --project ./src/test/tsconfig.json", "check:biome": "biome check .", "check:format": "npm run check:biome", - "check:prettier": "prettier --check . --cache --ignore-path ../../../.prettierignore", "clean": "rimraf --glob dist lib \"**/*.tsbuildinfo\" \"**/*.build.log\" nyc", "eslint": "eslint --format stylish src", "eslint:fix": "eslint --format stylish src --fix --fix-type problem,suggestion,layout", "format": "npm run format:biome", "format:biome": "biome check . --write", - "format:prettier": "prettier --write . --cache --ignore-path ../../../.prettierignore", "lint": "fluid-build . --task lint", "lint:fix": "fluid-build . --task eslint:fix --task format", "test": "npm run test:mocha", @@ -36,37 +34,24 @@ "test:mocha:verbose": "cross-env FLUID_TEST_VERBOSE=1 npm run test:mocha" }, "dependencies": { - "@fluidframework/aqueduct": "workspace:~", "@fluidframework/core-interfaces": "workspace:~", - "@fluidframework/datastore-definitions": "workspace:~", - "@fluidframework/fluid-static": "workspace:~", "@fluidframework/id-compressor": "workspace:~", - "@fluidframework/runtime-definitions": "workspace:~", "@fluidframework/runtime-utils": "workspace:~", - "@fluidframework/shared-object-base": "workspace:~", "@fluidframework/tree": "workspace:~", "@sinclair/typebox": "^0.32.29" }, "devDependencies": { - "@arethetypeswrong/cli": "^0.15.2", "@biomejs/biome": "~1.8.3", "@fluid-internal/mocha-test-setup": "workspace:~", - "@fluid-tools/build-cli": "^0.46.0", - "@fluidframework/build-common": "^2.0.3", "@fluidframework/build-tools": "^0.46.0", "@fluidframework/eslint-config-fluid": "^5.4.0", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", - "concurrently": "^8.2.1", - "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "eslint": "~8.55.0", - "eslint-config-prettier": "~9.0.0", "mocha": "^10.2.0", "mocha-json-output-reporter": "^2.0.1", "mocha-multi-reporters": "^1.5.1", - "moment": "^2.21.0", - "prettier": "~3.0.3", "rimraf": "^4.4.0", "typescript": "~5.4.5" }, diff --git a/examples/apps/tree-cli-app/tsdoc.json b/examples/apps/tree-cli-app/tsdoc.json deleted file mode 100644 index ecb918da5cb8..000000000000 --- a/examples/apps/tree-cli-app/tsdoc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", - "extends": ["../../../common/build/build-common/tsdoc-base.json"] -} diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 9e65f60b0880..267b102ad472 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -572,7 +572,7 @@ export const TreeBeta: { importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; - exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): JsonCompatible; + exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; exportCompressed(tree: TreeNode | TreeLeafValue, options: { diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index 74ea0351cdd8..1da016f714e0 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -459,7 +459,7 @@ export const TreeBeta: { importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; - exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): JsonCompatible; + exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; exportCompressed(tree: TreeNode | TreeLeafValue, options: { diff --git a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts index a4abece48eaf..ab138fb2d00d 100644 --- a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts +++ b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts @@ -229,7 +229,7 @@ export const TreeBeta: { exportConcise( node: TreeNode | TreeLeafValue, options?: Partial>, - ): JsonCompatible; + ): ConciseTree; /** * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. @@ -276,8 +276,6 @@ export const TreeBeta: { return treeNodeApi.on(node, eventName, listener); }, - // clone, - create( schema: TSchema, data: InsertableTreeFieldFromImplicitField, @@ -311,7 +309,7 @@ export const TreeBeta: { exportConcise( node: TreeNode | TreeLeafValue, options?: Partial>, - ): JsonCompatible { + ): ConciseTree { const config: EncodeOptions = { valueConverter(handle: IFluidHandle): T { return handle as T; 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 1edb54693790..9b6dfa6f75c8 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 @@ -944,7 +944,7 @@ export const TreeBeta: { importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; - exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): JsonCompatible; + exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; exportCompressed(tree: TreeNode | TreeLeafValue, options: { 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 6a2bf207d396..ee3764a4d99c 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 @@ -831,7 +831,7 @@ export const TreeBeta: { importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; - exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): JsonCompatible; + exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; exportCompressed(tree: TreeNode | TreeLeafValue, options: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0aac251b643..b70c85a789b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1124,30 +1124,15 @@ importers: examples/apps/tree-cli-app: dependencies: - '@fluidframework/aqueduct': - specifier: workspace:~ - version: link:../../../packages/framework/aqueduct '@fluidframework/core-interfaces': specifier: workspace:~ version: link:../../../packages/common/core-interfaces - '@fluidframework/datastore-definitions': - specifier: workspace:~ - version: link:../../../packages/runtime/datastore-definitions - '@fluidframework/fluid-static': - specifier: workspace:~ - version: link:../../../packages/framework/fluid-static '@fluidframework/id-compressor': specifier: workspace:~ version: link:../../../packages/runtime/id-compressor - '@fluidframework/runtime-definitions': - specifier: workspace:~ - version: link:../../../packages/runtime/runtime-definitions '@fluidframework/runtime-utils': specifier: workspace:~ version: link:../../../packages/runtime/runtime-utils - '@fluidframework/shared-object-base': - specifier: workspace:~ - version: link:../../../packages/dds/shared-object-base '@fluidframework/tree': specifier: workspace:~ version: link:../../../packages/dds/tree @@ -1155,21 +1140,12 @@ importers: specifier: ^0.32.29 version: 0.32.29 devDependencies: - '@arethetypeswrong/cli': - specifier: ^0.15.2 - version: 0.15.2 '@biomejs/biome': specifier: ~1.8.3 version: 1.8.3 '@fluid-internal/mocha-test-setup': specifier: workspace:~ version: link:../../../packages/test/mocha-test-setup - '@fluid-tools/build-cli': - specifier: ^0.46.0 - version: 0.46.0(@types/node@18.19.1)(typescript@5.4.5)(webpack-cli@5.1.4) - '@fluidframework/build-common': - specifier: ^2.0.3 - version: 2.0.3 '@fluidframework/build-tools': specifier: ^0.46.0 version: 0.46.0 @@ -1182,21 +1158,12 @@ importers: '@types/node': specifier: ^18.19.0 version: 18.19.1 - concurrently: - specifier: ^8.2.1 - version: 8.2.2 - copyfiles: - specifier: ^2.4.1 - version: 2.4.1 cross-env: specifier: ^7.0.3 version: 7.0.3 eslint: specifier: ~8.55.0 version: 8.55.0 - eslint-config-prettier: - specifier: ~9.0.0 - version: 9.0.0(eslint@8.55.0) mocha: specifier: ^10.2.0 version: 10.2.0 @@ -1206,12 +1173,6 @@ importers: mocha-multi-reporters: specifier: ^1.5.1 version: 1.5.1(mocha@10.2.0) - moment: - specifier: ^2.21.0 - version: 2.29.4 - prettier: - specifier: ~3.0.3 - version: 3.0.3 rimraf: specifier: ^4.4.0 version: 4.4.1 From 1c329005c1f340ded97182f51c7ebdc32a04142c Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:54:47 -0700 Subject: [PATCH 34/54] remove typeValidation block --- examples/apps/tree-cli-app/package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/examples/apps/tree-cli-app/package.json b/examples/apps/tree-cli-app/package.json index 0a62826cabaf..a40eb6316be5 100644 --- a/examples/apps/tree-cli-app/package.json +++ b/examples/apps/tree-cli-app/package.json @@ -54,10 +54,5 @@ "mocha-multi-reporters": "^1.5.1", "rimraf": "^4.4.0", "typescript": "~5.4.5" - }, - "typeValidation": { - "disabled": true, - "broken": {}, - "entrypoint": "internal" } } From dcd7518c309b67ef4e016a5a419ed99b1d0aad66 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:19:39 -0700 Subject: [PATCH 35/54] fix build --- packages/dds/tree/api-report/tree.alpha.api.md | 10 +++++----- packages/dds/tree/src/simple-tree/api/conciseTree.ts | 2 +- .../dds/tree/src/simple-tree/api/getStoredSchema.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 8e1f717c6fd7..3bff22b49790 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -772,6 +772,11 @@ export interface VerboseTreeNode { type: string; } +// @public @sealed +export interface ViewableTree { + viewWith(config: TreeViewConfiguration): TreeView; +} + // @alpha (undocumented) export interface ViewContent { readonly idCompressor: IIdCompressor; @@ -779,11 +784,6 @@ export interface ViewContent { readonly tree: JsonCompatible; } -// @public @sealed -export interface ViewableTree { - viewWith(config: TreeViewConfiguration): TreeView; -} - // @public @sealed export interface WithType { // @deprecated diff --git a/packages/dds/tree/src/simple-tree/api/conciseTree.ts b/packages/dds/tree/src/simple-tree/api/conciseTree.ts index 6bca25302812..62d6c01deb05 100644 --- a/packages/dds/tree/src/simple-tree/api/conciseTree.ts +++ b/packages/dds/tree/src/simple-tree/api/conciseTree.ts @@ -8,8 +8,8 @@ import type { IFluidHandle } from "@fluidframework/core-interfaces"; import type { ITreeCursor } from "../../core/index.js"; import type { TreeLeafValue, ImplicitAllowedTypes } from "../schemaTypes.js"; import type { TreeNodeSchema } from "../core/index.js"; -import { walkFieldSchema } from "../walkSchema.js"; import { customFromCursorInner, type EncodeOptions } from "./customTree.js"; +import { walkFieldSchema } from "../walkFieldSchema.js"; /** * Concise encoding of a {@link TreeNode} or {@link TreeLeafValue}. diff --git a/packages/dds/tree/src/simple-tree/api/getStoredSchema.ts b/packages/dds/tree/src/simple-tree/api/getStoredSchema.ts index 84ed3d3fa36e..ef468e80e13c 100644 --- a/packages/dds/tree/src/simple-tree/api/getStoredSchema.ts +++ b/packages/dds/tree/src/simple-tree/api/getStoredSchema.ts @@ -9,7 +9,6 @@ import { defaultSchemaPolicy, encodeTreeSchema, makeSchemaCodec, - ViewSchema, } from "../../feature-libraries/index.js"; // eslint-disable-next-line import/no-internal-modules import type { Format } from "../../feature-libraries/schema-index/index.js"; @@ -17,6 +16,7 @@ import type { JsonCompatible } from "../../util/index.js"; import type { ImplicitFieldSchema } from "../schemaTypes.js"; import { toStoredSchema } from "../toFlexSchema.js"; import type { SchemaCompatibilityStatus } from "./tree.js"; +import { ViewSchema } from "./view.js"; /** * Dumps the "persisted" schema subset of `schema` into a deterministic JSON compatible semi-human readable but unspecified format. @@ -49,7 +49,7 @@ export function extractPersistedSchema(schema: ImplicitFieldSchema): JsonCompati /** * Compares two schema extracted using {@link extractPersistedSchema}. * Reports the same compatibility that {@link TreeView.compatibility} would report if - * opening a document that used the `persisted` schema and provided `view` to {@link ITree.viewWith}. + * opening a document that used the `persisted` schema and provided `view` to {@link ViewableTree.viewWith}. * * @remarks * This uses the persisted formats for schema, meaning it only includes data which impacts compatibility. From e003bf407643bf3cfd70d8b04e2e0c14ec174400 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:24:33 -0700 Subject: [PATCH 36/54] update api report --- .../api-report/fluid-framework.alpha.api.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 ffb3c3b3fc25..43a732d0effa 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 @@ -1144,6 +1144,11 @@ export interface VerboseTreeNode { type: string; } +// @public @sealed +export interface ViewableTree { + viewWith(config: TreeViewConfiguration): TreeView; +} + // @alpha (undocumented) export interface ViewContent { readonly idCompressor: IIdCompressor; @@ -1151,11 +1156,6 @@ export interface ViewContent { readonly tree: JsonCompatible; } -// @public @sealed -export interface ViewableTree { - viewWith(config: TreeViewConfiguration): TreeView; -} - // @public @sealed export interface WithType { // @deprecated From 413e8e90cc50d83daa5a9574aaa87c28f0b2c8a1 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:21:24 -0700 Subject: [PATCH 37/54] fix build --- .../dds/tree/api-report/tree.alpha.api.md | 17 ------------- packages/dds/tree/src/index.ts | 17 +++++++++++-- .../api-report/fluid-framework.alpha.api.md | 25 ++++++------------- pnpm-lock.yaml | 10 ++++---- 4 files changed, 28 insertions(+), 41 deletions(-) diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 0c9ad79424a7..dcda7295911f 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -121,18 +121,6 @@ export enum FluidClientVersion { v2_3 = "v2_3" } -// @alpha (undocumented) -export interface ForestOptions { - readonly forest?: ForestType; -} - -// @alpha -export enum ForestType { - Expensive = 2, - Optimized = 1, - Reference = 0 -} - // @alpha export interface ForestOptions { readonly forest?: ForestType; @@ -168,11 +156,6 @@ export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; // @alpha export function independentInitializedView(config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent): TreeView; -// @alpha -export function independentView(config: TreeViewConfiguration, options: ForestOptions & { - idCompressor?: IIdCompressor | undefined; -}): TreeView; - // @public type _InlineTrick = 0; diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index 87c55d2404ef..aa86c89a6f50 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -139,6 +139,7 @@ export { type VerboseTree, extractPersistedSchema, comparePersistedSchema, + type ConciseTree, // Back to normal types type JsonTreeSchema, type JsonSchemaId, @@ -154,10 +155,22 @@ export { type JsonSchemaType, type JsonLeafSchemaType, getJsonSchema, + type LazyItem, + type Unenforced, } from "./simple-tree/index.js"; -export { SharedTree, configuredSharedTree } from "./treeFactory.js"; +export { + SharedTree, + configuredSharedTree, + independentInitializedView, + type ViewContent, +} from "./treeFactory.js"; -export type { ICodecOptions, JsonValidator, SchemaValidationFunction } from "./codec/index.js"; +export { + type ICodecOptions, + type JsonValidator, + type SchemaValidationFunction, + FluidClientVersion, +} from "./codec/index.js"; export { noopValidator } from "./codec/index.js"; export { typeboxValidator } from "./external-utilities/index.js"; 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 246cdc967376..7d609233ea4e 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 @@ -34,6 +34,14 @@ export interface CommitMetadata { readonly kind: CommitKind; } +// @alpha +export function comparePersistedSchema(persisted: JsonCompatible, view: JsonCompatible, options: ICodecOptions, canInitialize: boolean): SchemaCompatibilityStatus; + +// @beta +export type ConciseTree = Exclude | THandle | ConciseTree[] | { + [key: string]: ConciseTree; +}; + // @alpha export function configuredSharedTree(options: SharedTreeOptions): SharedObjectKind; @@ -165,18 +173,6 @@ export type FluidObject = { // @public export type FluidObjectProviderKeys = string extends TProp ? never : number extends TProp ? never : TProp extends keyof Required[TProp] ? Required[TProp] extends Required[TProp]>[TProp] ? TProp : never : never; -// @alpha (undocumented) -export interface ForestOptions { - readonly forest?: ForestType; -} - -// @alpha -export enum ForestType { - Expensive = 2, - Optimized = 1, - Reference = 0 -} - // @alpha export interface ForestOptions { readonly forest?: ForestType; @@ -465,11 +461,6 @@ export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; // @alpha export function independentInitializedView(config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent): TreeView; -// @alpha -export function independentView(config: TreeViewConfiguration, options: ForestOptions & { - idCompressor?: IIdCompressor | undefined; -}): TreeView; - // @public export type InitialObjects = { [K in keyof T["initialObjects"]]: T["initialObjects"][K] extends SharedObjectKind ? TChannel : never; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0f28d1b855d..6f0ba58117e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1139,7 +1139,7 @@ importers: version: link:../../../packages/dds/tree '@sinclair/typebox': specifier: ^0.32.29 - version: 0.32.29 + version: 0.32.35 devDependencies: '@biomejs/biome': specifier: ~1.8.3 @@ -1158,7 +1158,7 @@ importers: version: 9.1.1 '@types/node': specifier: ^18.19.0 - version: 18.19.1 + version: 18.19.54 cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -1167,13 +1167,13 @@ importers: version: 8.55.0 mocha: specifier: ^10.2.0 - version: 10.2.0 + version: 10.7.3 mocha-json-output-reporter: specifier: ^2.0.1 - version: 2.1.0(mocha@10.2.0)(moment@2.29.4) + version: 2.1.0(mocha@10.7.3)(moment@2.30.1) mocha-multi-reporters: specifier: ^1.5.1 - version: 1.5.1(mocha@10.2.0) + version: 1.5.1(mocha@10.7.3) rimraf: specifier: ^4.4.0 version: 4.4.1 From 0a60814cd116a9c139527d8d0554bf3db91a543b Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 3 Oct 2024 18:11:04 -0700 Subject: [PATCH 38/54] Fix build --- packages/dds/tree/api-report/tree.alpha.api.md | 6 ++++++ packages/dds/tree/src/simple-tree/api/conciseTree.ts | 1 + .../fluid-framework/api-report/fluid-framework.alpha.api.md | 6 ++++++ 3 files changed, 13 insertions(+) diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 88e08bd04d5a..62ef4a1e4634 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -48,6 +48,12 @@ export type ConciseTree = Exclude { } +// @beta +export interface EncodeOptions { + readonly useStoredKeys?: boolean; + valueConverter(data: IFluidHandle): TCustom; +} + // @alpha export function enumFromStrings(factory: SchemaFactory, members: readonly Members[]): ((value: TValue) => TreeNode & { readonly value: TValue; diff --git a/packages/dds/tree/src/simple-tree/api/conciseTree.ts b/packages/dds/tree/src/simple-tree/api/conciseTree.ts index 512a082e2bb3..ea28d6e31c2d 100644 --- a/packages/dds/tree/src/simple-tree/api/conciseTree.ts +++ b/packages/dds/tree/src/simple-tree/api/conciseTree.ts @@ -23,6 +23,7 @@ import { getUnhydratedContext } from "../createContext.js"; * @privateRemarks * This can store all possible simple trees, * but it can not store all possible trees representable by our internal representations like FlexTree and JsonableTree. + * @beta */ export type ConciseTree = | Exclude 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 b4184a4960a6..9c201db0c5a9 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 @@ -86,6 +86,12 @@ export interface ContainerSchema { interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @beta +export interface EncodeOptions { + readonly useStoredKeys?: boolean; + valueConverter(data: IFluidHandle): TCustom; +} + // @alpha export function enumFromStrings(factory: SchemaFactory, members: readonly Members[]): ((value: TValue) => TreeNode & { readonly value: TValue; From 69bdd80ea734faf2ca18728c10606c765a45895e Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 3 Oct 2024 19:01:17 -0700 Subject: [PATCH 39/54] Better docs --- packages/dds/tree/src/codec/codec.ts | 9 +++++++++ packages/dds/tree/src/treeFactory.ts | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/dds/tree/src/codec/codec.ts b/packages/dds/tree/src/codec/codec.ts index b7ef41d161b6..1272e6dff14b 100644 --- a/packages/dds/tree/src/codec/codec.ts +++ b/packages/dds/tree/src/codec/codec.ts @@ -330,6 +330,15 @@ export function withSchemaValidation< } /** + * Versions of FLuid Framework client packages. + * @remarks + * Used to express compatibility requirements by indicating the oldest version with which compatibility must be maintained. + * @privateRemarks + * This scheme assumes a single version will always be enough to communicate compatibility. + * For this to work, compatibility has to be strictly increasing. + * If this is violated (for example a subset of incompatible features from 3.x that are not in 3.0 are back ported to 2.x), + * a more complex scheme may be needed to allow safely opting into incompatible features in those cases: + * such a system can be added if/when its needed since it will be opt in and thus non-breaking. * @beta */ export enum FluidClientVersion { diff --git a/packages/dds/tree/src/treeFactory.ts b/packages/dds/tree/src/treeFactory.ts index 8ee06432ec2f..d19fd157bd04 100644 --- a/packages/dds/tree/src/treeFactory.ts +++ b/packages/dds/tree/src/treeFactory.ts @@ -244,6 +244,9 @@ function copyNodeCursor(cursor: ITreeCursorSynchronous): ITreeCursorSynchronous } /** + * The portion of SharedTree data typically persisted the container. + * Usable with {@link independentInitializedView} to create a {@link TreeView} + * without loading a container. * @alpha */ export interface ViewContent { @@ -259,7 +262,8 @@ export interface ViewContent { */ readonly schema: JsonCompatible; /** - * idCompressor + * IIdCompressor which will be used to decompress any compressed identifiers in `tree` + * as well as for any other identifiers added to the view. */ readonly idCompressor: IIdCompressor; } From 042899dcf9320767a8b89c53d71590b7cf66918a Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:56:19 -0700 Subject: [PATCH 40/54] simple-tree storedSchema tests --- .../dds/tree/api-report/tree.alpha.api.md | 4 +- packages/dds/tree/api-report/tree.beta.api.md | 2 +- .../dds/tree/src/simple-tree/api/create.ts | 15 ------ .../dds/tree/src/simple-tree/api/index.ts | 2 +- .../{getStoredSchema.ts => storedSchema.ts} | 6 +++ .../test/simple-tree/api/storedSchema.spec.ts | 48 +++++++++++++++++++ .../simple-tree-storedSchema/empty.json | 8 ++++ .../false boolean.json | 14 ++++++ .../simple-tree-storedSchema/handle.json | 14 ++++++ .../hasMinimalValueField.json | 24 ++++++++++ .../hasNumericValueField.json | 24 ++++++++++ .../hasOptionalField-empty.json | 24 ++++++++++ .../hasPolymorphicValueField.json | 28 +++++++++++ .../simple-tree-storedSchema/minimal.json | 14 ++++++ .../simple-tree-storedSchema/null.json | 14 ++++++ .../simple-tree-storedSchema/numeric.json | 14 ++++++ .../numericMap-empty.json | 22 +++++++++ .../numericMap-full.json | 22 +++++++++ .../recursiveType-deeper.json | 21 ++++++++ .../recursiveType-empty.json | 21 ++++++++ .../recursiveType-recursive.json | 21 ++++++++ .../true boolean.json | 14 ++++++ 22 files changed, 357 insertions(+), 19 deletions(-) rename packages/dds/tree/src/simple-tree/api/{getStoredSchema.ts => storedSchema.ts} (88%) create mode 100644 packages/dds/tree/src/test/simple-tree/api/storedSchema.spec.ts create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/empty.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/false boolean.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/handle.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasMinimalValueField.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasNumericValueField.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasOptionalField-empty.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasPolymorphicValueField.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/minimal.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/null.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/numeric.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/numericMap-empty.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/numericMap-full.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/recursiveType-deeper.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/recursiveType-empty.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/recursiveType-recursive.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/true boolean.json diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 62ef4a1e4634..d84e27807c46 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -127,7 +127,7 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; -// @beta (undocumented) +// @beta export enum FluidClientVersion { // (undocumented) v2_0 = "v2_0", @@ -824,7 +824,7 @@ export interface ViewableTree { viewWith(config: TreeViewConfiguration): TreeView; } -// @alpha (undocumented) +// @alpha export interface ViewContent { readonly idCompressor: IIdCompressor; readonly schema: JsonCompatible; diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index 1fbd3dd4c4d8..24f5b4a49010 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -100,7 +100,7 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; -// @beta (undocumented) +// @beta export enum FluidClientVersion { // (undocumented) v2_0 = "v2_0", diff --git a/packages/dds/tree/src/simple-tree/api/create.ts b/packages/dds/tree/src/simple-tree/api/create.ts index 6fba8b110050..de14fa88d569 100644 --- a/packages/dds/tree/src/simple-tree/api/create.ts +++ b/packages/dds/tree/src/simple-tree/api/create.ts @@ -3,11 +3,9 @@ * Licensed under the MIT License. */ -import type { IFluidHandle } from "@fluidframework/core-interfaces"; import { assert } from "@fluidframework/core-utils/internal"; import type { ITreeCursorSynchronous, SchemaAndPolicy } from "../../core/index.js"; -import { fail, type JsonCompatible } from "../../util/index.js"; import type { ImplicitFieldSchema, InsertableTreeFieldFromImplicitField, @@ -95,19 +93,6 @@ export function cursorFromInsertable( return cursorForMapTreeNode(mapTree); } -/** - * Construct tree content compatible with a field defined by the provided `schema`. - * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. - * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. - * @beta - */ -export function createFromCompressed( - schema: TSchema, - data: JsonCompatible, -): Unhydrated> { - fail("TODO"); -} - /** * Creates an unhydrated simple-tree field from a cursor in nodes mode. */ diff --git a/packages/dds/tree/src/simple-tree/api/index.ts b/packages/dds/tree/src/simple-tree/api/index.ts index 852ae326be2b..0cbde2d55957 100644 --- a/packages/dds/tree/src/simple-tree/api/index.ts +++ b/packages/dds/tree/src/simple-tree/api/index.ts @@ -80,7 +80,7 @@ export { extractPersistedSchema, comparePersistedSchemaInternal, comparePersistedSchema, -} from "./getStoredSchema.js"; +} from "./storedSchema.js"; // Exporting the schema (RecursiveObject) to test that recursive types are working correctly. // These are `@internal` so they can't be included in the `InternalClassTreeTypes` due to https://github.com/microsoft/rushstack/issues/3639 diff --git a/packages/dds/tree/src/simple-tree/api/getStoredSchema.ts b/packages/dds/tree/src/simple-tree/api/storedSchema.ts similarity index 88% rename from packages/dds/tree/src/simple-tree/api/getStoredSchema.ts rename to packages/dds/tree/src/simple-tree/api/storedSchema.ts index ef468e80e13c..5d7c17aa018e 100644 --- a/packages/dds/tree/src/simple-tree/api/getStoredSchema.ts +++ b/packages/dds/tree/src/simple-tree/api/storedSchema.ts @@ -51,6 +51,12 @@ export function extractPersistedSchema(schema: ImplicitFieldSchema): JsonCompati * Reports the same compatibility that {@link TreeView.compatibility} would report if * opening a document that used the `persisted` schema and provided `view` to {@link ViewableTree.viewWith}. * + * @param persisted - Schema persisted for a document. Typically persisted alongside the data and assumed to describe that data. + * @param view - Schema which would be used to view persisted content. Use {@link extractPersistedSchema} to convert the view schema into this format. + * @param options - {@link ICodecOptions} used when parsing the provided schema. + * @param canInitialize - Passed through to the return value unchanged and otherwise unused. + * @returns The {@link SchemaCompatibilityStatus} a {@link TreeView} would report for this combination of schema. + * * @remarks * This uses the persisted formats for schema, meaning it only includes data which impacts compatibility. * It also uses the persisted format so that this API can be used in tests to compare against saved schema from previous versions of the application. diff --git a/packages/dds/tree/src/test/simple-tree/api/storedSchema.spec.ts b/packages/dds/tree/src/test/simple-tree/api/storedSchema.spec.ts new file mode 100644 index 000000000000..ad911b7d7bee --- /dev/null +++ b/packages/dds/tree/src/test/simple-tree/api/storedSchema.spec.ts @@ -0,0 +1,48 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { strict as assert } from "node:assert"; + +import { + comparePersistedSchema, + extractPersistedSchema, + // eslint-disable-next-line import/no-internal-modules +} from "../../../simple-tree/api/storedSchema.js"; +import { testSimpleTrees } from "../../testTrees.js"; +import { takeJsonSnapshot, useSnapshotDirectory } from "../../snapshots/index.js"; +import { typeboxValidator } from "../../../external-utilities/index.js"; + +describe("simple-tree storedSchema", () => { + describe("test-schema", () => { + useSnapshotDirectory("simple-tree-storedSchema"); + for (const test of testSimpleTrees) { + it(test.name, () => { + const persisted = extractPersistedSchema(test.schema); + takeJsonSnapshot(persisted); + }); + + // comparePersistedSchema is a trivial wrapper around functionality that is tested elsewhere, + // but might as will give it a simple smoke test for the various test schema. + it(`comparePersistedSchema to self ${test.name}`, () => { + const persistedA = extractPersistedSchema(test.schema); + const persistedB = extractPersistedSchema(test.schema); + const status = comparePersistedSchema( + persistedA, + persistedB, + { + jsonValidator: typeboxValidator, + }, + false, + ); + assert.deepEqual(status, { + isEquivalent: true, + canView: true, + canUpgrade: true, + canInitialize: false, + }); + }); + } + }); +}); diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/empty.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/empty.json new file mode 100644 index 000000000000..0806be888946 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/empty.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "nodes": {}, + "root": { + "kind": "Optional", + "types": [] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/false boolean.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/false boolean.json new file mode 100644 index 000000000000..ce82882e2cba --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/false boolean.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "nodes": { + "com.fluidframework.leaf.boolean": { + "leaf": 2 + } + }, + "root": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.boolean" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/handle.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/handle.json new file mode 100644 index 000000000000..ed93bfc0f54f --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/handle.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "nodes": { + "com.fluidframework.leaf.handle": { + "leaf": 3 + } + }, + "root": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.handle" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasMinimalValueField.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasMinimalValueField.json new file mode 100644 index 000000000000..07cd15c7fb08 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasMinimalValueField.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "nodes": { + "test.hasMinimalValueField": { + "object": { + "field": { + "kind": "Value", + "types": [ + "test.minimal" + ] + } + } + }, + "test.minimal": { + "object": {} + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasMinimalValueField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasNumericValueField.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasNumericValueField.json new file mode 100644 index 000000000000..524e40587b80 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasNumericValueField.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "nodes": { + "com.fluidframework.leaf.number": { + "leaf": 0 + }, + "test.hasNumericValueField": { + "object": { + "field": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.number" + ] + } + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasNumericValueField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasOptionalField-empty.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasOptionalField-empty.json new file mode 100644 index 000000000000..74bf3e89685b --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasOptionalField-empty.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "nodes": { + "com.fluidframework.leaf.number": { + "leaf": 0 + }, + "test.hasOptionalField": { + "object": { + "field": { + "kind": "Optional", + "types": [ + "com.fluidframework.leaf.number" + ] + } + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasOptionalField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasPolymorphicValueField.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasPolymorphicValueField.json new file mode 100644 index 000000000000..21ed0605d8d3 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasPolymorphicValueField.json @@ -0,0 +1,28 @@ +{ + "version": 1, + "nodes": { + "com.fluidframework.leaf.number": { + "leaf": 0 + }, + "test.hasPolymorphicValueField": { + "object": { + "field": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.number", + "test.minimal" + ] + } + } + }, + "test.minimal": { + "object": {} + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasPolymorphicValueField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/minimal.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/minimal.json new file mode 100644 index 000000000000..01e8f32b14b2 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/minimal.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "nodes": { + "test.minimal": { + "object": {} + } + }, + "root": { + "kind": "Value", + "types": [ + "test.minimal" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/null.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/null.json new file mode 100644 index 000000000000..fec77a7590f9 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/null.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "nodes": { + "com.fluidframework.leaf.null": { + "leaf": 4 + } + }, + "root": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.null" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/numeric.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/numeric.json new file mode 100644 index 000000000000..e6d9032ca950 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/numeric.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "nodes": { + "com.fluidframework.leaf.number": { + "leaf": 0 + } + }, + "root": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.number" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/numericMap-empty.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/numericMap-empty.json new file mode 100644 index 000000000000..6682ae5a92bc --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/numericMap-empty.json @@ -0,0 +1,22 @@ +{ + "version": 1, + "nodes": { + "com.fluidframework.leaf.number": { + "leaf": 0 + }, + "test.numericMap": { + "map": { + "kind": "Optional", + "types": [ + "com.fluidframework.leaf.number" + ] + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.numericMap" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/numericMap-full.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/numericMap-full.json new file mode 100644 index 000000000000..6682ae5a92bc --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/numericMap-full.json @@ -0,0 +1,22 @@ +{ + "version": 1, + "nodes": { + "com.fluidframework.leaf.number": { + "leaf": 0 + }, + "test.numericMap": { + "map": { + "kind": "Optional", + "types": [ + "com.fluidframework.leaf.number" + ] + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.numericMap" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/recursiveType-deeper.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/recursiveType-deeper.json new file mode 100644 index 000000000000..752e2a9125aa --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/recursiveType-deeper.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "nodes": { + "test.recursiveType": { + "object": { + "field": { + "kind": "Optional", + "types": [ + "test.recursiveType" + ] + } + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.recursiveType" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/recursiveType-empty.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/recursiveType-empty.json new file mode 100644 index 000000000000..752e2a9125aa --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/recursiveType-empty.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "nodes": { + "test.recursiveType": { + "object": { + "field": { + "kind": "Optional", + "types": [ + "test.recursiveType" + ] + } + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.recursiveType" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/recursiveType-recursive.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/recursiveType-recursive.json new file mode 100644 index 000000000000..752e2a9125aa --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/recursiveType-recursive.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "nodes": { + "test.recursiveType": { + "object": { + "field": { + "kind": "Optional", + "types": [ + "test.recursiveType" + ] + } + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.recursiveType" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/true boolean.json b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/true boolean.json new file mode 100644 index 000000000000..ce82882e2cba --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/true boolean.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "nodes": { + "com.fluidframework.leaf.boolean": { + "leaf": 2 + } + }, + "root": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.boolean" + ] + } +} \ No newline at end of file From 35600cf916ab2228ff33e80b0790983044e50686 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:35:27 -0700 Subject: [PATCH 41/54] Fix from merge --- examples/apps/tree-cli-app/package.json | 2 +- .../api-report/fluid-framework.alpha.api.md | 19 ++++++++++++------- .../api-report/fluid-framework.beta.api.md | 2 +- pnpm-lock.yaml | 4 ++-- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/examples/apps/tree-cli-app/package.json b/examples/apps/tree-cli-app/package.json index a40eb6316be5..42076ce690aa 100644 --- a/examples/apps/tree-cli-app/package.json +++ b/examples/apps/tree-cli-app/package.json @@ -43,7 +43,7 @@ "devDependencies": { "@biomejs/biome": "~1.8.3", "@fluid-internal/mocha-test-setup": "workspace:~", - "@fluidframework/build-tools": "^0.46.0", + "@fluidframework/build-tools": "^0.47.0", "@fluidframework/eslint-config-fluid": "^5.4.0", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", 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 f9c3511705d6..aa48c40cfe58 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 @@ -46,6 +46,11 @@ export interface CommitMetadata { // @alpha export function comparePersistedSchema(persisted: JsonCompatible, view: JsonCompatible, options: ICodecOptions, canInitialize: boolean): SchemaCompatibilityStatus; +// @beta +export type ConciseTree = Exclude | THandle | ConciseTree[] | { + [key: string]: ConciseTree; +}; + // @alpha export function configuredSharedTree(options: SharedTreeOptions): SharedObjectKind; @@ -166,7 +171,7 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; -// @beta (undocumented) +// @beta export enum FluidClientVersion { // (undocumented) v2_0 = "v2_0", @@ -617,12 +622,12 @@ export interface JsonArrayNodeSchema extends JsonNodeSchemaBase = string | number | boolean | null | JsonCompatible[] | JsonCompatibleObject | TExtra; -// @alpha -export type JsonCompatibleObject = { - [P in string]?: JsonCompatible; +// @beta +export type JsonCompatibleObject = { + [P in string]?: JsonCompatible; }; // @alpha @sealed @@ -1194,7 +1199,7 @@ export interface ViewableTree { viewWith(config: TreeViewConfiguration): TreeView; } -// @alpha (undocumented) +// @alpha export interface ViewContent { readonly idCompressor: IIdCompressor; readonly schema: JsonCompatible; 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 ee7b0775fc92..a4e74870df0d 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 @@ -141,7 +141,7 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; -// @beta (undocumented) +// @beta export enum FluidClientVersion { // (undocumented) v2_0 = "v2_0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 061f14fce6f5..b459150bd380 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1154,8 +1154,8 @@ importers: specifier: workspace:~ version: link:../../../packages/test/mocha-test-setup '@fluidframework/build-tools': - specifier: ^0.46.0 - version: 0.46.0 + specifier: ^0.47.0 + version: 0.47.0 '@fluidframework/eslint-config-fluid': specifier: ^5.4.0 version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) From fedf8fc32acceb3828314228251812d96be5ac63 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:13:36 -0700 Subject: [PATCH 42/54] tests --- .../src/test/simple-tree/api/treeApi.spec.ts | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts b/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts index c748e0dd5d74..1a9baad1ea68 100644 --- a/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts @@ -22,6 +22,7 @@ import { treeNodeApi as Tree, TreeBeta, type TreeChangeEvents, + type TreeLeafValue, type TreeNode, TreeViewConfiguration, } from "../../../simple-tree/index.js"; @@ -39,6 +40,7 @@ import { } from "../../../simple-tree/leafNodeSchema.js"; // eslint-disable-next-line import/no-internal-modules import { tryGetSchema } from "../../../simple-tree/api/treeNodeApi.js"; +import { testSimpleTrees } from "../../testTrees.js"; const schema = new SchemaFactory("com.example"); @@ -1011,4 +1013,160 @@ describe("treeNodeApi", () => { assert.deepEqual(eventLog, [new Set(["prop1"])]); }); }); + + // create is mostly the same as node constructors which have their own tests, so just cover the new cases (optional and top level unions) here. + describe("create", () => { + it("undefined", () => { + // Valid + assert.equal(TreeBeta.create(schema.optional([]), undefined), undefined); + // Undefined where not allowed + assert.throws( + () => TreeBeta.create(schema.required([]), undefined as never), + validateUsageError("invalid"), + ); + // Undefined required, not provided + assert.throws( + () => TreeBeta.create(schema.optional([]), 1 as unknown as undefined), + validateUsageError("invalid"), + ); + }); + + it("union", () => { + // Valid + assert.equal(TreeBeta.create([schema.null, schema.number], null), null); + // invalid + assert.throws( + () => TreeBeta.create([schema.null, schema.number], "x" as unknown as number), + validateUsageError("invalid"), + ); + }); + + // Integration test object complex objects work (mainly covered by tests elsewhere) + it("object", () => { + const A = schema.object("A", { x: schema.number }); + const a = TreeBeta.create(A, { x: 1 }); + assert.equal(a, { x: 1 }); + }); + }); + + describe("concise", () => { + describe("importConcise", () => { + it("undefined", () => { + // Valid + assert.equal(TreeBeta.importConcise(schema.optional([]), undefined), undefined); + // Undefined where not allowed + assert.throws( + () => TreeBeta.importConcise(schema.required([]), undefined), + validateUsageError("invalid"), + ); + // Undefined required, not provided + assert.throws( + () => TreeBeta.importConcise(schema.optional([]), 1), + validateUsageError("invalid"), + ); + }); + + it("union", () => { + // Valid + assert.equal(TreeBeta.importConcise([schema.null, schema.number], null), null); + // invalid + assert.throws( + () => TreeBeta.importConcise([schema.null, schema.number], "x"), + validateUsageError("invalid"), + ); + }); + + it("object", () => { + const A = schema.object("A", { x: schema.number }); + const a = TreeBeta.importConcise(A, { x: 1 }); + assert.equal(a, { x: 1 }); + }); + }); + + describe("roundtrip", () => { + for (const testCase of testSimpleTrees) { + if (testCase.root !== undefined) { + it(testCase.name, () => { + const tree = TreeBeta.create(testCase.schema, testCase.root); + assert(tree !== undefined); + // TODO: fix typing + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const exported = TreeBeta.exportConcise(tree as any); + const imported = TreeBeta.importConcise(testCase.schema, exported); + // TODO: fix typing + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expectTreesEqual(tree as any, imported as any); + }); + } + } + }); + }); + + describe("concise", () => { + describe("importVerbose", () => { + it("undefined", () => { + // Valid + assert.equal(TreeBeta.importVerbose(schema.optional([]), undefined), undefined); + // Undefined where not allowed + assert.throws( + () => TreeBeta.importVerbose(schema.required([]), undefined), + validateUsageError("invalid"), + ); + // Undefined required, not provided + assert.throws( + () => TreeBeta.importVerbose(schema.optional([]), 1), + validateUsageError("invalid"), + ); + }); + + it("union", () => { + // Valid + assert.equal(TreeBeta.importVerbose([schema.null, schema.number], null), null); + // invalid + assert.throws( + () => TreeBeta.importVerbose([schema.null, schema.number], "x"), + validateUsageError("invalid"), + ); + }); + + it("object", () => { + const A = schema.object("A", { x: schema.number }); + const a = TreeBeta.importVerbose(A, { type: A.identifier, fields: { x: 1 } }); + assert.equal(a, { x: 1 }); + }); + }); + + describe("roundtrip", () => { + for (const testCase of testSimpleTrees) { + if (testCase.root !== undefined) { + it(testCase.name, () => { + const tree = TreeBeta.create(testCase.schema, testCase.root); + assert(tree !== undefined); + // TODO: fix typing + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const exported = TreeBeta.exportVerbose(tree as any); + const imported = TreeBeta.importVerbose(testCase.schema, exported); + // TODO: fix typing + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expectTreesEqual(tree as any, imported as any); + }); + } + } + }); + }); + + // TODO: test exportCompressed }); + +function expectTreesEqual(a: TreeNode | TreeLeafValue, b: TreeNode | TreeLeafValue): void { + // Validate the same schema objects are used. + assert.equal(Tree.schema(a), Tree.schema(b)); + + // This should catch all cases, assuming exportVerbose works correctly. + assert.deepEqual(TreeBeta.exportVerbose(a), TreeBeta.exportVerbose(b)); + + // Since this uses some of the tools to compare trees that this is testing for, perform the comparison in a few ways to reduce risk of a bug making this pass when it shouldn't: + // This case could have false negatives (two trees with ambiguous schema could export the same concise tree), + // but should have no false positives since equal trees always have the same concise tree. + assert.deepEqual(TreeBeta.exportConcise(a), TreeBeta.exportConcise(b)); +} From cb529369fdecd7f683637458bf86d9bdd1e1ffda Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:21:49 -0700 Subject: [PATCH 43/54] FIx lock file --- examples/apps/tree-cli-app/package.json | 4 ++-- pnpm-lock.yaml | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/apps/tree-cli-app/package.json b/examples/apps/tree-cli-app/package.json index 42076ce690aa..72f2e64519ae 100644 --- a/examples/apps/tree-cli-app/package.json +++ b/examples/apps/tree-cli-app/package.json @@ -41,9 +41,9 @@ "@sinclair/typebox": "^0.32.29" }, "devDependencies": { - "@biomejs/biome": "~1.8.3", + "@biomejs/biome": "~1.9.3", "@fluid-internal/mocha-test-setup": "workspace:~", - "@fluidframework/build-tools": "^0.47.0", + "@fluidframework/build-tools": "^0.49.0", "@fluidframework/eslint-config-fluid": "^5.4.0", "@types/mocha": "^9.1.1", "@types/node": "^18.19.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0997b8e49aa5..6aa66699b87c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1262,14 +1262,14 @@ importers: version: 0.32.35 devDependencies: '@biomejs/biome': - specifier: ~1.8.3 - version: 1.8.3 + specifier: ~1.9.3 + version: 1.9.3 '@fluid-internal/mocha-test-setup': specifier: workspace:~ version: link:../../../packages/test/mocha-test-setup '@fluidframework/build-tools': - specifier: ^0.47.0 - version: 0.47.0 + specifier: ^0.49.0 + version: 0.49.0 '@fluidframework/eslint-config-fluid': specifier: ^5.4.0 version: 5.4.0(eslint@8.55.0)(typescript@5.4.5) @@ -28444,6 +28444,7 @@ packages: /eslint@6.8.0: resolution: {integrity: sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==} engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true dependencies: '@babel/code-frame': 7.25.7 From f81ca740542a2e7cc9f0809b95b04e4dec745f02 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:44:45 -0700 Subject: [PATCH 44/54] FIx up merge --- examples/apps/tree-cli-app/package.json | 2 +- .../dds/tree/api-report/tree.alpha.api.md | 29 +++-- packages/dds/tree/api-report/tree.beta.api.md | 35 ++++++ .../dds/tree/src/simple-tree/api/create.ts | 2 - .../tree/src/simple-tree/api/treeApiBeta.ts | 105 +++++++++++------- .../dds/tree/src/simple-tree/schemaTypes.ts | 8 +- .../dds/tree/src/simple-tree/toMapTree.ts | 6 +- .../src/test/simple-tree/api/treeApi.spec.ts | 28 +++-- .../api-report/fluid-framework.alpha.api.md | 9 +- .../api-report/fluid-framework.beta.api.md | 12 ++ 10 files changed, 153 insertions(+), 83 deletions(-) diff --git a/examples/apps/tree-cli-app/package.json b/examples/apps/tree-cli-app/package.json index 72f2e64519ae..60a5796dd49d 100644 --- a/examples/apps/tree-cli-app/package.json +++ b/examples/apps/tree-cli-app/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/tree-cli-app", - "version": "2.4.0", + "version": "2.5.0", "private": true, "description": "SharedTree CLI app demo", "homepage": "https://fluidframework.com", diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 2880fe74e974..c879efc5c556 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -72,10 +72,10 @@ type ExtractItemType = Item extends () => infer Result ? // @alpha export function extractPersistedSchema(schema: ImplicitFieldSchema): JsonCompatible; -// @alpha +// @beta export type FactoryContent = IFluidHandle | string | number | boolean | null | Iterable | readonly InsertableContent[] | FactoryContentObject; -// @alpha +// @beta export type FactoryContentObject = { readonly [P in string]?: InsertableContent; }; @@ -188,13 +188,13 @@ export function independentInitializedView( // @public type _InlineTrick = 0; -// @alpha +// @beta export type Insertable = TSchema extends ImplicitAllowedTypes ? InsertableTreeNodeFromImplicitAllowedTypes : InsertableContent; -// @alpha +// @beta export type InsertableContent = Unhydrated | FactoryContent; -// @alpha +// @beta export type InsertableField = TSchema extends ImplicitFieldSchema ? InsertableTreeFieldFromImplicitField : InsertableContent | undefined; // @public @@ -643,22 +643,19 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; -<<<<<<< HEAD - create(schema: TSchema, data: InsertableTreeFieldFromImplicitField): Unhydrated>; - importConcise(schema: TSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; + create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; + importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated : TreeNode | TreeLeafValue | undefined>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; - exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; exportCompressed(tree: TreeNode | TreeLeafValue, options: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor; }): JsonCompatible; -======= - clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; ->>>>>>> 7a8c8d0e0f9eb4cb15516459f34e351a4904606f }; // @alpha @sealed @@ -852,10 +849,10 @@ export type Unenforced<_DesiredExtendsConstraint> = unknown; // @public export type Unhydrated = T; -// @alpha +// @beta export const UnsafeUnknownSchema: unique symbol; -// @alpha +// @beta export type UnsafeUnknownSchema = typeof UnsafeUnknownSchema; // @public diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index 563e2a6ea673..070450af19fe 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -48,6 +48,14 @@ export interface EncodeOptions { // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; +// @beta +export type FactoryContent = IFluidHandle | string | number | boolean | null | Iterable | readonly InsertableContent[] | FactoryContentObject; + +// @beta +export type FactoryContentObject = { + readonly [P in string]?: InsertableContent; +}; + // @public type FieldHasDefault = T extends FieldSchema ? true : false; @@ -124,6 +132,15 @@ export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; // @public type _InlineTrick = 0; +// @beta +export type Insertable = TSchema extends ImplicitAllowedTypes ? InsertableTreeNodeFromImplicitAllowedTypes : InsertableContent; + +// @beta +export type InsertableContent = Unhydrated | FactoryContent; + +// @beta +export type InsertableField = TSchema extends ImplicitFieldSchema ? InsertableTreeFieldFromImplicitField : InsertableContent | undefined; + // @public type InsertableObjectFromSchemaRecord> = FlattenKeys<{ readonly [Property in keyof T]?: InsertableTreeFieldFromImplicitField; @@ -470,6 +487,18 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; + create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; + importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated : TreeNode | TreeLeafValue | undefined>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + exportCompressed(tree: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; + }): JsonCompatible; }; // @public @sealed @@ -629,6 +658,12 @@ export type Unenforced<_DesiredExtendsConstraint> = unknown; // @public export type Unhydrated = T; +// @beta +export const UnsafeUnknownSchema: unique symbol; + +// @beta +export type UnsafeUnknownSchema = typeof UnsafeUnknownSchema; + // @public export type ValidateRecursiveSchema, { [NodeKind.Object]: T["info"] extends RestrictiveStringRecord ? InsertableObjectFromSchemaRecord : unknown; diff --git a/packages/dds/tree/src/simple-tree/api/create.ts b/packages/dds/tree/src/simple-tree/api/create.ts index 2e7f9e687273..7bd3ae215021 100644 --- a/packages/dds/tree/src/simple-tree/api/create.ts +++ b/packages/dds/tree/src/simple-tree/api/create.ts @@ -44,8 +44,6 @@ import { getUnhydratedContext } from "../createContext.js"; * * Like with {@link TreeNodeSchemaClass}'s constructor, its an error to provide an existing node to this API. * For that case, use {@link TreeBeta.clone}. - * @privateRemarks - * This could be exposed as a public `Tree.create` function. */ export function createFromInsertable< TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema, diff --git a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts index 7af5f5effb03..7c323e448273 100644 --- a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts +++ b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts @@ -16,6 +16,7 @@ import { treeNodeApi, tryGetSchema } from "./treeNodeApi.js"; import { createFromCursor, createFromInsertable, cursorFromInsertable } from "./create.js"; import type { ImplicitFieldSchema, + InsertableField, InsertableTreeFieldFromImplicitField, TreeFieldFromImplicitField, TreeLeafValue, @@ -177,18 +178,29 @@ export const TreeBeta: { // ): TreeFieldFromImplicitField; /** - * Generic tree constructor. + * Construct tree content that is compatible with the field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. * @remarks - * This is equivalent to calling {@link TreeNodeSchemaClass}'s constructor or `TreeNodeSchemaNonClass.create`, - * except that this also handles the case where the root is polymorphic, or optional. + * When providing a {@link TreeNodeSchemaClass}, this is the same as invoking its constructor except that an unhydrated node can also be provided. + * This function exists as a generalization that can be used in other cases as well, + * 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. * - * Documented (and thus recoverable) error handling/reporting for this is not yet implemented, - * but for now most invalid inputs will throw a recoverable error. + * Like with {@link TreeNodeSchemaClass}'s constructor, its an error to provide an existing node to this API. + * For that case, use {@link TreeBeta.clone}. + * @privateRemarks + * There should be a way to provide an source for defaulted identifiers, wither via this API or some way to add them to its output later. */ - create( - schema: TSchema, - data: InsertableTreeFieldFromImplicitField, - ): Unhydrated>; + create( + schema: UnsafeUnknownSchema extends TSchema + ? ImplicitFieldSchema + : TSchema & ImplicitFieldSchema, + data: InsertableField, + ): Unhydrated< + TSchema extends ImplicitFieldSchema + ? TreeFieldFromImplicitField + : TreeNode | TreeLeafValue | undefined + >; /** * Less type safe version of {@link TreeBeta.create}, suitable for importing data. @@ -205,9 +217,26 @@ export const TreeBeta: { * Documented (and thus recoverable) error handling/reporting for this is not yet implemented, * but for now most invalid inputs will throw a recoverable error. */ - importConcise( - schema: TSchema, + importConcise( + schema: UnsafeUnknownSchema extends TSchema + ? ImplicitFieldSchema + : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree, + ): Unhydrated< + TSchema extends ImplicitFieldSchema + ? TreeFieldFromImplicitField + : TreeNode | TreeLeafValue | undefined + >; + + /** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + */ + importVerbose( + schema: TSchema, + data: VerboseTree | undefined, + options?: Partial>, ): Unhydrated>; /** @@ -224,15 +253,12 @@ export const TreeBeta: { ): Unhydrated>; /** - * Construct tree content compatible with a field defined by the provided `schema`. - * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. - * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + * Same as generic overload, except leaves handles as is. */ - importVerbose( - schema: TSchema, - data: VerboseTree | undefined, - options?: Partial>, - ): Unhydrated>; + exportConcise( + node: TreeNode | TreeLeafValue, + options?: Partial>, + ): ConciseTree; /** * Copy a snapshot of the current version of a TreeNode into a {@link ConciseTree}. @@ -243,12 +269,12 @@ export const TreeBeta: { ): ConciseTree; /** - * Same as generic overload, except leaves handles as is. + * Same {@link TreeBeta.(exportVerbose:1)} except leaves handles as is. */ - exportConcise( + exportVerbose( node: TreeNode | TreeLeafValue, options?: Partial>, - ): ConciseTree; + ): VerboseTree; /** * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. @@ -266,14 +292,6 @@ export const TreeBeta: { */ exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; - /** - * Same {@link TreeBeta.(exportVerbose:1)} except leaves handles as is. - */ - exportVerbose( - node: TreeNode | TreeLeafValue, - options?: Partial>, - ): VerboseTree; - /** * Export the content of the provided `tree` in a compressed JSON compatible format. * @remarks @@ -327,21 +345,26 @@ export const TreeBeta: { return clonedNode; }, - create( - schema: TSchema, - data: InsertableTreeFieldFromImplicitField, - ): Unhydrated> { - return createFromInsertable(schema, data); - }, + create: createFromInsertable, - importConcise( - schema: TSchema, + importConcise( + schema: UnsafeUnknownSchema extends TSchema + ? ImplicitFieldSchema + : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree, - ): Unhydrated> { + ): Unhydrated< + TSchema extends ImplicitFieldSchema + ? TreeFieldFromImplicitField + : TreeNode | TreeLeafValue | undefined + > { return createFromInsertable( schema, - data as InsertableTreeFieldFromImplicitField, - ); + data as InsertableField, + ) as Unhydrated< + TSchema extends ImplicitFieldSchema + ? TreeFieldFromImplicitField + : TreeNode | TreeLeafValue | undefined + >; }, importVerbose( diff --git a/packages/dds/tree/src/simple-tree/schemaTypes.ts b/packages/dds/tree/src/simple-tree/schemaTypes.ts index 41336de3bfdd..3dccd59787c5 100644 --- a/packages/dds/tree/src/simple-tree/schemaTypes.ts +++ b/packages/dds/tree/src/simple-tree/schemaTypes.ts @@ -416,7 +416,7 @@ export type InsertableTreeFieldFromImplicitField< /** * {@inheritdoc (UnsafeUnknownSchema:type)} - * @alpha + * @beta */ export const UnsafeUnknownSchema: unique symbol = Symbol("UnsafeUnknownSchema"); @@ -436,7 +436,7 @@ export const UnsafeUnknownSchema: unique symbol = Symbol("UnsafeUnknownSchema"); * Any APIs which use this must produce UsageErrors when out of schema data is encountered, and never produce unrecoverable errors, * or silently accept invalid data. * This is currently only type exported from the package: the symbol is just used as a way to get a named type. - * @alpha + * @beta */ export type UnsafeUnknownSchema = typeof UnsafeUnknownSchema; @@ -444,7 +444,7 @@ export type UnsafeUnknownSchema = typeof UnsafeUnknownSchema; * Content which could be inserted into a tree. * @remarks * Extended version of {@link InsertableTreeNodeFromImplicitAllowedTypes} that also allows {@link (UnsafeUnknownSchema:type)}. - * @alpha + * @beta */ export type Insertable = TSchema extends ImplicitAllowedTypes @@ -455,7 +455,7 @@ export type Insertable = TSchema extends ImplicitFieldSchema diff --git a/packages/dds/tree/src/simple-tree/toMapTree.ts b/packages/dds/tree/src/simple-tree/toMapTree.ts index bd9767abc936..6bdd1635bb24 100644 --- a/packages/dds/tree/src/simple-tree/toMapTree.ts +++ b/packages/dds/tree/src/simple-tree/toMapTree.ts @@ -726,7 +726,7 @@ function tryGetInnerNode(target: unknown): InnerNode | undefined { * Content which can be used to build a node. * @remarks * Can contain unhydrated nodes, but can not be an unhydrated node at the root. - * @system @alpha + * @system @beta */ export type FactoryContent = | IFluidHandle @@ -745,7 +745,7 @@ export type FactoryContent = * Can contain unhydrated nodes, but can not be an unhydrated node at the root. * * Supports object and map nodes. - * @system @alpha + * @system @beta */ export type FactoryContentObject = { readonly [P in string]?: InsertableContent; @@ -753,6 +753,6 @@ export type FactoryContentObject = { /** * Content which can be inserted into a tree. - * @system @alpha + * @system @beta */ export type InsertableContent = Unhydrated | FactoryContent; diff --git a/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts b/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts index 6c8c92496668..f3e1ed91fd96 100644 --- a/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts @@ -25,6 +25,7 @@ import { type TreeLeafValue, type TreeNode, TreeViewConfiguration, + type UnsafeUnknownSchema, } from "../../../simple-tree/index.js"; import { getView, validateUsageError } from "../../utils.js"; import { getViewForForkedBranch, hydrate } from "../utils.js"; @@ -1193,15 +1194,14 @@ describe("treeNodeApi", () => { for (const testCase of testSimpleTrees) { if (testCase.root !== undefined) { it(testCase.name, () => { - const tree = TreeBeta.create(testCase.schema, testCase.root); + const tree = TreeBeta.create(testCase.schema, testCase.root); assert(tree !== undefined); - // TODO: fix typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const exported = TreeBeta.exportConcise(tree as any); - const imported = TreeBeta.importConcise(testCase.schema, exported); - // TODO: fix typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expectTreesEqual(tree as any, imported as any); + const exported = TreeBeta.exportConcise(tree); + const imported = TreeBeta.importConcise( + testCase.schema, + exported, + ); + expectTreesEqual(tree, imported); }); } } @@ -1246,7 +1246,7 @@ describe("treeNodeApi", () => { for (const testCase of testSimpleTrees) { if (testCase.root !== undefined) { it(testCase.name, () => { - const tree = TreeBeta.create(testCase.schema, testCase.root); + const tree = TreeBeta.create(testCase.schema, testCase.root); assert(tree !== undefined); // TODO: fix typing // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -1264,7 +1264,15 @@ describe("treeNodeApi", () => { // TODO: test exportCompressed }); -function expectTreesEqual(a: TreeNode | TreeLeafValue, b: TreeNode | TreeLeafValue): void { +function expectTreesEqual( + a: TreeNode | TreeLeafValue | undefined, + b: TreeNode | TreeLeafValue | undefined, +): void { + if (a === undefined || b === undefined) { + assert.equal(a === undefined, b === undefined); + return; + } + // Validate the same schema objects are used. assert.equal(Tree.schema(a), Tree.schema(b)); 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 687243099a8d..23b9bd9ebbfb 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 @@ -1018,9 +1018,9 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; -<<<<<<< HEAD - create(schema: TSchema, data: InsertableTreeFieldFromImplicitField): Unhydrated>; - importConcise(schema: TSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated>; + clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; + create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; + importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated : TreeNode | TreeLeafValue | undefined>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; @@ -1031,9 +1031,6 @@ export const TreeBeta: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor; }): JsonCompatible; -======= - clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; ->>>>>>> 7a8c8d0e0f9eb4cb15516459f34e351a4904606f }; // @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 081cfa87f0d2..c04f4ac9d233 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 @@ -842,6 +842,18 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; + create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; + importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated : TreeNode | TreeLeafValue | undefined>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; + exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + exportCompressed(tree: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; + }): JsonCompatible; }; // @public @sealed From 8b3811b3aeac08b4d72a6d924733e494f24e3e41 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:21:57 -0700 Subject: [PATCH 45/54] Fix build --- examples/apps/tree-cli-app/src/utils.ts | 3 +- .../api-report/fluid-framework.alpha.api.md | 20 ++++++------- .../api-report/fluid-framework.beta.api.md | 29 +++++++++++++++++-- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/examples/apps/tree-cli-app/src/utils.ts b/examples/apps/tree-cli-app/src/utils.ts index 26705bf18bd9..f6b9dae2d480 100644 --- a/examples/apps/tree-cli-app/src/utils.ts +++ b/examples/apps/tree-cli-app/src/utils.ts @@ -28,6 +28,7 @@ import { type JsonCompatible, type VerboseTree, type ViewContent, + type ConciseTree, // eslint-disable-next-line import/no-internal-modules } from "@fluidframework/tree/alpha"; import { type Static, Type } from "@sinclair/typebox"; @@ -55,7 +56,7 @@ export function loadDocument(source: string | undefined): List { switch (parts.at(-2)) { case "concise": { - return TreeBeta.importConcise(List, fileData); + return TreeBeta.importConcise(List, fileData as ConciseTree); } case "verbose": { return TreeBeta.importVerbose(List, fileData as VerboseTree); 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 23b9bd9ebbfb..f2f94ebf88a7 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 @@ -116,10 +116,10 @@ type ExtractItemType = Item extends () => infer Result ? // @alpha export function extractPersistedSchema(schema: ImplicitFieldSchema): JsonCompatible; -// @alpha +// @beta export type FactoryContent = IFluidHandle | string | number | boolean | null | Iterable | readonly InsertableContent[] | FactoryContentObject; -// @alpha +// @beta export type FactoryContentObject = { readonly [P in string]?: InsertableContent; }; @@ -498,13 +498,13 @@ export type InitialObjects = { // @public type _InlineTrick = 0; -// @alpha +// @beta export type Insertable = TSchema extends ImplicitAllowedTypes ? InsertableTreeNodeFromImplicitAllowedTypes : InsertableContent; -// @alpha +// @beta export type InsertableContent = Unhydrated | FactoryContent; -// @alpha +// @beta export type InsertableField = TSchema extends ImplicitFieldSchema ? InsertableTreeFieldFromImplicitField : InsertableContent | undefined; // @public @@ -1021,12 +1021,12 @@ export const TreeBeta: { clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated : TreeNode | TreeLeafValue | undefined>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; - exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; exportCompressed(tree: TreeNode | TreeLeafValue, options: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor; @@ -1224,10 +1224,10 @@ export type Unenforced<_DesiredExtendsConstraint> = unknown; // @public export type Unhydrated = T; -// @alpha +// @beta export const UnsafeUnknownSchema: unique symbol; -// @alpha +// @beta export type UnsafeUnknownSchema = typeof UnsafeUnknownSchema; // @public 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 c04f4ac9d233..1e89ffb9d2c8 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 @@ -89,6 +89,14 @@ export abstract class ErasedType { // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; +// @beta +export type FactoryContent = IFluidHandle | string | number | boolean | null | Iterable | readonly InsertableContent[] | FactoryContentObject; + +// @beta +export type FactoryContentObject = { + readonly [P in string]?: InsertableContent; +}; + // @public type FieldHasDefault = T extends FieldSchema ? true : false; @@ -431,6 +439,15 @@ export type InitialObjects = { // @public type _InlineTrick = 0; +// @beta +export type Insertable = TSchema extends ImplicitAllowedTypes ? InsertableTreeNodeFromImplicitAllowedTypes : InsertableContent; + +// @beta +export type InsertableContent = Unhydrated | FactoryContent; + +// @beta +export type InsertableField = TSchema extends ImplicitFieldSchema ? InsertableTreeFieldFromImplicitField : InsertableContent | undefined; + // @public type InsertableObjectFromSchemaRecord> = FlattenKeys<{ readonly [Property in keyof T]?: InsertableTreeFieldFromImplicitField; @@ -844,12 +861,12 @@ export const TreeBeta: { clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated : TreeNode | TreeLeafValue | undefined>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; - exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; exportCompressed(tree: TreeNode | TreeLeafValue, options: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor; @@ -1013,6 +1030,12 @@ export type Unenforced<_DesiredExtendsConstraint> = unknown; // @public export type Unhydrated = T; +// @beta +export const UnsafeUnknownSchema: unique symbol; + +// @beta +export type UnsafeUnknownSchema = typeof UnsafeUnknownSchema; + // @public export type ValidateRecursiveSchema, { [NodeKind.Object]: T["info"] extends RestrictiveStringRecord ? InsertableObjectFromSchemaRecord : unknown; From ce7e8fdb620d0028404e162ecb98e5a68db72b41 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:33:13 -0700 Subject: [PATCH 46/54] Tidy up app docs and data files --- examples/apps/tree-cli-app/README.md | 2 + .../tree-cli-app/data/concise.snapshot.json | 1 + ...ompressed.json => default.compressed.json} | 2 +- .../tree-cli-app/data/default.concise.json | 1 + .../tree-cli-app/data/default.snapshot.json | 1 + .../data/default.verbose-stored.json | 1 + .../tree-cli-app/data/default.verbose.json | 1 + .../tree-cli-app/data/large.compressed.json | 458 -------- .../data/large.concise-stored.json | 112 -- .../apps/tree-cli-app/data/large.concise.json | 112 -- .../data/large.verbose-stored.json | 1015 ----------------- .../apps/tree-cli-app/data/large.verbose.json | 1015 ----------------- .../apps/tree-cli-app/data/list.combo.json | 1 - .../apps/tree-cli-app/data/list.verbose.json | 1 - .../apps/tree-cli-app/data/test.verbose.json | 1 - .../apps/tree-cli-app/data/test2.concise.json | 1 - examples/apps/tree-cli-app/src/index.ts | 6 + 17 files changed, 14 insertions(+), 2717 deletions(-) create mode 100644 examples/apps/tree-cli-app/data/concise.snapshot.json rename examples/apps/tree-cli-app/data/{list.compressed.json => default.compressed.json} (59%) create mode 100644 examples/apps/tree-cli-app/data/default.concise.json create mode 100644 examples/apps/tree-cli-app/data/default.snapshot.json create mode 100644 examples/apps/tree-cli-app/data/default.verbose-stored.json create mode 100644 examples/apps/tree-cli-app/data/default.verbose.json delete mode 100644 examples/apps/tree-cli-app/data/large.compressed.json delete mode 100644 examples/apps/tree-cli-app/data/large.concise-stored.json delete mode 100644 examples/apps/tree-cli-app/data/large.concise.json delete mode 100644 examples/apps/tree-cli-app/data/large.verbose-stored.json delete mode 100644 examples/apps/tree-cli-app/data/large.verbose.json delete mode 100644 examples/apps/tree-cli-app/data/list.combo.json delete mode 100644 examples/apps/tree-cli-app/data/list.verbose.json delete mode 100644 examples/apps/tree-cli-app/data/test.verbose.json delete mode 100644 examples/apps/tree-cli-app/data/test2.concise.json diff --git a/examples/apps/tree-cli-app/README.md b/examples/apps/tree-cli-app/README.md index 2727ede5bcc3..b9ad694b6726 100644 --- a/examples/apps/tree-cli-app/README.md +++ b/examples/apps/tree-cli-app/README.md @@ -3,3 +3,5 @@ Example application using Shared-Tree to create a non-collaborative file editing CLI application. Note that its perfectly possible to write a collaborative online CLI app using tree as well: this simply is not an example of that. + +Run the app with `pnpm run app` after building. diff --git a/examples/apps/tree-cli-app/data/concise.snapshot.json b/examples/apps/tree-cli-app/data/concise.snapshot.json new file mode 100644 index 000000000000..964c77877191 --- /dev/null +++ b/examples/apps/tree-cli-app/data/concise.snapshot.json @@ -0,0 +1 @@ +{"tree":{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"d":0}],"data":[[0,0]]},"schema":{"version":1,"nodes":{"com.fluidframework.example.cli.Item":{"object":{"location":{"kind":"Value","types":["com.fluidframework.example.cli.Point"]},"name":{"kind":"Value","types":["com.fluidframework.leaf.string"]}}},"com.fluidframework.example.cli.List":{"object":{"":{"kind":"Sequence","types":["com.fluidframework.example.cli.Item","com.fluidframework.leaf.string"]}}},"com.fluidframework.example.cli.Point":{"object":{"x":{"kind":"Value","types":["com.fluidframework.leaf.number"]},"y":{"kind":"Value","types":["com.fluidframework.leaf.number"]}}},"com.fluidframework.leaf.number":{"leaf":0},"com.fluidframework.leaf.string":{"leaf":1}},"root":{"kind":"Value","types":["com.fluidframework.example.cli.List"]}},"idCompressor":"AAAAAAAAAEAAAAAAAADwPwAAAAAAAPA/AAAAAAAAAAAUJLVv6TZpgqHGKmI53QQCAAAAAAAAAAAAAAAAAADwPwAAAAAAAAAA"} \ No newline at end of file diff --git a/examples/apps/tree-cli-app/data/list.compressed.json b/examples/apps/tree-cli-app/data/default.compressed.json similarity index 59% rename from examples/apps/tree-cli-app/data/list.compressed.json rename to examples/apps/tree-cli-app/data/default.compressed.json index d54ac8d87e32..408bf9091374 100644 --- a/examples/apps/tree-cli-app/data/list.compressed.json +++ b/examples/apps/tree-cli-app/data/default.compressed.json @@ -1 +1 @@ -{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"c":{"type":"com.fluidframework.leaf.string","value":true}}],"data":[[0,["x"]]]} \ No newline at end of file +{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"d":0}],"data":[[0,0]]} \ No newline at end of file diff --git a/examples/apps/tree-cli-app/data/default.concise.json b/examples/apps/tree-cli-app/data/default.concise.json new file mode 100644 index 000000000000..0637a088a01e --- /dev/null +++ b/examples/apps/tree-cli-app/data/default.concise.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/examples/apps/tree-cli-app/data/default.snapshot.json b/examples/apps/tree-cli-app/data/default.snapshot.json new file mode 100644 index 000000000000..90c23add441b --- /dev/null +++ b/examples/apps/tree-cli-app/data/default.snapshot.json @@ -0,0 +1 @@ +{"tree":{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"d":0}],"data":[[0,0]]},"schema":{"version":1,"nodes":{"com.fluidframework.example.cli.Item":{"object":{"location":{"kind":"Value","types":["com.fluidframework.example.cli.Point"]},"name":{"kind":"Value","types":["com.fluidframework.leaf.string"]}}},"com.fluidframework.example.cli.List":{"object":{"":{"kind":"Sequence","types":["com.fluidframework.example.cli.Item","com.fluidframework.leaf.string"]}}},"com.fluidframework.example.cli.Point":{"object":{"x":{"kind":"Value","types":["com.fluidframework.leaf.number"]},"y":{"kind":"Value","types":["com.fluidframework.leaf.number"]}}},"com.fluidframework.leaf.number":{"leaf":0},"com.fluidframework.leaf.string":{"leaf":1}},"root":{"kind":"Value","types":["com.fluidframework.example.cli.List"]}},"idCompressor":"AAAAAAAAAEAAAAAAAADwPwAAAAAAAPA/AAAAAAAAAABhL9IeaYATlvd5A8Fp6aoDAAAAAAAAAAAAAAAAAADwPwAAAAAAAAAA"} \ No newline at end of file diff --git a/examples/apps/tree-cli-app/data/default.verbose-stored.json b/examples/apps/tree-cli-app/data/default.verbose-stored.json new file mode 100644 index 000000000000..a68c31892787 --- /dev/null +++ b/examples/apps/tree-cli-app/data/default.verbose-stored.json @@ -0,0 +1 @@ +{"type":"com.fluidframework.example.cli.List","fields":[]} \ No newline at end of file diff --git a/examples/apps/tree-cli-app/data/default.verbose.json b/examples/apps/tree-cli-app/data/default.verbose.json new file mode 100644 index 000000000000..a68c31892787 --- /dev/null +++ b/examples/apps/tree-cli-app/data/default.verbose.json @@ -0,0 +1 @@ +{"type":"com.fluidframework.example.cli.List","fields":[]} \ No newline at end of file diff --git a/examples/apps/tree-cli-app/data/large.compressed.json b/examples/apps/tree-cli-app/data/large.compressed.json deleted file mode 100644 index db6f91902972..000000000000 --- a/examples/apps/tree-cli-app/data/large.compressed.json +++ /dev/null @@ -1,458 +0,0 @@ -{ - "version": 1, - "identifiers": [], - "shapes": [ - { - "c": { - "type": "com.fluidframework.example.cli.Item", - "value": false, - "fields": [["location", 4], ["name", 1]] - } - }, - { "c": { "type": "com.fluidframework.leaf.string", "value": true } }, - { "c": { "type": "com.fluidframework.leaf.number", "value": true } }, - { - "c": { - "type": "com.fluidframework.example.cli.List", - "value": false, - "fields": [["", 5]] - } - }, - { - "c": { - "type": "com.fluidframework.example.cli.Point", - "value": false, - "fields": [["x", 2], ["y", 2]] - } - }, - { "a": 6 }, - { "d": 0 } - ], - "data": [ - [ - 3, - [ - 1, - "x", - 1, - "x", - 1, - "x", - 1, - "x", - 1, - "x", - 1, - "x", - 1, - "x", - 1, - "x", - 1, - "x", - 1, - "x", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item", - 0, - 0, - 0, - "item" - ] - ] - ] -} diff --git a/examples/apps/tree-cli-app/data/large.concise-stored.json b/examples/apps/tree-cli-app/data/large.concise-stored.json deleted file mode 100644 index 8219b2264520..000000000000 --- a/examples/apps/tree-cli-app/data/large.concise-stored.json +++ /dev/null @@ -1,112 +0,0 @@ -[ - "x", - "x", - "x", - "x", - "x", - "x", - "x", - "x", - "x", - "x", - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" }, - { "location": { "x": 0, "y": 0 }, "name": "item" } -] diff --git a/examples/apps/tree-cli-app/data/large.concise.json b/examples/apps/tree-cli-app/data/large.concise.json deleted file mode 100644 index c04c275b687a..000000000000 --- a/examples/apps/tree-cli-app/data/large.concise.json +++ /dev/null @@ -1,112 +0,0 @@ -[ - "x", - "x", - "x", - "x", - "x", - "x", - "x", - "x", - "x", - "x", - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" }, - { "position": { "x": 0, "y": 0 }, "name": "item" } -] diff --git a/examples/apps/tree-cli-app/data/large.verbose-stored.json b/examples/apps/tree-cli-app/data/large.verbose-stored.json deleted file mode 100644 index 7d8e47264644..000000000000 --- a/examples/apps/tree-cli-app/data/large.verbose-stored.json +++ /dev/null @@ -1,1015 +0,0 @@ -{ - "type": "com.fluidframework.example.cli.List", - "fields": [ - "x", - "x", - "x", - "x", - "x", - "x", - "x", - "x", - "x", - "x", - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "location": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - } - ] -} diff --git a/examples/apps/tree-cli-app/data/large.verbose.json b/examples/apps/tree-cli-app/data/large.verbose.json deleted file mode 100644 index 67460df52834..000000000000 --- a/examples/apps/tree-cli-app/data/large.verbose.json +++ /dev/null @@ -1,1015 +0,0 @@ -{ - "type": "com.fluidframework.example.cli.List", - "fields": [ - "x", - "x", - "x", - "x", - "x", - "x", - "x", - "x", - "x", - "x", - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - }, - { - "type": "com.fluidframework.example.cli.Item", - "fields": { - "position": { - "type": "com.fluidframework.example.cli.Point", - "fields": { "x": 0, "y": 0 } - }, - "name": "item" - } - } - ] -} diff --git a/examples/apps/tree-cli-app/data/list.combo.json b/examples/apps/tree-cli-app/data/list.combo.json deleted file mode 100644 index 60398a8de9b3..000000000000 --- a/examples/apps/tree-cli-app/data/list.combo.json +++ /dev/null @@ -1 +0,0 @@ -{"tree":{"version":1,"identifiers":[],"shapes":[{"c":{"type":"com.fluidframework.example.cli.List","value":false,"fields":[["",1]]}},{"a":2},{"c":{"type":"com.fluidframework.leaf.string","value":true}}],"data":[[0,["x"]]]},"schema":{"version":1,"nodes":{"com.fluidframework.example.cli.List":{"object":{"":{"kind":"Sequence","types":["com.fluidframework.leaf.string"]}}},"com.fluidframework.leaf.string":{"leaf":1}},"root":{"kind":"Value","types":["com.fluidframework.example.cli.List"]}},"idCompressor":"AAAAAAAAAEAAAAAAAADwPwAAAAAAAPA/AAAAAAAAAAC3byk/I3d3gUqlkUUy4+IAAAAAAAAAAAAAAAAAAADwPwAAAAAAAAAA"} \ No newline at end of file diff --git a/examples/apps/tree-cli-app/data/list.verbose.json b/examples/apps/tree-cli-app/data/list.verbose.json deleted file mode 100644 index bddf3e4640b6..000000000000 --- a/examples/apps/tree-cli-app/data/list.verbose.json +++ /dev/null @@ -1 +0,0 @@ -{"type":"com.fluidframework.example.cli.List","fields":["x"]} \ No newline at end of file diff --git a/examples/apps/tree-cli-app/data/test.verbose.json b/examples/apps/tree-cli-app/data/test.verbose.json deleted file mode 100644 index 5e552cdf566d..000000000000 --- a/examples/apps/tree-cli-app/data/test.verbose.json +++ /dev/null @@ -1 +0,0 @@ -{ "type": "com.fluidframework.example.cli.List", "fields": ["x"] } diff --git a/examples/apps/tree-cli-app/data/test2.concise.json b/examples/apps/tree-cli-app/data/test2.concise.json deleted file mode 100644 index 172d152c1c5c..000000000000 --- a/examples/apps/tree-cli-app/data/test2.concise.json +++ /dev/null @@ -1 +0,0 @@ -["x","x"] \ No newline at end of file diff --git a/examples/apps/tree-cli-app/src/index.ts b/examples/apps/tree-cli-app/src/index.ts index 9b02780fcbe3..28430ba62126 100644 --- a/examples/apps/tree-cli-app/src/index.ts +++ b/examples/apps/tree-cli-app/src/index.ts @@ -12,6 +12,12 @@ const args = process.argv.slice(2); console.log(`Requires arguments: [] [] []`); console.log(`Example arguments: default data/large.concise.json string:10,item:100`); +console.log( + `File formats are specified by extension, for example ".verbose.json" uses the "verbose" format.`, +); +console.log( + `See implementation for supported formats and edit syntax: this is just a demon, not a nice app!`, +); console.log(`Running with augments: ${args}`); if (args.length > 3) { From ed4b5921e5d2e0eec8eb186b0ba64a3319173d22 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:56:00 -0700 Subject: [PATCH 47/54] Add more test trees, and fix tests --- .../tree/src/simple-tree/api/treeApiBeta.ts | 26 +++-- .../src/test/simple-tree/api/treeApi.spec.ts | 96 ++++++++++++++----- .../hasAmbiguousField.json | 33 +++++++ .../hasRenamedField.json | 29 ++++++ .../schema-files/hasAmbiguousField.json | 28 ++++++ .../schema-files/hasRenamedField.json | 24 +++++ .../hasAmbiguousField.json | 28 ++++++ .../hasRenamedField.json | 24 +++++ packages/dds/tree/src/test/testTrees.ts | 32 ++++++- 9 files changed, 282 insertions(+), 38 deletions(-) create mode 100644 packages/dds/tree/src/test/snapshots/chunked-forest-schema-compressed/hasAmbiguousField.json create mode 100644 packages/dds/tree/src/test/snapshots/chunked-forest-schema-compressed/hasRenamedField.json create mode 100644 packages/dds/tree/src/test/snapshots/schema-files/hasAmbiguousField.json create mode 100644 packages/dds/tree/src/test/snapshots/schema-files/hasRenamedField.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasAmbiguousField.json create mode 100644 packages/dds/tree/src/test/snapshots/simple-tree-storedSchema/hasRenamedField.json diff --git a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts index 7c323e448273..0a044610e6ed 100644 --- a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts +++ b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts @@ -14,13 +14,15 @@ import { } from "../core/index.js"; import { treeNodeApi, tryGetSchema } from "./treeNodeApi.js"; import { createFromCursor, createFromInsertable, cursorFromInsertable } from "./create.js"; -import type { - ImplicitFieldSchema, - InsertableField, - InsertableTreeFieldFromImplicitField, - TreeFieldFromImplicitField, - TreeLeafValue, - UnsafeUnknownSchema, +import { + FieldKind, + normalizeFieldSchema, + type ImplicitFieldSchema, + type InsertableField, + type InsertableTreeFieldFromImplicitField, + type TreeFieldFromImplicitField, + type TreeLeafValue, + type UnsafeUnknownSchema, } from "../schemaTypes.js"; import { conciseFromCursor, type ConciseTree } from "./conciseTree.js"; import { @@ -49,6 +51,7 @@ import { import { createIdCompressor } from "@fluidframework/id-compressor/internal"; import { toStoredSchema } from "../toFlexSchema.js"; import type { EncodeOptions } from "./customTree.js"; +import { UsageError } from "@fluidframework/telemetry-utils/internal"; /** * Data included for {@link TreeChangeEventsBeta.nodeChanged}. @@ -379,7 +382,14 @@ export const TreeBeta: { ...options, }; const schemalessConfig = applySchemaToParserOptions(schema, config); - const cursor = cursorFromVerbose(data, schemalessConfig); + if (data === undefined) { + const field = normalizeFieldSchema(schema); + if (field.kind !== FieldKind.Optional) { + throw new UsageError("undefined provided for non-optional field."); + } + return undefined as Unhydrated>; + } + const cursor = cursorFromVerbose(data, schemalessConfig); return createFromCursor(schema, cursor); }, diff --git a/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts b/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts index f3e1ed91fd96..1caa9d3b4e65 100644 --- a/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts @@ -1129,12 +1129,12 @@ describe("treeNodeApi", () => { // Undefined where not allowed assert.throws( () => TreeBeta.create(schema.required([]), undefined as never), - validateUsageError("invalid"), + validateUsageError(/undefined for non-optional field/), ); // Undefined required, not provided assert.throws( () => TreeBeta.create(schema.optional([]), 1 as unknown as undefined), - validateUsageError("invalid"), + validateUsageError(/incompatible/), ); }); @@ -1144,7 +1144,7 @@ describe("treeNodeApi", () => { // invalid assert.throws( () => TreeBeta.create([schema.null, schema.number], "x" as unknown as number), - validateUsageError("invalid"), + validateUsageError(/incompatible/), ); }); @@ -1152,7 +1152,7 @@ describe("treeNodeApi", () => { it("object", () => { const A = schema.object("A", { x: schema.number }); const a = TreeBeta.create(A, { x: 1 }); - assert.equal(a, { x: 1 }); + assert.deepEqual(a, { x: 1 }); }); }); @@ -1164,12 +1164,12 @@ describe("treeNodeApi", () => { // Undefined where not allowed assert.throws( () => TreeBeta.importConcise(schema.required([]), undefined), - validateUsageError("invalid"), + validateUsageError(/Got undefined for non-optional field/), ); // Undefined required, not provided assert.throws( () => TreeBeta.importConcise(schema.optional([]), 1), - validateUsageError("invalid"), + validateUsageError(/incompatible with all of the types allowed/), ); }); @@ -1179,36 +1179,62 @@ describe("treeNodeApi", () => { // invalid assert.throws( () => TreeBeta.importConcise([schema.null, schema.number], "x"), - validateUsageError("invalid"), + validateUsageError(/The provided data is incompatible/), ); }); it("object", () => { const A = schema.object("A", { x: schema.number }); const a = TreeBeta.importConcise(A, { x: 1 }); - assert.equal(a, { x: 1 }); + assert.deepEqual(a, { x: 1 }); }); }); describe("roundtrip", () => { for (const testCase of testSimpleTrees) { - if (testCase.root !== undefined) { + if (testCase.root() !== undefined) { it(testCase.name, () => { - const tree = TreeBeta.create(testCase.schema, testCase.root); + const tree = TreeBeta.create( + testCase.schema, + testCase.root(), + ); assert(tree !== undefined); const exported = TreeBeta.exportConcise(tree); - const imported = TreeBeta.importConcise( + if (testCase.ambiguous) { + assert.throws( + () => TreeBeta.importConcise(testCase.schema, exported), + validateUsageError(/compatible with more than one type/), + ); + } else { + const imported = TreeBeta.importConcise( + testCase.schema, + exported, + ); + expectTreesEqual(tree, imported); + } + }); + } + } + }); + + describe("export-stored", () => { + for (const testCase of testSimpleTrees) { + if (testCase.root() !== undefined) { + it(testCase.name, () => { + const tree = TreeBeta.create( testCase.schema, - exported, + testCase.root(), ); - expectTreesEqual(tree, imported); + assert(tree !== undefined); + const _exported = TreeBeta.exportConcise(tree, { useStoredKeys: true }); + // We have nothing that imports concise trees with stored keys, so no validation here. }); } } }); }); - describe("concise", () => { + describe("verbose", () => { describe("importVerbose", () => { it("undefined", () => { // Valid @@ -1216,12 +1242,12 @@ describe("treeNodeApi", () => { // Undefined where not allowed assert.throws( () => TreeBeta.importVerbose(schema.required([]), undefined), - validateUsageError("invalid"), + validateUsageError(/non-optional/), ); // Undefined required, not provided assert.throws( () => TreeBeta.importVerbose(schema.optional([]), 1), - validateUsageError("invalid"), + validateUsageError(/does not conform to schema/), ); }); @@ -1231,30 +1257,48 @@ describe("treeNodeApi", () => { // invalid assert.throws( () => TreeBeta.importVerbose([schema.null, schema.number], "x"), - validateUsageError("invalid"), + validateUsageError(/does not conform to schema/), ); }); it("object", () => { const A = schema.object("A", { x: schema.number }); const a = TreeBeta.importVerbose(A, { type: A.identifier, fields: { x: 1 } }); - assert.equal(a, { x: 1 }); + assert.deepEqual(a, { x: 1 }); }); }); describe("roundtrip", () => { for (const testCase of testSimpleTrees) { - if (testCase.root !== undefined) { + if (testCase.root() !== undefined) { it(testCase.name, () => { - const tree = TreeBeta.create(testCase.schema, testCase.root); + const tree = TreeBeta.create( + testCase.schema, + testCase.root(), + ); assert(tree !== undefined); - // TODO: fix typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const exported = TreeBeta.exportVerbose(tree as any); + const exported = TreeBeta.exportVerbose(tree); const imported = TreeBeta.importVerbose(testCase.schema, exported); - // TODO: fix typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expectTreesEqual(tree as any, imported as any); + expectTreesEqual(tree, imported); + }); + } + } + }); + + describe("roundtrip-stored", () => { + for (const testCase of testSimpleTrees) { + if (testCase.root() !== undefined) { + it(testCase.name, () => { + const tree = TreeBeta.create( + testCase.schema, + testCase.root(), + ); + assert(tree !== undefined); + const exported = TreeBeta.exportVerbose(tree, { useStoredKeys: true }); + const imported = TreeBeta.importVerbose(testCase.schema, exported, { + useStoredKeys: true, + }); + expectTreesEqual(tree, imported); }); } } 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..fa6cc1a727be 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 { @@ -66,13 +69,20 @@ interface TestTree { 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, {}), From 220cde3e4980533181a1233e46b0b8bf5306151a Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Wed, 23 Oct 2024 17:28:39 -0700 Subject: [PATCH 48/54] clone updates --- .../tree/src/simple-tree/api/treeApiBeta.ts | 27 ++++------------- .../src/test/simple-tree/api/treeApi.spec.ts | 30 ++++++++++++++++++- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts index 0a044610e6ed..2440a82ad669 100644 --- a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts +++ b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts @@ -319,33 +319,16 @@ export const TreeBeta: { 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; } const kernel = getKernel(node); - /* - * For unhydrated nodes, we can create a cursor by calling `cursorFromInsertable` because the node - * hasn't been inserted yet. We can then create a new node from the cursor. - */ - if (!kernel.isHydrated()) { - return createFromCursor( - kernel.schema, - cursorFromInsertable(kernel.schema, node), - ) as Unhydrated>; - } - - // For hydrated nodes, create a new cursor in the forest and then create a new node from the cursor. - const forest = kernel.context.flexContext.checkout.forest; - const cursor = forest.allocateCursor("tree.clone"); - forest.moveCursorToPath(kernel.anchorNode, cursor); - const clonedNode = createFromCursor( - kernel.schema, - cursor as ITreeCursorSynchronous, - ) as Unhydrated>; - cursor.free(); - return clonedNode; + const cursor = kernel.getOrCreateInnerNode().borrowCursor(); + return createFromCursor(kernel.schema, cursor as ITreeCursorSynchronous) as Unhydrated< + TreeFieldFromImplicitField + >; }, create: createFromInsertable, diff --git a/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts b/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts index 1caa9d3b4e65..98fa71df89ed 100644 --- a/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts @@ -17,6 +17,7 @@ import { type StableNodeKey, } from "../../../feature-libraries/index.js"; import { + isTreeNode, type NodeFromSchema, SchemaFactory, treeNodeApi as Tree, @@ -1119,6 +1120,20 @@ describe("treeNodeApi", () => { const clonedMetadata = TreeBeta.clone(topLeftPoint.metadata); assert.equal(clonedMetadata, topLeftPoint.metadata, "String not cloned properly"); }); + + describe("test-trees", () => { + for (const testCase of testSimpleTrees) { + it(testCase.name, () => { + const tree = TreeBeta.create(testCase.schema, testCase.root()); + const exported = TreeBeta.clone(tree); + if (isTreeNode(tree)) { + // New instance + assert.notEqual(tree, exported); + } + expectTreesEqual(tree, exported); + }); + } + }); }); // create is mostly the same as node constructors which have their own tests, so just cover the new cases (optional and top level unions) here. @@ -1305,7 +1320,20 @@ describe("treeNodeApi", () => { }); }); - // TODO: test exportCompressed + // TODO: test exportCompressed, add importCompressed + + describe("compressed", () => { + describe("roundtrip", () => { + for (const testCase of testSimpleTrees) { + it(testCase.name, () => { + const tree = TreeBeta.create(testCase.schema, testCase.root()); + // const exported = TreeBeta.exportCompressed(tree, X); + // const imported = TreeBeta.importCompressed(testCase.schema, exported); + // expectTreesEqual(tree, imported); + }); + } + }); + }); }); function expectTreesEqual( From 54e84f247795390f1410143a75443771ecd1c9a5 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 24 Oct 2024 10:39:09 -0700 Subject: [PATCH 49/54] TreeAlpha --- packages/dds/tree/.vscode/settings.json | 1 + .../dds/tree/api-report/tree.alpha.api.md | 42 ++- packages/dds/tree/api-report/tree.beta.api.md | 29 -- packages/dds/tree/src/codec/codec.ts | 2 +- packages/dds/tree/src/index.ts | 6 +- .../tree/src/shared-tree/independentView.ts | 176 +++++++++ packages/dds/tree/src/shared-tree/index.ts | 8 + .../dds/tree/src/shared-tree/treeApiAlpha.ts | 349 ++++++++++++++++++ .../tree/src/simple-tree/api/conciseTree.ts | 4 +- .../dds/tree/src/simple-tree/api/index.ts | 17 +- packages/dds/tree/src/simple-tree/api/tree.ts | 7 +- .../tree/src/simple-tree/api/treeApiBeta.ts | 302 +-------------- packages/dds/tree/src/simple-tree/index.ts | 7 + .../src/test/simple-tree/api/create.spec.ts | 5 +- .../src/test/simple-tree/api/treeApi.spec.ts | 93 +++-- packages/dds/tree/src/treeFactory.ts | 174 +-------- 16 files changed, 652 insertions(+), 570 deletions(-) create mode 100644 packages/dds/tree/src/shared-tree/independentView.ts create mode 100644 packages/dds/tree/src/shared-tree/treeApiAlpha.ts diff --git a/packages/dds/tree/.vscode/settings.json b/packages/dds/tree/.vscode/settings.json index a2d97d35d22f..cd0ea00806f9 100644 --- a/packages/dds/tree/.vscode/settings.json +++ b/packages/dds/tree/.vscode/settings.json @@ -20,6 +20,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 c879efc5c556..cf327a19073a 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -42,7 +42,7 @@ export interface CommitMetadata { // @alpha export function comparePersistedSchema(persisted: JsonCompatible, view: JsonCompatible, options: ICodecOptions, canInitialize: boolean): SchemaCompatibilityStatus; -// @beta +// @alpha export type ConciseTree = Exclude | THandle | ConciseTree[] | { [key: string]: ConciseTree; }; @@ -138,7 +138,7 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; -// @beta +// @alpha export enum FluidClientVersion { // (undocumented) v2_0 = "v2_0", @@ -185,6 +185,11 @@ export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; // @alpha export function independentInitializedView(config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent): TreeView; +// @alpha +export function independentView(config: TreeViewConfiguration, options: ForestOptions & { + idCompressor?: IIdCompressor_2 | undefined; +}): TreeView; + // @public type _InlineTrick = 0; @@ -599,6 +604,25 @@ export type TransactionConstraint = NodeInDocumentConstraint; // @public export const Tree: TreeApi; +// @alpha @sealed +export const TreeAlpha: { + create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; + importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated : TreeNode | TreeLeafValue | undefined>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + exportCompressed(tree: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; + }): JsonCompatible; + importCompressed(schema: TSchema, compressedData: JsonCompatible, options: { + idCompressor?: IIdCompressor; + } & ICodecOptions): Unhydrated>; +}; + // @public @sealed interface TreeApi extends TreeNodeApi { contains(node: TreeNode, other: TreeNode): boolean; @@ -644,18 +668,6 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; - create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; - importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated : TreeNode | TreeLeafValue | undefined>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; - exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; - exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; - exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; - exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; - exportCompressed(tree: TreeNode | TreeLeafValue, options: { - oldestCompatibleClient: FluidClientVersion; - idCompressor?: IIdCompressor; - }): JsonCompatible; }; // @alpha @sealed @@ -884,7 +896,7 @@ export interface ViewableTree { // @alpha export interface ViewContent { - readonly idCompressor: IIdCompressor; + readonly idCompressor: IIdCompressor_2; readonly schema: JsonCompatible; readonly tree: JsonCompatible; } diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index 070450af19fe..17b38b63d09e 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -30,11 +30,6 @@ export interface CommitMetadata { readonly kind: CommitKind; } -// @beta -export type ConciseTree = Exclude | THandle | ConciseTree[] | { - [key: string]: ConciseTree; -}; - // @public @sealed interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } @@ -111,18 +106,6 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; -// @beta -export enum FluidClientVersion { - // (undocumented) - v2_0 = "v2_0", - // (undocumented) - v2_1 = "v2_1", - // (undocumented) - v2_2 = "v2_2", - // (undocumented) - v2_3 = "v2_3" -} - // @public export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; @@ -487,18 +470,6 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; - create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; - importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated : TreeNode | TreeLeafValue | undefined>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; - exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; - exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; - exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; - exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; - exportCompressed(tree: TreeNode | TreeLeafValue, options: { - oldestCompatibleClient: FluidClientVersion; - idCompressor?: IIdCompressor; - }): JsonCompatible; }; // @public @sealed diff --git a/packages/dds/tree/src/codec/codec.ts b/packages/dds/tree/src/codec/codec.ts index 1272e6dff14b..b1eb3ba2c06b 100644 --- a/packages/dds/tree/src/codec/codec.ts +++ b/packages/dds/tree/src/codec/codec.ts @@ -339,7 +339,7 @@ export function withSchemaValidation< * If this is violated (for example a subset of incompatible features from 3.x that are not in 3.0 are back ported to 2.x), * a more complex scheme may be needed to allow safely opting into incompatible features in those cases: * such a system can be added if/when its needed since it will be opt in and thus non-breaking. - * @beta + * @alpha */ export enum FluidClientVersion { v2_0 = "v2_0", diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index ead1fb199294..df0bdc3db02b 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -60,6 +60,10 @@ export { getBranch, type TreeBranch, type TreeBranchFork, + independentInitializedView, + type ViewContent, + TreeAlpha, + independentView, } from "./shared-tree/index.js"; export { @@ -169,8 +173,6 @@ export { export { SharedTree, configuredSharedTree, - independentInitializedView, - type ViewContent, } from "./treeFactory.js"; export { diff --git a/packages/dds/tree/src/shared-tree/independentView.ts b/packages/dds/tree/src/shared-tree/independentView.ts new file mode 100644 index 000000000000..bd8c1dd20b58 --- /dev/null +++ b/packages/dds/tree/src/shared-tree/independentView.ts @@ -0,0 +1,176 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import { assert } from "@fluidframework/core-utils/internal"; +import { + type IIdCompressor, + createIdCompressor, +} from "@fluidframework/id-compressor/internal"; +import type { ICodecOptions } from "../codec/index.js"; +import { + type RevisionTag, + RevisionTagCodec, + TreeStoredSchemaRepository, + initializeForest, + type ITreeCursorSynchronous, + mapCursorField, +} from "../core/index.js"; +import { + createNodeKeyManager, + makeFieldBatchCodec, + makeSchemaCodec, + type FieldBatchEncodingContext, + defaultSchemaPolicy, + chunkTree, + defaultChunkPolicy, + TreeCompressionStrategy, +} from "../feature-libraries/index.js"; +// eslint-disable-next-line import/no-internal-modules +import type { Format } from "../feature-libraries/schema-index/format.js"; +import type { + TreeViewConfiguration, + TreeView, + ImplicitFieldSchema, +} from "../simple-tree/index.js"; +import type { JsonCompatibleReadOnly, JsonCompatible } from "../util/index.js"; +import { + buildConfiguredForest, + defaultSharedTreeOptions, + type ForestOptions, +} from "./sharedTree.js"; +import { createTreeCheckout } from "./treeCheckout.js"; +import { SchematizingSimpleTreeView } from "./schematizingTreeView.js"; + +/** + * Create an uninitialized {@link TreeView} that is not tied to any {@link ITree} instance. + * + * @remarks + * Such a view can never experience collaboration or be persisted to to a Fluid Container. + * + * This can be useful for testing, as well as use-cases like working on local files instead of documents stored in some Fluid service. + * @alpha + */ +export function independentView( + config: TreeViewConfiguration, + options: ForestOptions & { idCompressor?: IIdCompressor | undefined }, +): TreeView { + const idCompressor: IIdCompressor = options.idCompressor ?? createIdCompressor(); + const mintRevisionTag = (): RevisionTag => idCompressor.generateCompressedId(); + const revisionTagCodec = new RevisionTagCodec(idCompressor); + const schema = new TreeStoredSchemaRepository(); + const forest = buildConfiguredForest( + options.forest ?? defaultSharedTreeOptions.forest, + schema, + idCompressor, + ); + const checkout = createTreeCheckout(idCompressor, mintRevisionTag, revisionTagCodec, { + forest, + schema, + }); + const out: TreeView = new SchematizingSimpleTreeView( + checkout, + config, + createNodeKeyManager(idCompressor), + ); + return out; +} +/** + * Create an uninitialized {@link TreeView} that is not tied to any {@link ITree} instance. + * + * @remarks + * Such a view can never experience collaboration or be persisted to to a Fluid Container. + * + * This can be useful for testing, as well as use-cases like working on local files instead of documents stored in some Fluid service. + * @alpha + */ +export function independentInitializedView( + config: TreeViewConfiguration, + options: ForestOptions & ICodecOptions, + content: ViewContent, +): TreeView { + const idCompressor: IIdCompressor = content.idCompressor; + const mintRevisionTag = (): RevisionTag => idCompressor.generateCompressedId(); + const revisionTagCodec = new RevisionTagCodec(idCompressor); + + const fieldBatchCodec = makeFieldBatchCodec(options, 1); + const schemaCodec = makeSchemaCodec(options); + + const schema = new TreeStoredSchemaRepository(schemaCodec.decode(content.schema as Format)); + const forest = buildConfiguredForest( + options.forest ?? defaultSharedTreeOptions.forest, + schema, + idCompressor, + ); + + const context: FieldBatchEncodingContext = { + encodeType: TreeCompressionStrategy.Compressed, + idCompressor, + originatorId: idCompressor.localSessionId, // Is this right? If so, why is is needed? + schema: { schema, policy: defaultSchemaPolicy }, + }; + + const fieldCursors = fieldBatchCodec.decode(content.tree as JsonCompatibleReadOnly, context); + assert(fieldCursors.length === 1, "must have exactly 1 field in batch"); + // Checked above. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const cursors = fieldCursorToNodesCursors(fieldCursors[0]!); + + initializeForest(forest, cursors, revisionTagCodec, idCompressor, false); + + const checkout = createTreeCheckout(idCompressor, mintRevisionTag, revisionTagCodec, { + forest, + schema, + }); + const out: TreeView = new SchematizingSimpleTreeView( + checkout, + config, + createNodeKeyManager(idCompressor), + ); + return out; +} + +function fieldCursorToNodesCursors( + fieldCursor: ITreeCursorSynchronous, +): ITreeCursorSynchronous[] { + return mapCursorField(fieldCursor, copyNodeCursor); +} + +/** + * TODO: avoid needing this, or optimize it. + */ +function copyNodeCursor(cursor: ITreeCursorSynchronous): ITreeCursorSynchronous { + const copy = chunkTree(cursor, { + policy: defaultChunkPolicy, + idCompressor: undefined, + }).cursor(); + copy.enterNode(0); + return copy; +} + +/** + * The portion of SharedTree data typically persisted the container. + * Usable with {@link independentInitializedView} to create a {@link TreeView} + * without loading a container. + * @alpha + */ +export interface ViewContent { + /** + * Compressed tree from {@link TreeAlpha.exportCompressed}. + * @remarks + * This is an owning reference: + * consumers of this content might modify this data in place (for example when applying edits) to avoid copying. + */ + readonly tree: JsonCompatible; + /** + * Persisted schema from {@link extractPersistedSchema}. + */ + readonly schema: JsonCompatible; + /** + * IIdCompressor which will be used to decompress any compressed identifiers in `tree` + * as well as for any other identifiers added to the view. + */ + readonly idCompressor: IIdCompressor; +} diff --git a/packages/dds/tree/src/shared-tree/index.ts b/packages/dds/tree/src/shared-tree/index.ts index be2f40a90ab1..1645b21fa168 100644 --- a/packages/dds/tree/src/shared-tree/index.ts +++ b/packages/dds/tree/src/shared-tree/index.ts @@ -46,3 +46,11 @@ export { type RunTransaction, rollback, } from "./treeApi.js"; + +export { TreeAlpha } from "./treeApiAlpha.js"; + +export { + independentInitializedView, + type ViewContent, + independentView, +} from "./independentView.js"; diff --git a/packages/dds/tree/src/shared-tree/treeApiAlpha.ts b/packages/dds/tree/src/shared-tree/treeApiAlpha.ts new file mode 100644 index 000000000000..22d461532700 --- /dev/null +++ b/packages/dds/tree/src/shared-tree/treeApiAlpha.ts @@ -0,0 +1,349 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { createIdCompressor } from "@fluidframework/id-compressor/internal"; +import { UsageError } from "@fluidframework/telemetry-utils/internal"; +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import type { IIdCompressor } from "@fluidframework/id-compressor"; + +import { + getKernel, + type TreeNode, + type Unhydrated, + TreeBeta, + tryGetSchema, + createFromCursor, + createFromInsertable, + cursorFromInsertable, + FieldKind, + normalizeFieldSchema, + type ImplicitFieldSchema, + type InsertableField, + type InsertableTreeFieldFromImplicitField, + type TreeFieldFromImplicitField, + type TreeLeafValue, + type UnsafeUnknownSchema, + conciseFromCursor, + type ConciseTree, + applySchemaToParserOptions, + cursorFromVerbose, + verboseFromCursor, + type ParseOptions, + type VerboseTree, + type VerboseTreeNode, + toStoredSchema, + type EncodeOptions, + extractPersistedSchema, + TreeViewConfiguration, +} from "../simple-tree/index.js"; +import { fail, type JsonCompatible } from "../util/index.js"; +import { noopValidator, type FluidClientVersion, type ICodecOptions } from "../codec/index.js"; +import type { ITreeCursorSynchronous } from "../core/index.js"; +import { + cursorForMapTreeField, + defaultSchemaPolicy, + isTreeValue, + makeFieldBatchCodec, + mapTreeFromCursor, + TreeCompressionStrategy, + type FieldBatch, + type FieldBatchEncodingContext, +} from "../feature-libraries/index.js"; +import { independentInitializedView, type ViewContent } from "./independentView.js"; + +/** + * Extensions to {@link Tree} and {@link TreeBeta} which are not yet stable. + * @sealed @alpha + */ +export const TreeAlpha: { + /** + * Construct tree content that is compatible with the field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. + * @remarks + * When providing a {@link TreeNodeSchemaClass}, this is the same as invoking its constructor except that an unhydrated node can also be provided. + * This function exists as a generalization that can be used in other cases as well, + * 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. + * For that case, use {@link TreeBeta.clone}. + * @privateRemarks + * There should be a way to provide an source for defaulted identifiers, wither via this API or some way to add them to its output later. + */ + create( + schema: UnsafeUnknownSchema extends TSchema + ? ImplicitFieldSchema + : TSchema & ImplicitFieldSchema, + data: InsertableField, + ): Unhydrated< + TSchema extends ImplicitFieldSchema + ? TreeFieldFromImplicitField + : TreeNode | TreeLeafValue | undefined + >; + + /** + * Less type safe version of {@link TreeAlpha.create}, suitable for importing data. + * @remarks + * Due to {@link ConciseTree} relying on type inference from the data, its use is somewhat limited. + * This does not support {@link ConciseTree}'s with customized handle encodings or using persisted keys. + * Use "compressed" or "verbose" formats to for more flexibility. + * + * When using this function, + * it is recommend to ensure you schema is unambiguous with {@link ITreeConfigurationOptions.preventAmbiguity}. + * If the schema is ambiguous, consider using {@link TreeAlpha.create} and {@link Unhydrated} nodes where needed, + * or using {@link TreeAlpha.(importVerbose:1)} and specify all types. + * + * Documented (and thus recoverable) error handling/reporting for this is not yet implemented, + * but for now most invalid inputs will throw a recoverable error. + */ + importConcise( + schema: UnsafeUnknownSchema extends TSchema + ? ImplicitFieldSchema + : TSchema & ImplicitFieldSchema, + data: InsertableTreeFieldFromImplicitField | ConciseTree, + ): Unhydrated< + TSchema extends ImplicitFieldSchema + ? TreeFieldFromImplicitField + : TreeNode | TreeLeafValue | undefined + >; + + /** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + */ + importVerbose( + schema: TSchema, + data: VerboseTree | undefined, + options?: Partial>, + ): Unhydrated>; + + /** + * Construct tree content compatible with a field defined by the provided `schema`. + * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. + * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. + * @privateRemarks + * This could be exposed as a public `Tree.createFromVerbose` function. + */ + importVerbose( + schema: TSchema, + data: VerboseTree | undefined, + options: ParseOptions, + ): Unhydrated>; + + /** + * Same as generic overload, except leaves handles as is. + */ + exportConcise( + node: TreeNode | TreeLeafValue, + options?: Partial>, + ): ConciseTree; + + /** + * Copy a snapshot of the current version of a TreeNode into a {@link ConciseTree}. + */ + exportConcise( + node: TreeNode | TreeLeafValue, + options?: EncodeOptions, + ): ConciseTree; + + /** + * Same {@link TreeAlpha.(exportVerbose:2)} except leaves handles as is. + */ + exportVerbose( + node: TreeNode | TreeLeafValue, + options?: Partial>, + ): VerboseTree; + + /** + * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. + * Verbose tree format, with explicit type on every node. + * + * @remarks + * There are several cases this may be preferred to {@link TreeAlpha.(exportConcise:2)}: + * + * 1. When not using {@link ITreeConfigurationOptions.preventAmbiguity} (or when using `useStableFieldKeys`), `exportConcise` can produce ambiguous data (the type may be unclear on some nodes). + * `exportVerbose` will always be unambiguous and thus lossless. + * + * 2. When the data might be interpreted without access to the exact same view schema. In such cases, the types may be unknowable if not included. + * + * 3. When easy access to the type is desired. + */ + exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + + /** + * Export the content of the provided `tree` in a compressed JSON compatible format. + * @remarks + * If an `idCompressor` is provided, it will be used to compress identifiers and thus will be needed to decompress the data. + * + * Always uses "stored" keys. + * See {@link EncodeOptions.useStoredKeys} for details. + */ + exportCompressed( + tree: TreeNode | TreeLeafValue, + options: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor }, + ): JsonCompatible; + + /** + * Import data encoded by {@link TreeAlpha.exportCompressed}. + * + * @param schema - Schema with witch the data must be compatible. This compatibility is not verified and must be ensured by the caller. + * @param compressedData - Data compressed by {@link TreeAlpha.exportCompressed}. + * @param options - If {@link TreeAlpha.exportCompressed} was given an `idCompressor`, it must be provided here. + * + * @remarks + * If the data could have been encoded with a different schema, consider encoding the schema along side it using {@link extractPersistedSchema} and loading the data using {@link independentView}. + * + * @privateRemarks + * This API could be improved: + * + * 1. It could validate that the schema is compatible, and return or throw an error in the invalid case (maybe add a "try" version). + * 2. A "try" version of this could return an error if the data isn't in a supported format (as determined by version and/or JasonValidator). + * 3. Requiring the caller provide a JsonValidator isn't the most friendly API. It might be practical to provide a default. + */ + importCompressed( + schema: TSchema, + compressedData: JsonCompatible, + options: { idCompressor?: IIdCompressor } & ICodecOptions, + ): Unhydrated>; +} = { + create: createFromInsertable, + + importConcise( + schema: UnsafeUnknownSchema extends TSchema + ? ImplicitFieldSchema + : TSchema & ImplicitFieldSchema, + data: InsertableTreeFieldFromImplicitField | ConciseTree, + ): Unhydrated< + TSchema extends ImplicitFieldSchema + ? TreeFieldFromImplicitField + : TreeNode | TreeLeafValue | undefined + > { + return createFromInsertable( + schema, + data as InsertableField, + ) as Unhydrated< + TSchema extends ImplicitFieldSchema + ? TreeFieldFromImplicitField + : TreeNode | TreeLeafValue | undefined + >; + }, + + importVerbose( + schema: TSchema, + data: VerboseTree | undefined, + options?: Partial>, + ): Unhydrated> { + const config: ParseOptions = { + valueConverter: (input: VerboseTree) => { + return input as TreeLeafValue | VerboseTreeNode; + }, + ...options, + }; + const schemalessConfig = applySchemaToParserOptions(schema, config); + if (data === undefined) { + const field = normalizeFieldSchema(schema); + if (field.kind !== FieldKind.Optional) { + throw new UsageError("undefined provided for non-optional field."); + } + return undefined as Unhydrated>; + } + const cursor = cursorFromVerbose(data, schemalessConfig); + return createFromCursor(schema, cursor); + }, + + exportConcise( + node: TreeNode | TreeLeafValue, + options?: Partial>, + ): ConciseTree { + const config: EncodeOptions = { + valueConverter(handle: IFluidHandle): T { + return handle as T; + }, + ...options, + }; + + const cursor = borrowCursorFromTreeNodeOrValue(node); + return conciseFromCursor(cursor, tryGetSchema(node) ?? fail("invalid input"), config); + }, + + exportVerbose( + node: TreeNode | TreeLeafValue, + options?: Partial>, + ): VerboseTree { + const config: EncodeOptions = { + valueConverter(handle: IFluidHandle): T { + return handle as T; + }, + ...options, + }; + + const cursor = borrowCursorFromTreeNodeOrValue(node); + return verboseFromCursor(cursor, tryGetSchema(node) ?? fail("invalid input"), config); + }, + + exportCompressed( + node: TreeNode | TreeLeafValue, + options: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor }, + ): JsonCompatible { + const schema = tryGetSchema(node) ?? fail("invalid input"); + const format = versionToFormat[options.oldestCompatibleClient]; + const codec = makeFieldBatchCodec({ jsonValidator: noopValidator }, format); + const cursor = borrowFieldCursorFromTreeNodeOrValue(node); + const batch: FieldBatch = [cursor]; + // If none provided, create a compressor which will not compress anything (TODO: is this the right way to do that?). + const idCompressor = options.idCompressor ?? createIdCompressor(); + const context: FieldBatchEncodingContext = { + encodeType: TreeCompressionStrategy.Compressed, + idCompressor, + originatorId: idCompressor.localSessionId, // Is this right? If so, why is is needed? + schema: { schema: toStoredSchema(schema), policy: defaultSchemaPolicy }, + }; + const result = codec.encode(batch, context); + return result; + }, + + importCompressed( + schema: TSchema, + compressedData: JsonCompatible, + options: { idCompressor?: IIdCompressor } & ICodecOptions, + ): Unhydrated> { + const content: ViewContent = { + schema: extractPersistedSchema(schema), + tree: compressedData, + idCompressor: options.idCompressor ?? createIdCompressor(), + }; + const config = new TreeViewConfiguration({ schema }); + const view = independentInitializedView(config, options, content); + return TreeBeta.clone(view.root); + }, +}; + +function borrowCursorFromTreeNodeOrValue( + node: TreeNode | TreeLeafValue, +): ITreeCursorSynchronous { + if (isTreeValue(node)) { + return cursorFromInsertable(tryGetSchema(node) ?? fail("missing schema"), node); + } + const kernel = getKernel(node); + const cursor = kernel.getOrCreateInnerNode().borrowCursor(); + return cursor; +} + +function borrowFieldCursorFromTreeNodeOrValue( + node: TreeNode | TreeLeafValue, +): ITreeCursorSynchronous { + const cursor = borrowCursorFromTreeNodeOrValue(node); + // TODO: avoid copy + const mapTree = mapTreeFromCursor(cursor); + return cursorForMapTreeField([mapTree]); +} + +const versionToFormat = { + v2_0: 1, + v2_1: 1, + v2_2: 1, + v2_3: 1, +}; diff --git a/packages/dds/tree/src/simple-tree/api/conciseTree.ts b/packages/dds/tree/src/simple-tree/api/conciseTree.ts index ea28d6e31c2d..78b91643f8f3 100644 --- a/packages/dds/tree/src/simple-tree/api/conciseTree.ts +++ b/packages/dds/tree/src/simple-tree/api/conciseTree.ts @@ -16,14 +16,14 @@ 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`. * @privateRemarks * This can store all possible simple trees, * but it can not store all possible trees representable by our internal representations like FlexTree and JsonableTree. - * @beta + * @alpha */ export type ConciseTree = | Exclude diff --git a/packages/dds/tree/src/simple-tree/api/index.ts b/packages/dds/tree/src/simple-tree/api/index.ts index 1c2881a028f8..491d3d0287fe 100644 --- a/packages/dds/tree/src/simple-tree/api/index.ts +++ b/packages/dds/tree/src/simple-tree/api/index.ts @@ -25,8 +25,8 @@ export { singletonSchema, typedObjectValues, } from "./schemaCreationUtilities.js"; -export { treeNodeApi, type TreeNodeApi } from "./treeNodeApi.js"; -export { createFromInsertable, cursorFromInsertable } from "./create.js"; +export { treeNodeApi, type TreeNodeApi, tryGetSchema } from "./treeNodeApi.js"; +export { createFromInsertable, cursorFromInsertable, createFromCursor } from "./create.js"; export type { SimpleTreeSchema } from "./simpleSchema.js"; export { type JsonSchemaId, @@ -69,15 +69,18 @@ export type { TreeNodeSchemaNonClassUnsafe, } from "./typesUnsafe.js"; -export type { - VerboseTreeNode, - ParseOptions, - VerboseTree, +export { + type VerboseTreeNode, + type ParseOptions, + type VerboseTree, + applySchemaToParserOptions, + cursorFromVerbose, + verboseFromCursor, } from "./verboseTree.js"; export type { EncodeOptions } from "./customTree.js"; -export type { ConciseTree } from "./conciseTree.js"; +export { type ConciseTree, conciseFromCursor } from "./conciseTree.js"; export { TreeBeta, type NodeChangedData, type TreeChangeEventsBeta } from "./treeApiBeta.js"; diff --git a/packages/dds/tree/src/simple-tree/api/tree.ts b/packages/dds/tree/src/simple-tree/api/tree.ts index e1107267e7cf..c5936c595629 100644 --- a/packages/dds/tree/src/simple-tree/api/tree.ts +++ b/packages/dds/tree/src/simple-tree/api/tree.ts @@ -67,6 +67,11 @@ export interface ViewableTree { viewWith( config: TreeViewConfiguration, ): TreeView; + + // TODO: + // Add stored key versions of Tree.exportVerbose, Tree.exportConcise and Tree.exportCompressed here. + // Add exportSimpleSchema and exportJsonSchema which constraints the concise format. + // Ensure schema exporting APIs here aline and reference APis for exporting view schema to the same formats (which should include stored vs property key choice) } /** @@ -242,7 +247,7 @@ export class TreeViewConfiguration< if (ambiguityErrors.length !== 0) { // Duplicate errors are common since when two types conflict, both orders error: const deduplicated = new Set(ambiguityErrors); - throw new UsageError(`Ambigious schema found:\n${[...deduplicated].join("\n")}`); + throw new UsageError(`Ambiguous schema found:\n${[...deduplicated].join("\n")}`); } // Eagerly perform this conversion to surface errors sooner. diff --git a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts index 2440a82ad669..86c718f69b47 100644 --- a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts +++ b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts @@ -12,46 +12,9 @@ import { type Unhydrated, type WithType, } from "../core/index.js"; -import { treeNodeApi, tryGetSchema } from "./treeNodeApi.js"; -import { createFromCursor, createFromInsertable, cursorFromInsertable } from "./create.js"; -import { - FieldKind, - normalizeFieldSchema, - type ImplicitFieldSchema, - type InsertableField, - type InsertableTreeFieldFromImplicitField, - type TreeFieldFromImplicitField, - type TreeLeafValue, - type UnsafeUnknownSchema, -} from "../schemaTypes.js"; -import { conciseFromCursor, type ConciseTree } from "./conciseTree.js"; -import { - applySchemaToParserOptions, - cursorFromVerbose, - verboseFromCursor, - type ParseOptions, - type VerboseTree, - type VerboseTreeNode, -} from "./verboseTree.js"; -import type { IFluidHandle } from "@fluidframework/core-interfaces"; -import { fail, type JsonCompatible } from "../../util/index.js"; -import { noopValidator, type FluidClientVersion } from "../../codec/index.js"; -import type { IIdCompressor } from "@fluidframework/id-compressor"; -import type { ITreeCursorSynchronous } from "../../core/index.js"; -import { - cursorForMapTreeField, - defaultSchemaPolicy, - isTreeValue, - makeFieldBatchCodec, - mapTreeFromCursor, - TreeCompressionStrategy, - type FieldBatch, - type FieldBatchEncodingContext, -} from "../../feature-libraries/index.js"; -import { createIdCompressor } from "@fluidframework/id-compressor/internal"; -import { toStoredSchema } from "../toFlexSchema.js"; -import type { EncodeOptions } from "./customTree.js"; -import { UsageError } from "@fluidframework/telemetry-utils/internal"; +import { treeNodeApi } from "./treeNodeApi.js"; +import { createFromCursor } from "./create.js"; +import type { ImplicitFieldSchema, TreeFieldFromImplicitField } from "../schemaTypes.js"; /** * Data included for {@link TreeChangeEventsBeta.nodeChanged}. @@ -179,134 +142,6 @@ export const TreeBeta: { // replaceIdentifiers?: true; // }, // ): TreeFieldFromImplicitField; - - /** - * Construct tree content that is compatible with the field defined by the provided `schema`. - * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. - * @param data - The data used to construct the field content. - * @remarks - * When providing a {@link TreeNodeSchemaClass}, this is the same as invoking its constructor except that an unhydrated node can also be provided. - * This function exists as a generalization that can be used in other cases as well, - * 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. - * For that case, use {@link TreeBeta.clone}. - * @privateRemarks - * There should be a way to provide an source for defaulted identifiers, wither via this API or some way to add them to its output later. - */ - create( - schema: UnsafeUnknownSchema extends TSchema - ? ImplicitFieldSchema - : TSchema & ImplicitFieldSchema, - data: InsertableField, - ): Unhydrated< - TSchema extends ImplicitFieldSchema - ? TreeFieldFromImplicitField - : TreeNode | TreeLeafValue | undefined - >; - - /** - * Less type safe version of {@link TreeBeta.create}, suitable for importing data. - * @remarks - * Due to {@link ConciseTree} relying on type inference from the data, its use is somewhat limited. - * This does not support {@link ConciseTree}'s with customized handle encodings or using persisted keys. - * Use "compressed" or "verbose" formats to for more flexibility. - * - * When using this function, - * it is recommend to ensure you schema is unambiguous with {@link ITreeConfigurationOptions.preventAmbiguity}. - * If the schema is ambiguous, consider using {@link TreeBeta.create} and {@link Unhydrated} nodes where needed, - * or using {@link TreeBeta.(importVerbose:1)} and specify all types. - * - * Documented (and thus recoverable) error handling/reporting for this is not yet implemented, - * but for now most invalid inputs will throw a recoverable error. - */ - importConcise( - schema: UnsafeUnknownSchema extends TSchema - ? ImplicitFieldSchema - : TSchema & ImplicitFieldSchema, - data: InsertableTreeFieldFromImplicitField | ConciseTree, - ): Unhydrated< - TSchema extends ImplicitFieldSchema - ? TreeFieldFromImplicitField - : TreeNode | TreeLeafValue | undefined - >; - - /** - * Construct tree content compatible with a field defined by the provided `schema`. - * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. - * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. - */ - importVerbose( - schema: TSchema, - data: VerboseTree | undefined, - options?: Partial>, - ): Unhydrated>; - - /** - * Construct tree content compatible with a field defined by the provided `schema`. - * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. - * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. - * @privateRemarks - * This could be exposed as a public `Tree.createFromVerbose` function. - */ - importVerbose( - schema: TSchema, - data: VerboseTree | undefined, - options: ParseOptions, - ): Unhydrated>; - - /** - * Same as generic overload, except leaves handles as is. - */ - exportConcise( - node: TreeNode | TreeLeafValue, - options?: Partial>, - ): ConciseTree; - - /** - * Copy a snapshot of the current version of a TreeNode into a {@link ConciseTree}. - */ - exportConcise( - node: TreeNode | TreeLeafValue, - options?: EncodeOptions, - ): ConciseTree; - - /** - * Same {@link TreeBeta.(exportVerbose:1)} except leaves handles as is. - */ - exportVerbose( - node: TreeNode | TreeLeafValue, - options?: Partial>, - ): VerboseTree; - - /** - * Copy a snapshot of the current version of a TreeNode into a JSON compatible plain old JavaScript Object. - * Verbose tree format, with explicit type on every node. - * - * @remarks - * There are several cases this may be preferred to {@link TreeBeta.(exportConcise:1)}: - * - * 1. When not using {@link ITreeConfigurationOptions.preventAmbiguity} (or when using `useStableFieldKeys`), `exportConcise` can produce ambiguous data (the type may be unclear on some nodes). - * `exportVerbose` will always be unambiguous and thus lossless. - * - * 2. When the data might be interpreted without access to the exact same view schema. In such cases, the types may be unknowable if not included. - * - * 3. When easy access to the type is desired. - */ - exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; - - /** - * Export the content of the provided `tree` in a compressed JSON compatible format. - * @remarks - * If an `idCompressor` is provided, it will be used to compress identifiers and thus will be needed to decompress the data. - * - * Always uses "stored" keys. - * See {@link EncodeOptions.useStoredKeys} for details. - */ - exportCompressed( - tree: TreeNode | TreeLeafValue, - options: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor }, - ): JsonCompatible; } = { on, TNode extends TreeNode>( node: TNode, @@ -315,7 +150,6 @@ export const TreeBeta: { ): () => void { return treeNodeApi.on(node, eventName, listener); }, - clone( node: TreeFieldFromImplicitField, ): Unhydrated> { @@ -326,136 +160,8 @@ export const TreeBeta: { const kernel = getKernel(node); const cursor = kernel.getOrCreateInnerNode().borrowCursor(); - return createFromCursor(kernel.schema, cursor as ITreeCursorSynchronous) as Unhydrated< + return createFromCursor(kernel.schema, cursor) as Unhydrated< TreeFieldFromImplicitField >; }, - - create: createFromInsertable, - - importConcise( - schema: UnsafeUnknownSchema extends TSchema - ? ImplicitFieldSchema - : TSchema & ImplicitFieldSchema, - data: InsertableTreeFieldFromImplicitField | ConciseTree, - ): Unhydrated< - TSchema extends ImplicitFieldSchema - ? TreeFieldFromImplicitField - : TreeNode | TreeLeafValue | undefined - > { - return createFromInsertable( - schema, - data as InsertableField, - ) as Unhydrated< - TSchema extends ImplicitFieldSchema - ? TreeFieldFromImplicitField - : TreeNode | TreeLeafValue | undefined - >; - }, - - importVerbose( - schema: TSchema, - data: VerboseTree | undefined, - options?: Partial>, - ): Unhydrated> { - const config: ParseOptions = { - valueConverter: (input: VerboseTree) => { - return input as TreeLeafValue | VerboseTreeNode; - }, - ...options, - }; - const schemalessConfig = applySchemaToParserOptions(schema, config); - if (data === undefined) { - const field = normalizeFieldSchema(schema); - if (field.kind !== FieldKind.Optional) { - throw new UsageError("undefined provided for non-optional field."); - } - return undefined as Unhydrated>; - } - const cursor = cursorFromVerbose(data, schemalessConfig); - return createFromCursor(schema, cursor); - }, - - exportConcise( - node: TreeNode | TreeLeafValue, - options?: Partial>, - ): ConciseTree { - const config: EncodeOptions = { - valueConverter(handle: IFluidHandle): T { - return handle as T; - }, - ...options, - }; - - const cursor = borrowCursorFromTreeNodeOrValue(node); - return conciseFromCursor(cursor, tryGetSchema(node) ?? fail("invalid input"), config); - }, - - exportVerbose( - node: TreeNode | TreeLeafValue, - options?: Partial>, - ): VerboseTree { - const config: EncodeOptions = { - valueConverter(handle: IFluidHandle): T { - return handle as T; - }, - ...options, - }; - - const cursor = borrowCursorFromTreeNodeOrValue(node); - return verboseFromCursor(cursor, tryGetSchema(node) ?? fail("invalid input"), config); - }, - - /** - * Construct tree content compatible with a field defined by the provided `schema`. - * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. - * @param data - The data used to construct the field content. See `Tree.cloneToVerbose`. - */ - exportCompressed( - node: TreeNode | TreeLeafValue, - options: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor }, - ): JsonCompatible { - const schema = tryGetSchema(node) ?? fail("invalid input"); - const format = versionToFormat[options.oldestCompatibleClient]; - const codec = makeFieldBatchCodec({ jsonValidator: noopValidator }, format); - const cursor = borrowFieldCursorFromTreeNodeOrValue(node); - const batch: FieldBatch = [cursor]; - // If none provided, create a compressor which will not compress anything (TODO: is this the right way to do that?). - const idCompressor = options.idCompressor ?? createIdCompressor(); - const context: FieldBatchEncodingContext = { - encodeType: TreeCompressionStrategy.Compressed, - idCompressor, - originatorId: idCompressor.localSessionId, // Is this right? If so, why is is needed? - schema: { schema: toStoredSchema(schema), policy: defaultSchemaPolicy }, - }; - const result = codec.encode(batch, context); - return result; - }, -}; - -function borrowCursorFromTreeNodeOrValue( - node: TreeNode | TreeLeafValue, -): ITreeCursorSynchronous { - if (isTreeValue(node)) { - return cursorFromInsertable(tryGetSchema(node) ?? fail("missing schema"), node); - } - const kernel = getKernel(node); - const cursor = kernel.getOrCreateInnerNode().borrowCursor(); - return cursor; -} - -function borrowFieldCursorFromTreeNodeOrValue( - node: TreeNode | TreeLeafValue, -): ITreeCursorSynchronous { - const cursor = borrowCursorFromTreeNodeOrValue(node); - // TODO: avoid copy - const mapTree = mapTreeFromCursor(cursor); - return cursorForMapTreeField([mapTree]); -} - -const versionToFormat = { - v2_0: 1, - v2_1: 1, - v2_2: 1, - v2_3: 1, }; diff --git a/packages/dds/tree/src/simple-tree/index.ts b/packages/dds/tree/src/simple-tree/index.ts index a7b83d4c37d5..006d0bc3d455 100644 --- a/packages/dds/tree/src/simple-tree/index.ts +++ b/packages/dds/tree/src/simple-tree/index.ts @@ -23,6 +23,7 @@ export { HydratedContext, SimpleContextSlot, getOrCreateInnerNode, + getKernel, } from "./core/index.js"; export { type ITree, @@ -97,6 +98,12 @@ export { type AllowedTypesUnsafe, type TreeNodeSchemaNonClassUnsafe, type TreeViewAlpha, + tryGetSchema, + applySchemaToParserOptions, + cursorFromVerbose, + verboseFromCursor, + conciseFromCursor, + createFromCursor, } from "./api/index.js"; export { type NodeFromSchema, diff --git a/packages/dds/tree/src/test/simple-tree/api/create.spec.ts b/packages/dds/tree/src/test/simple-tree/api/create.spec.ts index 0134a80d4252..4c74be3324cd 100644 --- a/packages/dds/tree/src/test/simple-tree/api/create.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/create.spec.ts @@ -5,7 +5,8 @@ import { strict as assert } from "node:assert"; -import { createFromInsertable, SchemaFactory, TreeBeta } from "../../../simple-tree/index.js"; +import { createFromInsertable, SchemaFactory } from "../../../simple-tree/index.js"; +import { TreeAlpha } from "../../../shared-tree/index.js"; const schema = new SchemaFactory("com.example"); @@ -23,7 +24,7 @@ describe("simple-tree create", () => { }); it("createFromVerbose", () => { - const canvas1 = TreeBeta.importVerbose(Canvas, { + const canvas1 = TreeAlpha.importVerbose(Canvas, { type: Canvas.identifier, fields: { stuff: { type: NodeList.identifier, fields: [] } }, }); diff --git a/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts b/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts index 98fa71df89ed..72de90a45980 100644 --- a/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/treeApi.spec.ts @@ -43,6 +43,9 @@ import { // eslint-disable-next-line import/no-internal-modules import { tryGetSchema } from "../../../simple-tree/api/treeNodeApi.js"; import { testSimpleTrees } from "../../testTrees.js"; +import { FluidClientVersion } from "../../../codec/index.js"; +import { ajvValidator } from "../../codec/index.js"; +import { TreeAlpha } from "../../../shared-tree/index.js"; const schema = new SchemaFactory("com.example"); @@ -1124,7 +1127,7 @@ describe("treeNodeApi", () => { describe("test-trees", () => { for (const testCase of testSimpleTrees) { it(testCase.name, () => { - const tree = TreeBeta.create(testCase.schema, testCase.root()); + const tree = TreeAlpha.create(testCase.schema, testCase.root()); const exported = TreeBeta.clone(tree); if (isTreeNode(tree)) { // New instance @@ -1140,25 +1143,25 @@ describe("treeNodeApi", () => { describe("create", () => { it("undefined", () => { // Valid - assert.equal(TreeBeta.create(schema.optional([]), undefined), undefined); + assert.equal(TreeAlpha.create(schema.optional([]), undefined), undefined); // Undefined where not allowed assert.throws( - () => TreeBeta.create(schema.required([]), undefined as never), + () => TreeAlpha.create(schema.required([]), undefined as never), validateUsageError(/undefined for non-optional field/), ); // Undefined required, not provided assert.throws( - () => TreeBeta.create(schema.optional([]), 1 as unknown as undefined), + () => TreeAlpha.create(schema.optional([]), 1 as unknown as undefined), validateUsageError(/incompatible/), ); }); it("union", () => { // Valid - assert.equal(TreeBeta.create([schema.null, schema.number], null), null); + assert.equal(TreeAlpha.create([schema.null, schema.number], null), null); // invalid assert.throws( - () => TreeBeta.create([schema.null, schema.number], "x" as unknown as number), + () => TreeAlpha.create([schema.null, schema.number], "x" as unknown as number), validateUsageError(/incompatible/), ); }); @@ -1166,7 +1169,7 @@ describe("treeNodeApi", () => { // Integration test object complex objects work (mainly covered by tests elsewhere) it("object", () => { const A = schema.object("A", { x: schema.number }); - const a = TreeBeta.create(A, { x: 1 }); + const a = TreeAlpha.create(A, { x: 1 }); assert.deepEqual(a, { x: 1 }); }); }); @@ -1175,32 +1178,32 @@ describe("treeNodeApi", () => { describe("importConcise", () => { it("undefined", () => { // Valid - assert.equal(TreeBeta.importConcise(schema.optional([]), undefined), undefined); + assert.equal(TreeAlpha.importConcise(schema.optional([]), undefined), undefined); // Undefined where not allowed assert.throws( - () => TreeBeta.importConcise(schema.required([]), undefined), + () => TreeAlpha.importConcise(schema.required([]), undefined), validateUsageError(/Got undefined for non-optional field/), ); // Undefined required, not provided assert.throws( - () => TreeBeta.importConcise(schema.optional([]), 1), + () => TreeAlpha.importConcise(schema.optional([]), 1), validateUsageError(/incompatible with all of the types allowed/), ); }); it("union", () => { // Valid - assert.equal(TreeBeta.importConcise([schema.null, schema.number], null), null); + assert.equal(TreeAlpha.importConcise([schema.null, schema.number], null), null); // invalid assert.throws( - () => TreeBeta.importConcise([schema.null, schema.number], "x"), + () => TreeAlpha.importConcise([schema.null, schema.number], "x"), validateUsageError(/The provided data is incompatible/), ); }); it("object", () => { const A = schema.object("A", { x: schema.number }); - const a = TreeBeta.importConcise(A, { x: 1 }); + const a = TreeAlpha.importConcise(A, { x: 1 }); assert.deepEqual(a, { x: 1 }); }); }); @@ -1209,19 +1212,19 @@ describe("treeNodeApi", () => { for (const testCase of testSimpleTrees) { if (testCase.root() !== undefined) { it(testCase.name, () => { - const tree = TreeBeta.create( + const tree = TreeAlpha.create( testCase.schema, testCase.root(), ); assert(tree !== undefined); - const exported = TreeBeta.exportConcise(tree); + const exported = TreeAlpha.exportConcise(tree); if (testCase.ambiguous) { assert.throws( - () => TreeBeta.importConcise(testCase.schema, exported), + () => TreeAlpha.importConcise(testCase.schema, exported), validateUsageError(/compatible with more than one type/), ); } else { - const imported = TreeBeta.importConcise( + const imported = TreeAlpha.importConcise( testCase.schema, exported, ); @@ -1236,12 +1239,12 @@ describe("treeNodeApi", () => { for (const testCase of testSimpleTrees) { if (testCase.root() !== undefined) { it(testCase.name, () => { - const tree = TreeBeta.create( + const tree = TreeAlpha.create( testCase.schema, testCase.root(), ); assert(tree !== undefined); - const _exported = TreeBeta.exportConcise(tree, { useStoredKeys: true }); + const _exported = TreeAlpha.exportConcise(tree, { useStoredKeys: true }); // We have nothing that imports concise trees with stored keys, so no validation here. }); } @@ -1253,32 +1256,32 @@ describe("treeNodeApi", () => { describe("importVerbose", () => { it("undefined", () => { // Valid - assert.equal(TreeBeta.importVerbose(schema.optional([]), undefined), undefined); + assert.equal(TreeAlpha.importVerbose(schema.optional([]), undefined), undefined); // Undefined where not allowed assert.throws( - () => TreeBeta.importVerbose(schema.required([]), undefined), + () => TreeAlpha.importVerbose(schema.required([]), undefined), validateUsageError(/non-optional/), ); // Undefined required, not provided assert.throws( - () => TreeBeta.importVerbose(schema.optional([]), 1), + () => TreeAlpha.importVerbose(schema.optional([]), 1), validateUsageError(/does not conform to schema/), ); }); it("union", () => { // Valid - assert.equal(TreeBeta.importVerbose([schema.null, schema.number], null), null); + assert.equal(TreeAlpha.importVerbose([schema.null, schema.number], null), null); // invalid assert.throws( - () => TreeBeta.importVerbose([schema.null, schema.number], "x"), + () => TreeAlpha.importVerbose([schema.null, schema.number], "x"), validateUsageError(/does not conform to schema/), ); }); it("object", () => { const A = schema.object("A", { x: schema.number }); - const a = TreeBeta.importVerbose(A, { type: A.identifier, fields: { x: 1 } }); + const a = TreeAlpha.importVerbose(A, { type: A.identifier, fields: { x: 1 } }); assert.deepEqual(a, { x: 1 }); }); }); @@ -1287,13 +1290,13 @@ describe("treeNodeApi", () => { for (const testCase of testSimpleTrees) { if (testCase.root() !== undefined) { it(testCase.name, () => { - const tree = TreeBeta.create( + const tree = TreeAlpha.create( testCase.schema, testCase.root(), ); assert(tree !== undefined); - const exported = TreeBeta.exportVerbose(tree); - const imported = TreeBeta.importVerbose(testCase.schema, exported); + const exported = TreeAlpha.exportVerbose(tree); + const imported = TreeAlpha.importVerbose(testCase.schema, exported); expectTreesEqual(tree, imported); }); } @@ -1304,13 +1307,13 @@ describe("treeNodeApi", () => { for (const testCase of testSimpleTrees) { if (testCase.root() !== undefined) { it(testCase.name, () => { - const tree = TreeBeta.create( + const tree = TreeAlpha.create( testCase.schema, testCase.root(), ); assert(tree !== undefined); - const exported = TreeBeta.exportVerbose(tree, { useStoredKeys: true }); - const imported = TreeBeta.importVerbose(testCase.schema, exported, { + const exported = TreeAlpha.exportVerbose(tree, { useStoredKeys: true }); + const imported = TreeAlpha.importVerbose(testCase.schema, exported, { useStoredKeys: true, }); expectTreesEqual(tree, imported); @@ -1320,17 +1323,25 @@ describe("treeNodeApi", () => { }); }); - // TODO: test exportCompressed, add importCompressed - describe("compressed", () => { describe("roundtrip", () => { for (const testCase of testSimpleTrees) { - it(testCase.name, () => { - const tree = TreeBeta.create(testCase.schema, testCase.root()); - // const exported = TreeBeta.exportCompressed(tree, X); - // const imported = TreeBeta.importCompressed(testCase.schema, exported); - // expectTreesEqual(tree, imported); - }); + if (testCase.root() !== undefined) { + it(testCase.name, () => { + const tree = TreeAlpha.create( + testCase.schema, + testCase.root(), + ); + assert(tree !== undefined); + const exported = TreeAlpha.exportCompressed(tree, { + oldestCompatibleClient: FluidClientVersion.v2_0, + }); + const imported = TreeAlpha.importCompressed(testCase.schema, exported, { + jsonValidator: ajvValidator, + }); + expectTreesEqual(tree, imported); + }); + } } }); }); @@ -1349,10 +1360,10 @@ function expectTreesEqual( assert.equal(Tree.schema(a), Tree.schema(b)); // This should catch all cases, assuming exportVerbose works correctly. - assert.deepEqual(TreeBeta.exportVerbose(a), TreeBeta.exportVerbose(b)); + assert.deepEqual(TreeAlpha.exportVerbose(a), TreeAlpha.exportVerbose(b)); // Since this uses some of the tools to compare trees that this is testing for, perform the comparison in a few ways to reduce risk of a bug making this pass when it shouldn't: // This case could have false negatives (two trees with ambiguous schema could export the same concise tree), // but should have no false positives since equal trees always have the same concise tree. - assert.deepEqual(TreeBeta.exportConcise(a), TreeBeta.exportConcise(b)); + assert.deepEqual(TreeAlpha.exportConcise(a), TreeAlpha.exportConcise(b)); } diff --git a/packages/dds/tree/src/treeFactory.ts b/packages/dds/tree/src/treeFactory.ts index d19fd157bd04..47a50af4e332 100644 --- a/packages/dds/tree/src/treeFactory.ts +++ b/packages/dds/tree/src/treeFactory.ts @@ -9,8 +9,6 @@ import type { IFluidDataStoreRuntime, IChannelServices, } from "@fluidframework/datastore-definitions/internal"; -import { createIdCompressor } from "@fluidframework/id-compressor/internal"; -import { assert } from "@fluidframework/core-utils/internal"; import type { SharedObjectKind } from "@fluidframework/shared-object-base"; import { type ISharedObjectKind, @@ -18,44 +16,8 @@ import { } from "@fluidframework/shared-object-base/internal"; import { pkgVersion } from "./packageVersion.js"; -import { - buildConfiguredForest, - createTreeCheckout, - SharedTree as SharedTreeImpl, - type ForestOptions, - type SharedTreeOptions, -} from "./shared-tree/index.js"; -import type { - ImplicitFieldSchema, - ITree, - TreeView, - TreeViewConfiguration, -} from "./simple-tree/index.js"; -import { SchematizingSimpleTreeView, defaultSharedTreeOptions } from "./shared-tree/index.js"; -import type { IIdCompressor } from "@fluidframework/id-compressor"; -import { - initializeForest, - mapCursorField, - RevisionTagCodec, - TreeStoredSchemaRepository, - type ITreeCursorSynchronous, - type RevisionTag, -} from "./core/index.js"; -import { - chunkTree, - createNodeKeyManager, - defaultChunkPolicy, - defaultSchemaPolicy, - makeFieldBatchCodec, - makeSchemaCodec, - TreeCompressionStrategy, - type FieldBatchEncodingContext, -} from "./feature-libraries/index.js"; -import type { JsonCompatible, JsonCompatibleReadOnly } from "./util/index.js"; -import type { IFluidHandle } from "@fluidframework/core-interfaces"; -import type { ICodecOptions } from "./codec/index.js"; -// eslint-disable-next-line import/no-internal-modules -import type { Format } from "./feature-libraries/schema-index/index.js"; +import { SharedTree as SharedTreeImpl, type SharedTreeOptions } from "./shared-tree/index.js"; +import type { ITree } from "./simple-tree/index.js"; /** * A channel factory that creates an {@link ITree}. @@ -135,135 +97,3 @@ export function configuredSharedTree( } return createSharedObjectKind(ConfiguredFactory); } - -/** - * Create an uninitialized {@link TreeView} that is not tied to any {@link ITree} instance. - * - * @remarks - * Such a view can never experience collaboration or be persisted to to a Fluid Container. - * - * This can be useful for testing, as well as use-cases like working on local files instead of documents stored in some Fluid service. - * @alpha - */ -export function independentView( - config: TreeViewConfiguration, - options: ForestOptions & { idCompressor?: IIdCompressor | undefined }, -): TreeView { - const idCompressor: IIdCompressor = options.idCompressor ?? createIdCompressor(); - const mintRevisionTag = (): RevisionTag => idCompressor.generateCompressedId(); - const revisionTagCodec = new RevisionTagCodec(idCompressor); - const schema = new TreeStoredSchemaRepository(); - const forest = buildConfiguredForest( - options.forest ?? defaultSharedTreeOptions.forest, - schema, - idCompressor, - ); - const checkout = createTreeCheckout(idCompressor, mintRevisionTag, revisionTagCodec, { - forest, - schema, - }); - const out: TreeView = new SchematizingSimpleTreeView( - checkout, - config, - createNodeKeyManager(idCompressor), - ); - return out; -} - -/** - * Create an uninitialized {@link TreeView} that is not tied to any {@link ITree} instance. - * - * @remarks - * Such a view can never experience collaboration or be persisted to to a Fluid Container. - * - * This can be useful for testing, as well as use-cases like working on local files instead of documents stored in some Fluid service. - * @alpha - */ -export function independentInitializedView( - config: TreeViewConfiguration, - options: ForestOptions & ICodecOptions, - content: ViewContent, -): TreeView { - const idCompressor: IIdCompressor = content.idCompressor; - const mintRevisionTag = (): RevisionTag => idCompressor.generateCompressedId(); - const revisionTagCodec = new RevisionTagCodec(idCompressor); - - const fieldBatchCodec = makeFieldBatchCodec(options, 1); - const schemaCodec = makeSchemaCodec(options); - - const schema = new TreeStoredSchemaRepository(schemaCodec.decode(content.schema as Format)); - const forest = buildConfiguredForest( - options.forest ?? defaultSharedTreeOptions.forest, - schema, - idCompressor, - ); - - const context: FieldBatchEncodingContext = { - encodeType: TreeCompressionStrategy.Compressed, - idCompressor, - originatorId: idCompressor.localSessionId, // Is this right? If so, why is is needed? - schema: { schema, policy: defaultSchemaPolicy }, - }; - - const fieldCursors = fieldBatchCodec.decode(content.tree as JsonCompatibleReadOnly, context); - assert(fieldCursors.length === 1, "must have exactly 1 field in batch"); - // Checked above. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const cursors = fieldCursorToNodesCursors(fieldCursors[0]!); - - initializeForest(forest, cursors, revisionTagCodec, idCompressor, false); - - const checkout = createTreeCheckout(idCompressor, mintRevisionTag, revisionTagCodec, { - forest, - schema, - }); - const out: TreeView = new SchematizingSimpleTreeView( - checkout, - config, - createNodeKeyManager(idCompressor), - ); - return out; -} - -function fieldCursorToNodesCursors( - fieldCursor: ITreeCursorSynchronous, -): ITreeCursorSynchronous[] { - return mapCursorField(fieldCursor, copyNodeCursor); -} - -/** - * TODO: avoid needing this, or optimize it. - */ -function copyNodeCursor(cursor: ITreeCursorSynchronous): ITreeCursorSynchronous { - const copy = chunkTree(cursor, { - policy: defaultChunkPolicy, - idCompressor: undefined, - }).cursor(); - copy.enterNode(0); - return copy; -} - -/** - * The portion of SharedTree data typically persisted the container. - * Usable with {@link independentInitializedView} to create a {@link TreeView} - * without loading a container. - * @alpha - */ -export interface ViewContent { - /** - * Compressed tree from {@link TreeBeta.exportCompressed}. - * @remarks - * This is an owning reference: - * consumers of this content might modify this data in place (for example when applying edits) to avoid copying. - */ - readonly tree: JsonCompatible; - /** - * Persisted schema from {@link extractPersistedSchema}. - */ - readonly schema: JsonCompatible; - /** - * IIdCompressor which will be used to decompress any compressed identifiers in `tree` - * as well as for any other identifiers added to the view. - */ - readonly idCompressor: IIdCompressor; -} From 6c4beee352f79e02f4a2d29c718027ae149638b5 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 24 Oct 2024 10:54:49 -0700 Subject: [PATCH 50/54] fix build --- .../apps/tree-cli-app/src/test/schema.spec.ts | 4 +- examples/apps/tree-cli-app/src/utils.ts | 28 +++---- .../dds/tree/api-report/tree.alpha.api.md | 26 +++--- packages/dds/tree/api-report/tree.beta.api.md | 54 ------------ .../tree/src/simple-tree/api/customTree.ts | 2 +- .../tree/src/simple-tree/api/verboseTree.ts | 6 +- .../dds/tree/src/simple-tree/schemaTypes.ts | 8 +- .../dds/tree/src/simple-tree/toMapTree.ts | 6 +- packages/dds/tree/src/util/utils.ts | 4 +- .../api-report/fluid-framework.alpha.api.md | 68 ++++++++------- .../api-report/fluid-framework.beta.api.md | 83 ------------------- 11 files changed, 80 insertions(+), 209 deletions(-) diff --git a/examples/apps/tree-cli-app/src/test/schema.spec.ts b/examples/apps/tree-cli-app/src/test/schema.spec.ts index 04eb7f874232..46147c5897e7 100644 --- a/examples/apps/tree-cli-app/src/test/schema.spec.ts +++ b/examples/apps/tree-cli-app/src/test/schema.spec.ts @@ -17,6 +17,8 @@ import { import { List } from "../schema.js"; +// This file demonstrates how applications can write tests which ensure they maintain compatibility the schema from with previously released versions. + describe("schema", () => { it("current schema matches latest historical schema", () => { const current = extractPersistedSchema(List); @@ -61,7 +63,7 @@ describe("schema", () => { /** * List of schema from previous versions of this application. - * Storing these as .json files ina folder may make more sense for more complex applications. + * Storing these as .json files in a folder may make more sense for more complex applications. * * The `schema` field is generated by passing the schema to `extractPersistedSchema`. */ diff --git a/examples/apps/tree-cli-app/src/utils.ts b/examples/apps/tree-cli-app/src/utils.ts index f6b9dae2d480..84ceb376c2a3 100644 --- a/examples/apps/tree-cli-app/src/utils.ts +++ b/examples/apps/tree-cli-app/src/utils.ts @@ -21,7 +21,6 @@ import { extractPersistedSchema, FluidClientVersion, independentInitializedView, - TreeBeta, typeboxValidator, type ForestOptions, type ICodecOptions, @@ -29,6 +28,7 @@ import { type VerboseTree, type ViewContent, type ConciseTree, + TreeAlpha, // eslint-disable-next-line import/no-internal-modules } from "@fluidframework/tree/alpha"; import { type Static, Type } from "@sinclair/typebox"; @@ -56,24 +56,18 @@ export function loadDocument(source: string | undefined): List { switch (parts.at(-2)) { case "concise": { - return TreeBeta.importConcise(List, fileData as ConciseTree); + return TreeAlpha.importConcise(List, fileData as ConciseTree); } case "verbose": { - return TreeBeta.importVerbose(List, fileData as VerboseTree); + return TreeAlpha.importVerbose(List, fileData as VerboseTree); } case "verbose-stored": { - return TreeBeta.importVerbose(List, fileData as VerboseTree, { + return TreeAlpha.importVerbose(List, fileData as VerboseTree, { useStoredKeys: true, }); } case "compressed": { - const content: ViewContent = { - schema: extractPersistedSchema(List), - tree: fileData, - idCompressor: createIdCompressor(), - }; - const view = independentInitializedView(config, options, content); - return view.root; + return TreeAlpha.importCompressed(List, fileData, { jsonValidator: typeboxValidator }); } case "snapshot": { // TODO: validate @@ -126,19 +120,19 @@ export function exportContent(destination: string, tree: List): JsonCompatible { switch (parts.at(-2)) { case "concise": { - return TreeBeta.exportConcise(tree) as JsonCompatible; + return TreeAlpha.exportConcise(tree) as JsonCompatible; } case "verbose": { - return TreeBeta.exportVerbose(tree) as JsonCompatible; + return TreeAlpha.exportVerbose(tree) as JsonCompatible; } case "concise-stored": { - return TreeBeta.exportConcise(tree, { useStoredKeys: true }) as JsonCompatible; + return TreeAlpha.exportConcise(tree, { useStoredKeys: true }) as JsonCompatible; } case "verbose-stored": { - return TreeBeta.exportVerbose(tree, { useStoredKeys: true }) as JsonCompatible; + return TreeAlpha.exportVerbose(tree, { useStoredKeys: true }) as JsonCompatible; } case "compressed": { - return TreeBeta.exportCompressed(tree, { + return TreeAlpha.exportCompressed(tree, { ...options, oldestCompatibleClient: FluidClientVersion.v2_3, }) as JsonCompatible; @@ -146,7 +140,7 @@ export function exportContent(destination: string, tree: List): JsonCompatible { case "snapshot": { const idCompressor = createIdCompressor(); // TODO: get from tree? const file: File = { - tree: TreeBeta.exportCompressed(tree, { + tree: TreeAlpha.exportCompressed(tree, { oldestCompatibleClient: FluidClientVersion.v2_3, idCompressor, }), diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index cf327a19073a..25c0e998fb34 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -51,7 +51,7 @@ export type ConciseTree = Exclude { } -// @beta +// @alpha export interface EncodeOptions { readonly useStoredKeys?: boolean; valueConverter(data: IFluidHandle): TCustom; @@ -72,10 +72,10 @@ type ExtractItemType = Item extends () => infer Result ? // @alpha export function extractPersistedSchema(schema: ImplicitFieldSchema): JsonCompatible; -// @beta +// @alpha export type FactoryContent = IFluidHandle | string | number | boolean | null | Iterable | readonly InsertableContent[] | FactoryContentObject; -// @beta +// @alpha export type FactoryContentObject = { readonly [P in string]?: InsertableContent; }; @@ -193,13 +193,13 @@ export function independentView(config: Tre // @public type _InlineTrick = 0; -// @beta +// @alpha export type Insertable = TSchema extends ImplicitAllowedTypes ? InsertableTreeNodeFromImplicitAllowedTypes : InsertableContent; -// @beta +// @alpha export type InsertableContent = Unhydrated | FactoryContent; -// @beta +// @alpha export type InsertableField = TSchema extends ImplicitFieldSchema ? InsertableTreeFieldFromImplicitField : InsertableContent | undefined; // @public @@ -310,10 +310,10 @@ export interface JsonArrayNodeSchema extends JsonNodeSchemaBase = string | number | boolean | null | JsonCompatible[] | JsonCompatibleObject | TExtra; -// @beta +// @alpha export type JsonCompatibleObject = { [P in string]?: JsonCompatible; }; @@ -449,7 +449,7 @@ type ObjectFromSchemaRecordUnsafe void; -// @beta +// @alpha export interface ParseOptions { readonly useStoredKeys?: boolean; valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; @@ -861,10 +861,10 @@ export type Unenforced<_DesiredExtendsConstraint> = unknown; // @public export type Unhydrated = T; -// @beta +// @alpha export const UnsafeUnknownSchema: unique symbol; -// @beta +// @alpha export type UnsafeUnknownSchema = typeof UnsafeUnknownSchema; // @public @@ -878,10 +878,10 @@ export type ValidateRecursiveSchema> = true; -// @beta +// @alpha export type VerboseTree = VerboseTreeNode | Exclude | THandle; -// @beta +// @alpha export interface VerboseTreeNode { fields: VerboseTree[] | { [key: string]: VerboseTree; diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index 17b38b63d09e..c8cf8b575972 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -34,23 +34,9 @@ export interface CommitMetadata { interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } -// @beta -export interface EncodeOptions { - readonly useStoredKeys?: boolean; - valueConverter(data: IFluidHandle): TCustom; -} - // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; -// @beta -export type FactoryContent = IFluidHandle | string | number | boolean | null | Iterable | readonly InsertableContent[] | FactoryContentObject; - -// @beta -export type FactoryContentObject = { - readonly [P in string]?: InsertableContent; -}; - // @public type FieldHasDefault = T extends FieldSchema ? true : false; @@ -115,15 +101,6 @@ export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; // @public type _InlineTrick = 0; -// @beta -export type Insertable = TSchema extends ImplicitAllowedTypes ? InsertableTreeNodeFromImplicitAllowedTypes : InsertableContent; - -// @beta -export type InsertableContent = Unhydrated | FactoryContent; - -// @beta -export type InsertableField = TSchema extends ImplicitFieldSchema ? InsertableTreeFieldFromImplicitField : InsertableContent | undefined; - // @public type InsertableObjectFromSchemaRecord> = FlattenKeys<{ readonly [Property in keyof T]?: InsertableTreeFieldFromImplicitField; @@ -227,14 +204,6 @@ export interface ITreeViewConfiguration = string | number | boolean | null | JsonCompatible[] | JsonCompatibleObject | TExtra; - -// @beta -export type JsonCompatibleObject = { - [P in string]?: JsonCompatible; -}; - // @public export type LazyItem = Item | (() => Item); @@ -301,12 +270,6 @@ type ObjectFromSchemaRecordUnsafe void; -// @beta -export interface ParseOptions { - readonly useStoredKeys?: boolean; - valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; -} - // @public @sealed export interface ReadonlyArrayNode extends ReadonlyArray, Awaited> { } @@ -629,12 +592,6 @@ export type Unenforced<_DesiredExtendsConstraint> = unknown; // @public export type Unhydrated = T; -// @beta -export const UnsafeUnknownSchema: unique symbol; - -// @beta -export type UnsafeUnknownSchema = typeof UnsafeUnknownSchema; - // @public export type ValidateRecursiveSchema, { [NodeKind.Object]: T["info"] extends RestrictiveStringRecord ? InsertableObjectFromSchemaRecord : unknown; @@ -646,17 +603,6 @@ export type ValidateRecursiveSchema> = true; -// @beta -export type VerboseTree = VerboseTreeNode | Exclude | THandle; - -// @beta -export interface VerboseTreeNode { - fields: VerboseTree[] | { - [key: string]: VerboseTree; - }; - type: string; -} - // @public @sealed export interface ViewableTree { viewWith(config: TreeViewConfiguration): TreeView; diff --git a/packages/dds/tree/src/simple-tree/api/customTree.ts b/packages/dds/tree/src/simple-tree/api/customTree.ts index a680b3890e19..50c0c31f22f3 100644 --- a/packages/dds/tree/src/simple-tree/api/customTree.ts +++ b/packages/dds/tree/src/simple-tree/api/customTree.ts @@ -28,7 +28,7 @@ import { isObjectNodeSchema } from "../objectNodeTypes.js"; /** * Options for how to encode a tree. - * @beta + * @alpha */ export interface EncodeOptions { /** diff --git a/packages/dds/tree/src/simple-tree/api/verboseTree.ts b/packages/dds/tree/src/simple-tree/api/verboseTree.ts index 9df548e30011..e86cef6aa945 100644 --- a/packages/dds/tree/src/simple-tree/api/verboseTree.ts +++ b/packages/dds/tree/src/simple-tree/api/verboseTree.ts @@ -53,7 +53,7 @@ import { getUnhydratedContext } from "../createContext.js"; * @privateRemarks * This can store all possible simple trees, * but it can not store all possible trees representable by our internal representations like FlexTree and JsonableTree. - * @beta + * @alpha */ export type VerboseTree = | VerboseTreeNode @@ -83,7 +83,7 @@ export type VerboseTree = * Unlike `JsonableTree`, leaf nodes are not boxed into node objects, and instead have their schema inferred from the value. * Additionally, sequence fields can only occur on a node that has a single sequence field (with the empty key) * replicating the behavior of simple-tree ArrayNodes. - * @beta + * @alpha */ export interface VerboseTreeNode { /** @@ -111,7 +111,7 @@ export interface VerboseTreeNode { /** * Options for how to interpret a `VerboseTree` when schema information is available. - * @beta + * @alpha */ export interface ParseOptions { /** diff --git a/packages/dds/tree/src/simple-tree/schemaTypes.ts b/packages/dds/tree/src/simple-tree/schemaTypes.ts index 3dccd59787c5..41336de3bfdd 100644 --- a/packages/dds/tree/src/simple-tree/schemaTypes.ts +++ b/packages/dds/tree/src/simple-tree/schemaTypes.ts @@ -416,7 +416,7 @@ export type InsertableTreeFieldFromImplicitField< /** * {@inheritdoc (UnsafeUnknownSchema:type)} - * @beta + * @alpha */ export const UnsafeUnknownSchema: unique symbol = Symbol("UnsafeUnknownSchema"); @@ -436,7 +436,7 @@ export const UnsafeUnknownSchema: unique symbol = Symbol("UnsafeUnknownSchema"); * Any APIs which use this must produce UsageErrors when out of schema data is encountered, and never produce unrecoverable errors, * or silently accept invalid data. * This is currently only type exported from the package: the symbol is just used as a way to get a named type. - * @beta + * @alpha */ export type UnsafeUnknownSchema = typeof UnsafeUnknownSchema; @@ -444,7 +444,7 @@ export type UnsafeUnknownSchema = typeof UnsafeUnknownSchema; * Content which could be inserted into a tree. * @remarks * Extended version of {@link InsertableTreeNodeFromImplicitAllowedTypes} that also allows {@link (UnsafeUnknownSchema:type)}. - * @beta + * @alpha */ export type Insertable = TSchema extends ImplicitAllowedTypes @@ -455,7 +455,7 @@ export type Insertable = TSchema extends ImplicitFieldSchema diff --git a/packages/dds/tree/src/simple-tree/toMapTree.ts b/packages/dds/tree/src/simple-tree/toMapTree.ts index 6bdd1635bb24..bd9767abc936 100644 --- a/packages/dds/tree/src/simple-tree/toMapTree.ts +++ b/packages/dds/tree/src/simple-tree/toMapTree.ts @@ -726,7 +726,7 @@ function tryGetInnerNode(target: unknown): InnerNode | undefined { * Content which can be used to build a node. * @remarks * Can contain unhydrated nodes, but can not be an unhydrated node at the root. - * @system @beta + * @system @alpha */ export type FactoryContent = | IFluidHandle @@ -745,7 +745,7 @@ export type FactoryContent = * Can contain unhydrated nodes, but can not be an unhydrated node at the root. * * Supports object and map nodes. - * @system @beta + * @system @alpha */ export type FactoryContentObject = { readonly [P in string]?: InsertableContent; @@ -753,6 +753,6 @@ export type FactoryContentObject = { /** * Content which can be inserted into a tree. - * @system @beta + * @system @alpha */ export type InsertableContent = Unhydrated | FactoryContent; diff --git a/packages/dds/tree/src/util/utils.ts b/packages/dds/tree/src/util/utils.ts index c27f1dec3f58..32d67ef33714 100644 --- a/packages/dds/tree/src/util/utils.ts +++ b/packages/dds/tree/src/util/utils.ts @@ -222,7 +222,7 @@ export function count(iterable: Iterable): number { * @remarks * This does not robustly forbid non json comparable data via type checking, * but instead mostly restricts access to it. - * @beta + * @alpha */ export type JsonCompatible = | string @@ -239,7 +239,7 @@ export type JsonCompatible = * @remarks * This does not robustly forbid non json comparable data via type checking, * but instead mostly restricts access to it. - * @beta + * @alpha */ export type JsonCompatibleObject = { [P in string]?: JsonCompatible }; 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 f2f94ebf88a7..0af4d5fd5e68 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 @@ -49,7 +49,7 @@ export interface CommitMetadata { // @alpha export function comparePersistedSchema(persisted: JsonCompatible, view: JsonCompatible, options: ICodecOptions, canInitialize: boolean): SchemaCompatibilityStatus; -// @beta +// @alpha export type ConciseTree = Exclude | THandle | ConciseTree[] | { [key: string]: ConciseTree; }; @@ -89,7 +89,7 @@ export interface ContainerSchema { interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } -// @beta +// @alpha export interface EncodeOptions { readonly useStoredKeys?: boolean; valueConverter(data: IFluidHandle): TCustom; @@ -116,10 +116,10 @@ type ExtractItemType = Item extends () => infer Result ? // @alpha export function extractPersistedSchema(schema: ImplicitFieldSchema): JsonCompatible; -// @beta +// @alpha export type FactoryContent = IFluidHandle | string | number | boolean | null | Iterable | readonly InsertableContent[] | FactoryContentObject; -// @beta +// @alpha export type FactoryContentObject = { readonly [P in string]?: InsertableContent; }; @@ -182,7 +182,7 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; -// @beta +// @alpha export enum FluidClientVersion { // (undocumented) v2_0 = "v2_0", @@ -490,6 +490,11 @@ export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; // @alpha export function independentInitializedView(config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent): TreeView; +// @alpha +export function independentView(config: TreeViewConfiguration, options: ForestOptions & { + idCompressor?: IIdCompressor_2 | undefined; +}): TreeView; + // @public export type InitialObjects = { [K in keyof T["initialObjects"]]: T["initialObjects"][K] extends SharedObjectKind ? TChannel : never; @@ -498,13 +503,13 @@ export type InitialObjects = { // @public type _InlineTrick = 0; -// @beta +// @alpha export type Insertable = TSchema extends ImplicitAllowedTypes ? InsertableTreeNodeFromImplicitAllowedTypes : InsertableContent; -// @beta +// @alpha export type InsertableContent = Unhydrated | FactoryContent; -// @beta +// @alpha export type InsertableField = TSchema extends ImplicitFieldSchema ? InsertableTreeFieldFromImplicitField : InsertableContent | undefined; // @public @@ -645,10 +650,10 @@ export interface JsonArrayNodeSchema extends JsonNodeSchemaBase = string | number | boolean | null | JsonCompatible[] | JsonCompatibleObject | TExtra; -// @beta +// @alpha export type JsonCompatibleObject = { [P in string]?: JsonCompatible; }; @@ -792,7 +797,7 @@ type ObjectFromSchemaRecordUnsafe void; -// @beta +// @alpha export interface ParseOptions { readonly useStoredKeys?: boolean; valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; @@ -974,6 +979,25 @@ export type TransformedEvent = (event: E, listener: ( // @public export const Tree: TreeApi; +// @alpha @sealed +export const TreeAlpha: { + create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; + importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated : TreeNode | TreeLeafValue | undefined>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; + exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; + exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; + exportCompressed(tree: TreeNode | TreeLeafValue, options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; + }): JsonCompatible; + importCompressed(schema: TSchema, compressedData: JsonCompatible, options: { + idCompressor?: IIdCompressor; + } & ICodecOptions): Unhydrated>; +}; + // @public @sealed interface TreeApi extends TreeNodeApi { contains(node: TreeNode, other: TreeNode): boolean; @@ -1019,18 +1043,6 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; - create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; - importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated : TreeNode | TreeLeafValue | undefined>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; - exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; - exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; - exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; - exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; - exportCompressed(tree: TreeNode | TreeLeafValue, options: { - oldestCompatibleClient: FluidClientVersion; - idCompressor?: IIdCompressor; - }): JsonCompatible; }; // @alpha @sealed @@ -1224,10 +1236,10 @@ export type Unenforced<_DesiredExtendsConstraint> = unknown; // @public export type Unhydrated = T; -// @beta +// @alpha export const UnsafeUnknownSchema: unique symbol; -// @beta +// @alpha export type UnsafeUnknownSchema = typeof UnsafeUnknownSchema; // @public @@ -1241,10 +1253,10 @@ export type ValidateRecursiveSchema> = true; -// @beta +// @alpha export type VerboseTree = VerboseTreeNode | Exclude | THandle; -// @beta +// @alpha export interface VerboseTreeNode { fields: VerboseTree[] | { [key: string]: VerboseTree; @@ -1259,7 +1271,7 @@ export interface ViewableTree { // @alpha export interface ViewContent { - readonly idCompressor: IIdCompressor; + readonly idCompressor: IIdCompressor_2; readonly schema: JsonCompatible; readonly tree: JsonCompatible; } 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 1e89ffb9d2c8..faf63f1d1f98 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 @@ -37,11 +37,6 @@ export interface CommitMetadata { readonly kind: CommitKind; } -// @beta -export type ConciseTree = Exclude | THandle | ConciseTree[] | { - [key: string]: ConciseTree; -}; - // @public export enum ConnectionState { CatchingUp = 1, @@ -74,12 +69,6 @@ export interface ContainerSchema { interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } -// @beta -export interface EncodeOptions { - readonly useStoredKeys?: boolean; - valueConverter(data: IFluidHandle): TCustom; -} - // @public @sealed export abstract class ErasedType { static [Symbol.hasInstance](value: never): value is never; @@ -89,14 +78,6 @@ export abstract class ErasedType { // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; -// @beta -export type FactoryContent = IFluidHandle | string | number | boolean | null | Iterable | readonly InsertableContent[] | FactoryContentObject; - -// @beta -export type FactoryContentObject = { - readonly [P in string]?: InsertableContent; -}; - // @public type FieldHasDefault = T extends FieldSchema ? true : false; @@ -152,18 +133,6 @@ type FlexList = readonly LazyItem[]; // @public type FlexListToUnion = ExtractItemType; -// @beta -export enum FluidClientVersion { - // (undocumented) - v2_0 = "v2_0", - // (undocumented) - v2_1 = "v2_1", - // (undocumented) - v2_2 = "v2_2", - // (undocumented) - v2_3 = "v2_3" -} - // @public export type FluidObject = { [P in FluidObjectProviderKeys]?: T[P]; @@ -439,15 +408,6 @@ export type InitialObjects = { // @public type _InlineTrick = 0; -// @beta -export type Insertable = TSchema extends ImplicitAllowedTypes ? InsertableTreeNodeFromImplicitAllowedTypes : InsertableContent; - -// @beta -export type InsertableContent = Unhydrated | FactoryContent; - -// @beta -export type InsertableField = TSchema extends ImplicitFieldSchema ? InsertableTreeFieldFromImplicitField : InsertableContent | undefined; - // @public type InsertableObjectFromSchemaRecord> = FlattenKeys<{ readonly [Property in keyof T]?: InsertableTreeFieldFromImplicitField; @@ -581,14 +541,6 @@ export interface ITreeViewConfiguration = string | number | boolean | null | JsonCompatible[] | JsonCompatibleObject | TExtra; - -// @beta -export type JsonCompatibleObject = { - [P in string]?: JsonCompatible; -}; - // @public export type LazyItem = Item | (() => Item); @@ -663,12 +615,6 @@ type ObjectFromSchemaRecordUnsafe void; -// @beta -export interface ParseOptions { - readonly useStoredKeys?: boolean; - valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; -} - // @public @sealed export interface ReadonlyArrayNode extends ReadonlyArray, Awaited> { } @@ -859,18 +805,6 @@ export interface TreeArrayNodeUnsafe, TNode extends TreeNode>(node: TNode, eventName: K, listener: NoInfer[K]>): () => void; clone(node: TreeFieldFromImplicitField): TreeFieldFromImplicitField; - create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; - importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated : TreeNode | TreeLeafValue | undefined>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; - exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; - exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; - exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; - exportVerbose(node: TreeNode | TreeLeafValue, options: EncodeOptions): VerboseTree; - exportCompressed(tree: TreeNode | TreeLeafValue, options: { - oldestCompatibleClient: FluidClientVersion; - idCompressor?: IIdCompressor; - }): JsonCompatible; }; // @public @sealed @@ -1030,12 +964,6 @@ export type Unenforced<_DesiredExtendsConstraint> = unknown; // @public export type Unhydrated = T; -// @beta -export const UnsafeUnknownSchema: unique symbol; - -// @beta -export type UnsafeUnknownSchema = typeof UnsafeUnknownSchema; - // @public export type ValidateRecursiveSchema, { [NodeKind.Object]: T["info"] extends RestrictiveStringRecord ? InsertableObjectFromSchemaRecord : unknown; @@ -1047,17 +975,6 @@ export type ValidateRecursiveSchema> = true; -// @beta -export type VerboseTree = VerboseTreeNode | Exclude | THandle; - -// @beta -export interface VerboseTreeNode { - fields: VerboseTree[] | { - [key: string]: VerboseTree; - }; - type: string; -} - // @public @sealed export interface ViewableTree { viewWith(config: TreeViewConfiguration): TreeView; From 53c24767ffd4b78046bb2a543e6945cb3fb3e703 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:24:20 -0700 Subject: [PATCH 51/54] Add changeset --- .changeset/chubby-olives-say.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .changeset/chubby-olives-say.md diff --git a/.changeset/chubby-olives-say.md b/.changeset/chubby-olives-say.md new file mode 100644 index 000000000000..e51208675583 --- /dev/null +++ b/.changeset/chubby-olives-say.md @@ -0,0 +1,25 @@ +--- +"fluid-framework": minor +"@fluidframework/tree": minor +--- +--- +"section": tree +--- + +New Alpha APIs for tree data import and export + +A collection of new `@alpha` APIs for important and exporting tree content and schema from SharedTrees has been added to `TreeAlpha`. +These include import and export APIs for for `VerboseTree`, `ConciseTree` and compressed tree formats. + +`TreeAlpha.create` is also added to allow constructing trees with a more general API than having to use the schema constructor directly (since that doesn't handle polymorphic roots, or non-schema aware code). + +The function `independentInitializedView` has been added to provide a way to combine data from the existing `extractPersistedSchema` and new `TreeAlpha.exportCompressed` back into a `TreeView` in a way which can support safely importing data which could have been exported with a different schema. +This allows replicating the schema evolution process for Fluid documents stored in a service, but entirely locally without involving any collaboration services. +`independentView` has also been added, which is similar but handles the case of creating a new view without an existing schema or tree. + +Together these APIs address several use-cases: + +1. Using SharedTree as a in memory non-collaborative datastore. +2. Importing and exporting data from a SharedTree to and from other services or storage locations (such as locally saved files). +3. Testing various scenarios without relying on a service. +4. Using SharedTree libraries for just the schema system and encode/decode support. From 32156bd28b6edf4b8e4580ec62760ecb380d5787 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:28:01 -0700 Subject: [PATCH 52/54] update todo --- packages/dds/tree/src/simple-tree/api/tree.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/dds/tree/src/simple-tree/api/tree.ts b/packages/dds/tree/src/simple-tree/api/tree.ts index c5936c595629..8a8a60181965 100644 --- a/packages/dds/tree/src/simple-tree/api/tree.ts +++ b/packages/dds/tree/src/simple-tree/api/tree.ts @@ -69,9 +69,11 @@ export interface ViewableTree { ): TreeView; // TODO: - // Add stored key versions of Tree.exportVerbose, Tree.exportConcise and Tree.exportCompressed here. - // Add exportSimpleSchema and exportJsonSchema which constraints the concise format. - // Ensure schema exporting APIs here aline and reference APis for exporting view schema to the same formats (which should include stored vs property key choice) + // Add stored key versions of Tree.exportVerbose, Tree.exportConcise and Tree.exportCompressed here so tree content can be accessed without a view schema. + // Add exportSimpleSchema and exportJsonSchema methods (which should exactly match the concise format, and match the free functions for exporting view schema). + // Maybe rename "exportJsonSchema" to align on "concise" terminology. + // Ensure schema exporting APIs here aline and reference APIs for exporting view schema to the same formats (which should include stored vs property key choice). + // Make sure users of independentView can use these export APIs (maybe provide a reference back to the ViewableTree from the TreeView to accomplish that). } /** From 5239016a97f13669112cfb8d72cb797212afac08 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:29:44 -0700 Subject: [PATCH 53/54] Apply suggestions from code review Co-authored-by: jzaffiro <110866475+jzaffiro@users.noreply.github.com> --- .changeset/chubby-olives-say.md | 6 +++--- examples/apps/tree-cli-app/README.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/chubby-olives-say.md b/.changeset/chubby-olives-say.md index e51208675583..0d54c117a73f 100644 --- a/.changeset/chubby-olives-say.md +++ b/.changeset/chubby-olives-say.md @@ -8,10 +8,10 @@ New Alpha APIs for tree data import and export -A collection of new `@alpha` APIs for important and exporting tree content and schema from SharedTrees has been added to `TreeAlpha`. +A collection of new `@alpha` APIs for importing and exporting tree content and schema from SharedTrees has been added to `TreeAlpha`. These include import and export APIs for for `VerboseTree`, `ConciseTree` and compressed tree formats. -`TreeAlpha.create` is also added to allow constructing trees with a more general API than having to use the schema constructor directly (since that doesn't handle polymorphic roots, or non-schema aware code). +`TreeAlpha.create` is also added to allow constructing trees with a more general API instead of having to use the schema constructor directly (since that doesn't handle polymorphic roots, or non-schema aware code). The function `independentInitializedView` has been added to provide a way to combine data from the existing `extractPersistedSchema` and new `TreeAlpha.exportCompressed` back into a `TreeView` in a way which can support safely importing data which could have been exported with a different schema. This allows replicating the schema evolution process for Fluid documents stored in a service, but entirely locally without involving any collaboration services. @@ -19,7 +19,7 @@ This allows replicating the schema evolution process for Fluid documents stored Together these APIs address several use-cases: -1. Using SharedTree as a in memory non-collaborative datastore. +1. Using SharedTree as an in-memory non-collaborative datastore. 2. Importing and exporting data from a SharedTree to and from other services or storage locations (such as locally saved files). 3. Testing various scenarios without relying on a service. 4. Using SharedTree libraries for just the schema system and encode/decode support. diff --git a/examples/apps/tree-cli-app/README.md b/examples/apps/tree-cli-app/README.md index b9ad694b6726..ed6caee92a35 100644 --- a/examples/apps/tree-cli-app/README.md +++ b/examples/apps/tree-cli-app/README.md @@ -2,6 +2,6 @@ Example application using Shared-Tree to create a non-collaborative file editing CLI application. -Note that its perfectly possible to write a collaborative online CLI app using tree as well: this simply is not an example of that. +Note that it's perfectly possible to write a collaborative online CLI app using tree as well: this simply is not an example of that. Run the app with `pnpm run app` after building. From db3fee2bff1f46b93e94ee516f941a71d2fc9190 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:26:12 -0700 Subject: [PATCH 54/54] Fix APi reports and const schema parameters --- .../dds/tree/api-report/tree.alpha.api.md | 28 +++++++++++----- packages/dds/tree/api-report/tree.beta.api.md | 2 +- .../tree/src/shared-tree/independentView.ts | 4 +-- .../dds/tree/src/shared-tree/treeApiAlpha.ts | 33 +++++++++++-------- .../dds/tree/src/simple-tree/api/create.ts | 4 +-- .../tree/src/simple-tree/api/treeApiBeta.ts | 6 ++-- .../tree/src/test/simple-tree/object.spec.ts | 9 +++-- .../src/test/simple-tree/primitives.spec.ts | 18 +++++----- .../dds/tree/src/test/simple-tree/utils.ts | 2 +- packages/dds/tree/src/test/testTrees.ts | 2 +- packages/dds/tree/src/test/utils.ts | 4 +-- .../api-report/fluid-framework.alpha.api.md | 28 +++++++++++----- .../api-report/fluid-framework.beta.api.md | 2 +- 13 files changed, 88 insertions(+), 54 deletions(-) diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 51da32f120b3..ae2be70ed4d1 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -62,6 +62,12 @@ export type ConciseTree = Exclude { } +// @alpha +export interface EncodeOptions { + readonly useStoredKeys?: boolean; + valueConverter(data: IFluidHandle): TCustom; +} + // @alpha export function enumFromStrings(factory: SchemaFactory, members: Members): ((value: TValue) => TreeNode & { readonly value: TValue; @@ -194,10 +200,10 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; // @alpha -export function independentInitializedView(config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent): TreeView; +export function independentInitializedView(config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent): TreeView; // @alpha -export function independentView(config: TreeViewConfiguration, options: ForestOptions & { +export function independentView(config: TreeViewConfiguration, options: ForestOptions & { idCompressor?: IIdCompressor_2 | undefined; }): TreeView; @@ -477,6 +483,12 @@ type ObjectFromSchemaRecordUnsafe void; +// @alpha +export interface ParseOptions { + readonly useStoredKeys?: boolean; + valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; +} + // @alpha export type PopUnion void : never>> = AsOverloadedFunction extends (a: infer First) => void ? First : never; @@ -631,10 +643,10 @@ export const Tree: TreeApi; // @alpha @sealed export const TreeAlpha: { - create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; - importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated : TreeNode | TreeLeafValue | undefined>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; + importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: ConciseTree | undefined): Unhydrated : TreeNode | TreeLeafValue | undefined>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; @@ -643,7 +655,7 @@ export const TreeAlpha: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor; }): JsonCompatible; - importCompressed(schema: TSchema, compressedData: JsonCompatible, options: { + importCompressed(schema: TSchema, compressedData: JsonCompatible, options: { idCompressor?: IIdCompressor; } & ICodecOptions): Unhydrated>; }; @@ -692,7 +704,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/shared-tree/independentView.ts b/packages/dds/tree/src/shared-tree/independentView.ts index bd8c1dd20b58..3a590a929778 100644 --- a/packages/dds/tree/src/shared-tree/independentView.ts +++ b/packages/dds/tree/src/shared-tree/independentView.ts @@ -53,7 +53,7 @@ import { SchematizingSimpleTreeView } from "./schematizingTreeView.js"; * This can be useful for testing, as well as use-cases like working on local files instead of documents stored in some Fluid service. * @alpha */ -export function independentView( +export function independentView( config: TreeViewConfiguration, options: ForestOptions & { idCompressor?: IIdCompressor | undefined }, ): TreeView { @@ -86,7 +86,7 @@ export function independentView( * This can be useful for testing, as well as use-cases like working on local files instead of documents stored in some Fluid service. * @alpha */ -export function independentInitializedView( +export function independentInitializedView( config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent, diff --git a/packages/dds/tree/src/shared-tree/treeApiAlpha.ts b/packages/dds/tree/src/shared-tree/treeApiAlpha.ts index 22d461532700..db2e175c14c3 100644 --- a/packages/dds/tree/src/shared-tree/treeApiAlpha.ts +++ b/packages/dds/tree/src/shared-tree/treeApiAlpha.ts @@ -21,7 +21,6 @@ import { normalizeFieldSchema, type ImplicitFieldSchema, type InsertableField, - type InsertableTreeFieldFromImplicitField, type TreeFieldFromImplicitField, type TreeLeafValue, type UnsafeUnknownSchema, @@ -72,7 +71,7 @@ export const TreeAlpha: { * @privateRemarks * There should be a way to provide an source for defaulted identifiers, wither via this API or some way to add them to its output later. */ - create( + create( schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, @@ -98,11 +97,11 @@ export const TreeAlpha: { * Documented (and thus recoverable) error handling/reporting for this is not yet implemented, * but for now most invalid inputs will throw a recoverable error. */ - importConcise( + importConcise( schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, - data: InsertableTreeFieldFromImplicitField | ConciseTree, + data: ConciseTree | undefined, ): Unhydrated< TSchema extends ImplicitFieldSchema ? TreeFieldFromImplicitField @@ -114,7 +113,7 @@ export const TreeAlpha: { * @param schema - The schema for what to construct. As this is an {@link ImplicitFieldSchema}, a {@link FieldSchema}, {@link TreeNodeSchema} or {@link AllowedTypes} array can be provided. * @param data - The data used to construct the field content. See `Tree.cloneToJSONVerbose`. */ - importVerbose( + importVerbose( schema: TSchema, data: VerboseTree | undefined, options?: Partial>, @@ -127,7 +126,7 @@ export const TreeAlpha: { * @privateRemarks * This could be exposed as a public `Tree.createFromVerbose` function. */ - importVerbose( + importVerbose( schema: TSchema, data: VerboseTree | undefined, options: ParseOptions, @@ -203,7 +202,7 @@ export const TreeAlpha: { * 2. A "try" version of this could return an error if the data isn't in a supported format (as determined by version and/or JasonValidator). * 3. Requiring the caller provide a JsonValidator isn't the most friendly API. It might be practical to provide a default. */ - importCompressed( + importCompressed( schema: TSchema, compressedData: JsonCompatible, options: { idCompressor?: IIdCompressor } & ICodecOptions, @@ -215,7 +214,7 @@ export const TreeAlpha: { schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, - data: InsertableTreeFieldFromImplicitField | ConciseTree, + data: ConciseTree | undefined, ): Unhydrated< TSchema extends ImplicitFieldSchema ? TreeFieldFromImplicitField @@ -231,7 +230,7 @@ export const TreeAlpha: { >; }, - importVerbose( + importVerbose( schema: TSchema, data: VerboseTree | undefined, options?: Partial>, @@ -286,7 +285,10 @@ export const TreeAlpha: { exportCompressed( node: TreeNode | TreeLeafValue, - options: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor }, + options: { + oldestCompatibleClient: FluidClientVersion; + idCompressor?: IIdCompressor; + }, ): JsonCompatible { const schema = tryGetSchema(node) ?? fail("invalid input"); const format = versionToFormat[options.oldestCompatibleClient]; @@ -305,10 +307,12 @@ export const TreeAlpha: { return result; }, - importCompressed( + importCompressed( schema: TSchema, compressedData: JsonCompatible, - options: { idCompressor?: IIdCompressor } & ICodecOptions, + options: { + idCompressor?: IIdCompressor; + } & ICodecOptions, ): Unhydrated> { const content: ViewContent = { schema: extractPersistedSchema(schema), @@ -325,7 +329,10 @@ function borrowCursorFromTreeNodeOrValue( node: TreeNode | TreeLeafValue, ): ITreeCursorSynchronous { if (isTreeValue(node)) { - return cursorFromInsertable(tryGetSchema(node) ?? fail("missing schema"), node); + return cursorFromInsertable( + tryGetSchema(node) ?? fail("missing schema"), + node, + ); } const kernel = getKernel(node); const cursor = kernel.getOrCreateInnerNode().borrowCursor(); diff --git a/packages/dds/tree/src/simple-tree/api/create.ts b/packages/dds/tree/src/simple-tree/api/create.ts index 7bd3ae215021..b14b22b6ed13 100644 --- a/packages/dds/tree/src/simple-tree/api/create.ts +++ b/packages/dds/tree/src/simple-tree/api/create.ts @@ -46,7 +46,7 @@ import { getUnhydratedContext } from "../createContext.js"; * For that case, use {@link TreeBeta.clone}. */ export function createFromInsertable< - TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema, + const TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema, >( schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema @@ -113,7 +113,7 @@ export function cursorFromInsertable< /** * 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 86c718f69b47..34b2521b6eb2 100644 --- a/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts +++ b/packages/dds/tree/src/simple-tree/api/treeApiBeta.ts @@ -120,7 +120,7 @@ export const TreeBeta: { * - 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. */ - clone( + clone( node: TreeFieldFromImplicitField, ): TreeFieldFromImplicitField; @@ -132,7 +132,7 @@ export const TreeBeta: { // * Local state, such as properties added to customized schema classes, will not be cloned: // * they will be initialized however they end up after running the constructor, just like if a remote client had inserted the same nodes. // */ - // clone( + // clone( // original: TreeFieldFromImplicitField, // options?: { // /** @@ -150,7 +150,7 @@ export const TreeBeta: { ): () => void { return treeNodeApi.on(node, eventName, listener); }, - clone( + clone( node: TreeFieldFromImplicitField, ): Unhydrated> { /** The only non-TreeNode cases are {@link TreeLeafValue} and `undefined` (for an empty optional field) which can be returned as is. */ 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/testTrees.ts b/packages/dds/tree/src/test/testTrees.ts index fa6cc1a727be..057f5a0090e0 100644 --- a/packages/dds/tree/src/test/testTrees.ts +++ b/packages/dds/tree/src/test/testTrees.ts @@ -66,7 +66,7 @@ interface TestTree { readonly treeFactory: (idCompressor?: IIdCompressor) => JsonableTree[]; } -function testSimpleTree( +function testSimpleTree( name: string, schema: TSchema, root: LazyItem>, 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 0dd704c1c9fd..1b08a76a9444 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 @@ -100,6 +100,12 @@ export interface ContainerSchema { interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @alpha +export interface EncodeOptions { + readonly useStoredKeys?: boolean; + valueConverter(data: IFluidHandle): TCustom; +} + // @alpha export function enumFromStrings(factory: SchemaFactory, members: Members): ((value: TValue) => TreeNode & { readonly value: TValue; @@ -499,10 +505,10 @@ export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes; // @alpha -export function independentInitializedView(config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent): TreeView; +export function independentInitializedView(config: TreeViewConfiguration, options: ForestOptions & ICodecOptions, content: ViewContent): TreeView; // @alpha -export function independentView(config: TreeViewConfiguration, options: ForestOptions & { +export function independentView(config: TreeViewConfiguration, options: ForestOptions & { idCompressor?: IIdCompressor_2 | undefined; }): TreeView; @@ -825,6 +831,12 @@ type ObjectFromSchemaRecordUnsafe void; +// @alpha +export interface ParseOptions { + readonly useStoredKeys?: boolean; + valueConverter(data: VerboseTree): TreeLeafValue | VerboseTreeNode; +} + // @alpha export type PopUnion void : never>> = AsOverloadedFunction extends (a: infer First) => void ? First : never; @@ -1006,10 +1018,10 @@ export const Tree: TreeApi; // @alpha @sealed export const TreeAlpha: { - create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; - importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableTreeFieldFromImplicitField | ConciseTree): Unhydrated : TreeNode | TreeLeafValue | undefined>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; - importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; + create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; + importConcise(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: ConciseTree | undefined): Unhydrated : TreeNode | TreeLeafValue | undefined>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options?: Partial>): Unhydrated>; + importVerbose(schema: TSchema, data: VerboseTree | undefined, options: ParseOptions): Unhydrated>; exportConcise(node: TreeNode | TreeLeafValue, options?: Partial>): ConciseTree; exportConcise(node: TreeNode | TreeLeafValue, options?: EncodeOptions): ConciseTree; exportVerbose(node: TreeNode | TreeLeafValue, options?: Partial>): VerboseTree; @@ -1018,7 +1030,7 @@ export const TreeAlpha: { oldestCompatibleClient: FluidClientVersion; idCompressor?: IIdCompressor; }): JsonCompatible; - importCompressed(schema: TSchema, compressedData: JsonCompatible, options: { + importCompressed(schema: TSchema, compressedData: JsonCompatible, options: { idCompressor?: IIdCompressor; } & ICodecOptions): Unhydrated>; }; @@ -1067,7 +1079,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