diff --git a/.changeset/clever-dancers-post.md b/.changeset/clever-dancers-post.md new file mode 100644 index 000000000000..b3e239018533 --- /dev/null +++ b/.changeset/clever-dancers-post.md @@ -0,0 +1,32 @@ +--- +"fluid-framework": minor +"@fluidframework/tree": minor +--- +--- +"section": tree +--- + +Add alpha API for providing SharedTree configuration options + +A new alpha `configuredSharedTree` had been added. +This allows providing configuration options, primarily for debugging, testing and evaluation of upcoming features. +The resulting configured `SharedTree` object can then be used in-place of the regular `SharedTree` imported from `fluid-framework`. + +```typescript +import { + ForestType, + TreeCompressionStrategy, + configuredSharedTree, + typeboxValidator, +} from "@fluid-framework/alpha"; +// Maximum debuggability and validation enabled: +const SharedTree = configuredSharedTree({ + forest: ForestType.Expensive, + jsonValidator: typeboxValidator, + treeEncodeType: TreeCompressionStrategy.Uncompressed, +}); +// Opts into the under development optimized tree storage planned to be the eventual default implementation: +const SharedTree = configuredSharedTree({ + forest: ForestType.Optimized, +}); +``` diff --git a/.vscode/settings.json b/.vscode/settings.json index 5446441c615a..cf2a3739c287 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -34,6 +34,7 @@ "cSpell.words": [ "boop", "contoso", + "debuggability", "denormalized", "endregion", "fluidframework", diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 1b048cf345b0..4343c34a067b 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 +export interface ForestOptions { + readonly forest?: ForestType; +} + +// @alpha +export enum ForestType { + Expensive = 2, + Optimized = 1, + Reference = 0 +} + // @alpha export function getBranch(tree: ITree): TreeBranch; @@ -101,6 +113,11 @@ export function getBranch(view: TreeView): TreeBranch; // @alpha export function getJsonSchema(schema: ImplicitFieldSchema): JsonTreeSchema; +// @alpha +export interface ICodecOptions { + readonly jsonValidator: JsonValidator; +} + // @public export type ImplicitAllowedTypes = AllowedTypes | TreeNodeSchema; @@ -272,6 +289,11 @@ export type JsonTreeSchema = JsonFieldSchema & { readonly $defs: Record; }; +// @alpha +export interface JsonValidator { + compile(schema: Schema): SchemaValidationFunction; +} + // @public export type LazyItem = Item | (() => Item); @@ -325,6 +347,9 @@ export enum NodeKind { Object = 2 } +// @alpha +export const noopValidator: JsonValidator; + // @public type ObjectFromSchemaRecord> = { -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; @@ -443,9 +468,33 @@ export class SchemaFactory; } +// @alpha +export interface SchemaValidationFunction { + check(data: unknown): data is Static; +} + // @public type ScopedSchemaName = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`; +// @alpha +export interface SharedTreeFormatOptions { + formatVersion: SharedTreeFormatVersion[keyof SharedTreeFormatVersion]; + treeEncodeType: TreeCompressionStrategy; +} + +// @alpha +export const SharedTreeFormatVersion: { + readonly v1: 1; + readonly v2: 2; + readonly v3: 3; +}; + +// @alpha +export type SharedTreeFormatVersion = typeof SharedTreeFormatVersion; + +// @alpha +export type SharedTreeOptions = Partial & Partial & ForestOptions; + // @public export type TransactionConstraint = NodeInDocumentConstraint; @@ -522,6 +571,12 @@ export interface TreeChangeEventsBeta extends nodeChanged: (data: NodeChangedData & (TNode extends WithType ? Required, "changedProperties">> : unknown)) => void; } +// @alpha +export enum TreeCompressionStrategy { + Compressed = 0, + Uncompressed = 1 +} + // @public export type TreeFieldFromImplicitField = TSchema extends FieldSchema ? ApplyKind, Kind, false> : TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : unknown; @@ -641,6 +696,9 @@ export interface TreeViewEvents { schemaChanged(): void; } +// @alpha +export const typeboxValidator: JsonValidator; + // @public @deprecated const typeNameSymbol: unique symbol; diff --git a/packages/dds/tree/src/codec/codec.ts b/packages/dds/tree/src/codec/codec.ts index 9cfc25d34913..17d2e7db2730 100644 --- a/packages/dds/tree/src/codec/codec.ts +++ b/packages/dds/tree/src/codec/codec.ts @@ -35,18 +35,18 @@ export interface IDecoder { /** * Validates data complies with some particular schema. * Implementations are typically created by a {@link JsonValidator}. - * @internal + * @alpha */ export interface SchemaValidationFunction { /** - * @returns Whether the data matches a schema. + * Returns whether the data matches a schema. */ check(data: unknown): data is Static; } /** * 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/feature-libraries/treeCompressionUtils.ts b/packages/dds/tree/src/feature-libraries/treeCompressionUtils.ts index b86d3eef86cc..a622b7ec3d64 100644 --- a/packages/dds/tree/src/feature-libraries/treeCompressionUtils.ts +++ b/packages/dds/tree/src/feature-libraries/treeCompressionUtils.ts @@ -7,7 +7,7 @@ * Selects which heuristics to use when encoding tree content. * All encoding options here are compatible with the same decoder: * the selection here does not impact compatibility. - * @internal + * @alpha */ export enum TreeCompressionStrategy { /** diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index 9af8b7f9c603..e85c2e26c301 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, getBranch, type TreeBranch, type TreeBranchFork, @@ -150,9 +151,16 @@ export { type JsonLeafSchemaType, getJsonSchema, } from "./simple-tree/index.js"; -export { SharedTree, configuredSharedTree } from "./treeFactory.js"; +export { + SharedTree, + configuredSharedTree, +} from "./treeFactory.js"; -export type { ICodecOptions, JsonValidator, SchemaValidationFunction } from "./codec/index.js"; +export type { + ICodecOptions, + JsonValidator, + SchemaValidationFunction, +} from "./codec/index.js"; export { noopValidator } from "./codec/index.js"; export { typeboxValidator } from "./external-utilities/index.js"; diff --git a/packages/dds/tree/src/shared-tree/index.ts b/packages/dds/tree/src/shared-tree/index.ts index 8b2f66ba1412..be2f40a90ab1 100644 --- a/packages/dds/tree/src/shared-tree/index.ts +++ b/packages/dds/tree/src/shared-tree/index.ts @@ -13,6 +13,9 @@ export { type SharedTreeContentSnapshot, type SharedTreeFormatOptions, SharedTreeFormatVersion, + buildConfiguredForest, + defaultSharedTreeOptions, + type ForestOptions, } from "./sharedTree.js"; export { @@ -29,6 +32,8 @@ export { export { type TreeStoredContent } 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 dc163fdc5e20..37fe8fd66acf 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,11 +16,13 @@ import { UsageError } from "@fluidframework/telemetry-utils/internal"; import { type ICodecOptions, noopValidator } from "../codec/index.js"; import { + type IEditableForest, type JsonableTree, RevisionTagCodec, type TaggedChange, type TreeStoredSchema, TreeStoredSchemaRepository, + type TreeStoredSchemaSubscription, makeDetachedFieldIndex, moveToDetachedField, } from "../core/index.js"; @@ -67,6 +69,7 @@ import { 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. @@ -150,6 +153,30 @@ function getCodecVersions(formatVersion: number): ExplicitCodecVersions { return versions; } +/** + * Build and return a forest of the requested type. + */ +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. * @@ -182,16 +209,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", @@ -350,7 +368,7 @@ export function getBranch(treeOrView: ITree | TreeView): Tr * Format versions supported by SharedTree. * * Each version documents a required minimum version of the \@fluidframework/tree package. - * @internal + * @alpha */ export const SharedTreeFormatVersion = { /** @@ -376,26 +394,38 @@ export const SharedTreeFormatVersion = { * Format versions supported by SharedTree. * * Each version documents a required minimum version of the \@fluidframework/tree package. - * @internal + * @alpha * @privateRemarks * See packages/dds/tree/docs/main/compatibility.md for information on how to add support for a new format. + * + * TODO: Before this gets promoted past Alpha, + * a separate abstraction more suited for use in the public API should be adopted rather than reusing the same types used internally. + * Such an abstraction should probably be in the form of a Fluid-Framework wide compatibility enum. */ export type SharedTreeFormatVersion = typeof SharedTreeFormatVersion; /** - * @internal + * Configuration options for SharedTree. + * @alpha */ export type SharedTreeOptions = Partial & - Partial & { - /** - * The {@link ForestType} indicating which forest type should be created for the SharedTree. - */ - forest?: ForestType; - }; + Partial & + ForestOptions; + +/** + * Configuration options for SharedTree's internal tree storage. + * @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. - * @internal + * @alpha */ export interface SharedTreeFormatOptions { /** @@ -419,7 +449,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 5e67f76878b2..47a50af4e332 100644 --- a/packages/dds/tree/src/treeFactory.ts +++ b/packages/dds/tree/src/treeFactory.ts @@ -81,6 +81,7 @@ export const SharedTree = configuredSharedTree({}); * }); * ``` * @privateRemarks + * This should be legacy, but has to be internal due to limitations of API tagging preventing it from being both alpha and alpha+legacy. * TODO: * Expose Ajv validator for better error message quality somehow. * Maybe as part of a test utils or dev-tool package? 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 0eda69947629..402f4ffee00c 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 configuredSharedTree(options: SharedTreeOptions): SharedObjectKind; + // @public export enum ConnectionState { CatchingUp = 1, @@ -141,6 +144,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 +export interface ForestOptions { + readonly forest?: ForestType; +} + +// @alpha +export enum ForestType { + Expensive = 2, + Optimized = 1, + Reference = 0 +} + // @alpha export function getBranch(tree: ITree): TreeBranch; @@ -150,6 +165,11 @@ export function getBranch(view: TreeView): TreeBranch; // @alpha export function getJsonSchema(schema: ImplicitFieldSchema): JsonTreeSchema; +// @alpha +export interface ICodecOptions { + readonly jsonValidator: JsonValidator; +} + // @public export interface IConnection { readonly id: string; @@ -609,6 +629,11 @@ export type JsonTreeSchema = JsonFieldSchema & { readonly $defs: Record; }; +// @alpha +export interface JsonValidator { + compile(schema: Schema): SchemaValidationFunction; +} + // @public export type LazyItem = Item | (() => Item); @@ -670,6 +695,9 @@ export enum NodeKind { Object = 2 } +// @alpha +export const noopValidator: JsonValidator; + // @public type ObjectFromSchemaRecord> = { -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; @@ -793,6 +821,11 @@ export class SchemaFactory; } +// @alpha +export interface SchemaValidationFunction { + check(data: unknown): data is Static; +} + // @public type ScopedSchemaName = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`; @@ -804,6 +837,25 @@ export interface SharedObjectKind extends ErasedTyp // @public export const SharedTree: SharedObjectKind; +// @alpha +export interface SharedTreeFormatOptions { + formatVersion: SharedTreeFormatVersion[keyof SharedTreeFormatVersion]; + treeEncodeType: TreeCompressionStrategy; +} + +// @alpha +export const SharedTreeFormatVersion: { + readonly v1: 1; + readonly v2: 2; + readonly v3: 3; +}; + +// @alpha +export type SharedTreeFormatVersion = typeof SharedTreeFormatVersion; + +// @alpha +export type SharedTreeOptions = Partial & Partial & ForestOptions; + // @public export interface Tagged { // (undocumented) @@ -894,6 +946,12 @@ export interface TreeChangeEventsBeta extends nodeChanged: (data: NodeChangedData & (TNode extends WithType ? Required, "changedProperties">> : unknown)) => void; } +// @alpha +export enum TreeCompressionStrategy { + Compressed = 0, + Uncompressed = 1 +} + // @public export type TreeFieldFromImplicitField = TSchema extends FieldSchema ? ApplyKind, Kind, false> : TSchema extends ImplicitAllowedTypes ? TreeNodeFromImplicitAllowedTypes : unknown; @@ -1013,6 +1071,9 @@ export interface TreeViewEvents { schemaChanged(): void; } +// @alpha +export const typeboxValidator: JsonValidator; + // @public @deprecated const typeNameSymbol: unique symbol; diff --git a/packages/framework/fluid-framework/src/index.ts b/packages/framework/fluid-framework/src/index.ts index a1c7dbbdce4b..d93b8ce4c529 100644 --- a/packages/framework/fluid-framework/src/index.ts +++ b/packages/framework/fluid-framework/src/index.ts @@ -71,7 +71,11 @@ export * from "@fluidframework/tree/alpha"; import type { SharedObjectKind } from "@fluidframework/shared-object-base"; import type { ITree } from "@fluidframework/tree"; -import { SharedTree as OriginalSharedTree } from "@fluidframework/tree/internal"; +import { + SharedTree as OriginalSharedTree, + configuredSharedTree as originalConfiguredSharedTree, + type SharedTreeOptions, +} from "@fluidframework/tree/internal"; /** * A hierarchical data structure for collaboratively editing strongly typed JSON-like trees @@ -85,6 +89,30 @@ import { SharedTree as OriginalSharedTree } from "@fluidframework/tree/internal" */ export const SharedTree: SharedObjectKind = OriginalSharedTree; +/** + * {@link SharedTree} but allowing a non-default configuration. + * @remarks + * This is useful for debugging and testing to opt into extra validation or see if opting out of some optimizations fixes an issue. + * @example + * ```typescript + * import { + * ForestType, + * TreeCompressionStrategy, + * configuredSharedTree, + * typeboxValidator, + * } from "@fluid-framework/alpha"; + * const SharedTree = configuredSharedTree({ + * forest: ForestType.Reference, + * jsonValidator: typeboxValidator, + * treeEncodeType: TreeCompressionStrategy.Uncompressed, + * }); + * ``` + * @alpha + */ +export function configuredSharedTree(options: SharedTreeOptions): SharedObjectKind { + return originalConfiguredSharedTree(options); +} + // #endregion Custom re-exports // #endregion