@fluidframework/odsp-driver 2.110.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 (137) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/api-report/odsp-driver.legacy.beta.api.md +2 -2
  3. package/dist/WriteBufferUtils.d.ts.map +1 -1
  4. package/dist/WriteBufferUtils.js +1 -1
  5. package/dist/WriteBufferUtils.js.map +1 -1
  6. package/dist/fetchSnapshot.d.ts +2 -3
  7. package/dist/fetchSnapshot.d.ts.map +1 -1
  8. package/dist/fetchSnapshot.js +4 -5
  9. package/dist/fetchSnapshot.js.map +1 -1
  10. package/dist/getFileLink.js +1 -1
  11. package/dist/getFileLink.js.map +1 -1
  12. package/dist/localOdspDriver/localOdspDocumentService.d.ts +1 -1
  13. package/dist/localOdspDriver/localOdspDocumentService.d.ts.map +1 -1
  14. package/dist/localOdspDriver/localOdspDocumentService.js +1 -1
  15. package/dist/localOdspDriver/localOdspDocumentService.js.map +1 -1
  16. package/dist/localOdspDriver/localOdspDocumentServiceFactory.d.ts +1 -3
  17. package/dist/localOdspDriver/localOdspDocumentServiceFactory.d.ts.map +1 -1
  18. package/dist/localOdspDriver/localOdspDocumentServiceFactory.js +1 -1
  19. package/dist/localOdspDriver/localOdspDocumentServiceFactory.js.map +1 -1
  20. package/dist/localOdspDriver/localOdspDocumentStorageManager.d.ts +2 -2
  21. package/dist/localOdspDriver/localOdspDocumentStorageManager.d.ts.map +1 -1
  22. package/dist/localOdspDriver/localOdspDocumentStorageManager.js +1 -1
  23. package/dist/localOdspDriver/localOdspDocumentStorageManager.js.map +1 -1
  24. package/dist/odspDelayLoadedDeltaStream.d.ts +1 -1
  25. package/dist/odspDelayLoadedDeltaStream.d.ts.map +1 -1
  26. package/dist/odspDelayLoadedDeltaStream.js +2 -2
  27. package/dist/odspDelayLoadedDeltaStream.js.map +1 -1
  28. package/dist/odspDocumentDeltaConnection.d.ts.map +1 -1
  29. package/dist/odspDocumentDeltaConnection.js +4 -4
  30. package/dist/odspDocumentDeltaConnection.js.map +1 -1
  31. package/dist/odspDocumentStorageManager.d.ts.map +1 -1
  32. package/dist/odspDocumentStorageManager.js +3 -3
  33. package/dist/odspDocumentStorageManager.js.map +1 -1
  34. package/dist/odspDocumentStorageServiceBase.d.ts +2 -2
  35. package/dist/odspDocumentStorageServiceBase.d.ts.map +1 -1
  36. package/dist/odspDocumentStorageServiceBase.js +1 -1
  37. package/dist/odspDocumentStorageServiceBase.js.map +1 -1
  38. package/dist/odspVersionManager/index.d.ts +6 -0
  39. package/dist/odspVersionManager/index.d.ts.map +1 -0
  40. package/dist/odspVersionManager/index.js +10 -0
  41. package/dist/odspVersionManager/index.js.map +1 -0
  42. package/dist/odspVersionManager/odspFileVersionFetcher.d.ts +22 -0
  43. package/dist/odspVersionManager/odspFileVersionFetcher.d.ts.map +1 -0
  44. package/dist/odspVersionManager/odspFileVersionFetcher.js +81 -0
  45. package/dist/odspVersionManager/odspFileVersionFetcher.js.map +1 -0
  46. package/dist/odspVersionManager/odspVersionManager.d.ts +98 -0
  47. package/dist/odspVersionManager/odspVersionManager.d.ts.map +1 -0
  48. package/dist/odspVersionManager/odspVersionManager.js +87 -0
  49. package/dist/odspVersionManager/odspVersionManager.js.map +1 -0
  50. package/dist/packageVersion.d.ts +1 -1
  51. package/dist/packageVersion.js +1 -1
  52. package/dist/packageVersion.js.map +1 -1
  53. package/dist/prefetchLatestSnapshot.d.ts +5 -5
  54. package/dist/prefetchLatestSnapshot.d.ts.map +1 -1
  55. package/dist/prefetchLatestSnapshot.js +6 -6
  56. package/dist/prefetchLatestSnapshot.js.map +1 -1
  57. package/dist/zipItDataRepresentationUtils.d.ts +1 -1
  58. package/dist/zipItDataRepresentationUtils.d.ts.map +1 -1
  59. package/dist/zipItDataRepresentationUtils.js +2 -2
  60. package/dist/zipItDataRepresentationUtils.js.map +1 -1
  61. package/lib/WriteBufferUtils.d.ts.map +1 -1
  62. package/lib/WriteBufferUtils.js +1 -1
  63. package/lib/WriteBufferUtils.js.map +1 -1
  64. package/lib/fetchSnapshot.d.ts +2 -3
  65. package/lib/fetchSnapshot.d.ts.map +1 -1
  66. package/lib/fetchSnapshot.js +4 -5
  67. package/lib/fetchSnapshot.js.map +1 -1
  68. package/lib/getFileLink.js +1 -1
  69. package/lib/getFileLink.js.map +1 -1
  70. package/lib/localOdspDriver/localOdspDocumentService.d.ts +1 -1
  71. package/lib/localOdspDriver/localOdspDocumentService.d.ts.map +1 -1
  72. package/lib/localOdspDriver/localOdspDocumentService.js +1 -1
  73. package/lib/localOdspDriver/localOdspDocumentService.js.map +1 -1
  74. package/lib/localOdspDriver/localOdspDocumentServiceFactory.d.ts +1 -3
  75. package/lib/localOdspDriver/localOdspDocumentServiceFactory.d.ts.map +1 -1
  76. package/lib/localOdspDriver/localOdspDocumentServiceFactory.js +1 -1
  77. package/lib/localOdspDriver/localOdspDocumentServiceFactory.js.map +1 -1
  78. package/lib/localOdspDriver/localOdspDocumentStorageManager.d.ts +2 -2
  79. package/lib/localOdspDriver/localOdspDocumentStorageManager.d.ts.map +1 -1
  80. package/lib/localOdspDriver/localOdspDocumentStorageManager.js +1 -1
  81. package/lib/localOdspDriver/localOdspDocumentStorageManager.js.map +1 -1
  82. package/lib/odspDelayLoadedDeltaStream.d.ts +1 -1
  83. package/lib/odspDelayLoadedDeltaStream.d.ts.map +1 -1
  84. package/lib/odspDelayLoadedDeltaStream.js +2 -2
  85. package/lib/odspDelayLoadedDeltaStream.js.map +1 -1
  86. package/lib/odspDocumentDeltaConnection.d.ts.map +1 -1
  87. package/lib/odspDocumentDeltaConnection.js +4 -4
  88. package/lib/odspDocumentDeltaConnection.js.map +1 -1
  89. package/lib/odspDocumentStorageManager.d.ts.map +1 -1
  90. package/lib/odspDocumentStorageManager.js +3 -3
  91. package/lib/odspDocumentStorageManager.js.map +1 -1
  92. package/lib/odspDocumentStorageServiceBase.d.ts +2 -2
  93. package/lib/odspDocumentStorageServiceBase.d.ts.map +1 -1
  94. package/lib/odspDocumentStorageServiceBase.js +1 -1
  95. package/lib/odspDocumentStorageServiceBase.js.map +1 -1
  96. package/lib/odspVersionManager/index.d.ts +6 -0
  97. package/lib/odspVersionManager/index.d.ts.map +1 -0
  98. package/lib/odspVersionManager/index.js +6 -0
  99. package/lib/odspVersionManager/index.js.map +1 -0
  100. package/lib/odspVersionManager/odspFileVersionFetcher.d.ts +22 -0
  101. package/lib/odspVersionManager/odspFileVersionFetcher.d.ts.map +1 -0
  102. package/lib/odspVersionManager/odspFileVersionFetcher.js +77 -0
  103. package/lib/odspVersionManager/odspFileVersionFetcher.js.map +1 -0
  104. package/lib/odspVersionManager/odspVersionManager.d.ts +98 -0
  105. package/lib/odspVersionManager/odspVersionManager.d.ts.map +1 -0
  106. package/lib/odspVersionManager/odspVersionManager.js +82 -0
  107. package/lib/odspVersionManager/odspVersionManager.js.map +1 -0
  108. package/lib/packageVersion.d.ts +1 -1
  109. package/lib/packageVersion.js +1 -1
  110. package/lib/packageVersion.js.map +1 -1
  111. package/lib/prefetchLatestSnapshot.d.ts +5 -5
  112. package/lib/prefetchLatestSnapshot.d.ts.map +1 -1
  113. package/lib/prefetchLatestSnapshot.js +6 -6
  114. package/lib/prefetchLatestSnapshot.js.map +1 -1
  115. package/lib/zipItDataRepresentationUtils.d.ts +1 -1
  116. package/lib/zipItDataRepresentationUtils.d.ts.map +1 -1
  117. package/lib/zipItDataRepresentationUtils.js +2 -2
  118. package/lib/zipItDataRepresentationUtils.js.map +1 -1
  119. package/package.json +14 -18
  120. package/src/WriteBufferUtils.ts +1 -5
  121. package/src/fetchSnapshot.ts +2 -5
  122. package/src/getFileLink.ts +1 -1
  123. package/src/localOdspDriver/localOdspDocumentService.ts +1 -1
  124. package/src/localOdspDriver/localOdspDocumentServiceFactory.ts +1 -8
  125. package/src/localOdspDriver/localOdspDocumentStorageManager.ts +1 -2
  126. package/src/odspDelayLoadedDeltaStream.ts +2 -2
  127. package/src/odspDocumentDeltaConnection.ts +2 -4
  128. package/src/odspDocumentStorageManager.ts +0 -3
  129. package/src/odspDocumentStorageServiceBase.ts +2 -2
  130. package/src/odspVersionManager/DEV.md +277 -0
  131. package/src/odspVersionManager/index.ts +12 -0
  132. package/src/odspVersionManager/odspFileVersionFetcher.ts +138 -0
  133. package/src/odspVersionManager/odspVersionManager.ts +170 -0
  134. package/src/packageVersion.ts +1 -1
  135. package/src/prefetchLatestSnapshot.ts +7 -8
  136. package/src/zipItDataRepresentationUtils.ts +2 -7
  137. package/tsconfig.json +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/odsp-driver",
3
- "version": "2.110.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,33 +69,33 @@
69
69
  "temp-directory": "nyc/.nyc_output"
70
70
  },
71
71
  "dependencies": {
72
- "@fluid-internal/client-utils": "~2.110.0",
73
- "@fluidframework/core-interfaces": "~2.110.0",
74
- "@fluidframework/core-utils": "~2.110.0",
75
- "@fluidframework/driver-base": "~2.110.0",
76
- "@fluidframework/driver-definitions": "~2.110.0",
77
- "@fluidframework/driver-utils": "~2.110.0",
78
- "@fluidframework/odsp-doclib-utils": "~2.110.0",
79
- "@fluidframework/odsp-driver-definitions": "~2.110.0",
80
- "@fluidframework/telemetry-utils": "~2.110.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.110.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.103.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",
96
96
  "@types/sinon": "^17.0.3",
97
97
  "c8": "^10.1.3",
98
- "concurrently": "^9.2.1",
98
+ "concurrently": "^10.0.3",
99
99
  "copyfiles": "^2.4.1",
100
100
  "cross-env": "^10.1.0",
101
101
  "eslint": "~9.39.1",
@@ -107,11 +107,7 @@
107
107
  "typescript": "~5.4.5"
108
108
  },
109
109
  "typeValidation": {
110
- "broken": {
111
- "Interface_OdspFluidDataStoreLocator": {
112
- "backCompat": false
113
- }
114
- },
110
+ "broken": {},
115
111
  "entrypoint": "legacy"
116
112
  },
117
113
  "scripts": {
@@ -188,11 +188,7 @@ function serializeDictionaryString(
188
188
  buffer.write(id, idLength);
189
189
  }
190
190
 
191
- function serializeString(
192
- buffer: WriteBuffer,
193
- content: string,
194
- codeMap = binaryBytesToCodeMap,
195
- ): void {
191
+ function serializeString(buffer: WriteBuffer, content: string): void {
196
192
  serializeBlob(buffer, IsoBuffer.from(content, "utf8"), utf8StringBytesToCodeMap);
197
193
  }
198
194
 
@@ -85,7 +85,6 @@ export enum SnapshotFormatSupportType {
85
85
  * @param snapshotUrl - snapshot url from where the odsp snapshot will be fetched
86
86
  * @param versionId - id of specific snapshot to be fetched
87
87
  * @param fetchFullSnapshot - whether we want to fetch full snapshot(with blobs)
88
- * @param forceAccessTokenViaAuthorizationHeader - Deprecated and not used, true value always used instead. Whether to force passing given token via authorization header
89
88
  * @param snapshotDownloader - Implementation of the get/post methods used to fetch the snapshot. snapshotDownloader is responsible for generating the appropriate headers (including Authorization header) as well as handling any token refreshes before retrying.
90
89
  * @returns A promise of the snapshot and the status code of the response
91
90
  */
@@ -93,7 +92,6 @@ export async function fetchSnapshot(
93
92
  snapshotUrl: string,
94
93
  versionId: string,
95
94
  fetchFullSnapshot: boolean,
96
- forceAccessTokenViaAuthorizationHeader: boolean,
97
95
  logger: TelemetryLoggerExt,
98
96
  snapshotDownloader: (url: string) => Promise<IOdspResponse<unknown>>,
99
97
  ): Promise<ISnapshot> {
@@ -120,7 +118,6 @@ export async function fetchSnapshotWithRedeem(
120
118
  odspResolvedUrl: IOdspResolvedUrl,
121
119
  storageTokenFetcher: InstrumentedStorageTokenFetcher,
122
120
  snapshotOptions: ISnapshotOptions | undefined,
123
- forceAccessTokenViaAuthorizationHeader: boolean,
124
121
  logger: TelemetryLoggerExt,
125
122
  snapshotDownloader: (
126
123
  finalOdspResolvedUrl: IOdspResolvedUrl,
@@ -411,7 +408,7 @@ async function fetchLatestSnapshotCore(
411
408
  }
412
409
  return res;
413
410
  })
414
- .catch((error) =>
411
+ .catch((_error) =>
415
412
  // Parsing can fail and message could contain full request URI, including
416
413
  // tokens, etc. So do not log error object itself.
417
414
  throwOdspNetworkError(
@@ -455,7 +452,7 @@ async function fetchLatestSnapshotCore(
455
452
  }
456
453
  return res;
457
454
  })
458
- .catch((error) =>
455
+ .catch((_error) =>
459
456
  // Parsing can fail and message could contain full request URI, including
460
457
  // tokens, etc. So do not log error object itself.
461
458
  throwOdspNetworkError(
@@ -72,7 +72,7 @@ export const getFileLink = mockify(
72
72
  {
73
73
  // TODO: use a stronger type
74
74
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
- onRetry(delayInMs: number, error: any) {
75
+ onRetry(_delayInMs: number, error: any) {
76
76
  retryCount++;
77
77
  if (retryCount === 5) {
78
78
  if (error !== undefined && typeof error === "object") {
@@ -58,7 +58,7 @@ export class LocalOdspDocumentService
58
58
  throw toThrow;
59
59
  }
60
60
 
61
- public dispose(error?: unknown): void {
61
+ public dispose(): void {
62
62
  // Do nothing
63
63
  }
64
64
  }
@@ -3,8 +3,6 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import type { ITelemetryBaseLogger } from "@fluidframework/core-interfaces";
7
- import type { ISummaryTree } from "@fluidframework/driver-definitions";
8
6
  import type {
9
7
  IDocumentService,
10
8
  IResolvedUrl,
@@ -42,12 +40,7 @@ export class LocalOdspDocumentServiceFactory extends OdspDocumentServiceFactoryC
42
40
  throw toThrow;
43
41
  }
44
42
 
45
- public createContainer(
46
- _createNewSummary: ISummaryTree | undefined,
47
- _createNewResolvedUrl: IResolvedUrl,
48
- logger?: ITelemetryBaseLogger,
49
- _clientIsSummarizer?: boolean,
50
- ): never {
43
+ public createContainer(): never {
51
44
  const toThrow = new UsageError(
52
45
  '"createContainer" is not supported by LocalOdspDocumentServiceFactory',
53
46
  );
@@ -7,7 +7,6 @@ import { assert } from "@fluidframework/core-utils/internal";
7
7
  import type { ISummaryTree } from "@fluidframework/driver-definitions";
8
8
  import type {
9
9
  ISnapshot,
10
- ISnapshotFetchOptions,
11
10
  ISummaryContext,
12
11
  IVersion,
13
12
  } from "@fluidframework/driver-definitions/internal";
@@ -66,7 +65,7 @@ export class LocalOdspDocumentStorageService extends OdspDocumentStorageServiceB
66
65
  return this.getSnapshotVersion();
67
66
  }
68
67
 
69
- public async getSnapshot(snapshotFetchOptions?: ISnapshotFetchOptions): Promise<ISnapshot> {
68
+ public async getSnapshot(): Promise<ISnapshot> {
70
69
  this.throwUsageError("getSnapshot");
71
70
  }
72
71
 
@@ -214,7 +214,7 @@ export class OdspDelayLoadedDeltaStream {
214
214
  websocketEndpoint.deltaStreamSocketUrl,
215
215
  connectionId,
216
216
  );
217
- connection.on("op", (documentId, ops: ISequencedDocumentMessage[]) => {
217
+ connection.on("op", (_documentId, ops: ISequencedDocumentMessage[]) => {
218
218
  this.opsReceived(ops);
219
219
  });
220
220
  connection.on("signal", this.signalHandler);
@@ -573,7 +573,7 @@ export class OdspDelayLoadedDeltaStream {
573
573
  return connection;
574
574
  }
575
575
 
576
- public dispose(error?: unknown): void {
576
+ public dispose(): void {
577
577
  this.clearJoinSessionTimer();
578
578
  this.currentConnection?.dispose();
579
579
  delete this.currentConnection;
@@ -67,7 +67,7 @@ class SocketReference extends TypedEventEmitter<ISocketEvents> {
67
67
  // Map of all existing socket io sockets. [url, tenantId, documentId] -> socket
68
68
  private static readonly socketIoSockets: Map<string, SocketReference> = new Map();
69
69
 
70
- public static find(key: string, logger: TelemetryLoggerExt): SocketReference | undefined {
70
+ public static find(key: string): SocketReference | undefined {
71
71
  const socketReference = SocketReference.socketIoSockets.get(key);
72
72
 
73
73
  // Verify the socket is healthy before reusing it
@@ -279,7 +279,6 @@ export class OdspDocumentDeltaConnection extends DocumentDeltaConnection {
279
279
  enableMultiplexing,
280
280
  tenantId,
281
281
  documentId,
282
- telemetryLogger,
283
282
  );
284
283
 
285
284
  const socket = socketReference.socket;
@@ -389,10 +388,9 @@ export class OdspDocumentDeltaConnection extends DocumentDeltaConnection {
389
388
  enableMultiplexing: boolean,
390
389
  tenantId: string,
391
390
  documentId: string,
392
- logger: TelemetryLoggerExt,
393
391
  ): SocketReference {
394
392
  // eslint-disable-next-line unicorn/no-array-method-this-argument
395
- const existingSocketReference = SocketReference.find(key, logger);
393
+ const existingSocketReference = SocketReference.find(key);
396
394
  if (existingSocketReference) {
397
395
  return existingSocketReference;
398
396
  }
@@ -662,7 +662,6 @@ export class OdspDocumentStorageService extends OdspDocumentStorageServiceBase {
662
662
  this.odspResolvedUrl,
663
663
  this.getAuthHeader,
664
664
  snapshotOptions,
665
- !!this.hostPolicy.sessionOptions?.forceAccessTokenViaAuthorizationHeader,
666
665
  this.logger,
667
666
  snapshotDownloader,
668
667
  putInCache,
@@ -704,7 +703,6 @@ export class OdspDocumentStorageService extends OdspDocumentStorageServiceBase {
704
703
  this.odspResolvedUrl,
705
704
  this.getAuthHeader,
706
705
  snapshotOptionsWithoutBlobs,
707
- !!this.hostPolicy.sessionOptions?.forceAccessTokenViaAuthorizationHeader,
708
706
  this.logger,
709
707
  snapshotDownloader,
710
708
  putInCache,
@@ -886,7 +884,6 @@ export class OdspDocumentStorageService extends OdspDocumentStorageServiceBase {
886
884
  this.snapshotUrl!,
887
885
  id,
888
886
  this.fetchFullSnapshot,
889
- !!this.hostPolicy.sessionOptions?.forceAccessTokenViaAuthorizationHeader,
890
887
  this.logger,
891
888
  snapshotDownloader,
892
889
  );
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { assert } from "@fluidframework/core-utils/internal";
7
- import type { ISummaryHandle, ISummaryTree } from "@fluidframework/driver-definitions";
7
+ import type { ISummaryTree } from "@fluidframework/driver-definitions";
8
8
  import {
9
9
  type FetchSource,
10
10
  type FiveDaysMs,
@@ -227,7 +227,7 @@ export abstract class OdspDocumentStorageServiceBase implements IDocumentStorage
227
227
  context: ISummaryContext,
228
228
  ): Promise<string>;
229
229
 
230
- public async downloadSummary(commit: ISummaryHandle): Promise<ISummaryTree> {
230
+ public async downloadSummary(): Promise<ISummaryTree> {
231
231
  throw new Error("Not implemented yet");
232
232
  }
233
233
 
@@ -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";