@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,138 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ /*
7
+ * An {@link IOdspFileVersionFetcher} backed by the ODSP REST APIs:
8
+ * - GET /_api/v2.1/.../versions -- enumerate the file's versions.
9
+ * - GET /_api/v2.1/.../versions/{label}/opStream/snapshots/trees/latest?blobs=2 -- fetch a version's
10
+ * snapshot and read its sequence number, parsed with the driver's snapshot parser.
11
+ */
12
+
13
+ import type {
14
+ IOdspUrlParts,
15
+ InstrumentedStorageTokenFetcher,
16
+ } from "@fluidframework/odsp-driver-definitions/internal";
17
+ import type { TelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal";
18
+
19
+ import { currentReadVersion, parseCompactSnapshotResponse } from "../compactSnapshotParser.js";
20
+ import type { IOdspSnapshot } from "../contracts.js";
21
+ import type { EpochTracker } from "../epochTracker.js";
22
+ import { getHeadersWithAuth } from "../getUrlAndHeadersWithAuth.js";
23
+ import { convertOdspSnapshotToSnapshotTreeAndBlobs } from "../odspSnapshotParser.js";
24
+ import { getApiRoot } from "../odspUrlHelper.js";
25
+ import { getWithRetryForTokenRefresh } from "../odspUtils.js";
26
+
27
+ import type { OdspFileVersionRef, IOdspFileVersionFetcher } from "./odspVersionManager.js";
28
+
29
+ /**
30
+ * Raw shape of a OneDrive/SharePoint driveItem version (an entry in the `/versions` response).
31
+ * @see https://learn.microsoft.com/en-us/onedrive/developer/rest-api/resources/driveitemversion
32
+ */
33
+ interface IDriveItemVersion {
34
+ /** The version's label, e.g. "42.0". */
35
+ readonly id: string;
36
+ readonly lastModifiedDateTime: string;
37
+ }
38
+
39
+ /** A single page of the driveItem `/versions` response. */
40
+ interface IDriveItemVersionsPage {
41
+ readonly value?: IDriveItemVersion[];
42
+ /** Absolute URL of the next page, present only while more versions remain. */
43
+ readonly "@odata.nextLink"?: string;
44
+ }
45
+
46
+ /**
47
+ * Inputs needed to make authenticated requests against a specific ODSP file.
48
+ */
49
+ export interface OdspFileVersionFetcherProps {
50
+ readonly urlParts: IOdspUrlParts;
51
+ readonly getAuthHeader: InstrumentedStorageTokenFetcher;
52
+ readonly epochTracker: EpochTracker;
53
+ readonly logger: TelemetryLoggerExt;
54
+ }
55
+
56
+ /**
57
+ * Create an {@link IOdspFileVersionFetcher} that talks to a specific ODSP file.
58
+ */
59
+ export function createOdspFileVersionFetcher(
60
+ props: OdspFileVersionFetcherProps,
61
+ ): IOdspFileVersionFetcher {
62
+ const { urlParts, getAuthHeader, epochTracker, logger } = props;
63
+ const { siteUrl, driveId, itemId } = urlParts;
64
+
65
+ const listFileVersions = async (): Promise<OdspFileVersionRef[]> =>
66
+ getWithRetryForTokenRefresh(async (options) => {
67
+ const method = "GET";
68
+ const versions: OdspFileVersionRef[] = [];
69
+ // The file's version history (distinct from the driver's snapshot list), from the same API
70
+ // root as the snapshot call so consumer (ODC) and enterprise (SPO) hosts are handled alike.
71
+ // A long history is paged, so follow `@odata.nextLink` until it is absent; otherwise a base
72
+ // version beyond the first page would be missed and wrongly reported as "no base version".
73
+ let url = `${getApiRoot(new URL(siteUrl))}/drives/${driveId}/items/${itemId}/versions`;
74
+ do {
75
+ const token = await getAuthHeader(
76
+ { ...options, request: { url, method } },
77
+ "FileVersions",
78
+ );
79
+ const headers = getHeadersWithAuth(token);
80
+ const response = await epochTracker.fetchAndParseAsJSON<{
81
+ value?: IDriveItemVersion[];
82
+ }>(url, { method, headers }, "versions");
83
+ const page = response.content as IDriveItemVersionsPage;
84
+ // The API returns versions newest-first.
85
+ for (const version of page.value ?? []) {
86
+ versions.push({
87
+ versionId: version.id,
88
+ lastModifiedDateTime: version.lastModifiedDateTime,
89
+ });
90
+ }
91
+ url = page["@odata.nextLink"] ?? "";
92
+ } while (url);
93
+ return versions;
94
+ });
95
+
96
+ const resolveSequenceNumber = async (versionId: string): Promise<number> =>
97
+ getWithRetryForTokenRefresh(async (options) => {
98
+ // A file version's sequence number lives inside that version's snapshot, so fetch the snapshot
99
+ // from the version-scoped endpoint. `blobs=2` inlines blob contents so the `.protocol/attributes`
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.
102
+ const url = `${getApiRoot(new URL(siteUrl))}/drives/${driveId}/items/${itemId}/versions/${encodeURIComponent(
103
+ versionId,
104
+ )}/opStream/snapshots/trees/latest?blobs=2`;
105
+ const method = "GET";
106
+ const token = await getAuthHeader(
107
+ { ...options, request: { url, method } },
108
+ "FileVersionSnapshot",
109
+ );
110
+ const headers = getHeadersWithAuth(token);
111
+ // The server can return the snapshot in one of two equivalent framings: verbose JSON, or
112
+ // "ms-fluid" — ODSP's compact binary encoding of the same snapshot. Advertise both, and pin the
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.
115
+ headers.accept = `application/json, application/ms-fluid; v=${currentReadVersion}`;
116
+ const response = await epochTracker.fetch(url, { method, headers }, "treesLatest");
117
+ const contentType = response.headers.get("content-type") ?? "";
118
+ let sequenceNumber: number | undefined;
119
+ if (contentType.includes("application/json")) {
120
+ // JSON framing: read it with the driver's JSON snapshot parser.
121
+ const snapshotJson = (await response.content.json()) as IOdspSnapshot;
122
+ sequenceNumber =
123
+ convertOdspSnapshotToSnapshotTreeAndBlobs(snapshotJson).sequenceNumber;
124
+ } else {
125
+ // ms-fluid framing: the compact binary form; read it with the driver's compact-snapshot parser.
126
+ const bytes = new Uint8Array(await response.content.arrayBuffer());
127
+ sequenceNumber = parseCompactSnapshotResponse(bytes, logger).sequenceNumber;
128
+ }
129
+ // A version's snapshot must carry a sequence number; a missing one is surfaced as an error
130
+ // naming the version, rather than returning a wrong value.
131
+ if (sequenceNumber === undefined) {
132
+ throw new Error(`ODSP file version ${versionId} snapshot is missing a sequenceNumber`);
133
+ }
134
+ return sequenceNumber;
135
+ });
136
+
137
+ return { listFileVersions, resolveSequenceNumber };
138
+ }
@@ -0,0 +1,170 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ /**
7
+ * Selects the ODSP file version whose snapshot sits at or before a target Fluid sequence number —
8
+ * the base to load or replay from when materializing a document at a point in time.
9
+ *
10
+ * The selection logic depends on an injected {@link IOdspFileVersionFetcher}, so it is independent of
11
+ * how versions are enumerated and resolved (real ODSP, a test double, or an alternative backend).
12
+ */
13
+
14
+ import {
15
+ createOdspFileVersionFetcher,
16
+ type OdspFileVersionFetcherProps,
17
+ } from "./odspFileVersionFetcher.js";
18
+
19
+ /**
20
+ * A single ODSP file version, as listed by the file's version history.
21
+ */
22
+ export interface OdspFileVersionRef {
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
+ }
32
+
33
+ /**
34
+ * An ODSP file version together with its resolved Fluid sequence number.
35
+ */
36
+ export interface ResolvedVersion extends OdspFileVersionRef {
37
+ /**
38
+ * The Fluid sequence number the version's snapshot represents.
39
+ */
40
+ readonly sequenceNumber: number;
41
+ }
42
+
43
+ /**
44
+ * Result of resolving the base version for a target sequence number.
45
+ *
46
+ * @remarks
47
+ * There is intentionally no `targetIsLive` case: when the target is at/after the newest recoverable
48
+ * version, the greatest version with `seq <= target` IS that newest version, so it is a normal
49
+ * `found`. A consumer may separately choose to load the live file when the target is near the head.
50
+ */
51
+ export type BaseForSeq =
52
+ | {
53
+ /** A recoverable version with `sequenceNumber <= target` was found. */
54
+ readonly kind: "found";
55
+ readonly base: ResolvedVersion;
56
+ }
57
+ | {
58
+ /** No recoverable version has `sequenceNumber <= target` (target predates retained history). */
59
+ readonly kind: "noBaseVersion";
60
+ /** The oldest sequence number that was resolved while searching, if any. */
61
+ readonly oldestResolvedSeq?: number;
62
+ };
63
+
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
+ /**
81
+ * Selects the file version to use as the base for loading or replaying to a target sequence number.
82
+ */
83
+ export interface IOdspVersionManager {
84
+ /**
85
+ * Given a target sequence number, return the closest version at or before it (`found`), or
86
+ * `noBaseVersion` if the target predates the oldest retained version.
87
+ */
88
+ findBaseForSeq(target: number): Promise<BaseForSeq>;
89
+ }
90
+
91
+ /**
92
+ * Default {@link IOdspVersionManager}. Caches the version list and resolved sequence numbers. The
93
+ * resolution strategy (eager, newest-to-oldest, stopping at the first usable base) is hidden behind
94
+ * {@link findBaseForSeq} and can change without affecting callers.
95
+ */
96
+ export class OdspVersionManager implements IOdspVersionManager {
97
+ private versionsCache: Promise<OdspFileVersionRef[]> | undefined;
98
+ private readonly seqByVersion = new Map<string, Promise<number>>();
99
+
100
+ public constructor(private readonly fetcher: IOdspFileVersionFetcher) {}
101
+
102
+ public refresh(): void {
103
+ this.versionsCache = undefined;
104
+ this.seqByVersion.clear();
105
+ }
106
+
107
+ public async findBaseForSeq(target: number): Promise<BaseForSeq> {
108
+ // Recoverable base candidates = every version except the tip (index 0 ≈ the live document).
109
+ const versions = await this.getVersions();
110
+ const candidates = versions.slice(1);
111
+
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
+ let oldestResolvedSeq: number | undefined;
119
+ for (const version of candidates) {
120
+ const sequenceNumber = await this.resolveSeq(version.versionId);
121
+ oldestResolvedSeq =
122
+ oldestResolvedSeq === undefined
123
+ ? sequenceNumber
124
+ : Math.min(oldestResolvedSeq, sequenceNumber);
125
+ if (sequenceNumber <= target) {
126
+ return { kind: "found", base: { ...version, sequenceNumber } };
127
+ }
128
+ }
129
+ return { kind: "noBaseVersion", oldestResolvedSeq };
130
+ }
131
+
132
+ public async listVersions(): Promise<ResolvedVersion[]> {
133
+ const versions = await this.getVersions();
134
+ // Resolution order does not matter here, so resolve concurrently; the newest-first array order is
135
+ // preserved by Promise.all regardless of completion order.
136
+ return Promise.all(
137
+ versions.map(async (version) => ({
138
+ ...version,
139
+ sequenceNumber: await this.resolveSeq(version.versionId),
140
+ })),
141
+ );
142
+ }
143
+
144
+ private async getVersions(): Promise<OdspFileVersionRef[]> {
145
+ // Cache the pending promise, not the awaited value, so concurrent callers share one fetch and a
146
+ // refresh() that runs while the fetch is in flight is not overwritten when the fetch settles.
147
+ this.versionsCache ??= this.fetcher.listFileVersions();
148
+ return this.versionsCache;
149
+ }
150
+
151
+ private async resolveSeq(versionId: string): Promise<number> {
152
+ // Cache the pending promise (a version's sequence number never changes) so concurrent callers
153
+ // coalesce and a refresh() is not clobbered by a fetch that was already in flight.
154
+ let pending = this.seqByVersion.get(versionId);
155
+ if (pending === undefined) {
156
+ pending = this.fetcher.resolveSequenceNumber(versionId);
157
+ this.seqByVersion.set(versionId, pending);
158
+ }
159
+ return pending;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Create an {@link IOdspVersionManager} for a specific ODSP file, wired to the real ODSP REST APIs.
165
+ */
166
+ export function createOdspVersionManager(
167
+ props: OdspFileVersionFetcherProps,
168
+ ): IOdspVersionManager {
169
+ return new OdspVersionManager(createOdspFileVersionFetcher(props));
170
+ }
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/odsp-driver";
9
- export const pkgVersion = "2.111.0";
9
+ export const pkgVersion = "2.112.0";