@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.
- package/CHANGELOG.md +4 -0
- package/dist/odspVersionManager/index.d.ts +6 -0
- package/dist/odspVersionManager/index.d.ts.map +1 -0
- package/dist/odspVersionManager/index.js +10 -0
- package/dist/odspVersionManager/index.js.map +1 -0
- package/dist/odspVersionManager/odspFileVersionFetcher.d.ts +22 -0
- package/dist/odspVersionManager/odspFileVersionFetcher.d.ts.map +1 -0
- package/dist/odspVersionManager/odspFileVersionFetcher.js +81 -0
- package/dist/odspVersionManager/odspFileVersionFetcher.js.map +1 -0
- package/dist/odspVersionManager/odspVersionManager.d.ts +98 -0
- package/dist/odspVersionManager/odspVersionManager.d.ts.map +1 -0
- package/dist/odspVersionManager/odspVersionManager.js +87 -0
- package/dist/odspVersionManager/odspVersionManager.js.map +1 -0
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/lib/odspVersionManager/index.d.ts +6 -0
- package/lib/odspVersionManager/index.d.ts.map +1 -0
- package/lib/odspVersionManager/index.js +6 -0
- package/lib/odspVersionManager/index.js.map +1 -0
- package/lib/odspVersionManager/odspFileVersionFetcher.d.ts +22 -0
- package/lib/odspVersionManager/odspFileVersionFetcher.d.ts.map +1 -0
- package/lib/odspVersionManager/odspFileVersionFetcher.js +77 -0
- package/lib/odspVersionManager/odspFileVersionFetcher.js.map +1 -0
- package/lib/odspVersionManager/odspVersionManager.d.ts +98 -0
- package/lib/odspVersionManager/odspVersionManager.d.ts.map +1 -0
- package/lib/odspVersionManager/odspVersionManager.js +82 -0
- package/lib/odspVersionManager/odspVersionManager.js.map +1 -0
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/package.json +12 -12
- package/src/odspVersionManager/DEV.md +277 -0
- package/src/odspVersionManager/index.ts +12 -0
- package/src/odspVersionManager/odspFileVersionFetcher.ts +138 -0
- package/src/odspVersionManager/odspVersionManager.ts +170 -0
- 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"]}
|
package/lib/packageVersion.d.ts
CHANGED
|
@@ -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.
|
|
8
|
+
export declare const pkgVersion = "2.112.0";
|
|
9
9
|
//# sourceMappingURL=packageVersion.d.ts.map
|
package/lib/packageVersion.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
73
|
-
"@fluidframework/core-interfaces": "~2.
|
|
74
|
-
"@fluidframework/core-utils": "~2.
|
|
75
|
-
"@fluidframework/driver-base": "~2.
|
|
76
|
-
"@fluidframework/driver-definitions": "~2.
|
|
77
|
-
"@fluidframework/driver-utils": "~2.
|
|
78
|
-
"@fluidframework/odsp-doclib-utils": "~2.
|
|
79
|
-
"@fluidframework/odsp-driver-definitions": "~2.
|
|
80
|
-
"@fluidframework/telemetry-utils": "~2.
|
|
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.
|
|
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.
|
|
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";
|