diff --git a/.changeset/warm-windows-cross.md b/.changeset/warm-windows-cross.md new file mode 100644 index 000000000000..df4f21739207 --- /dev/null +++ b/.changeset/warm-windows-cross.md @@ -0,0 +1,44 @@ +--- +"@fluidframework/tree": minor +"fluid-framework": minor +"__section": tree +--- +MinimumVersionForCollab is now used in place of tree's alpha FluidClientVersion + +`FluidClientVersion`: No longer used as the type for Fluid Client versions in APIs/codecs (for example, `oldestCompatibleClient`). +Additionally, `FluidClientVersion` is now a const object with members that declare specific [`MinimumVersionForCollab`](https://fluidframework.com/docs/api/runtime-definitions/minimumversionforcollab-typealias) versions. +These are intended to be used with APIs that require a version (such as `TreeAlpha.exportCompressed`). + +`CodecWriteOptions` and `SharedTreeOptions`: `oldestCompatibleClient` has been replaced by `minVersionForCollab`. +See migration guide below. + +`TreeAlpha.exportCompressed`: The `options` parameter previously had `oldestCompatibleClient` and now has `minVersionForCollab`. +Migrating requires a rename. Existing `FluidClientVersion.*` values are now `MinimumClientVersion`s. + +#### Migrating + +If an application is calling `loadContainerRuntime` directly and previously specified the minimum client version when +initializing Shared Tree like: + +```ts + const factory = configuredSharedTree({ ..., oldestCompatibleClient: FluidClientVersion.v2_52 }); +``` + +Then the new implementation depends on how the application initializes Fluid. + +##### Applications using `AzureClient`/`OdspClient` + +If an application is using the declarative model (for example, `AzureClient`/`OdspClient`), it should continue to call `configuredSharedTree` +but specify `minVersionForCollab` instead: + +```ts + const factory = configuredSharedTree({ ..., minVersionForCollab: "2.52.0" }); +``` + +##### Applications calling `loadContainerRuntime` + +If an application is initializing the `ContainerRuntime` directly, it should now specify the `minVersionForCollab` there: + +```ts + const runtime = await loadContainerRuntime({ ..., minVersionForCollab: "2.52.0" }); +``` diff --git a/examples/apps/tree-cli-app/src/utils.ts b/examples/apps/tree-cli-app/src/utils.ts index 88171c45f8d8..c31f3853a07f 100644 --- a/examples/apps/tree-cli-app/src/utils.ts +++ b/examples/apps/tree-cli-app/src/utils.ts @@ -159,7 +159,7 @@ export function exportContent(destination: string, tree: List): JsonCompatible { case "compressed": { return TreeAlpha.exportCompressed(tree, { ...options, - oldestCompatibleClient: compatVersion, + minVersionForCollab: compatVersion, }) as JsonCompatible; } case "snapshot": { @@ -167,7 +167,7 @@ export function exportContent(destination: string, tree: List): JsonCompatible { const idCompressor = createIdCompressor(); const file: File = { tree: TreeAlpha.exportCompressed(tree, { - oldestCompatibleClient: compatVersion, + minVersionForCollab: compatVersion, idCompressor, }), diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index b2f86c449787..2051adaf35af 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -136,7 +136,7 @@ export function cloneWithReplacements(root: unknown, rootKey: string, replacer: // @alpha @input export interface CodecWriteOptions extends ICodecOptions { - readonly oldestCompatibleClient: FluidClientVersion; + readonly minVersionForCollab: MinimumVersionForCollab; } // @public @@ -211,7 +211,7 @@ export function evaluateLazySchema(value: LazyItem) type ExtractItemType = Item extends () => infer Result ? Result : Item; // @alpha -export function extractPersistedSchema(schema: ImplicitFieldSchema, oldestCompatibleClient: FluidClientVersion, includeStaged: (upgrade: SchemaUpgrade) => boolean): JsonCompatible; +export function extractPersistedSchema(schema: ImplicitFieldSchema, minVersionForCollab: MinimumVersionForCollab, includeStaged: (upgrade: SchemaUpgrade) => boolean): JsonCompatible; // @alpha @system export type FactoryContent = IFluidHandle | string | number | boolean | null | Iterable | readonly InsertableContent[] | FactoryContentObject; @@ -296,11 +296,10 @@ type FlexList = readonly LazyItem[]; type FlexListToUnion = ExtractItemType; // @alpha -export enum FluidClientVersion { - EnableUnstableFeatures, - v2_0 = 2, - v2_52 = 2.052 -} +export const FluidClientVersion: { + readonly v2_0: "2.0.0"; + readonly v2_52: "2.52.0"; +}; // @alpha export namespace FluidSerializableAsTree { @@ -1347,7 +1346,7 @@ export interface TreeAlpha { create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; exportCompressed(tree: TreeNode | TreeLeafValue, options: { idCompressor?: IIdCompressor; - } & Pick): JsonCompatible; + } & Pick): JsonCompatible; exportConcise(node: TreeNode | TreeLeafValue, options?: TreeEncodingOptions): ConciseTree; exportConcise(node: TreeNode | TreeLeafValue | undefined, options?: TreeEncodingOptions): ConciseTree | undefined; exportVerbose(node: TreeNode | TreeLeafValue, options?: TreeEncodingOptions): VerboseTree; diff --git a/packages/dds/tree/package.json b/packages/dds/tree/package.json index 7c0ceb4a63cc..99deaa27e479 100644 --- a/packages/dds/tree/package.json +++ b/packages/dds/tree/package.json @@ -180,6 +180,7 @@ "@tylerbu/sorted-btree-es6": "^1.8.0", "@types/ungap__structured-clone": "^1.2.0", "@ungap/structured-clone": "^1.2.0", + "semver-ts": "^1.0.3", "uuid": "^11.1.0" }, "devDependencies": { diff --git a/packages/dds/tree/src/codec/codec.ts b/packages/dds/tree/src/codec/codec.ts index a8bc63cf21f8..dd36536d9df7 100644 --- a/packages/dds/tree/src/codec/codec.ts +++ b/packages/dds/tree/src/codec/codec.ts @@ -10,6 +10,7 @@ import type { Static, TAnySchema, TSchema } from "@sinclair/typebox"; import type { ChangeEncodingContext } from "../core/index.js"; import type { JsonCompatibleReadOnly } from "../util/index.js"; +import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions/internal"; /** * Translates decoded data to encoded data. @@ -151,7 +152,7 @@ export interface CodecWriteOptions extends ICodecOptions { * Note that versions older than this should not result in data corruption if they access the data: * the data's format should be versioned and if they can't handle the format they should error. */ - readonly oldestCompatibleClient: FluidClientVersion; + readonly minVersionForCollab: MinimumVersionForCollab; } /** @@ -466,16 +467,6 @@ export function withSchemaValidation< * For example, document if there is an encoding efficiency improvement of oping into that version or newer. * Versions with no notable impact can be omitted. * - * These use numeric values for easy threshold comparisons. - * Without zero padding, version 2.10 is treated as 2.1, which is numerically less than 2.2. - * Adding leading zeros to the minor version ensures correct comparisons. - * For example, version 2.20.0 is encoded as 2.020, and version 2.2.0 is encoded as 2.002. - * For example FF 2.20.0 is encoded as 2.020 and FF 2.2.0 is encoded as 2.002. - * - * Three digits was selected as that will likely be enough, while two digits could easily be too few. - * If three digits ends up being too few, minor releases of 1000 and higher - * could still be handled using something like 2.999_00001 without having to change the lower releases. - * * 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), @@ -487,7 +478,7 @@ export function withSchemaValidation< * For example, if needed, would adding more leading zeros to the minor version break things. * @alpha */ -export enum FluidClientVersion { +export const FluidClientVersion = { /** * Fluid Framework Client 1.4 and newer. * @remarks @@ -497,8 +488,10 @@ export enum FluidClientVersion { */ // v1_4 = 1.004, - /** Fluid Framework Client 2.0 and newer. */ - v2_0 = 2.0, + /** + * Fluid Framework Client 2.0 and newer. + */ + v2_0: "2.0.0", /** Fluid Framework Client 2.1 and newer. */ // If we think we might want to start allowing opting into something that landed in 2.1 (without opting into something newer), @@ -509,19 +502,8 @@ export enum FluidClientVersion { /** Fluid Framework Client 2.52 and newer. */ // New formats introduced in 2.52: // - DetachedFieldIndex FormatV2 - v2_52 = 2.052, - - /** - * Enable unreleased and unfinished features. - * @remarks - * Using this value can result in documents which can not be opened in future versions of the framework. - * It can also result in data corruption by enabling unfinished features which may not handle all cases correctly. - * - * This can be used with specific APIs when the caller has knowledge of what specific features those APIs will be opted into with it. - * This is useful for testing features before they are released, but should not be used in production code. - */ - EnableUnstableFeatures = Number.POSITIVE_INFINITY, -} + v2_52: "2.52.0", +} as const satisfies Record; /** * An up to date version which includes all the important stable features. @@ -532,7 +514,7 @@ export enum FluidClientVersion { * Update as needed. * TODO: Consider using packageVersion.ts to keep this current. */ -export const currentVersion: FluidClientVersion = FluidClientVersion.v2_0; +export const currentVersion: MinimumVersionForCollab = FluidClientVersion.v2_0; export interface CodecTree { readonly name: string; diff --git a/packages/dds/tree/src/codec/versioned/codec.ts b/packages/dds/tree/src/codec/versioned/codec.ts index de09370d168a..e100b2731089 100644 --- a/packages/dds/tree/src/codec/versioned/codec.ts +++ b/packages/dds/tree/src/codec/versioned/codec.ts @@ -14,12 +14,12 @@ import { type IJsonCodec, withSchemaValidation, type FormatVersion, - type FluidClientVersion, type CodecWriteOptions, } from "../codec.js"; import { Versioned } from "./format.js"; import { pkgVersion } from "../../packageVersion.js"; +import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions/internal"; export function makeVersionedCodec< TDecoded, @@ -45,7 +45,7 @@ export function makeVersionedCodec< if (!supportedVersions.has(versioned.version)) { throw new UsageError( `Unsupported version ${versioned.version} encountered while decoding data. Supported versions for this data are: ${Array.from(supportedVersions).join(", ")}. -The client which encoded this data likely specified an "oldestCompatibleClient" value which corresponds to a version newer than the version of this client ("${pkgVersion}").`, +The client which encoded this data likely specified an "minVersionForCollab" value which corresponds to a version newer than the version of this client ("${pkgVersion}").`, ); } const decoded = inner.decode(data, context); @@ -103,7 +103,7 @@ export function makeVersionDispatchingCodec( } /** - * Creates a codec which dispatches to the appropriate member of a codec family based on the `oldestCompatibleClient` for encode and the + * Creates a codec which dispatches to the appropriate member of a codec family based on the `minVersionForCollab` for encode and the * version number in data it encounters for decode. * @privateRemarks * This is a two stage builder so the first stage can encapsulate all codec specific details and the second can bring in configuration. @@ -115,17 +115,17 @@ export class ClientVersionDispatchingCodecBuilder { */ private readonly family: ICodecFamily, /** - * A function which maps a {@link FluidClientVersion} to a version number for the codec family which is supported by that version. + * A function which maps a {@link MinimumVersionForCollab} to a version number for the codec family which is supported by that version. * This can (and typically does) pick the newest version of the codec which is known to be compatible with the client version so that * any improvements in newer versions of the codec can be used when allowed. */ - private readonly versionMapping: (oldestCompatibleClient: FluidClientVersion) => number, + private readonly versionMapping: (minVersionForCollab: MinimumVersionForCollab) => number, ) {} public build( options: CodecWriteOptions, ): IJsonCodec { - const writeVersion = this.versionMapping(options.oldestCompatibleClient); + const writeVersion = this.versionMapping(options.minVersionForCollab); return makeVersionDispatchingCodec(this.family, { ...options, writeVersion }); } } diff --git a/packages/dds/tree/src/core/tree/detachedFieldIndex.ts b/packages/dds/tree/src/core/tree/detachedFieldIndex.ts index ae11f8398fca..7236f297d86b 100644 --- a/packages/dds/tree/src/core/tree/detachedFieldIndex.ts +++ b/packages/dds/tree/src/core/tree/detachedFieldIndex.ts @@ -84,7 +84,7 @@ export class DetachedFieldIndex { ) { this.options = options ?? { jsonValidator: FormatValidatorNoOp, - oldestCompatibleClient: FluidClientVersion.v2_0, + minVersionForCollab: FluidClientVersion.v2_0, }; this.codec = makeDetachedFieldIndexCodec(revisionTagCodec, this.options, idCompressor); } diff --git a/packages/dds/tree/src/core/tree/detachedFieldIndexCodecs.ts b/packages/dds/tree/src/core/tree/detachedFieldIndexCodecs.ts index f31cd718ef03..1f4d2ad98b5a 100644 --- a/packages/dds/tree/src/core/tree/detachedFieldIndexCodecs.ts +++ b/packages/dds/tree/src/core/tree/detachedFieldIndexCodecs.ts @@ -30,7 +30,7 @@ export function makeDetachedFieldIndexCodec( ): IJsonCodec { const family = makeDetachedFieldIndexCodecFamily(revisionTagCodec, options, idCompressor); const writeVersion = - options.oldestCompatibleClient < FluidClientVersion.v2_52 ? version1 : version2; + options.minVersionForCollab < FluidClientVersion.v2_52 ? version1 : version2; return makeVersionDispatchingCodec(family, { ...options, writeVersion }); } diff --git a/packages/dds/tree/src/feature-libraries/chunked-forest/codec/codecs.ts b/packages/dds/tree/src/feature-libraries/chunked-forest/codec/codecs.ts index be50e937a68c..88578d9f4e5d 100644 --- a/packages/dds/tree/src/feature-libraries/chunked-forest/codec/codecs.ts +++ b/packages/dds/tree/src/feature-libraries/chunked-forest/codec/codecs.ts @@ -8,7 +8,6 @@ import type { IIdCompressor, SessionId } from "@fluidframework/id-compressor"; import { type CodecTree, - type FluidClientVersion, type ICodecOptions, type IJsonCodec, makeVersionedValidatedCodec, @@ -35,6 +34,7 @@ import type { FieldBatch } from "./fieldBatch.js"; import { EncodedFieldBatch, validVersions, type FieldBatchFormatVersion } from "./format.js"; import { schemaCompressedEncode } from "./schemaBasedEncode.js"; import { uncompressedEncode } from "./uncompressedEncode.js"; +import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions/internal"; import type { IncrementalEncodingPolicy } from "./incrementalEncodingPolicy.js"; /** @@ -115,12 +115,12 @@ export type FieldBatchCodec = IJsonCodec< >; /** - * Get the write version for {@link makeFieldBatchCodec} based on the `oldestCompatibleClient` version. + * Get the write version for {@link makeFieldBatchCodec} based on the `minVersionForCollab` version. * @privateRemarks * TODO: makeFieldBatchCodec (and makeVersionDispatchingCodec transitively) should bake in this versionToFormat logic and the resulting codec can then support use with FluidClientVersion directly. */ export function fluidVersionToFieldBatchCodecWriteVersion( - oldestCompatibleClient: FluidClientVersion, + minVersionForCollab: MinimumVersionForCollab, ): number { // There is currently on only 1 version. return 1; diff --git a/packages/dds/tree/src/feature-libraries/schema-index/codec.ts b/packages/dds/tree/src/feature-libraries/schema-index/codec.ts index e0f9c8e2ada3..c3f2192bcc1a 100644 --- a/packages/dds/tree/src/feature-libraries/schema-index/codec.ts +++ b/packages/dds/tree/src/feature-libraries/schema-index/codec.ts @@ -7,7 +7,6 @@ import { fail, unreachableCase } from "@fluidframework/core-utils/internal"; import { type CodecTree, - type FluidClientVersion, type ICodecFamily, type ICodecOptions, type IJsonCodec, @@ -29,14 +28,15 @@ import { brand, type JsonCompatible } from "../../util/index.js"; import { Format as FormatV1 } from "./formatV1.js"; import { Format as FormatV2 } from "./formatV2.js"; +import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions/internal"; /** - * Convert a FluidClientVersion to a SchemaVersion. - * @param clientVersion - The FluidClientVersion to convert. - * @returns The SchemaVersion that corresponds to the provided FluidClientVersion. + * Convert a MinimumVersionForCollab to a SchemaVersion. + * @param clientVersion - The MinimumVersionForCollab to convert. + * @returns The SchemaVersion that corresponds to the provided MinimumVersionForCollab. */ export function clientVersionToSchemaVersion( - clientVersion: FluidClientVersion, + clientVersion: MinimumVersionForCollab, ): SchemaVersion { // Only one version of the schema codec is currently supported. return SchemaVersion.v1; diff --git a/packages/dds/tree/src/shared-tree/sharedTree.ts b/packages/dds/tree/src/shared-tree/sharedTree.ts index b8a8719c2984..d5167388efd2 100644 --- a/packages/dds/tree/src/shared-tree/sharedTree.ts +++ b/packages/dds/tree/src/shared-tree/sharedTree.ts @@ -942,7 +942,7 @@ export function buildConfiguredForest( export const defaultSharedTreeOptions: Required = { jsonValidator: FormatValidatorNoOp, - oldestCompatibleClient: FluidClientVersion.v2_0, + minVersionForCollab: FluidClientVersion.v2_0, forest: ForestTypeReference, treeEncodeType: TreeCompressionStrategy.Compressed, formatVersion: SharedTreeFormatVersion.v3, diff --git a/packages/dds/tree/src/shared-tree/treeAlpha.ts b/packages/dds/tree/src/shared-tree/treeAlpha.ts index 19532b34abea..d01265b0a89a 100644 --- a/packages/dds/tree/src/shared-tree/treeAlpha.ts +++ b/packages/dds/tree/src/shared-tree/treeAlpha.ts @@ -331,10 +331,7 @@ export interface TreeAlpha { */ exportCompressed( tree: TreeNode | TreeLeafValue, - options: { idCompressor?: IIdCompressor } & Pick< - CodecWriteOptions, - "oldestCompatibleClient" - >, + options: { idCompressor?: IIdCompressor } & Pick, ): JsonCompatible; /** @@ -812,13 +809,10 @@ export const TreeAlpha: TreeAlpha = { exportCompressed( node: TreeNode | TreeLeafValue, - options: { idCompressor?: IIdCompressor } & Pick< - CodecWriteOptions, - "oldestCompatibleClient" - >, + options: { idCompressor?: IIdCompressor } & Pick, ): JsonCompatible { const schema = tryGetSchema(node) ?? fail(0xacf /* invalid input */); - const format = fluidVersionToFieldBatchCodecWriteVersion(options.oldestCompatibleClient); + const format = fluidVersionToFieldBatchCodecWriteVersion(options.minVersionForCollab); const codec = makeFieldBatchCodec({ jsonValidator: FormatValidatorNoOp }, format); const cursor = borrowFieldCursorFromTreeNodeOrValue(node); const batch: FieldBatch = [cursor]; diff --git a/packages/dds/tree/src/shared-tree/treeCheckout.ts b/packages/dds/tree/src/shared-tree/treeCheckout.ts index 9bce1d945ded..1623a4bfe498 100644 --- a/packages/dds/tree/src/shared-tree/treeCheckout.ts +++ b/packages/dds/tree/src/shared-tree/treeCheckout.ts @@ -298,7 +298,7 @@ export function createTreeCheckout( const forest = args?.forest ?? buildForest(breaker, schema); const defaultCodecOptions = { jsonValidator: FormatValidatorNoOp, - oldestCompatibleClient: FluidClientVersion.v2_0, + minVersionForCollab: FluidClientVersion.v2_0, }; const defaultFieldBatchVersion = 1; const changeFamily = diff --git a/packages/dds/tree/src/simple-tree/api/storedSchema.ts b/packages/dds/tree/src/simple-tree/api/storedSchema.ts index d04fe2db7340..bd2e4c3ac9c1 100644 --- a/packages/dds/tree/src/simple-tree/api/storedSchema.ts +++ b/packages/dds/tree/src/simple-tree/api/storedSchema.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. */ -import type { FluidClientVersion, ICodecOptions } from "../../codec/index.js"; +import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions/internal"; +import type { ICodecOptions } from "../../codec/index.js"; import { SchemaVersion } from "../../core/index.js"; import { encodeTreeSchema, makeSchemaCodec } from "../../feature-libraries/index.js"; import { @@ -24,7 +25,7 @@ import type { SchemaCompatibilityStatus } from "./tree.js"; * Dumps the "persisted" schema subset of the provided `schema` into a deterministic JSON-compatible, semi-human-readable format. * * @param schema - The schema to dump. - * @param oldestCompatibleClient - The oldest client version which can read the schema: impacts the format used. + * @param minVersionForCollab - The oldest client version which can read the schema: impacts the format used. * @param includeStaged - filter for selecting which staged allowed types to include in the output. * * @remarks @@ -54,11 +55,11 @@ import type { SchemaCompatibilityStatus } from "./tree.js"; */ export function extractPersistedSchema( schema: ImplicitFieldSchema, - oldestCompatibleClient: FluidClientVersion, + minVersionForCollab: MinimumVersionForCollab, includeStaged: (upgrade: SchemaUpgrade) => boolean, ): JsonCompatible { const stored = toStoredSchema(schema, { includeStaged }); - const schemaWriteVersion = clientVersionToSchemaVersion(oldestCompatibleClient); + const schemaWriteVersion = clientVersionToSchemaVersion(minVersionForCollab); return encodeTreeSchema(stored, schemaWriteVersion); } diff --git a/packages/dds/tree/src/test/codec/versioned/codec.spec.ts b/packages/dds/tree/src/test/codec/versioned/codec.spec.ts index 7ebf0adc5e27..278bead59efa 100644 --- a/packages/dds/tree/src/test/codec/versioned/codec.spec.ts +++ b/packages/dds/tree/src/test/codec/versioned/codec.spec.ts @@ -5,17 +5,14 @@ import { strict as assert } from "node:assert"; -import { - type FluidClientVersion, - type ICodecFamily, - type IJsonCodec, - makeCodecFamily, -} from "../../../codec/index.js"; +import { type ICodecFamily, type IJsonCodec, makeCodecFamily } from "../../../codec/index.js"; import { FormatValidatorBasic } from "../../../external-utilities/index.js"; // eslint-disable-next-line import/no-internal-modules import { ClientVersionDispatchingCodecBuilder } from "../../../codec/versioned/codec.js"; import { validateUsageError } from "../../utils.js"; import { pkgVersion } from "../../../packageVersion.js"; +import { gt } from "semver-ts"; +import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions/internal"; describe("versioned Codecs", () => { describe("ClientVersionDispatchingCodecBuilder", () => { @@ -42,16 +39,19 @@ describe("versioned Codecs", () => { ]); const builder = new ClientVersionDispatchingCodecBuilder( family, - (oldestCompatibleClient: FluidClientVersion) => (oldestCompatibleClient > 5 ? 2 : 1), + (minVersionForCollab: MinimumVersionForCollab) => + // Arbitrary version selection logic for test purposes. Versions greater than 5.0.0 get v2 codec. + gt(minVersionForCollab, "5.0.0") ? 2 : 1, ); it("round trip", () => { const codec1 = builder.build({ - oldestCompatibleClient: 2 as FluidClientVersion, + minVersionForCollab: "2.0.0", jsonValidator: FormatValidatorBasic, }); const codec2 = builder.build({ - oldestCompatibleClient: 6 as FluidClientVersion, + // We have to cast to a `MinimumVersionForCollab` because "6.0.0" is not a valid value for that type. + minVersionForCollab: "6.0.0" as MinimumVersionForCollab, jsonValidator: FormatValidatorBasic, }); const v1 = codec1.encode(42); @@ -66,7 +66,7 @@ describe("versioned Codecs", () => { assert.throws( () => codec1.decode({ version: 3, value2: 42 }), validateUsageError(`Unsupported version 3 encountered while decoding data. Supported versions for this data are: 1, 2. -The client which encoded this data likely specified an "oldestCompatibleClient" value which corresponds to a version newer than the version of this client ("${pkgVersion}").`), +The client which encoded this data likely specified an "minVersionForCollab" value which corresponds to a version newer than the version of this client ("${pkgVersion}").`), ); }); }); diff --git a/packages/dds/tree/src/test/core/tree/detachedFieldIndex.spec.ts b/packages/dds/tree/src/test/core/tree/detachedFieldIndex.spec.ts index bd1075bf4b45..7705f2aa49ff 100644 --- a/packages/dds/tree/src/test/core/tree/detachedFieldIndex.spec.ts +++ b/packages/dds/tree/src/test/core/tree/detachedFieldIndex.spec.ts @@ -224,7 +224,7 @@ function generateTestCases( describe("DetachedFieldIndex Codecs", () => { const options: CodecWriteOptions = { jsonValidator: FormatValidatorBasic, - oldestCompatibleClient: FluidClientVersion.v2_0, + minVersionForCollab: FluidClientVersion.v2_0, }; it("encodes with a version stamp.", () => { diff --git a/packages/dds/tree/src/test/feature-libraries/chunked-forest/chunkEncodingEndToEnd.spec.ts b/packages/dds/tree/src/test/feature-libraries/chunked-forest/chunkEncodingEndToEnd.spec.ts index 09c79f1e76e3..116934974bf3 100644 --- a/packages/dds/tree/src/test/feature-libraries/chunked-forest/chunkEncodingEndToEnd.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/chunked-forest/chunkEncodingEndToEnd.spec.ts @@ -91,7 +91,7 @@ import { FluidClientVersion, type CodecWriteOptions } from "../../../codec/index const options: CodecWriteOptions = { jsonValidator: FormatValidatorBasic, - oldestCompatibleClient: FluidClientVersion.v2_0, + minVersionForCollab: FluidClientVersion.v2_0, }; const fieldBatchCodec = makeFieldBatchCodec({ jsonValidator: FormatValidatorBasic }, 1); diff --git a/packages/dds/tree/src/test/feature-libraries/forest-summary/forestSummarizer.spec.ts b/packages/dds/tree/src/test/feature-libraries/forest-summary/forestSummarizer.spec.ts index 98913d5bf146..ee6d4e0597c8 100644 --- a/packages/dds/tree/src/test/feature-libraries/forest-summary/forestSummarizer.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/forest-summary/forestSummarizer.spec.ts @@ -76,7 +76,7 @@ function createForestSummarizer(args: { const fieldBatchCodec = makeFieldBatchCodec({ jsonValidator: FormatValidatorBasic }, 1); const options: CodecWriteOptions = { jsonValidator: FormatValidatorBasic, - oldestCompatibleClient: FluidClientVersion.v2_0, + minVersionForCollab: FluidClientVersion.v2_0, }; const checkout = checkoutWithContent(initialContent, { forestType, diff --git a/packages/dds/tree/src/test/forestTestSuite.ts b/packages/dds/tree/src/test/forestTestSuite.ts index ba6401971b8b..8255c9b30335 100644 --- a/packages/dds/tree/src/test/forestTestSuite.ts +++ b/packages/dds/tree/src/test/forestTestSuite.ts @@ -433,7 +433,7 @@ export function testForest(config: ForestTestConfiguration): void { idAllocatorFromMaxId() as IdAllocator, testRevisionTagCodec, testIdCompressor, - { jsonValidator: FormatValidatorBasic, oldestCompatibleClient: FluidClientVersion.v2_0 }, + { jsonValidator: FormatValidatorBasic, minVersionForCollab: FluidClientVersion.v2_0 }, ); const delta: DeltaFieldMap = new Map([ [rootFieldKey, [mark]], diff --git a/packages/dds/tree/src/test/shared-tree/fuzz/baseModel.ts b/packages/dds/tree/src/test/shared-tree/fuzz/baseModel.ts index a374e4a00004..7ab604110a0e 100644 --- a/packages/dds/tree/src/test/shared-tree/fuzz/baseModel.ts +++ b/packages/dds/tree/src/test/shared-tree/fuzz/baseModel.ts @@ -10,8 +10,8 @@ import { SharedTreeFuzzTestFactory, createOnCreate } from "./fuzzUtils.js"; import type { Operation } from "./operationTypes.js"; import { takeAsync } from "@fluid-private/stochastic-test-utils"; import { type EditGeneratorOpWeights, makeOpGenerator } from "./fuzzEditGenerators.js"; -import { FluidClientVersion } from "../../../codec/index.js"; import { ForestTypeOptimized, ForestTypeReference } from "../../../shared-tree/index.js"; +import { pkgVersion } from "../../../packageVersion.js"; export const runsPerBatch = 50; // TODO: Enable other types of ops. @@ -41,7 +41,7 @@ export const baseTreeModel: DDSFuzzModel< > = { workloadName: "SharedTree (Reference Forest)", factory: new SharedTreeFuzzTestFactory(createOnCreate(undefined), undefined, { - oldestCompatibleClient: FluidClientVersion.EnableUnstableFeatures, + minVersionForCollab: pkgVersion, forest: ForestTypeReference, }), generatorFactory, @@ -56,7 +56,7 @@ export const optimizedForestTreeModel: DDSFuzzModel< > = { workloadName: "SharedTree (Optimized Forest)", factory: new SharedTreeFuzzTestFactory(createOnCreate(undefined), undefined, { - oldestCompatibleClient: FluidClientVersion.EnableUnstableFeatures, + minVersionForCollab: pkgVersion, forest: ForestTypeOptimized, }), generatorFactory, diff --git a/packages/dds/tree/src/test/shared-tree/independentView.spec.ts b/packages/dds/tree/src/test/shared-tree/independentView.spec.ts index 0c82e73c6b89..a5c7a9d6288e 100644 --- a/packages/dds/tree/src/test/shared-tree/independentView.spec.ts +++ b/packages/dds/tree/src/test/shared-tree/independentView.spec.ts @@ -35,7 +35,7 @@ describe("independentView", () => { { schema: extractPersistedSchema(config.schema, FluidClientVersion.v2_0, () => true), tree: TreeAlpha.exportCompressed(1, { - oldestCompatibleClient: FluidClientVersion.v2_0, + minVersionForCollab: FluidClientVersion.v2_0, }), idCompressor: testIdCompressor, }, diff --git a/packages/dds/tree/src/test/shared-tree/sharedTree.spec.ts b/packages/dds/tree/src/test/shared-tree/sharedTree.spec.ts index c2b84c869802..d1f18693071c 100644 --- a/packages/dds/tree/src/test/shared-tree/sharedTree.spec.ts +++ b/packages/dds/tree/src/test/shared-tree/sharedTree.spec.ts @@ -2433,11 +2433,13 @@ describe("SharedTree", () => { }); it("summarize with pre-attach removed nodes", () => { - const runtime = new MockFluidDataStoreRuntime({ idCompressor: createIdCompressor() }); + const runtime = new MockFluidDataStoreRuntime({ + idCompressor: createIdCompressor(), + minVersionForCollab: FluidClientVersion.v2_52, + }); const sharedObject = configuredSharedTree({ jsonValidator: FormatValidatorBasic, forest: ForestTypeExpensiveDebug, - oldestCompatibleClient: FluidClientVersion.v2_52, }) as SharedObjectKind & ISharedObjectKind; const tree = sharedObject.getFactory().create(runtime, "tree"); const runtimeFactory = new MockContainerRuntimeFactory(); diff --git a/packages/dds/tree/src/test/shared-tree/sharedTreeChangeEnricher.spec.ts b/packages/dds/tree/src/test/shared-tree/sharedTreeChangeEnricher.spec.ts index 3328bb2489c3..0f2507788281 100644 --- a/packages/dds/tree/src/test/shared-tree/sharedTreeChangeEnricher.spec.ts +++ b/packages/dds/tree/src/test/shared-tree/sharedTreeChangeEnricher.spec.ts @@ -93,7 +93,7 @@ export function setupEnricher() { idAllocatorFromMaxId() as IdAllocator, testRevisionTagCodec, testIdCompressor, - { jsonValidator: FormatValidatorBasic, oldestCompatibleClient: FluidClientVersion.v2_0 }, + { jsonValidator: FormatValidatorBasic, minVersionForCollab: FluidClientVersion.v2_0 }, ); const schema = new TreeStoredSchemaRepository(jsonSequenceRootSchema); const forest = buildTestForest({ additionalAsserts: true, schema }); diff --git a/packages/dds/tree/src/test/simple-tree/api/stagedSchemaUpgrade.spec.ts b/packages/dds/tree/src/test/simple-tree/api/stagedSchemaUpgrade.spec.ts index 5c9a14ae31a3..46975d0ce217 100644 --- a/packages/dds/tree/src/test/simple-tree/api/stagedSchemaUpgrade.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/stagedSchemaUpgrade.spec.ts @@ -173,7 +173,7 @@ describe("staged schema upgrade", () => { idCompressor, // TODO: this should use the framework level options, not this packages temporary placeholder - oldestCompatibleClient: FluidClientVersion.v2_0, + minVersionForCollab: FluidClientVersion.v2_0, }), // TODO: we need a way to get the stored schema from independent views. Allow constructing a ViewAbleTree instead of a view directly (maybe an independentTree API?)? diff --git a/packages/dds/tree/src/test/simple-tree/api/treeNodeApi.spec.ts b/packages/dds/tree/src/test/simple-tree/api/treeNodeApi.spec.ts index e1c3cc55ae44..b79a81b652bb 100644 --- a/packages/dds/tree/src/test/simple-tree/api/treeNodeApi.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/treeNodeApi.spec.ts @@ -194,7 +194,7 @@ describe("treeNodeApi", () => { () => invalidations.add("exportCompressed"), () => TreeAlpha.exportCompressed(node, { - oldestCompatibleClient: FluidClientVersion.v2_0, + minVersionForCollab: FluidClientVersion.v2_0, }), ); TreeAlpha.trackObservations( @@ -3533,7 +3533,7 @@ describe("treeNodeApi", () => { ); assert(tree !== undefined); const exported = TreeAlpha.exportCompressed(tree, { - oldestCompatibleClient: FluidClientVersion.v2_0, + minVersionForCollab: FluidClientVersion.v2_0, }); const imported = TreeAlpha.importCompressed(testCase.schema, exported, { jsonValidator: ajvValidator, diff --git a/packages/dds/tree/src/treeFactory.ts b/packages/dds/tree/src/treeFactory.ts index f4767e0dd38b..727e94d6fb11 100644 --- a/packages/dds/tree/src/treeFactory.ts +++ b/packages/dds/tree/src/treeFactory.ts @@ -49,10 +49,23 @@ function treeKernelFactory( if (args.idCompressor === undefined) { throw new UsageError("IdCompressor must be enabled to use SharedTree"); } - const adjustedOptions = { ...options }; - // TODO: get default from runtime once something like runtime.oldestCompatibleClient exists. - // Using default of 2.0 since that is the oldest version that supports SharedTree. - adjustedOptions.oldestCompatibleClient ??= FluidClientVersion.v2_0; + + const { minVersionForCollab, ...otherOptions } = options; + + const adjustedOptions = { + ...otherOptions, + // Cases: + // A. If options specifies minVersionForCollab, it takes precedence over args.minVersionForCollab. + // This value is set when: + // - A customer using the declarative SharedTree API specifies the setting at the Shared Tree level. + // There is currently no way to set it via the declarative API, but it could be added in the future. + // - treeKernelFactory is invoked in a fuzz test with a specific minVersionForCollab + // B. Otherwise, we use args.minVersionForCollab, which is propagated from the ContainerRuntime. + // C. If neither specifies it, we fall back to a default value default of 2.0 since that is the oldest version that supports SharedTree. + minVersionForCollab: + minVersionForCollab ?? args.minVersionForCollab ?? FluidClientVersion.v2_0, + }; + return new SharedTreeKernel( new Breakable("SharedTree"), args.sharedObject, 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 b92ef5e2da07..2f7e18e94e64 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 @@ -143,7 +143,7 @@ export function cloneWithReplacements(root: unknown, rootKey: string, replacer: // @alpha @input export interface CodecWriteOptions extends ICodecOptions { - readonly oldestCompatibleClient: FluidClientVersion; + readonly minVersionForCollab: MinimumVersionForCollab; } // @public @@ -265,7 +265,7 @@ export function evaluateLazySchema(value: LazyItem) type ExtractItemType = Item extends () => infer Result ? Result : Item; // @alpha -export function extractPersistedSchema(schema: ImplicitFieldSchema, oldestCompatibleClient: FluidClientVersion, includeStaged: (upgrade: SchemaUpgrade) => boolean): JsonCompatible; +export function extractPersistedSchema(schema: ImplicitFieldSchema, minVersionForCollab: MinimumVersionForCollab, includeStaged: (upgrade: SchemaUpgrade) => boolean): JsonCompatible; // @alpha @system export type FactoryContent = IFluidHandle | string | number | boolean | null | Iterable | readonly InsertableContent[] | FactoryContentObject; @@ -350,11 +350,10 @@ type FlexList = readonly LazyItem[]; type FlexListToUnion = ExtractItemType; // @alpha -export enum FluidClientVersion { - EnableUnstableFeatures, - v2_0 = 2, - v2_52 = 2.052 -} +export const FluidClientVersion: { + readonly v2_0: "2.0.0"; + readonly v2_52: "2.52.0"; +}; // @public export type FluidObject = { @@ -1739,7 +1738,7 @@ export interface TreeAlpha { create(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField): Unhydrated : TreeNode | TreeLeafValue | undefined>; exportCompressed(tree: TreeNode | TreeLeafValue, options: { idCompressor?: IIdCompressor; - } & Pick): JsonCompatible; + } & Pick): JsonCompatible; exportConcise(node: TreeNode | TreeLeafValue, options?: TreeEncodingOptions): ConciseTree; exportConcise(node: TreeNode | TreeLeafValue | undefined, options?: TreeEncodingOptions): ConciseTree | undefined; exportVerbose(node: TreeNode | TreeLeafValue, options?: TreeEncodingOptions): VerboseTree; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34a7674c7f01..1ebef77e8cb3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9350,6 +9350,9 @@ importers: '@ungap/structured-clone': specifier: ^1.2.0 version: 1.2.1 + semver-ts: + specifier: ^1.0.3 + version: 1.0.3 uuid: specifier: ^11.1.0 version: 11.1.0