diff --git a/packages/dds/tree/src/codec/versioned/codec.ts b/packages/dds/tree/src/codec/versioned/codec.ts index 49c4934ee4ee..9845f290b796 100644 --- a/packages/dds/tree/src/codec/versioned/codec.ts +++ b/packages/dds/tree/src/codec/versioned/codec.ts @@ -399,31 +399,61 @@ export class ClientVersionDispatchingCodecBuilder< */ readonly writeVersion: TFormatVersion; } { - const applied = this.applyOptions(options); + const [applied, decoder] = this.buildDecoderInternal(options); const writeVersion = getWriteVersion(this.name, options, applied); - const fromFormatVersion = new Map< - FormatVersion, - EvaluatedCodecVersion - >(applied.map((codec) => [codec.formatVersion, codec])); return { + ...decoder, encode: (data: TDecoded, context: TContext): JsonCompatibleReadOnly => { return writeVersion.codec.encode(data, context); }, - decode: (data: JsonCompatibleReadOnly, context: TContext): TDecoded => { - const versioned = data as Partial; - const codec = fromFormatVersion.get(versioned.version); - if (codec === undefined) { - throw new UsageError( - `Unsupported version ${versioned.version} encountered while decoding ${this.name} data. Supported versions for this data are: ${versionList(applied)}. -The client which encoded this data likely specified an "minVersionForCollab" value which corresponds to a version newer than the version of this client ("${pkgVersion}").`, - ); - } - return codec.codec.decode(data, context); - }, writeVersion: writeVersion.formatVersion, }; } + private buildDecoderInternal( + options: TBuildOptions, + ): [ + EvaluatedCodecVersion[], + Pick< + IJsonCodec, + "decode" + >, + ] { + const applied = this.applyOptions(options); + const fromFormatVersion = new Map< + FormatVersion, + EvaluatedCodecVersion + >(applied.map((codec) => [codec.formatVersion, codec])); + return [ + applied, + { + decode: (data: JsonCompatibleReadOnly, context: TContext): TDecoded => { + const versioned = data as Partial; + const codec = fromFormatVersion.get(versioned.version); + if (codec === undefined) { + throw new UsageError( + `Unsupported version ${versioned.version} encountered while decoding ${this.name} data. Supported versions for this data are: ${versionList(applied)}. +The client which encoded this data likely specified an "minVersionForCollab" value which corresponds to a version newer than the version of this client ("${pkgVersion}").`, + ); + } + return codec.codec.decode(data, context); + }, + }, + ]; + } + + /** + * Produce a single codec which can read any supported format. + */ + public buildDecoder( + options: TBuildOptions, + ): Pick< + IJsonCodec, + "decode" + > { + return this.buildDecoderInternal(options)[1]; + } + public getCodecTree(clientVersion: MinimumVersionForCollab): CodecTree { // TODO: add support for children codecs. const selected = getWriteVersionNoOverrides(this.registry, clientVersion); diff --git a/packages/dds/tree/src/shared-tree/independentView.ts b/packages/dds/tree/src/shared-tree/independentView.ts index a0cb034df5da..99f8f76c5342 100644 --- a/packages/dds/tree/src/shared-tree/independentView.ts +++ b/packages/dds/tree/src/shared-tree/independentView.ts @@ -10,29 +10,22 @@ import { createIdCompressor, } from "@fluidframework/id-compressor/internal"; -import { - FluidClientVersion, - type CodecWriteOptions, - type ICodecOptions, -} from "../codec/index.js"; +import type { CodecWriteOptions, ICodecOptions } from "../codec/index.js"; import { type RevisionTag, RevisionTagCodec, - SchemaFormatVersion, TreeStoredSchemaRepository, } from "../core/index.js"; import { createNodeIdentifierManager, fieldBatchCodecBuilder, - makeSchemaCodec, type FieldBatchEncodingContext, defaultSchemaPolicy, TreeCompressionStrategy, defaultIncrementalEncodingPolicy, + schemaCodecBuilder, } from "../feature-libraries/index.js"; import { combineChunks } from "../feature-libraries/index.js"; -// eslint-disable-next-line import-x/no-internal-modules -import type { Format } from "../feature-libraries/schema-index/formatV1.js"; import type { TreeViewConfiguration, ImplicitFieldSchema, @@ -236,15 +229,9 @@ export function createIndependentTreeAlpha { - // Any version can be passed down to makeSchemaCodec here. - // We only use the decode part, which always dispatches to the correct codec based on the version in the data, not the version passed to `makeSchemaCodec`. - const schemaCodec = makeSchemaCodec( - { ...options, minVersionForCollab: FluidClientVersion.v2_0 }, - SchemaFormatVersion.v1, - ); - const stored = schemaCodec.decode(persisted as FormatV1); + const schemaCodec = schemaCodecBuilder.buildDecoder(options); + const stored = schemaCodec.decode(persisted); const config = new TreeViewConfigurationAlpha({ schema: normalizeFieldSchema(view), }); 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 dc53a897bfae..c8b1c0912098 100644 --- a/packages/dds/tree/src/test/codec/versioned/codec.spec.ts +++ b/packages/dds/tree/src/test/codec/versioned/codec.spec.ts @@ -152,6 +152,26 @@ The client which encoded this data likely specified an "minVersionForCollab" val ]); }); + describe("buildDecoder", () => { + it("decodes all supported format versions", () => { + const decoder = builder.buildDecoder({ jsonValidator: FormatValidatorBasic }); + assert.equal(decoder.decode({ version: 1, value1: 42 }), 42); + assert.equal(decoder.decode({ version: 2, value2: 99 }), 99); + assert.equal(decoder.decode({ version: "X", valueX: 7 }), 7); + }); + + it("throws UsageError for unsupported version", () => { + const decoder = builder.buildDecoder({ jsonValidator: FormatValidatorBasic }); + assert.throws( + () => decoder.decode({ version: 3, value2: 42 }), + validateUsageError( + `Unsupported version 3 encountered while decoding Test data. Supported versions for this data are: [1,2,"X"]. +The client which encoded this data likely specified an "minVersionForCollab" value which corresponds to a version newer than the version of this client ("${pkgVersion}").`, + ), + ); + }); + }); + it("bad builds", () => { // Build asserts are debugAsserts, so only test them when those are enabled. if (nonProductionConditionalsIncluded()) { diff --git a/packages/dds/tree/src/test/feature-libraries/schema-index/schemaSummarizer.spec.ts b/packages/dds/tree/src/test/feature-libraries/schema-index/schemaSummarizer.spec.ts index 2a029ce86472..c432fd075482 100644 --- a/packages/dds/tree/src/test/feature-libraries/schema-index/schemaSummarizer.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/schema-index/schemaSummarizer.spec.ts @@ -54,10 +54,7 @@ describe("schemaSummarizer", () => { for (const schemaFormat of schemaCodecBuilder.registry) { const encode = (schema: TreeStoredSchema): JsonCompatibleReadOnly => { - assert(schemaFormat.minVersionForCollab !== undefined); - const codec = schemaFormat.codec({ - jsonValidator: FormatValidatorBasic, - }); + const codec = schemaFormat.codec({ jsonValidator: FormatValidatorBasic }); const result: JsonCompatibleReadOnly = codec.encode(schema); return result; }; diff --git a/packages/dds/tree/src/test/snapshots/schema.spec.ts b/packages/dds/tree/src/test/snapshots/schema.spec.ts index f9a4ce4ad381..aad4a0b59851 100644 --- a/packages/dds/tree/src/test/snapshots/schema.spec.ts +++ b/packages/dds/tree/src/test/snapshots/schema.spec.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. */ -import { strict as assert } from "node:assert"; - import { FormatValidatorBasic } from "../../external-utilities/index.js"; // eslint-disable-next-line import-x/no-internal-modules import { schemaCodecBuilder } from "../../feature-libraries/schema-index/codec.js"; @@ -18,11 +16,8 @@ describe("schema snapshots", () => { for (const schemaFormat of schemaCodecBuilder.registry) { for (const { name, schemaData } of testTrees) { it(`${name} - schema v${schemaFormat.formatVersion}`, () => { - assert(schemaFormat.minVersionForCollab !== undefined); const encoded = schemaFormat - .codec({ - jsonValidator: FormatValidatorBasic, - }) + .codec({ jsonValidator: FormatValidatorBasic }) .encode(schemaData); takeJsonSnapshot(encoded); });