@fluidframework/odsp-driver 2.113.1 → 2.115.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 +8 -0
- package/api-extractor/api-extractor-lint-legacyAlpha.cjs.json +5 -0
- package/api-extractor/api-extractor-lint-legacyAlpha.esm.json +5 -0
- package/api-extractor/api-extractor.legacy.json +5 -1
- package/api-report/odsp-driver.legacy.alpha.api.md +232 -0
- package/dist/getUrlAndHeadersWithAuth.d.ts +4 -0
- package/dist/getUrlAndHeadersWithAuth.d.ts.map +1 -1
- package/dist/getUrlAndHeadersWithAuth.js +4 -0
- package/dist/getUrlAndHeadersWithAuth.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/legacy.d.ts +1 -1
- package/dist/legacyAlpha.d.ts +49 -0
- package/dist/odspVersionManager/odspFileVersionFetcher.d.ts +40 -1
- package/dist/odspVersionManager/odspFileVersionFetcher.d.ts.map +1 -1
- package/dist/odspVersionManager/odspFileVersionFetcher.js +52 -14
- package/dist/odspVersionManager/odspFileVersionFetcher.js.map +1 -1
- package/dist/odspVersionManager/odspVersionManager.d.ts +23 -45
- package/dist/odspVersionManager/odspVersionManager.d.ts.map +1 -1
- package/dist/odspVersionManager/odspVersionManager.js +68 -33
- package/dist/odspVersionManager/odspVersionManager.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/pointInTimeDriver/odspPointInTimeDocumentService.d.ts +5 -0
- package/dist/pointInTimeDriver/odspPointInTimeDocumentService.d.ts.map +1 -1
- package/dist/pointInTimeDriver/odspPointInTimeDocumentService.js +6 -3
- package/dist/pointInTimeDriver/odspPointInTimeDocumentService.js.map +1 -1
- package/dist/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.d.ts +25 -33
- package/dist/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.d.ts.map +1 -1
- package/dist/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.js +62 -38
- package/dist/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.js.map +1 -1
- package/dist/public.d.ts +1 -1
- package/internal.d.ts +1 -1
- package/legacy/alpha.d.ts +11 -0
- package/legacy.d.ts +1 -1
- package/lib/getUrlAndHeadersWithAuth.d.ts +4 -0
- package/lib/getUrlAndHeadersWithAuth.d.ts.map +1 -1
- package/lib/getUrlAndHeadersWithAuth.js +4 -0
- package/lib/getUrlAndHeadersWithAuth.js.map +1 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +5 -3
- package/lib/index.js.map +1 -1
- package/lib/legacy.d.ts +1 -1
- package/lib/legacyAlpha.d.ts +49 -0
- package/lib/odspVersionManager/odspFileVersionFetcher.d.ts +40 -1
- package/lib/odspVersionManager/odspFileVersionFetcher.d.ts.map +1 -1
- package/lib/odspVersionManager/odspFileVersionFetcher.js +53 -15
- package/lib/odspVersionManager/odspFileVersionFetcher.js.map +1 -1
- package/lib/odspVersionManager/odspVersionManager.d.ts +23 -45
- package/lib/odspVersionManager/odspVersionManager.d.ts.map +1 -1
- package/lib/odspVersionManager/odspVersionManager.js +68 -33
- package/lib/odspVersionManager/odspVersionManager.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pointInTimeDriver/odspPointInTimeDocumentService.d.ts +5 -0
- package/lib/pointInTimeDriver/odspPointInTimeDocumentService.d.ts.map +1 -1
- package/lib/pointInTimeDriver/odspPointInTimeDocumentService.js +6 -3
- package/lib/pointInTimeDriver/odspPointInTimeDocumentService.js.map +1 -1
- package/lib/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.d.ts +25 -33
- package/lib/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.d.ts.map +1 -1
- package/lib/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.js +62 -38
- package/lib/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.js.map +1 -1
- package/lib/public.d.ts +1 -1
- package/package.json +29 -17
- package/src/getUrlAndHeadersWithAuth.ts +4 -0
- package/src/index.ts +8 -3
- package/src/odspVersionManager/DEV.md +376 -54
- package/src/odspVersionManager/odspFileVersionFetcher.ts +111 -16
- package/src/odspVersionManager/odspVersionManager.ts +106 -68
- package/src/packageVersion.ts +1 -1
- package/src/pointInTimeDriver/odspPointInTimeDocumentService.ts +8 -4
- package/src/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.ts +123 -48
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
* - GET /_api/v2.1/.../versions -- enumerate the file's versions.
|
|
9
9
|
* - GET /_api/v2.1/.../versions/{label}/opStream/snapshots/trees/latest?blobs=2 -- fetch a version's
|
|
10
10
|
* snapshot and read its sequence number, parsed with the driver's snapshot parser.
|
|
11
|
+
* - GET /_api/v2.1/.../[versions/{label}/]opStream/snapshots/trees/latest?blobs=0 -- read a version's
|
|
12
|
+
* or the live document's ODSP epoch (`x-fluid-epoch`) to compare their lineage.
|
|
11
13
|
*/
|
|
12
14
|
|
|
15
|
+
import { NonRetryableError } from "@fluidframework/driver-utils/internal";
|
|
16
|
+
import { OdspErrorTypes } from "@fluidframework/odsp-driver-definitions/internal";
|
|
13
17
|
import type {
|
|
14
18
|
IOdspUrlParts,
|
|
15
19
|
InstrumentedStorageTokenFetcher,
|
|
@@ -22,9 +26,50 @@ import type { EpochTracker } from "../epochTracker.js";
|
|
|
22
26
|
import { getHeadersWithAuth } from "../getUrlAndHeadersWithAuth.js";
|
|
23
27
|
import { convertOdspSnapshotToSnapshotTreeAndBlobs } from "../odspSnapshotParser.js";
|
|
24
28
|
import { getApiRoot } from "../odspUrlHelper.js";
|
|
25
|
-
import { getWithRetryForTokenRefresh } from "../odspUtils.js";
|
|
29
|
+
import { fetchArray, getWithRetryForTokenRefresh } from "../odspUtils.js";
|
|
30
|
+
import { pkgVersion as driverVersion } from "../packageVersion.js";
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
/**
|
|
33
|
+
* A single ODSP file version, as listed by the file's version history.
|
|
34
|
+
*/
|
|
35
|
+
export interface OdspFileVersionRef {
|
|
36
|
+
/**
|
|
37
|
+
* The version's label (e.g. `"42.0"`), used to address the version when fetching it.
|
|
38
|
+
*/
|
|
39
|
+
readonly versionId: string;
|
|
40
|
+
/**
|
|
41
|
+
* Last-modified timestamp of this version, ISO-8601.
|
|
42
|
+
*/
|
|
43
|
+
readonly lastModifiedDateTime: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Provides a file's versions and resolves each version's Fluid sequence number. Injected into
|
|
48
|
+
* the version manager so the selection logic does not depend on how versions are fetched.
|
|
49
|
+
*/
|
|
50
|
+
export interface IOdspFileVersionFetcher {
|
|
51
|
+
/**
|
|
52
|
+
* Enumerate the file's versions, newest-first.
|
|
53
|
+
*/
|
|
54
|
+
listFileVersions(): Promise<OdspFileVersionRef[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a single version's Fluid sequence number. Throws on failure rather than returning a
|
|
57
|
+
* wrong value.
|
|
58
|
+
*/
|
|
59
|
+
resolveSequenceNumber(versionId: string): Promise<number>;
|
|
60
|
+
/**
|
|
61
|
+
* Read the live document's current ODSP epoch (`x-fluid-epoch`), or `undefined`. Epoch identifies
|
|
62
|
+
* the file's binary lineage and changes on a version restore or download-then-reupload; compared
|
|
63
|
+
* with {@link IOdspFileVersionFetcher.getRecoverableVersionEpoch} to confirm a base is on the live
|
|
64
|
+
* document's lineage.
|
|
65
|
+
*/
|
|
66
|
+
getLiveDocumentEpoch(): Promise<string | undefined>;
|
|
67
|
+
/**
|
|
68
|
+
* Read the ODSP epoch of a specific file version, or `undefined`. See
|
|
69
|
+
* {@link IOdspFileVersionFetcher.getLiveDocumentEpoch}.
|
|
70
|
+
*/
|
|
71
|
+
getRecoverableVersionEpoch(versionId: string): Promise<string | undefined>;
|
|
72
|
+
}
|
|
28
73
|
|
|
29
74
|
/**
|
|
30
75
|
* Raw shape of a OneDrive/SharePoint driveItem version (an entry in the `/versions` response).
|
|
@@ -95,10 +140,8 @@ export function createOdspFileVersionFetcher(
|
|
|
95
140
|
|
|
96
141
|
const resolveSequenceNumber = async (versionId: string): Promise<number> =>
|
|
97
142
|
getWithRetryForTokenRefresh(async (options) => {
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
// blob (which carries the sequence number) is included; `deltas=1` is intentionally omitted, as
|
|
101
|
-
// it would bundle the op stream and its op-level sequence numbers.
|
|
143
|
+
// The sequence number lives in the version snapshot's `.protocol/attributes` blob, so fetch the
|
|
144
|
+
// version-scoped snapshot with `blobs=2` to inline it. No op stream needed.
|
|
102
145
|
const url = `${getApiRoot(new URL(siteUrl))}/drives/${driveId}/items/${itemId}/versions/${encodeURIComponent(
|
|
103
146
|
versionId,
|
|
104
147
|
)}/opStream/snapshots/trees/latest?blobs=2`;
|
|
@@ -108,10 +151,8 @@ export function createOdspFileVersionFetcher(
|
|
|
108
151
|
"FileVersionSnapshot",
|
|
109
152
|
);
|
|
110
153
|
const headers = getHeadersWithAuth(token);
|
|
111
|
-
// The
|
|
112
|
-
//
|
|
113
|
-
// binary format version (as the driver's own snapshot fetch does) so the server cannot hand back
|
|
114
|
-
// a binary version this code's parser does not understand.
|
|
154
|
+
// The snapshot comes back as JSON or "ms-fluid" (ODSP's compact binary form). Accept both and
|
|
155
|
+
// pin the binary version (as the driver's snapshot fetch does) so the parser can read it.
|
|
115
156
|
headers.accept = `application/json, application/ms-fluid; v=${currentReadVersion}`;
|
|
116
157
|
const response = await epochTracker.fetch(url, { method, headers }, "treesLatest");
|
|
117
158
|
const contentType = response.headers.get("content-type") ?? "";
|
|
@@ -121,18 +162,72 @@ export function createOdspFileVersionFetcher(
|
|
|
121
162
|
const snapshotJson = (await response.content.json()) as IOdspSnapshot;
|
|
122
163
|
sequenceNumber =
|
|
123
164
|
convertOdspSnapshotToSnapshotTreeAndBlobs(snapshotJson).sequenceNumber;
|
|
124
|
-
} else {
|
|
165
|
+
} else if (contentType.includes("application/ms-fluid")) {
|
|
125
166
|
// ms-fluid framing: the compact binary form; read it with the driver's compact-snapshot parser.
|
|
126
167
|
const bytes = new Uint8Array(await response.content.arrayBuffer());
|
|
127
168
|
sequenceNumber = parseCompactSnapshotResponse(bytes, logger).sequenceNumber;
|
|
169
|
+
} else {
|
|
170
|
+
// Neither framing (e.g. an HTML error page). Throw the driver's typed bad-response error
|
|
171
|
+
// (like fetchSnapshot.ts): canRetry=false stops the loader re-driving, while the
|
|
172
|
+
// incorrectServerResponse errorType still earns one wire-retry from getWithRetryForTokenRefresh.
|
|
173
|
+
throw new NonRetryableError(
|
|
174
|
+
`ODSP file version ${versionId} snapshot returned an unexpected content-type`,
|
|
175
|
+
OdspErrorTypes.incorrectServerResponse,
|
|
176
|
+
{ driverVersion, contentType, accept: headers.accept },
|
|
177
|
+
);
|
|
128
178
|
}
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
if (
|
|
132
|
-
|
|
179
|
+
// The sequence number must be a non-negative integer; a missing or malformed one throws the same
|
|
180
|
+
// typed error as above rather than feeding a wrong value into base selection.
|
|
181
|
+
if (
|
|
182
|
+
!(
|
|
183
|
+
typeof sequenceNumber === "number" &&
|
|
184
|
+
Number.isInteger(sequenceNumber) &&
|
|
185
|
+
sequenceNumber >= 0
|
|
186
|
+
)
|
|
187
|
+
) {
|
|
188
|
+
throw new NonRetryableError(
|
|
189
|
+
`ODSP file version ${versionId} snapshot has a missing or invalid sequenceNumber (${String(sequenceNumber)})`,
|
|
190
|
+
OdspErrorTypes.incorrectServerResponse,
|
|
191
|
+
{ driverVersion, contentType, accept: headers.accept },
|
|
192
|
+
);
|
|
133
193
|
}
|
|
134
194
|
return sequenceNumber;
|
|
135
195
|
});
|
|
136
196
|
|
|
137
|
-
|
|
197
|
+
const itemRoot = `${getApiRoot(new URL(siteUrl))}/drives/${driveId}/items/${itemId}`;
|
|
198
|
+
|
|
199
|
+
// Reads the `x-fluid-epoch` header from `url`. Deliberately uses the raw fetch helper instead of
|
|
200
|
+
// `epochTracker.fetch`: the whole point is to COMPARE the base version's epoch against the live
|
|
201
|
+
// document's epoch, but the shared EpochTracker pins to the first epoch it sees and throws on the
|
|
202
|
+
// second (divergent) read - so it could never yield two epochs to compare. `fetchArray` also lets
|
|
203
|
+
// the body (JSON or ms-fluid binary) be consumed and discarded; only the header is needed.
|
|
204
|
+
const readEpoch = async (url: string, scenarioName: string): Promise<string | undefined> =>
|
|
205
|
+
getWithRetryForTokenRefresh(async (options) => {
|
|
206
|
+
const method = "GET";
|
|
207
|
+
const token = await getAuthHeader(
|
|
208
|
+
{ ...options, request: { url, method } },
|
|
209
|
+
scenarioName,
|
|
210
|
+
);
|
|
211
|
+
const headers = getHeadersWithAuth(token);
|
|
212
|
+
const response = await fetchArray(url, { method, headers });
|
|
213
|
+
return response.headers.get("x-fluid-epoch") ?? undefined;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const getLiveDocumentEpoch = async (): Promise<string | undefined> =>
|
|
217
|
+
// The (unversioned) live snapshot endpoint is a current-file read, so its epoch is the live
|
|
218
|
+
// document's epoch. `blobs=0` keeps the response to the tree metadata.
|
|
219
|
+
readEpoch(`${itemRoot}/opStream/snapshots/trees/latest?blobs=0`, "LiveEpoch");
|
|
220
|
+
|
|
221
|
+
const getRecoverableVersionEpoch = async (versionId: string): Promise<string | undefined> =>
|
|
222
|
+
readEpoch(
|
|
223
|
+
`${itemRoot}/versions/${encodeURIComponent(versionId)}/opStream/snapshots/trees/latest?blobs=0`,
|
|
224
|
+
"FileVersionEpoch",
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
listFileVersions,
|
|
229
|
+
resolveSequenceNumber,
|
|
230
|
+
getLiveDocumentEpoch,
|
|
231
|
+
getRecoverableVersionEpoch,
|
|
232
|
+
};
|
|
138
233
|
}
|
|
@@ -11,24 +11,23 @@
|
|
|
11
11
|
* how versions are enumerated and resolved (real ODSP, a test double, or an alternative backend).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { PromiseCache } from "@fluidframework/core-utils/internal";
|
|
15
|
+
import { NonRetryableError } from "@fluidframework/driver-utils/internal";
|
|
16
|
+
import { OdspErrorTypes } from "@fluidframework/odsp-driver-definitions/internal";
|
|
17
|
+
|
|
18
|
+
import { pkgVersion as driverVersion } from "../packageVersion.js";
|
|
19
|
+
|
|
14
20
|
import {
|
|
15
21
|
createOdspFileVersionFetcher,
|
|
16
22
|
type OdspFileVersionFetcherProps,
|
|
23
|
+
type OdspFileVersionRef,
|
|
24
|
+
type IOdspFileVersionFetcher,
|
|
17
25
|
} from "./odspFileVersionFetcher.js";
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export
|
|
23
|
-
/**
|
|
24
|
-
* The version's label (e.g. `"42.0"`), used to address the version when fetching it.
|
|
25
|
-
*/
|
|
26
|
-
readonly versionId: string;
|
|
27
|
-
/**
|
|
28
|
-
* Last-modified timestamp of this version, ISO-8601.
|
|
29
|
-
*/
|
|
30
|
-
readonly lastModifiedDateTime: string;
|
|
31
|
-
}
|
|
27
|
+
// Re-exported so consumers (and this module's own index) can keep importing these fetcher-owned
|
|
28
|
+
// types from the version manager. The definitions live in odspFileVersionFetcher.ts so that file
|
|
29
|
+
// does not depend on this one, avoiding a circular dependency between the two modules.
|
|
30
|
+
export type { OdspFileVersionRef, IOdspFileVersionFetcher } from "./odspFileVersionFetcher.js";
|
|
32
31
|
|
|
33
32
|
/**
|
|
34
33
|
* An ODSP file version together with its resolved Fluid sequence number.
|
|
@@ -44,9 +43,11 @@ export interface ResolvedVersion extends OdspFileVersionRef {
|
|
|
44
43
|
* Result of resolving the base version for a target sequence number.
|
|
45
44
|
*
|
|
46
45
|
* @remarks
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
46
|
+
* The tip (newest) version is excluded from base selection, so when the target is at or after the head
|
|
47
|
+
* the base is the newest *sealed* version with `seq <= target` (a normal `found`); if the file's only
|
|
48
|
+
* version is the tip, the result is `noBaseVersion`. The wired consumer surfaces `noBaseVersion` as a
|
|
49
|
+
* `UsageError`; loading the live file for a near-head target is a possible future consumer choice, not
|
|
50
|
+
* current behavior.
|
|
50
51
|
*/
|
|
51
52
|
export type BaseForSeq =
|
|
52
53
|
| {
|
|
@@ -55,28 +56,15 @@ export type BaseForSeq =
|
|
|
55
56
|
readonly base: ResolvedVersion;
|
|
56
57
|
}
|
|
57
58
|
| {
|
|
58
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* No sealed version has `sequenceNumber <= target` — the target predates retained history, or
|
|
61
|
+
* the only version is the excluded tip.
|
|
62
|
+
*/
|
|
59
63
|
readonly kind: "noBaseVersion";
|
|
60
64
|
/** The oldest sequence number that was resolved while searching, if any. */
|
|
61
65
|
readonly oldestResolvedSeq?: number;
|
|
62
66
|
};
|
|
63
67
|
|
|
64
|
-
/**
|
|
65
|
-
* Provides a file's versions and resolves each version's Fluid sequence number. Injected into
|
|
66
|
-
* the version manager so the selection logic does not depend on how versions are fetched.
|
|
67
|
-
*/
|
|
68
|
-
export interface IOdspFileVersionFetcher {
|
|
69
|
-
/**
|
|
70
|
-
* Enumerate the file's versions, newest-first.
|
|
71
|
-
*/
|
|
72
|
-
listFileVersions(): Promise<OdspFileVersionRef[]>;
|
|
73
|
-
/**
|
|
74
|
-
* Resolve a single version's Fluid sequence number. Throws on failure rather than returning a
|
|
75
|
-
* wrong value.
|
|
76
|
-
*/
|
|
77
|
-
resolveSequenceNumber(versionId: string): Promise<number>;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
68
|
/**
|
|
81
69
|
* Selects the file version to use as the base for loading or replaying to a target sequence number.
|
|
82
70
|
*/
|
|
@@ -84,37 +72,44 @@ export interface IOdspVersionManager {
|
|
|
84
72
|
/**
|
|
85
73
|
* Given a target sequence number, return the closest version at or before it (`found`), or
|
|
86
74
|
* `noBaseVersion` if the target predates the oldest retained version.
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* A `found` base is guaranteed to share the live document's ODSP epoch (lineage): before returning
|
|
78
|
+
* it, the chosen base's epoch is compared with the live document's, and a mismatch throws a non-retryable error
|
|
79
|
+
* rather than returning a base that cannot be replayed. Op availability is enforced separately and
|
|
80
|
+
* lazily as the loader reads the bridging ops.
|
|
87
81
|
*/
|
|
88
82
|
findBaseForSeq(target: number): Promise<BaseForSeq>;
|
|
89
83
|
}
|
|
90
84
|
|
|
91
85
|
/**
|
|
92
|
-
* Default {@link IOdspVersionManager}. Caches
|
|
86
|
+
* Default {@link IOdspVersionManager}. Caches resolved sequence numbers (which never change); the version
|
|
87
|
+
* list is re-enumerated on each query rather than cached, since new versions are cut over time. The
|
|
93
88
|
* resolution strategy (eager, newest-to-oldest, stopping at the first usable base) is hidden behind
|
|
94
89
|
* {@link findBaseForSeq} and can change without affecting callers.
|
|
95
90
|
*/
|
|
91
|
+
// Exported only so the same-package tests can construct it with a fake IOdspFileVersionFetcher.
|
|
92
|
+
// Deliberately kept out of the folder barrel and the package public index, so it is not public API.
|
|
96
93
|
export class OdspVersionManager implements IOdspVersionManager {
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
// Sealed versions' sequence numbers, memoized so each is resolved at most once per manager instance
|
|
95
|
+
// (a sealed version's number is fixed once the version exists).
|
|
96
|
+
private readonly seqCache = new PromiseCache<string, number>();
|
|
97
|
+
// Sealed versions' ODSP epochs, memoized like their sequence numbers. The live document's epoch is NOT
|
|
98
|
+
// cached — it can change (restore/reupload), so validateLineageEpoch always reads it fresh.
|
|
99
|
+
private readonly epochCache = new PromiseCache<string, string | undefined>();
|
|
99
100
|
|
|
100
101
|
public constructor(private readonly fetcher: IOdspFileVersionFetcher) {}
|
|
101
102
|
|
|
102
|
-
public refresh(): void {
|
|
103
|
-
this.versionsCache = undefined;
|
|
104
|
-
this.seqByVersion.clear();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
103
|
public async findBaseForSeq(target: number): Promise<BaseForSeq> {
|
|
108
|
-
//
|
|
109
|
-
const versions = await this.
|
|
104
|
+
// Re-enumerate the list each call (it changes as new versions are cut).
|
|
105
|
+
const versions = await this.fetcher.listFileVersions();
|
|
106
|
+
|
|
107
|
+
// Start past the tip (index 0): the newest version's sequence number can still advance until a newer
|
|
108
|
+
// version is cut, so it is treated as the live head rather than a stable base. Scan the remaining
|
|
109
|
+
// (sealed) versions newest-first and return the first with sequence number <= target — the closest
|
|
110
|
+
// base — or noBaseVersion, reporting the oldest sequence number seen.
|
|
110
111
|
const candidates = versions.slice(1);
|
|
111
112
|
|
|
112
|
-
// Versions are listed newest-first, and version order is expected to track sequence number, so
|
|
113
|
-
// the first candidate whose seq is at or before the target is taken as the closest base. Because
|
|
114
|
-
// any base at or before the target replays forward to the same state, this early stop is an
|
|
115
|
-
// optimization, not a correctness requirement: if version order and sequence order ever diverge,
|
|
116
|
-
// a base that is valid but not strictly the closest may be chosen.
|
|
117
|
-
// Scanning newest-first also yields the newest of versions sharing a sequence number (dedup).
|
|
118
113
|
let oldestResolvedSeq: number | undefined;
|
|
119
114
|
for (const version of candidates) {
|
|
120
115
|
const sequenceNumber = await this.resolveSeq(version.versionId);
|
|
@@ -123,40 +118,83 @@ export class OdspVersionManager implements IOdspVersionManager {
|
|
|
123
118
|
? sequenceNumber
|
|
124
119
|
: Math.min(oldestResolvedSeq, sequenceNumber);
|
|
125
120
|
if (sequenceNumber <= target) {
|
|
126
|
-
|
|
121
|
+
const base = { ...version, sequenceNumber };
|
|
122
|
+
// Confirm the chosen base shares the live document's lineage before handing it back
|
|
123
|
+
await this.validateLineageEpoch(base);
|
|
124
|
+
return { kind: "found", base };
|
|
127
125
|
}
|
|
128
126
|
}
|
|
129
127
|
return { kind: "noBaseVersion", oldestResolvedSeq };
|
|
130
128
|
}
|
|
131
129
|
|
|
130
|
+
private async validateLineageEpoch(base: ResolvedVersion): Promise<void> {
|
|
131
|
+
// The live document's epoch can change (a restore or download-and-reupload bumps it), so it is
|
|
132
|
+
// always read fresh. A numbered version's snapshot is immutable, so its epoch never changes and
|
|
133
|
+
// is cached per versionId (see resolveVersionEpoch).
|
|
134
|
+
const [liveEpoch, baseEpoch] = await Promise.all([
|
|
135
|
+
this.fetcher.getLiveDocumentEpoch(),
|
|
136
|
+
this.resolveVersionEpoch(base.versionId),
|
|
137
|
+
]);
|
|
138
|
+
if (liveEpoch === undefined || baseEpoch === undefined) {
|
|
139
|
+
throw new NonRetryableError(
|
|
140
|
+
`Cannot verify that ODSP file version ${base.versionId} shares the live document's ` +
|
|
141
|
+
`lineage: the storage response is missing an epoch (base epoch: ${baseEpoch ?? "unknown"}, ` +
|
|
142
|
+
`live epoch: ${liveEpoch ?? "unknown"}).`,
|
|
143
|
+
OdspErrorTypes.incorrectServerResponse,
|
|
144
|
+
{
|
|
145
|
+
driverVersion,
|
|
146
|
+
serverEpoch: liveEpoch,
|
|
147
|
+
clientEpoch: baseEpoch,
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (liveEpoch !== baseEpoch) {
|
|
152
|
+
throw new NonRetryableError(
|
|
153
|
+
`ODSP file version ${base.versionId} is on epoch "${baseEpoch}" but the live document is ` +
|
|
154
|
+
`on epoch "${liveEpoch}". A binary file change (e.g. a version restore or ` +
|
|
155
|
+
`download-and-reupload) renumbered the op stream, so ops cannot be replayed from this ` +
|
|
156
|
+
`base onto the live document.`,
|
|
157
|
+
OdspErrorTypes.fileOverwrittenInStorage,
|
|
158
|
+
{
|
|
159
|
+
driverVersion,
|
|
160
|
+
serverEpoch: liveEpoch,
|
|
161
|
+
clientEpoch: baseEpoch,
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
132
167
|
public async listVersions(): Promise<ResolvedVersion[]> {
|
|
133
|
-
const versions = await this.
|
|
134
|
-
// Resolution order does not matter
|
|
168
|
+
const versions = await this.fetcher.listFileVersions();
|
|
169
|
+
// Resolution order does not matter, so resolve concurrently; the newest-first array order is
|
|
135
170
|
// preserved by Promise.all regardless of completion order.
|
|
136
171
|
return Promise.all(
|
|
137
|
-
versions.map(async (version) => ({
|
|
172
|
+
versions.map(async (version, index) => ({
|
|
138
173
|
...version,
|
|
139
|
-
|
|
174
|
+
// Resolve the tip (index 0) fresh each call, since its sequence number can still change;
|
|
175
|
+
// sealed versions come from the cache.
|
|
176
|
+
sequenceNumber:
|
|
177
|
+
index === 0
|
|
178
|
+
? await this.fetcher.resolveSequenceNumber(version.versionId)
|
|
179
|
+
: await this.resolveSeq(version.versionId),
|
|
140
180
|
})),
|
|
141
181
|
);
|
|
142
182
|
}
|
|
143
183
|
|
|
144
|
-
private async
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
184
|
+
private async resolveSeq(versionId: string): Promise<number> {
|
|
185
|
+
// Cached indefinitely (a sealed version's number is fixed); concurrent calls coalesce and a failed
|
|
186
|
+
// resolution is evicted so a later call retries.
|
|
187
|
+
return this.seqCache.addOrGet(versionId, async () =>
|
|
188
|
+
this.fetcher.resolveSequenceNumber(versionId),
|
|
189
|
+
);
|
|
149
190
|
}
|
|
150
191
|
|
|
151
|
-
private async
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this.seqByVersion.set(versionId, pending);
|
|
158
|
-
}
|
|
159
|
-
return pending;
|
|
192
|
+
private async resolveVersionEpoch(versionId: string): Promise<string | undefined> {
|
|
193
|
+
// Cached like resolveSeq (a sealed version's epoch is fixed). The live document's epoch is read
|
|
194
|
+
// fresh instead (see validateLineageEpoch).
|
|
195
|
+
return this.epochCache.addOrGet(versionId, async () =>
|
|
196
|
+
this.fetcher.getRecoverableVersionEpoch(versionId),
|
|
197
|
+
);
|
|
160
198
|
}
|
|
161
199
|
}
|
|
162
200
|
|
package/src/packageVersion.ts
CHANGED
|
@@ -30,6 +30,11 @@ import type {
|
|
|
30
30
|
* still catches up from the snapshot's sequence number through delta storage, which is exactly the
|
|
31
31
|
* bounded replay we want. As a result no live delta-stream connection is ever established.
|
|
32
32
|
*
|
|
33
|
+
* Op availability is enforced by the delta storage stack itself: it validates that fetched batches
|
|
34
|
+
* are contiguous from the requested start, keeps requesting until the bounded range is fully
|
|
35
|
+
* delivered, and fails the fetch if the ops never materialize. So a stream that completes has
|
|
36
|
+
* necessarily served the whole bridge, and no additional checks are needed here.
|
|
37
|
+
*
|
|
33
38
|
* @internal
|
|
34
39
|
*/
|
|
35
40
|
export class OdspPointInTimeDocumentService
|
|
@@ -67,15 +72,14 @@ export class OdspPointInTimeDocumentService
|
|
|
67
72
|
// The exclusive upper bound needed to include the target op itself.
|
|
68
73
|
const boundedTo = this.targetSequenceNumber + 1;
|
|
69
74
|
return {
|
|
70
|
-
fetchMessages: (from, to, abortSignal, cachedOnly, fetchReason) =>
|
|
71
|
-
|
|
75
|
+
fetchMessages: (from, to, abortSignal, cachedOnly, fetchReason) =>
|
|
76
|
+
liveDeltaStorage.fetchMessages(
|
|
72
77
|
from,
|
|
73
78
|
to === undefined ? boundedTo : Math.min(to, boundedTo),
|
|
74
79
|
abortSignal,
|
|
75
80
|
cachedOnly,
|
|
76
81
|
fetchReason,
|
|
77
|
-
)
|
|
78
|
-
},
|
|
82
|
+
),
|
|
79
83
|
};
|
|
80
84
|
}
|
|
81
85
|
|