@fluidframework/odsp-driver 2.111.0 → 2.112.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/odspVersionManager/index.d.ts +6 -0
  3. package/dist/odspVersionManager/index.d.ts.map +1 -0
  4. package/dist/odspVersionManager/index.js +10 -0
  5. package/dist/odspVersionManager/index.js.map +1 -0
  6. package/dist/odspVersionManager/odspFileVersionFetcher.d.ts +22 -0
  7. package/dist/odspVersionManager/odspFileVersionFetcher.d.ts.map +1 -0
  8. package/dist/odspVersionManager/odspFileVersionFetcher.js +81 -0
  9. package/dist/odspVersionManager/odspFileVersionFetcher.js.map +1 -0
  10. package/dist/odspVersionManager/odspVersionManager.d.ts +98 -0
  11. package/dist/odspVersionManager/odspVersionManager.d.ts.map +1 -0
  12. package/dist/odspVersionManager/odspVersionManager.js +87 -0
  13. package/dist/odspVersionManager/odspVersionManager.js.map +1 -0
  14. package/dist/packageVersion.d.ts +1 -1
  15. package/dist/packageVersion.js +1 -1
  16. package/dist/packageVersion.js.map +1 -1
  17. package/lib/odspVersionManager/index.d.ts +6 -0
  18. package/lib/odspVersionManager/index.d.ts.map +1 -0
  19. package/lib/odspVersionManager/index.js +6 -0
  20. package/lib/odspVersionManager/index.js.map +1 -0
  21. package/lib/odspVersionManager/odspFileVersionFetcher.d.ts +22 -0
  22. package/lib/odspVersionManager/odspFileVersionFetcher.d.ts.map +1 -0
  23. package/lib/odspVersionManager/odspFileVersionFetcher.js +77 -0
  24. package/lib/odspVersionManager/odspFileVersionFetcher.js.map +1 -0
  25. package/lib/odspVersionManager/odspVersionManager.d.ts +98 -0
  26. package/lib/odspVersionManager/odspVersionManager.d.ts.map +1 -0
  27. package/lib/odspVersionManager/odspVersionManager.js +82 -0
  28. package/lib/odspVersionManager/odspVersionManager.js.map +1 -0
  29. package/lib/packageVersion.d.ts +1 -1
  30. package/lib/packageVersion.js +1 -1
  31. package/lib/packageVersion.js.map +1 -1
  32. package/package.json +12 -12
  33. package/src/odspVersionManager/DEV.md +277 -0
  34. package/src/odspVersionManager/index.ts +12 -0
  35. package/src/odspVersionManager/odspFileVersionFetcher.ts +138 -0
  36. package/src/odspVersionManager/odspVersionManager.ts +170 -0
  37. package/src/packageVersion.ts +1 -1
@@ -0,0 +1,98 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ /**
6
+ * Selects the ODSP file version whose snapshot sits at or before a target Fluid sequence number —
7
+ * the base to load or replay from when materializing a document at a point in time.
8
+ *
9
+ * The selection logic depends on an injected {@link IOdspFileVersionFetcher}, so it is independent of
10
+ * how versions are enumerated and resolved (real ODSP, a test double, or an alternative backend).
11
+ */
12
+ import { type OdspFileVersionFetcherProps } from "./odspFileVersionFetcher.js";
13
+ /**
14
+ * A single ODSP file version, as listed by the file's version history.
15
+ */
16
+ export interface OdspFileVersionRef {
17
+ /**
18
+ * The version's label (e.g. `"42.0"`), used to address the version when fetching it.
19
+ */
20
+ readonly versionId: string;
21
+ /**
22
+ * Last-modified timestamp of this version, ISO-8601.
23
+ */
24
+ readonly lastModifiedDateTime: string;
25
+ }
26
+ /**
27
+ * An ODSP file version together with its resolved Fluid sequence number.
28
+ */
29
+ export interface ResolvedVersion extends OdspFileVersionRef {
30
+ /**
31
+ * The Fluid sequence number the version's snapshot represents.
32
+ */
33
+ readonly sequenceNumber: number;
34
+ }
35
+ /**
36
+ * Result of resolving the base version for a target sequence number.
37
+ *
38
+ * @remarks
39
+ * There is intentionally no `targetIsLive` case: when the target is at/after the newest recoverable
40
+ * version, the greatest version with `seq <= target` IS that newest version, so it is a normal
41
+ * `found`. A consumer may separately choose to load the live file when the target is near the head.
42
+ */
43
+ export type BaseForSeq = {
44
+ /** A recoverable version with `sequenceNumber <= target` was found. */
45
+ readonly kind: "found";
46
+ readonly base: ResolvedVersion;
47
+ } | {
48
+ /** No recoverable version has `sequenceNumber <= target` (target predates retained history). */
49
+ readonly kind: "noBaseVersion";
50
+ /** The oldest sequence number that was resolved while searching, if any. */
51
+ readonly oldestResolvedSeq?: number;
52
+ };
53
+ /**
54
+ * Provides a file's versions and resolves each version's Fluid sequence number. Injected into
55
+ * the version manager so the selection logic does not depend on how versions are fetched.
56
+ */
57
+ export interface IOdspFileVersionFetcher {
58
+ /**
59
+ * Enumerate the file's versions, newest-first.
60
+ */
61
+ listFileVersions(): Promise<OdspFileVersionRef[]>;
62
+ /**
63
+ * Resolve a single version's Fluid sequence number. Throws on failure rather than returning a
64
+ * wrong value.
65
+ */
66
+ resolveSequenceNumber(versionId: string): Promise<number>;
67
+ }
68
+ /**
69
+ * Selects the file version to use as the base for loading or replaying to a target sequence number.
70
+ */
71
+ export interface IOdspVersionManager {
72
+ /**
73
+ * Given a target sequence number, return the closest version at or before it (`found`), or
74
+ * `noBaseVersion` if the target predates the oldest retained version.
75
+ */
76
+ findBaseForSeq(target: number): Promise<BaseForSeq>;
77
+ }
78
+ /**
79
+ * Default {@link IOdspVersionManager}. Caches the version list and resolved sequence numbers. The
80
+ * resolution strategy (eager, newest-to-oldest, stopping at the first usable base) is hidden behind
81
+ * {@link findBaseForSeq} and can change without affecting callers.
82
+ */
83
+ export declare class OdspVersionManager implements IOdspVersionManager {
84
+ private readonly fetcher;
85
+ private versionsCache;
86
+ private readonly seqByVersion;
87
+ constructor(fetcher: IOdspFileVersionFetcher);
88
+ refresh(): void;
89
+ findBaseForSeq(target: number): Promise<BaseForSeq>;
90
+ listVersions(): Promise<ResolvedVersion[]>;
91
+ private getVersions;
92
+ private resolveSeq;
93
+ }
94
+ /**
95
+ * Create an {@link IOdspVersionManager} for a specific ODSP file, wired to the real ODSP REST APIs.
96
+ */
97
+ export declare function createOdspVersionManager(props: OdspFileVersionFetcherProps): IOdspVersionManager;
98
+ //# sourceMappingURL=odspVersionManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"odspVersionManager.d.ts","sourceRoot":"","sources":["../../src/odspVersionManager/odspVersionManager.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;GAMG;AAEH,OAAO,EAEN,KAAK,2BAA2B,EAChC,MAAM,6BAA6B,CAAC;AAErC;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;OAEG;IACH,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;CACtC;AAED;;GAEG;AACH,MAAM,WAAW,eAAgB,SAAQ,kBAAkB;IAC1D;;OAEG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,GACnB;IACA,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;CAC9B,GACD;IACA,gGAAgG;IAChG,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,4EAA4E;IAC5E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACnC,CAAC;AAEL;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACvC;;OAEG;IACH,gBAAgB,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;IAClD;;;OAGG;IACH,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1D;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;OAGG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACpD;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,YAAW,mBAAmB;IAI1C,OAAO,CAAC,QAAQ,CAAC,OAAO;IAH3C,OAAO,CAAC,aAAa,CAA4C;IACjE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsC;gBAE/B,OAAO,EAAE,uBAAuB;IAE7D,OAAO,IAAI,IAAI;IAKT,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAyBnD,YAAY,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;YAYzC,WAAW;YAOX,UAAU;CAUxB;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACvC,KAAK,EAAE,2BAA2B,GAChC,mBAAmB,CAErB"}
@@ -0,0 +1,82 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ /**
6
+ * Selects the ODSP file version whose snapshot sits at or before a target Fluid sequence number —
7
+ * the base to load or replay from when materializing a document at a point in time.
8
+ *
9
+ * The selection logic depends on an injected {@link IOdspFileVersionFetcher}, so it is independent of
10
+ * how versions are enumerated and resolved (real ODSP, a test double, or an alternative backend).
11
+ */
12
+ import { createOdspFileVersionFetcher, } from "./odspFileVersionFetcher.js";
13
+ /**
14
+ * Default {@link IOdspVersionManager}. Caches the version list and resolved sequence numbers. The
15
+ * resolution strategy (eager, newest-to-oldest, stopping at the first usable base) is hidden behind
16
+ * {@link findBaseForSeq} and can change without affecting callers.
17
+ */
18
+ export class OdspVersionManager {
19
+ constructor(fetcher) {
20
+ this.fetcher = fetcher;
21
+ this.seqByVersion = new Map();
22
+ }
23
+ refresh() {
24
+ this.versionsCache = undefined;
25
+ this.seqByVersion.clear();
26
+ }
27
+ async findBaseForSeq(target) {
28
+ // Recoverable base candidates = every version except the tip (index 0 ≈ the live document).
29
+ const versions = await this.getVersions();
30
+ const candidates = versions.slice(1);
31
+ // Versions are listed newest-first, and version order is expected to track sequence number, so
32
+ // the first candidate whose seq is at or before the target is taken as the closest base. Because
33
+ // any base at or before the target replays forward to the same state, this early stop is an
34
+ // optimization, not a correctness requirement: if version order and sequence order ever diverge,
35
+ // a base that is valid but not strictly the closest may be chosen.
36
+ // Scanning newest-first also yields the newest of versions sharing a sequence number (dedup).
37
+ let oldestResolvedSeq;
38
+ for (const version of candidates) {
39
+ const sequenceNumber = await this.resolveSeq(version.versionId);
40
+ oldestResolvedSeq =
41
+ oldestResolvedSeq === undefined
42
+ ? sequenceNumber
43
+ : Math.min(oldestResolvedSeq, sequenceNumber);
44
+ if (sequenceNumber <= target) {
45
+ return { kind: "found", base: { ...version, sequenceNumber } };
46
+ }
47
+ }
48
+ return { kind: "noBaseVersion", oldestResolvedSeq };
49
+ }
50
+ async listVersions() {
51
+ const versions = await this.getVersions();
52
+ // Resolution order does not matter here, so resolve concurrently; the newest-first array order is
53
+ // preserved by Promise.all regardless of completion order.
54
+ return Promise.all(versions.map(async (version) => ({
55
+ ...version,
56
+ sequenceNumber: await this.resolveSeq(version.versionId),
57
+ })));
58
+ }
59
+ async getVersions() {
60
+ // Cache the pending promise, not the awaited value, so concurrent callers share one fetch and a
61
+ // refresh() that runs while the fetch is in flight is not overwritten when the fetch settles.
62
+ this.versionsCache ??= this.fetcher.listFileVersions();
63
+ return this.versionsCache;
64
+ }
65
+ async resolveSeq(versionId) {
66
+ // Cache the pending promise (a version's sequence number never changes) so concurrent callers
67
+ // coalesce and a refresh() is not clobbered by a fetch that was already in flight.
68
+ let pending = this.seqByVersion.get(versionId);
69
+ if (pending === undefined) {
70
+ pending = this.fetcher.resolveSequenceNumber(versionId);
71
+ this.seqByVersion.set(versionId, pending);
72
+ }
73
+ return pending;
74
+ }
75
+ }
76
+ /**
77
+ * Create an {@link IOdspVersionManager} for a specific ODSP file, wired to the real ODSP REST APIs.
78
+ */
79
+ export function createOdspVersionManager(props) {
80
+ return new OdspVersionManager(createOdspFileVersionFetcher(props));
81
+ }
82
+ //# sourceMappingURL=odspVersionManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"odspVersionManager.js","sourceRoot":"","sources":["../../src/odspVersionManager/odspVersionManager.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;GAMG;AAEH,OAAO,EACN,4BAA4B,GAE5B,MAAM,6BAA6B,CAAC;AA0ErC;;;;GAIG;AACH,MAAM,OAAO,kBAAkB;IAI9B,YAAoC,OAAgC;QAAhC,YAAO,GAAP,OAAO,CAAyB;QAFnD,iBAAY,GAAG,IAAI,GAAG,EAA2B,CAAC;IAEI,CAAC;IAEjE,OAAO;QACb,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;QAC/B,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;IAC3B,CAAC;IAEM,KAAK,CAAC,cAAc,CAAC,MAAc;QACzC,4FAA4F;QAC5F,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAC1C,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAErC,+FAA+F;QAC/F,iGAAiG;QACjG,4FAA4F;QAC5F,iGAAiG;QACjG,mEAAmE;QACnE,8FAA8F;QAC9F,IAAI,iBAAqC,CAAC;QAC1C,KAAK,MAAM,OAAO,IAAI,UAAU,EAAE,CAAC;YAClC,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAChE,iBAAiB;gBAChB,iBAAiB,KAAK,SAAS;oBAC9B,CAAC,CAAC,cAAc;oBAChB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;YAChD,IAAI,cAAc,IAAI,MAAM,EAAE,CAAC;gBAC9B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,GAAG,OAAO,EAAE,cAAc,EAAE,EAAE,CAAC;YAChE,CAAC;QACF,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,iBAAiB,EAAE,CAAC;IACrD,CAAC;IAEM,KAAK,CAAC,YAAY;QACxB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAC1C,kGAAkG;QAClG,2DAA2D;QAC3D,OAAO,OAAO,CAAC,GAAG,CACjB,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;YAChC,GAAG,OAAO;YACV,cAAc,EAAE,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC;SACxD,CAAC,CAAC,CACH,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,WAAW;QACxB,gGAAgG;QAChG,8FAA8F;QAC9F,IAAI,CAAC,aAAa,KAAK,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC;QACvD,OAAO,IAAI,CAAC,aAAa,CAAC;IAC3B,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,SAAiB;QACzC,8FAA8F;QAC9F,mFAAmF;QACnF,IAAI,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC/C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC3B,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,qBAAqB,CAAC,SAAS,CAAC,CAAC;YACxD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,OAAO,CAAC;IAChB,CAAC;CACD;AAED;;GAEG;AACH,MAAM,UAAU,wBAAwB,CACvC,KAAkC;IAElC,OAAO,IAAI,kBAAkB,CAAC,4BAA4B,CAAC,KAAK,CAAC,CAAC,CAAC;AACpE,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\n/**\n * Selects the ODSP file version whose snapshot sits at or before a target Fluid sequence number —\n * the base to load or replay from when materializing a document at a point in time.\n *\n * The selection logic depends on an injected {@link IOdspFileVersionFetcher}, so it is independent of\n * how versions are enumerated and resolved (real ODSP, a test double, or an alternative backend).\n */\n\nimport {\n\tcreateOdspFileVersionFetcher,\n\ttype OdspFileVersionFetcherProps,\n} from \"./odspFileVersionFetcher.js\";\n\n/**\n * A single ODSP file version, as listed by the file's version history.\n */\nexport interface OdspFileVersionRef {\n\t/**\n\t * The version's label (e.g. `\"42.0\"`), used to address the version when fetching it.\n\t */\n\treadonly versionId: string;\n\t/**\n\t * Last-modified timestamp of this version, ISO-8601.\n\t */\n\treadonly lastModifiedDateTime: string;\n}\n\n/**\n * An ODSP file version together with its resolved Fluid sequence number.\n */\nexport interface ResolvedVersion extends OdspFileVersionRef {\n\t/**\n\t * The Fluid sequence number the version's snapshot represents.\n\t */\n\treadonly sequenceNumber: number;\n}\n\n/**\n * Result of resolving the base version for a target sequence number.\n *\n * @remarks\n * There is intentionally no `targetIsLive` case: when the target is at/after the newest recoverable\n * version, the greatest version with `seq <= target` IS that newest version, so it is a normal\n * `found`. A consumer may separately choose to load the live file when the target is near the head.\n */\nexport type BaseForSeq =\n\t| {\n\t\t\t/** A recoverable version with `sequenceNumber <= target` was found. */\n\t\t\treadonly kind: \"found\";\n\t\t\treadonly base: ResolvedVersion;\n\t }\n\t| {\n\t\t\t/** No recoverable version has `sequenceNumber <= target` (target predates retained history). */\n\t\t\treadonly kind: \"noBaseVersion\";\n\t\t\t/** The oldest sequence number that was resolved while searching, if any. */\n\t\t\treadonly oldestResolvedSeq?: number;\n\t };\n\n/**\n * Provides a file's versions and resolves each version's Fluid sequence number. Injected into\n * the version manager so the selection logic does not depend on how versions are fetched.\n */\nexport interface IOdspFileVersionFetcher {\n\t/**\n\t * Enumerate the file's versions, newest-first.\n\t */\n\tlistFileVersions(): Promise<OdspFileVersionRef[]>;\n\t/**\n\t * Resolve a single version's Fluid sequence number. Throws on failure rather than returning a\n\t * wrong value.\n\t */\n\tresolveSequenceNumber(versionId: string): Promise<number>;\n}\n\n/**\n * Selects the file version to use as the base for loading or replaying to a target sequence number.\n */\nexport interface IOdspVersionManager {\n\t/**\n\t * Given a target sequence number, return the closest version at or before it (`found`), or\n\t * `noBaseVersion` if the target predates the oldest retained version.\n\t */\n\tfindBaseForSeq(target: number): Promise<BaseForSeq>;\n}\n\n/**\n * Default {@link IOdspVersionManager}. Caches the version list and resolved sequence numbers. The\n * resolution strategy (eager, newest-to-oldest, stopping at the first usable base) is hidden behind\n * {@link findBaseForSeq} and can change without affecting callers.\n */\nexport class OdspVersionManager implements IOdspVersionManager {\n\tprivate versionsCache: Promise<OdspFileVersionRef[]> | undefined;\n\tprivate readonly seqByVersion = new Map<string, Promise<number>>();\n\n\tpublic constructor(private readonly fetcher: IOdspFileVersionFetcher) {}\n\n\tpublic refresh(): void {\n\t\tthis.versionsCache = undefined;\n\t\tthis.seqByVersion.clear();\n\t}\n\n\tpublic async findBaseForSeq(target: number): Promise<BaseForSeq> {\n\t\t// Recoverable base candidates = every version except the tip (index 0 ≈ the live document).\n\t\tconst versions = await this.getVersions();\n\t\tconst candidates = versions.slice(1);\n\n\t\t// Versions are listed newest-first, and version order is expected to track sequence number, so\n\t\t// the first candidate whose seq is at or before the target is taken as the closest base. Because\n\t\t// any base at or before the target replays forward to the same state, this early stop is an\n\t\t// optimization, not a correctness requirement: if version order and sequence order ever diverge,\n\t\t// a base that is valid but not strictly the closest may be chosen.\n\t\t// Scanning newest-first also yields the newest of versions sharing a sequence number (dedup).\n\t\tlet oldestResolvedSeq: number | undefined;\n\t\tfor (const version of candidates) {\n\t\t\tconst sequenceNumber = await this.resolveSeq(version.versionId);\n\t\t\toldestResolvedSeq =\n\t\t\t\toldestResolvedSeq === undefined\n\t\t\t\t\t? sequenceNumber\n\t\t\t\t\t: Math.min(oldestResolvedSeq, sequenceNumber);\n\t\t\tif (sequenceNumber <= target) {\n\t\t\t\treturn { kind: \"found\", base: { ...version, sequenceNumber } };\n\t\t\t}\n\t\t}\n\t\treturn { kind: \"noBaseVersion\", oldestResolvedSeq };\n\t}\n\n\tpublic async listVersions(): Promise<ResolvedVersion[]> {\n\t\tconst versions = await this.getVersions();\n\t\t// Resolution order does not matter here, so resolve concurrently; the newest-first array order is\n\t\t// preserved by Promise.all regardless of completion order.\n\t\treturn Promise.all(\n\t\t\tversions.map(async (version) => ({\n\t\t\t\t...version,\n\t\t\t\tsequenceNumber: await this.resolveSeq(version.versionId),\n\t\t\t})),\n\t\t);\n\t}\n\n\tprivate async getVersions(): Promise<OdspFileVersionRef[]> {\n\t\t// Cache the pending promise, not the awaited value, so concurrent callers share one fetch and a\n\t\t// refresh() that runs while the fetch is in flight is not overwritten when the fetch settles.\n\t\tthis.versionsCache ??= this.fetcher.listFileVersions();\n\t\treturn this.versionsCache;\n\t}\n\n\tprivate async resolveSeq(versionId: string): Promise<number> {\n\t\t// Cache the pending promise (a version's sequence number never changes) so concurrent callers\n\t\t// coalesce and a refresh() is not clobbered by a fetch that was already in flight.\n\t\tlet pending = this.seqByVersion.get(versionId);\n\t\tif (pending === undefined) {\n\t\t\tpending = this.fetcher.resolveSequenceNumber(versionId);\n\t\t\tthis.seqByVersion.set(versionId, pending);\n\t\t}\n\t\treturn pending;\n\t}\n}\n\n/**\n * Create an {@link IOdspVersionManager} for a specific ODSP file, wired to the real ODSP REST APIs.\n */\nexport function createOdspVersionManager(\n\tprops: OdspFileVersionFetcherProps,\n): IOdspVersionManager {\n\treturn new OdspVersionManager(createOdspFileVersionFetcher(props));\n}\n"]}
@@ -5,5 +5,5 @@
5
5
  * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
6
6
  */
7
7
  export declare const pkgName = "@fluidframework/odsp-driver";
8
- export declare const pkgVersion = "2.111.0";
8
+ export declare const pkgVersion = "2.112.0";
9
9
  //# sourceMappingURL=packageVersion.d.ts.map
@@ -5,5 +5,5 @@
5
5
  * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
6
6
  */
7
7
  export const pkgName = "@fluidframework/odsp-driver";
8
- export const pkgVersion = "2.111.0";
8
+ export const pkgVersion = "2.112.0";
9
9
  //# sourceMappingURL=packageVersion.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"packageVersion.js","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,OAAO,GAAG,6BAA6B,CAAC;AACrD,MAAM,CAAC,MAAM,UAAU,GAAG,SAAS,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/odsp-driver\";\nexport const pkgVersion = \"2.111.0\";\n"]}
1
+ {"version":3,"file":"packageVersion.js","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,OAAO,GAAG,6BAA6B,CAAC;AACrD,MAAM,CAAC,MAAM,UAAU,GAAG,SAAS,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/odsp-driver\";\nexport const pkgVersion = \"2.112.0\";\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/odsp-driver",
3
- "version": "2.111.0",
3
+ "version": "2.112.0",
4
4
  "description": "Socket storage implementation for SPO and ODC",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -69,27 +69,27 @@
69
69
  "temp-directory": "nyc/.nyc_output"
70
70
  },
71
71
  "dependencies": {
72
- "@fluid-internal/client-utils": "~2.111.0",
73
- "@fluidframework/core-interfaces": "~2.111.0",
74
- "@fluidframework/core-utils": "~2.111.0",
75
- "@fluidframework/driver-base": "~2.111.0",
76
- "@fluidframework/driver-definitions": "~2.111.0",
77
- "@fluidframework/driver-utils": "~2.111.0",
78
- "@fluidframework/odsp-doclib-utils": "~2.111.0",
79
- "@fluidframework/odsp-driver-definitions": "~2.111.0",
80
- "@fluidframework/telemetry-utils": "~2.111.0",
72
+ "@fluid-internal/client-utils": "~2.112.0",
73
+ "@fluidframework/core-interfaces": "~2.112.0",
74
+ "@fluidframework/core-utils": "~2.112.0",
75
+ "@fluidframework/driver-base": "~2.112.0",
76
+ "@fluidframework/driver-definitions": "~2.112.0",
77
+ "@fluidframework/driver-utils": "~2.112.0",
78
+ "@fluidframework/odsp-doclib-utils": "~2.112.0",
79
+ "@fluidframework/odsp-driver-definitions": "~2.112.0",
80
+ "@fluidframework/telemetry-utils": "~2.112.0",
81
81
  "socket.io-client": "^4.8.3",
82
82
  "uuid": "^11.1.0"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@arethetypeswrong/cli": "^0.18.2",
86
86
  "@biomejs/biome": "~2.4.5",
87
- "@fluid-internal/mocha-test-setup": "~2.111.0",
87
+ "@fluid-internal/mocha-test-setup": "~2.112.0",
88
88
  "@fluid-tools/build-cli": "^0.65.0",
89
89
  "@fluidframework/build-common": "^2.0.3",
90
90
  "@fluidframework/build-tools": "^0.65.0",
91
91
  "@fluidframework/eslint-config-fluid": "^13.0.0",
92
- "@fluidframework/odsp-driver-previous": "npm:@fluidframework/odsp-driver@2.110.0",
92
+ "@fluidframework/odsp-driver-previous": "npm:@fluidframework/odsp-driver@2.111.0",
93
93
  "@microsoft/api-extractor": "7.58.1",
94
94
  "@types/mocha": "^10.0.10",
95
95
  "@types/node": "~22.19.17",
@@ -0,0 +1,277 @@
1
+ # ODSP Version History Load — Design Catechism
2
+
3
+ ## How to read this document
4
+
5
+ This spec is a **catechism**: a hierarchical series of questions and answers that is the prose
6
+ mirror of the test suite. Each topic-level question (`###`) corresponds to a `describe` block; each
7
+ leaf question maps to one test, linked by a stable **code ID** at the end of the answer. The same ID
8
+ appears as a `// @q <id>` tag on the matching test.
9
+
10
+ ID format: `<area>-<topic>-<nn>`. Area is `M` (the version manager — [Part II](#part-ii--the-version-manager))
11
+ or `F` (the file-version fetcher — [Part III](#part-iii--the-file-version-fetcher)). Topic is a short
12
+ mnemonic (e.g. `SELECT`, `RESOLVE`). `nn` is a zero-padded counter. IDs are append-only: existing IDs
13
+ never renumber, so links stay stable across edits.
14
+
15
+ The contract:
16
+
17
+ - **Every leaf Q&A has a test, and every test has a leaf Q&A**, joined by its code ID. A change to
18
+ behavior is a change to both.
19
+ - **This document reflects the code as it is now.** Future/aspirational work lives in
20
+ [Part IV — Directional](#part-iv--directional), written as questions that cannot yet be answered
21
+ "yes".
22
+
23
+ ### Terminology (legend)
24
+
25
+ This code is one piece of a larger capability — letting a document be viewed or recovered at an
26
+ earlier point in time — which spans two repositories:
27
+
28
+ - **Part 1 — point-in-time load** (this repository): given a target sequence number, produce a
29
+ read-only view of the document at that number.
30
+ - **Part 2 — capture & marker** (a separate host repository): record the markers that name the points
31
+ worth returning to. Out of scope for this document.
32
+
33
+ Part 1 is built in three components:
34
+
35
+ - **Component A — the version manager**: choose which file version to load or replay from. **This
36
+ folder is Component A**, and this document is mostly about it.
37
+ - **Component B — the recomposed driver**: load the chosen version and replay ops forward to the exact
38
+ target. Not built yet.
39
+ - **Component C — the loader hookup**: expose Component B through the container loader. Not built yet.
40
+
41
+ ## Part I — Foundations
42
+
43
+ These are conceptual answers with no single test; they frame everything below.
44
+
45
+ ### What problem does this code solve?
46
+
47
+ Given a target Fluid **sequence number** (Fluid numbers every change to a document: op 1, 2, 3, …),
48
+ we want to materialize the document as it was at that number. The first step is choosing a **base**:
49
+ the most recent saved version whose state is at or before the target, from which the remaining ops
50
+ can be replayed. This code finds that base.
51
+
52
+ ### What is an ODSP file version, and how is it different from a Fluid snapshot?
53
+
54
+ Two different things are both called a "version":
55
+
56
+ - A **file version** is an entry in the file's version history — a recoverable saved state of the whole
57
+ file, addressed by a label such as `"42.0"`. These are what a user could restore to.
58
+ - A **Fluid snapshot** is an internal checkpoint the runtime writes; the driver's snapshot list
59
+ (`getVersions`) enumerates these, not the file versions.
60
+
61
+ Selecting a base uses the **file version history**, enumerated by the driveItem `/versions` API — not
62
+ the driver's snapshot list.
63
+
64
+ ### Why the closest version at or before the target, rather than any earlier one?
65
+
66
+ Any base at or before the target can be replayed forward to the target and yields the same state, so
67
+ the choice is not about correctness. The **closest** one minimizes how many ops must be replayed, and
68
+ minimizes the chance that the needed ops have been trimmed from retention. Selection therefore aims for
69
+ the greatest version sequence number at or before the target. Because versions are enumerated
70
+ newest-first and version order is expected to track sequence order, an early-stop scan finds it; if that
71
+ ordering is ever violated, a valid but not-strictly-closest base may be chosen (still correct, just less
72
+ optimal) — see [Part IV](#part-iv--directional) for the planned order-tolerant search.
73
+
74
+ ### How is a version's sequence number obtained?
75
+
76
+ By fetching that version's snapshot from the **version-scoped snapshot endpoint**
77
+ (`.../versions/{label}/opStream/snapshots/trees/latest?blobs=2`), which returns the snapshot in the
78
+ driver's normal (`application/json` or `application/ms-fluid`) framing. The driver's existing snapshot
79
+ parser reads it, and the sequence number is `trees[0].sequenceNumber`. `blobs=2` inlines blob contents
80
+ so the parser has everything it needs.
81
+
82
+ ### What is deliberately not built here?
83
+
84
+ Loading the base and replaying ops to the exact target (Component B), the loader hookup (Component C),
85
+ and any test against a live ODSP file. See [Part IV](#part-iv--directional).
86
+
87
+ ## Part II — The Version Manager
88
+
89
+ `OdspVersionManager` selects the base version. It depends on an injected `IOdspFileVersionFetcher`, so
90
+ these behaviors are tested with an in-memory fake.
91
+
92
+ ### Which version does `findBaseForSeq` pick for a target sequence number?
93
+
94
+ The list is newest-first, and the tip (index 0, the live document) is not a base candidate. Among the
95
+ remaining versions, the answer is the closest one at or before the target — the greatest sequence number
96
+ at or before the target when version order tracks sequence order, which an early-stop newest-first scan
97
+ finds.
98
+
99
+ - **Target between two versions?** The closer, older one. `M-SELECT-01`
100
+ - **Target equal to a version?** That version, an exact match (zero ops to replay). `M-SELECT-02`
101
+ - **Target newer than every version?** The newest recoverable version. `M-SELECT-03`
102
+ - **Target older than every version?** `noBaseVersion`, reporting the oldest sequence number seen.
103
+ `M-SELECT-04`
104
+
105
+ ### How does it handle duplicate versions and the tip?
106
+
107
+ - **Two versions share a sequence number?** Return the newest label (a metadata-only re-save leaves the
108
+ sequence number unchanged; the newest is closest to the head). `M-DEDUP-01`
109
+ - **The tip (index 0)?** Never treated as a base; its sequence number is never even resolved.
110
+ `M-TIP-01`
111
+ - **Only the tip exists?** `noBaseVersion`. `M-TIP-02`
112
+ - **No versions at all?** `noBaseVersion`. `M-EMPTY-01`
113
+
114
+ ### What work does it avoid?
115
+
116
+ - **Resolving more versions than needed?** It stops at the first version at or before the target and
117
+ does not resolve older ones. `M-STOP-01`
118
+ - **Re-fetching across calls?** The version list and each resolved sequence number are cached.
119
+ `M-CACHE-01`
120
+ - **Stale caches?** `refresh()` drops both the version list and the resolved sequence numbers, so the
121
+ next query re-enumerates and re-resolves. `M-CACHE-02`
122
+ - **A `refresh()` while a fetch is still in flight?** The cache holds the pending fetch rather than its
123
+ eventual value, so a fetch that started before the refresh cannot write its now-stale result back over
124
+ the cleared cache; the next query re-fetches. `M-CACHE-03`
125
+
126
+ ### What happens when a version cannot be resolved?
127
+
128
+ The failure propagates; it is never swallowed into a wrong base. `M-ERR-01`
129
+
130
+ ### What does `listVersions` return?
131
+
132
+ Every version with its resolved sequence number, newest-first. `M-LIST-01`
133
+
134
+ ## Part III — The File-Version Fetcher
135
+
136
+ `createOdspFileVersionFetcher` is the real `IOdspFileVersionFetcher`, talking to ODSP. Its behaviors
137
+ are tested against a stubbed `fetch` that returns canned responses through the real request,
138
+ authentication, and snapshot-parsing code.
139
+
140
+ ### How does it enumerate versions?
141
+
142
+ It calls the driveItem versions URL — built from the same API root as the snapshot call — and maps the
143
+ `value` array of each page to versions (newest-first). `F-LIST-01` A long history is paged, so it follows
144
+ `@odata.nextLink` until it is absent and concatenates every page; a base version beyond the first page is
145
+ therefore still found rather than mistaken for `noBaseVersion`. `F-LIST-02` A response without a `value`
146
+ field yields an empty list rather than an error. `F-LIST-03`
147
+
148
+ ### How does it resolve a version's sequence number?
149
+
150
+ - **A well-formed snapshot?** It calls the version-scoped snapshot URL (`.../versions/{label}/opStream/snapshots/trees/latest?blobs=2`),
151
+ parses the response, and returns `trees[0].sequenceNumber`. `F-RESOLVE-01`
152
+ - **A snapshot with no sequence number?** It throws, naming the version, rather than returning a wrong
153
+ value. `F-RESOLVE-02`
154
+ - **A binary (`application/ms-fluid`) snapshot?** It reads it with the driver's compact-snapshot parser
155
+ and returns the same sequence number the JSON path would. `F-RESOLVE-03`
156
+
157
+ ### How does it handle request failures?
158
+
159
+ - **A non-success response while enumerating?** The failure propagates rather than being read as an
160
+ empty result. `F-ERROR-01`
161
+ - **A non-success response while resolving?** Likewise, it propagates rather than yielding a wrong value.
162
+ `F-ERROR-03`
163
+ - **An authentication failure while enumerating?** The shared token-refresh wrapper refreshes the token
164
+ and retries the request once. `F-ERROR-04`
165
+ - **An authentication failure while resolving?** Likewise, it refreshes the token and retries once.
166
+ `F-ERROR-02`
167
+
168
+ ## Part IV — Directional
169
+
170
+ Aspirational behaviors, written as questions that cannot yet be answered "yes".
171
+
172
+ ### Should sequence-number resolution be lazy or binary-search, rather than eager?
173
+
174
+ Resolving each version costs one snapshot fetch. With up to ~50 versions, an eager newest-to-oldest
175
+ scan can fetch more than necessary. The public contract (`findBaseForSeq`) already hides the strategy,
176
+ so a binary search over versions could replace it without changing callers.
177
+
178
+ The version list is effectively a sorted array: it is newest-first, and a version's sequence number is
179
+ monotonically non-increasing toward older versions (a newer version is a later state). That makes it
180
+ searchable for "the greatest sequence number at or before the target". The search must be "fuzzy" rather
181
+ than textbook, for two reasons: versions can share a sequence number (a metadata-only re-save leaves it
182
+ unchanged), so it is a sorted array with duplicates; and the ordering can have small local inversions.
183
+ The robust shape is therefore binary/interpolation to get close, then a short local walk (older if the
184
+ probe overshot the target, newer while still at or before it) to pin the exact base and absorb ties and
185
+ inversions.
186
+
187
+ Two further refinements reduce fetches. First, a version's sequence number never changes, so once
188
+ resolved it can be cached indefinitely; refreshing only needs to reconcile which versions still exist
189
+ (dropping ones that aged out), not re-resolve sequence numbers. Second, selection does not need the exact
190
+ closest version — any version within a bounded number of ops of the target is "close enough", because the
191
+ recomposed driver replays the remaining ops anyway; a tolerance lets the search stop early.
192
+
193
+ ### Could the version list's `lastModifiedDateTime` seed the search?
194
+
195
+ Each version carries a `lastModifiedDateTime` in the list response, for free — unlike a sequence number,
196
+ which costs a fetch to resolve. If the target is accompanied by a wall-clock time (for example, a time
197
+ recorded when a mark was made), that timestamp does not replace the search — it replaces its **first
198
+ probe**. Instead of starting at the blind midpoint, seed at the newest version whose
199
+ `lastModifiedDateTime` is at or before the target time (a comparison over the already-fetched list, zero
200
+ fetches), then converge:
201
+
202
+ 1. Resolve the seed version's sequence number (the first fetch).
203
+ 2. If it overshot the target (`seq > target`), step toward older versions; if it is at or before the
204
+ target, step toward newer versions while still at or before it — to land on the greatest sequence
205
+ number at or before the target.
206
+ 3. Because time, list order, and sequence number all move together, this correction is usually zero or
207
+ one step. If the seed is far off (large clock drift), fall back to binary search over the residual
208
+ interval, bounding the worst case at ~log N.
209
+
210
+ The timestamp is only a seed, never the answer: time does not map linearly to sequence number (edits are
211
+ bursty) and clocks can skew, so the neighbourhood it points to must still be pinned by resolving sequence
212
+ numbers. Timestamps are ISO-8601 UTC; any caller-supplied time must be normalized to UTC before
213
+ comparison. It also allows locating a version by time when no sequence number is available. This is why
214
+ `lastModifiedDateTime` is carried on a version even though base selection itself does not use it today.
215
+
216
+
217
+
218
+ ### Should there be a component that loads the base and replays ops to the exact target? (Component B)
219
+
220
+ The manager only chooses the base. Materializing the document at an arbitrary target requires loading
221
+ that version read-only and replaying the ops between the base and the target — sourcing later ops from
222
+ the live op stream when the historical range has been trimmed.
223
+
224
+ Where would it live? In **this package**. Loading a historical file version is a storage-layer concern:
225
+ it needs the version-scoped snapshot fetch, the epoch tracker, and authentication — all internal to this
226
+ driver — and it consumes the version manager directly. A generic wrapping driver that only replays ops
227
+ over an inner document service (the pattern `@fluidframework/replay-driver` uses) cannot reach the
228
+ version-scoped base fetch, so the recomposition belongs beside `OdspDocumentService` /
229
+ `OdspDocumentServiceFactory` rather than in a separate package. Because it consumes the version manager
230
+ in-package, the version manager itself needs no exported surface; only the recomposed factory is exposed
231
+ (defaulting to an internal entry point) for the loader hookup to construct.
232
+
233
+ A base is only needed when the live document's own snapshot no longer covers the target; when it does,
234
+ loading paused at the target from the live snapshot suffices, and no historical version is loaded.
235
+
236
+ ### How would Component B reach a target between snapshots, and handle trimmed ops?
237
+
238
+ A snapshot already contains the full accumulated state at its sequence number — every op at or below it
239
+ is baked in. So to reach a target `T`, Component B loads the closest base snapshot (`seq ≤ T`) and
240
+ replays only the ops in `(base, T]` on top of it. Those ops come from the op stream (delta storage), and
241
+ may also be bundled with a snapshot (the `deltas=1` query parameter, deliberately omitted here because
242
+ the manager only needs the sequence number, not the ops).
243
+
244
+ Ops in the op stream are retained for a window and can be trimmed. The resolution is not to fetch the
245
+ trimmed ops from somewhere else — it is to **start from a newer snapshot that already absorbed them**. If
246
+ the ops just after the base are gone but another snapshot exists later in `(base, T]`, that snapshot's
247
+ state already includes the trimmed ops, so Component B starts there and replays only the retained tail.
248
+ Trimmed ops are never re-fetched; a later snapshot makes them unnecessary.
249
+
250
+ The target is only unreachable when all of the following hold: the nearest snapshot at or before `T` is
251
+ old, the ops between it and `T` have been trimmed, and no snapshot falls anywhere in between to bridge
252
+ the gap. In that case the exact state at `T` cannot be reconstructed, and Component B reports it
253
+ (for example, a `missing ops` / not-materializable outcome) rather than returning a wrong state — a
254
+ consumer may still choose to fall back to the nearest reachable state at or before `T`. This is rare in
255
+ practice because snapshots are written frequently relative to the op-retention window.
256
+
257
+ Note that `minimumSequenceNumber` is not the signal for any of this: it is the collaboration-window floor
258
+ baked into a snapshot, used when a snapshot is loaded, not an indicator of which ops the op stream still
259
+ retains. Op availability is determined by asking the op stream for the range, not by a version's minimum
260
+ sequence number.
261
+
262
+ ### Should this be exposed through the container loader? (Component C)
263
+
264
+ Once Component B exists, a thin combining layer would construct the recomposed factory together with a
265
+ standard loader, letting callers request "load at sequence number N" directly. It depends only on
266
+ Component B's exposed factory, never on the version manager.
267
+
268
+ ### Should there be an end-to-end test against a real ODSP file?
269
+
270
+ The fetcher is covered by stubbed-`fetch` integration tests, but not against a live file (which needs
271
+ tenant credentials). An end-to-end test would exercise the real endpoints.
272
+
273
+ ### Should the raw driveItem `/content` download be a supported fallback?
274
+
275
+ The `/content` download also contains a version's snapshot, but wrapped in a container framing the
276
+ snapshot parser does not read directly. If the version-scoped snapshot endpoint is ever unavailable,
277
+ unwrapping `/content` could be a fallback path.
@@ -0,0 +1,12 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ export {
7
+ createOdspVersionManager,
8
+ type BaseForSeq,
9
+ type IOdspVersionManager,
10
+ type OdspFileVersionRef,
11
+ type ResolvedVersion,
12
+ } from "./odspVersionManager.js";