Skip to content

Commit 93fe943

Browse files
refactor(tree): make codec dependencies more explicit and auditable (#25619)
## Description The main goal of this PR to make the hierarchy of codec dependencies that are scattered across the SharedTree codebase more explicit and more auditable. This is accomplished in two ways: ### Specific Version Types Specific types are introduced for each format version and used whenever possible. For example, referring to a specific version of the `EditManager` format is now done with an instance of `EditManagerFormatVersion` instead of a plain `number` or `FormatVersion`. This makes clearer what the space of version number is being addressed. The type acts as a code reference that can be followed with Ctrl+Click. This also enforces that version literals correspond to a valid version for the target version space. For example, the code `const editManagerVersion: EditManagerFormatVersion = 42;` will not compile because there is no version 42 for the edit manager format. ### Queryable Codec Trees This PR introduces the `CodecTree` type so that dependencies between codec versions can be reified. Most codecs involved in SharedTree now come with an associated free function that returns a `CodecTree` for a given version of that codec. These are used in a new set of snapshot tests that capture the codec dependency trees of each SharedTree format version. While the primary motivation of such tests is to end up with files that can easily be inspected, these tests also have value in preventing unintentional changes in codec dependencies. ## Other Changes The changes involved in building the above have also led to the realization that some dependencies were hard coded in the wrong place: the format version for representing SharedTree changes used to be hard coded in the `EditManager` and `Message` codecs. This was questionable because `EditManager` and `Message` are completely agnostic to such details. These dependencies are now hard-coded in sharedTree.ts. ## Breaking Changes None
1 parent b0df485 commit 93fe943

36 files changed

+1213
-93
lines changed

packages/dds/tree/docs/main/compatibility.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,14 @@ For example, a format change in `SchemaChangeCodec` could be implemented by addi
215215
This new version would use the same code for all bits of `MessageCodec`, `EditManagerCodec`, and `SharedTreeChangeFamilyCodec`, but pass enough context down to `SchemaChangeCodec` to resolve to the newer format.
216216
In this manner, the mapping between explicitly versioned data and implicitly versioned data for composed codecs is managed in code.
217217

218+
## Auditing Codec Resolution
219+
220+
Because the dependencies between different codec versions are distributed over the various codec layers,
221+
it can be hard to understand the relationship between top-level `SharedTreeFormatVersion` values from [sharedTree.ts](../../src/shared-tree/sharedTree.ts)
222+
and the myriad of codec versions available.
223+
To help audit such relationships, the `getCodecTreeForSharedTreeFormat` function in [sharedTree.ts](../../src/shared-tree/sharedTree.ts) can be used.
224+
Snapshots of the dependencies are captured in the ["SharedTree Codecs"](../../src/test/shared-tree/sharedTreeCodecs.spec.ts) test suite and can be inspected [on disc](../../src/test/snapshots/codec-tree/SharedTreeFormatVersion.v1.json).
225+
218226
## Current code guidelines
219227

220228
Codecs which explicitly version their data should export a codec which takes in a write version and supports reading all supported versions.

packages/dds/tree/src/codec/codec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,52 @@ export interface ICodecFamily<TDecoded, TContext = void> {
255255
*/
256256
export type FormatVersion = number | undefined;
257257

258+
/**
259+
* A format version which is dependent on some parent format version.
260+
*/
261+
export interface DependentFormatVersion<
262+
TParentVersion extends FormatVersion = FormatVersion,
263+
TChildVersion extends FormatVersion = FormatVersion,
264+
> {
265+
/**
266+
* Looks up the child format version for a given parent format version.
267+
* @param parent - The parent format version.
268+
* @returns The corresponding child format version.
269+
*/
270+
lookup(parent: TParentVersion): TChildVersion;
271+
}
272+
273+
export class UniqueDependentFormatVersion<TChildVersion extends FormatVersion>
274+
implements DependentFormatVersion<FormatVersion, TChildVersion>
275+
{
276+
public constructor(private readonly child: TChildVersion) {}
277+
public lookup(_parent: FormatVersion): TChildVersion {
278+
return this.child;
279+
}
280+
}
281+
282+
export class MappedDependentFormatVersion<
283+
TParentVersion extends FormatVersion = FormatVersion,
284+
TChildVersion extends FormatVersion = FormatVersion,
285+
> implements DependentFormatVersion<TParentVersion, TChildVersion>
286+
{
287+
public constructor(private readonly map: ReadonlyMap<TParentVersion, TChildVersion>) {}
288+
public lookup(parent: TParentVersion): TChildVersion {
289+
return this.map.get(parent) ?? fail("Unknown parent version");
290+
}
291+
}
292+
293+
export const DependentFormatVersion = {
294+
fromUnique: <TChildVersion extends FormatVersion>(child: TChildVersion) =>
295+
new UniqueDependentFormatVersion(child),
296+
fromMap: <TParentVersion extends FormatVersion, TChildVersion extends FormatVersion>(
297+
map: ReadonlyMap<TParentVersion, TChildVersion>,
298+
) => new MappedDependentFormatVersion(map),
299+
fromPairs: <TParentVersion extends FormatVersion, TChildVersion extends FormatVersion>(
300+
pairs: Iterable<[TParentVersion, TChildVersion]>,
301+
) => new MappedDependentFormatVersion(new Map(pairs)),
302+
};
303+
258304
/**
259305
* Creates a codec family from a registry of codecs.
260306
* Any codec that is not a {@link IMultiFormatCodec} will be wrapped with a default binary encoding.
@@ -487,3 +533,17 @@ export enum FluidClientVersion {
487533
* TODO: Consider using packageVersion.ts to keep this current.
488534
*/
489535
export const currentVersion: FluidClientVersion = FluidClientVersion.v2_0;
536+
537+
export interface CodecTree {
538+
readonly name: string;
539+
readonly version: FormatVersion;
540+
readonly children?: readonly CodecTree[];
541+
}
542+
543+
export function jsonableCodecTree(tree: CodecTree): JsonCompatibleReadOnly {
544+
return {
545+
name: tree.name,
546+
version: tree.version ?? "null",
547+
children: tree.children?.map(jsonableCodecTree),
548+
};
549+
}

packages/dds/tree/src/codec/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
export {
77
type FormatVersion,
8+
DependentFormatVersion,
89
type IBinaryCodec,
910
type ICodecFamily,
1011
type ICodecOptions,
@@ -24,6 +25,8 @@ export {
2425
toFormatValidator,
2526
FormatValidatorNoOp,
2627
type FormatValidator,
28+
type CodecTree,
29+
jsonableCodecTree,
2730
extractJsonValidator,
2831
} from "./codec.js";
2932
export {

packages/dds/tree/src/core/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ export {
102102
cursorChunk,
103103
tryGetChunk,
104104
type ChunkedCursor,
105+
type DetachedFieldIndexFormatVersion,
106+
getCodecTreeForDetachedFieldIndexFormat,
105107
} from "./tree/index.js";
106108

107109
export {

packages/dds/tree/src/core/tree/detachedFieldIndexCodecs.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import type { IIdCompressor } from "@fluidframework/id-compressor";
77

88
import {
9+
type CodecTree,
910
type CodecWriteOptions,
1011
FluidClientVersion,
1112
type ICodecFamily,
@@ -42,3 +43,10 @@ export function makeDetachedFieldIndexCodecFamily(
4243
[version2, makeDetachedNodeToFieldCodecV2(revisionTagCodec, options, idCompressor)],
4344
]);
4445
}
46+
47+
export type DetachedFieldIndexFormatVersion = 1 | 2;
48+
export function getCodecTreeForDetachedFieldIndexFormat(
49+
version: DetachedFieldIndexFormatVersion,
50+
): CodecTree {
51+
return { name: "DetachedFieldIndex", version };
52+
}

packages/dds/tree/src/core/tree/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,10 @@ export {
118118
} from "./chunk.js";
119119

120120
export { DetachedFieldIndex } from "./detachedFieldIndex.js";
121+
122+
export {
123+
type DetachedFieldIndexFormatVersion,
124+
getCodecTreeForDetachedFieldIndexFormat,
125+
} from "./detachedFieldIndexCodecs.js";
126+
121127
export { type ForestRootId } from "./detachedFieldIndexTypes.js";

packages/dds/tree/src/feature-libraries/chunked-forest/codec/codecs.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
77
import type { IIdCompressor, SessionId } from "@fluidframework/id-compressor";
88

99
import {
10+
type CodecTree,
1011
type FluidClientVersion,
1112
type ICodecOptions,
1213
type IJsonCodec,
@@ -33,7 +34,7 @@ import {
3334

3435
import { decode } from "./chunkDecoding.js";
3536
import type { FieldBatch } from "./fieldBatch.js";
36-
import { EncodedFieldBatch, validVersions } from "./format.js";
37+
import { EncodedFieldBatch, validVersions, type FieldBatchFormatVersion } from "./format.js";
3738
import { schemaCompressedEncode } from "./schemaBasedEncode.js";
3839
import { uncompressedEncode } from "./uncompressedEncode.js";
3940

@@ -197,3 +198,7 @@ export function makeFieldBatchCodec(
197198
},
198199
});
199200
}
201+
202+
export function getCodecTreeForFieldBatchFormat(version: FieldBatchFormatVersion): CodecTree {
203+
return { name: "FieldBatch", version };
204+
}

packages/dds/tree/src/feature-libraries/chunked-forest/codec/format.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "./formatGeneric.js";
1616

1717
export const version = 1;
18+
export type FieldBatchFormatVersion = 1;
1819

1920
// Compatible versions used for format/version validation.
2021
// TODO: A proper version update policy will need to be documented.

packages/dds/tree/src/feature-libraries/chunked-forest/codec/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License.
44
*/
55

6-
export { EncodedFieldBatch } from "./format.js";
6+
export { EncodedFieldBatch, type FieldBatchFormatVersion } from "./format.js";
77
export type { FieldBatch } from "./fieldBatch.js";
88
export {
99
type FieldBatchCodec,
@@ -14,4 +14,5 @@ export {
1414
type IncrementalEncoder,
1515
type IncrementalDecoder,
1616
type ChunkReferenceId,
17+
getCodecTreeForFieldBatchFormat,
1718
} from "./codecs.js";

packages/dds/tree/src/feature-libraries/chunked-forest/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export {
1616
export { buildChunkedForest } from "./chunkedForest.js";
1717
export {
1818
EncodedFieldBatch,
19+
type FieldBatchFormatVersion,
20+
getCodecTreeForFieldBatchFormat,
1921
type FieldBatch,
2022
type FieldBatchCodec,
2123
makeFieldBatchCodec,

0 commit comments

Comments
 (0)