@fluidframework/odsp-driver 2.112.0 → 2.113.0-411909

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 (38) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +4 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/packageVersion.d.ts +1 -1
  6. package/dist/packageVersion.d.ts.map +1 -1
  7. package/dist/packageVersion.js +1 -1
  8. package/dist/packageVersion.js.map +1 -1
  9. package/dist/pointInTimeDriver/odspPointInTimeDocumentService.d.ts +37 -0
  10. package/dist/pointInTimeDriver/odspPointInTimeDocumentService.d.ts.map +1 -0
  11. package/dist/pointInTimeDriver/odspPointInTimeDocumentService.js +70 -0
  12. package/dist/pointInTimeDriver/odspPointInTimeDocumentService.js.map +1 -0
  13. package/dist/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.d.ts +49 -0
  14. package/dist/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.d.ts.map +1 -0
  15. package/dist/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.js +108 -0
  16. package/dist/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.js.map +1 -0
  17. package/lib/index.d.ts +1 -0
  18. package/lib/index.d.ts.map +1 -1
  19. package/lib/index.js +2 -0
  20. package/lib/index.js.map +1 -1
  21. package/lib/packageVersion.d.ts +1 -1
  22. package/lib/packageVersion.d.ts.map +1 -1
  23. package/lib/packageVersion.js +1 -1
  24. package/lib/packageVersion.js.map +1 -1
  25. package/lib/pointInTimeDriver/odspPointInTimeDocumentService.d.ts +37 -0
  26. package/lib/pointInTimeDriver/odspPointInTimeDocumentService.d.ts.map +1 -0
  27. package/lib/pointInTimeDriver/odspPointInTimeDocumentService.js +66 -0
  28. package/lib/pointInTimeDriver/odspPointInTimeDocumentService.js.map +1 -0
  29. package/lib/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.d.ts +49 -0
  30. package/lib/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.d.ts.map +1 -0
  31. package/lib/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.js +104 -0
  32. package/lib/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.js.map +1 -0
  33. package/package.json +11 -11
  34. package/src/index.ts +2 -0
  35. package/src/odspVersionManager/DEV.md +101 -29
  36. package/src/packageVersion.ts +1 -1
  37. package/src/pointInTimeDriver/odspPointInTimeDocumentService.ts +94 -0
  38. package/src/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.ts +185 -0
@@ -0,0 +1,49 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ import type { ITelemetryBaseLogger } from "@fluidframework/core-interfaces";
6
+ import type { IDocumentService, IPersistedCache, IResolvedUrl } from "@fluidframework/driver-definitions/internal";
7
+ import type { HostStoragePolicy, OdspResourceTokenFetchOptions, TokenFetcher } from "@fluidframework/odsp-driver-definitions/internal";
8
+ import { OdspDocumentServiceFactoryCore } from "../odspDocumentServiceFactoryCore.js";
9
+ /**
10
+ * ODSP document service factory that additionally supports point-in-time (sequence-number-based)
11
+ * loading.
12
+ *
13
+ * @remarks
14
+ * This extends {@link OdspDocumentServiceFactoryCore} with the ability to materialize a read-only
15
+ * document service at a requested Fluid sequence number. The loader detects this capability via the
16
+ * presence of {@link OdspPointInTimeDocumentServiceFactory.createPointInTimeDocumentService}, so
17
+ * hosts that want to load a container to a target sequence number must construct this factory
18
+ * (rather than the legacy `OdspDocumentServiceFactory`) and pass it to the loader.
19
+ *
20
+ * @internal
21
+ */
22
+ export declare class OdspPointInTimeDocumentServiceFactory extends OdspDocumentServiceFactoryCore {
23
+ /**
24
+ * The storage token fetcher, captured here because the base class keeps it private.
25
+ */
26
+ private readonly getStorageTokenForVersions;
27
+ constructor(getStorageToken: TokenFetcher<OdspResourceTokenFetchOptions>, getWebsocketToken: TokenFetcher<OdspResourceTokenFetchOptions> | undefined, persistedCache?: IPersistedCache, hostPolicy?: HostStoragePolicy);
28
+ /**
29
+ * Creates a document service that reads its snapshot from the closest file version at or before
30
+ * the target and its deltas from the live document, materializing a requested sequence number
31
+ * through replay.
32
+ */
33
+ createPointInTimeDocumentService(resolvedUrl: IResolvedUrl, targetSequenceNumber: number, logger?: ITelemetryBaseLogger, clientIsSummarizer?: boolean): Promise<IDocumentService>;
34
+ /**
35
+ * Builds an IOdspVersionManager for the given file, which enumerates the file's stored
36
+ * versions and resolves the closest version at or before a target sequence number.
37
+ *
38
+ * @remarks
39
+ * This wires up the plumbing the version manager needs to talk to ODSP: it resolves the URL to
40
+ * its ODSP parts (site/drive/item), creates a scoped child logger, an epoch tracker (from a fresh
41
+ * NonPersistentCache, since only the tracker is needed for consistency checks), and an
42
+ * instrumented storage-token/auth-header fetcher. The resulting manager is used by
43
+ * {@link OdspPointInTimeDocumentServiceFactory.createPointInTimeDocumentService} to pick the base
44
+ * snapshot for point-in-time loading.
45
+ */
46
+ private createVersionManager;
47
+ private resolveFileVersion;
48
+ }
49
+ //# sourceMappingURL=odspPointInTimeDocumentServiceFactory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"odspPointInTimeDocumentServiceFactory.d.ts","sourceRoot":"","sources":["../../src/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAC5E,OAAO,KAAK,EACX,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EACX,iBAAiB,EAEjB,6BAA6B,EAC7B,YAAY,EACZ,MAAM,kDAAkD,CAAC;AAK1D,OAAO,EAAE,8BAA8B,EAAE,MAAM,sCAAsC,CAAC;AActF;;;;;;;;;;;;GAYG;AACH,qBAAa,qCAAsC,SAAQ,8BAA8B;IACxF;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAA8C;gBAGxF,eAAe,EAAE,YAAY,CAAC,6BAA6B,CAAC,EAC5D,iBAAiB,EAAE,YAAY,CAAC,6BAA6B,CAAC,GAAG,SAAS,EAC1E,cAAc,CAAC,EAAE,eAAe,EAChC,UAAU,CAAC,EAAE,iBAAiB;IAM/B;;;;OAIG;IACU,gCAAgC,CAC5C,WAAW,EAAE,YAAY,EACzB,oBAAoB,EAAE,MAAM,EAC5B,MAAM,CAAC,EAAE,oBAAoB,EAC7B,kBAAkB,CAAC,EAAE,OAAO,GAC1B,OAAO,CAAC,gBAAgB,CAAC;IAuC5B;;;;;;;;;;;OAWG;YACW,oBAAoB;YAuCpB,kBAAkB;CAoBhC"}
@@ -0,0 +1,104 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ import { UsageError, createChildLogger } from "@fluidframework/telemetry-utils/internal";
6
+ import { createOdspCacheAndTracker } from "../epochTracker.js";
7
+ import { NonPersistentCache } from "../odspCache.js";
8
+ import { OdspDocumentServiceFactoryCore } from "../odspDocumentServiceFactoryCore.js";
9
+ import { OdspDriverUrlResolver } from "../odspDriverUrlResolver.js";
10
+ import { createOdspLogger, getOdspResolvedUrl, toInstrumentedOdspStorageTokenFetcher, } from "../odspUtils.js";
11
+ import { createOdspVersionManager, } from "../odspVersionManager/index.js";
12
+ import { OdspPointInTimeDocumentService } from "./odspPointInTimeDocumentService.js";
13
+ /**
14
+ * ODSP document service factory that additionally supports point-in-time (sequence-number-based)
15
+ * loading.
16
+ *
17
+ * @remarks
18
+ * This extends {@link OdspDocumentServiceFactoryCore} with the ability to materialize a read-only
19
+ * document service at a requested Fluid sequence number. The loader detects this capability via the
20
+ * presence of {@link OdspPointInTimeDocumentServiceFactory.createPointInTimeDocumentService}, so
21
+ * hosts that want to load a container to a target sequence number must construct this factory
22
+ * (rather than the legacy `OdspDocumentServiceFactory`) and pass it to the loader.
23
+ *
24
+ * @internal
25
+ */
26
+ export class OdspPointInTimeDocumentServiceFactory extends OdspDocumentServiceFactoryCore {
27
+ constructor(getStorageToken, getWebsocketToken, persistedCache, hostPolicy) {
28
+ super(getStorageToken, getWebsocketToken, persistedCache, hostPolicy);
29
+ this.getStorageTokenForVersions = getStorageToken;
30
+ }
31
+ /**
32
+ * Creates a document service that reads its snapshot from the closest file version at or before
33
+ * the target and its deltas from the live document, materializing a requested sequence number
34
+ * through replay.
35
+ */
36
+ async createPointInTimeDocumentService(resolvedUrl, targetSequenceNumber, logger, clientIsSummarizer) {
37
+ const versionManager = await this.createVersionManager(resolvedUrl, logger, clientIsSummarizer);
38
+ const baseResult = await versionManager.findBaseForSeq(targetSequenceNumber);
39
+ if (baseResult.kind === "noBaseVersion") {
40
+ const oldestResolvedSequenceDetail = baseResult.oldestResolvedSeq === undefined
41
+ ? ""
42
+ : ` The oldest resolved file version is at sequence number ${baseResult.oldestResolvedSeq}.`;
43
+ throw new UsageError(`No ODSP file version is available at or before sequence number ${targetSequenceNumber}.${oldestResolvedSequenceDetail}`);
44
+ }
45
+ const recoverableResolvedUrl = await this.resolveFileVersion(resolvedUrl, baseResult.base.versionId);
46
+ const recoverableDocumentService = await this.createDocumentService(recoverableResolvedUrl, logger, clientIsSummarizer);
47
+ const liveDocumentService = await this.createDocumentService(resolvedUrl, logger, clientIsSummarizer);
48
+ return new OdspPointInTimeDocumentService(recoverableResolvedUrl, recoverableDocumentService, liveDocumentService, targetSequenceNumber);
49
+ }
50
+ /**
51
+ * Builds an IOdspVersionManager for the given file, which enumerates the file's stored
52
+ * versions and resolves the closest version at or before a target sequence number.
53
+ *
54
+ * @remarks
55
+ * This wires up the plumbing the version manager needs to talk to ODSP: it resolves the URL to
56
+ * its ODSP parts (site/drive/item), creates a scoped child logger, an epoch tracker (from a fresh
57
+ * NonPersistentCache, since only the tracker is needed for consistency checks), and an
58
+ * instrumented storage-token/auth-header fetcher. The resulting manager is used by
59
+ * {@link OdspPointInTimeDocumentServiceFactory.createPointInTimeDocumentService} to pick the base
60
+ * snapshot for point-in-time loading.
61
+ */
62
+ async createVersionManager(resolvedUrl, logger, clientIsSummarizer) {
63
+ const odspLogger = createOdspLogger(logger);
64
+ const extLogger = createChildLogger({ logger: odspLogger });
65
+ const odspResolvedUrl = getOdspResolvedUrl(resolvedUrl);
66
+ const urlParts = {
67
+ siteUrl: odspResolvedUrl.siteUrl,
68
+ driveId: odspResolvedUrl.driveId,
69
+ itemId: odspResolvedUrl.itemId,
70
+ };
71
+ // Only the epochTracker from the returned cacheAndTracker is used below, so a fresh
72
+ // NonPersistentCache is sufficient here.
73
+ const cacheAndTracker = createOdspCacheAndTracker(this.persistedCache, new NonPersistentCache(), {
74
+ resolvedUrl: odspResolvedUrl,
75
+ docId: odspResolvedUrl.hashedDocumentId,
76
+ fileVersion: odspResolvedUrl.fileVersion,
77
+ }, extLogger, clientIsSummarizer);
78
+ const getAuthHeader = toInstrumentedOdspStorageTokenFetcher(extLogger, urlParts, this.getStorageTokenForVersions);
79
+ return createOdspVersionManager({
80
+ urlParts,
81
+ getAuthHeader,
82
+ epochTracker: cacheAndTracker.epochTracker,
83
+ logger: extLogger,
84
+ });
85
+ }
86
+ async resolveFileVersion(resolvedUrl, fileVersion) {
87
+ const odspResolvedUrl = getOdspResolvedUrl(resolvedUrl);
88
+ const query = new URLSearchParams({
89
+ driveId: odspResolvedUrl.driveId,
90
+ itemId: odspResolvedUrl.itemId,
91
+ fileVersion,
92
+ });
93
+ if (odspResolvedUrl.dataStorePath !== undefined) {
94
+ query.set("path", odspResolvedUrl.dataStorePath);
95
+ }
96
+ if (odspResolvedUrl.codeHint?.containerPackageName !== undefined) {
97
+ query.set("containerPackageName", odspResolvedUrl.codeHint.containerPackageName);
98
+ }
99
+ return new OdspDriverUrlResolver().resolve({
100
+ url: `${odspResolvedUrl.siteUrl}?${query.toString()}`,
101
+ });
102
+ }
103
+ }
104
+ //# sourceMappingURL=odspPointInTimeDocumentServiceFactory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"odspPointInTimeDocumentServiceFactory.js","sourceRoot":"","sources":["../../src/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAcH,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,0CAA0C,CAAC;AAEzF,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,8BAA8B,EAAE,MAAM,sCAAsC,CAAC;AACtF,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EACN,gBAAgB,EAChB,kBAAkB,EAClB,qCAAqC,GACrC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACN,wBAAwB,GAExB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAE,8BAA8B,EAAE,MAAM,qCAAqC,CAAC;AAErF;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,qCAAsC,SAAQ,8BAA8B;IAMxF,YACC,eAA4D,EAC5D,iBAA0E,EAC1E,cAAgC,EAChC,UAA8B;QAE9B,KAAK,CAAC,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,UAAU,CAAC,CAAC;QACtE,IAAI,CAAC,0BAA0B,GAAG,eAAe,CAAC;IACnD,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,gCAAgC,CAC5C,WAAyB,EACzB,oBAA4B,EAC5B,MAA6B,EAC7B,kBAA4B;QAE5B,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,oBAAoB,CACrD,WAAW,EACX,MAAM,EACN,kBAAkB,CAClB,CAAC;QACF,MAAM,UAAU,GAAG,MAAM,cAAc,CAAC,cAAc,CAAC,oBAAoB,CAAC,CAAC;QAC7E,IAAI,UAAU,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;YACzC,MAAM,4BAA4B,GACjC,UAAU,CAAC,iBAAiB,KAAK,SAAS;gBACzC,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,2DAA2D,UAAU,CAAC,iBAAiB,GAAG,CAAC;YAC/F,MAAM,IAAI,UAAU,CACnB,kEAAkE,oBAAoB,IAAI,4BAA4B,EAAE,CACxH,CAAC;QACH,CAAC;QAED,MAAM,sBAAsB,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAC3D,WAAW,EACX,UAAU,CAAC,IAAI,CAAC,SAAS,CACzB,CAAC;QACF,MAAM,0BAA0B,GAAG,MAAM,IAAI,CAAC,qBAAqB,CAClE,sBAAsB,EACtB,MAAM,EACN,kBAAkB,CAClB,CAAC;QACF,MAAM,mBAAmB,GAAG,MAAM,IAAI,CAAC,qBAAqB,CAC3D,WAAW,EACX,MAAM,EACN,kBAAkB,CAClB,CAAC;QACF,OAAO,IAAI,8BAA8B,CACxC,sBAAsB,EACtB,0BAA0B,EAC1B,mBAAmB,EACnB,oBAAoB,CACpB,CAAC;IACH,CAAC;IAED;;;;;;;;;;;OAWG;IACK,KAAK,CAAC,oBAAoB,CACjC,WAAyB,EACzB,MAA6B,EAC7B,kBAA4B;QAE5B,MAAM,UAAU,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAC5C,MAAM,SAAS,GAAG,iBAAiB,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;QAC5D,MAAM,eAAe,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC;QACxD,MAAM,QAAQ,GAAkB;YAC/B,OAAO,EAAE,eAAe,CAAC,OAAO;YAChC,OAAO,EAAE,eAAe,CAAC,OAAO;YAChC,MAAM,EAAE,eAAe,CAAC,MAAM;SAC9B,CAAC;QACF,oFAAoF;QACpF,yCAAyC;QACzC,MAAM,eAAe,GAAG,yBAAyB,CAChD,IAAI,CAAC,cAAc,EACnB,IAAI,kBAAkB,EAAE,EACxB;YACC,WAAW,EAAE,eAAe;YAC5B,KAAK,EAAE,eAAe,CAAC,gBAAgB;YACvC,WAAW,EAAE,eAAe,CAAC,WAAW;SACxC,EACD,SAAS,EACT,kBAAkB,CAClB,CAAC;QACF,MAAM,aAAa,GAAG,qCAAqC,CAC1D,SAAS,EACT,QAAQ,EACR,IAAI,CAAC,0BAA0B,CAC/B,CAAC;QACF,OAAO,wBAAwB,CAAC;YAC/B,QAAQ;YACR,aAAa;YACb,YAAY,EAAE,eAAe,CAAC,YAAY;YAC1C,MAAM,EAAE,SAAS;SACjB,CAAC,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,kBAAkB,CAC/B,WAAyB,EACzB,WAAmB;QAEnB,MAAM,eAAe,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC;QACxD,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC;YACjC,OAAO,EAAE,eAAe,CAAC,OAAO;YAChC,MAAM,EAAE,eAAe,CAAC,MAAM;YAC9B,WAAW;SACX,CAAC,CAAC;QACH,IAAI,eAAe,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;YACjD,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,aAAa,CAAC,CAAC;QAClD,CAAC;QACD,IAAI,eAAe,CAAC,QAAQ,EAAE,oBAAoB,KAAK,SAAS,EAAE,CAAC;YAClE,KAAK,CAAC,GAAG,CAAC,sBAAsB,EAAE,eAAe,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAC;QAClF,CAAC;QACD,OAAO,IAAI,qBAAqB,EAAE,CAAC,OAAO,CAAC;YAC1C,GAAG,EAAE,GAAG,eAAe,CAAC,OAAO,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE;SACrD,CAAC,CAAC;IACJ,CAAC;CACD","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport type { ITelemetryBaseLogger } from \"@fluidframework/core-interfaces\";\nimport type {\n\tIDocumentService,\n\tIPersistedCache,\n\tIResolvedUrl,\n} from \"@fluidframework/driver-definitions/internal\";\nimport type {\n\tHostStoragePolicy,\n\tIOdspUrlParts,\n\tOdspResourceTokenFetchOptions,\n\tTokenFetcher,\n} from \"@fluidframework/odsp-driver-definitions/internal\";\nimport { UsageError, createChildLogger } from \"@fluidframework/telemetry-utils/internal\";\n\nimport { createOdspCacheAndTracker } from \"../epochTracker.js\";\nimport { NonPersistentCache } from \"../odspCache.js\";\nimport { OdspDocumentServiceFactoryCore } from \"../odspDocumentServiceFactoryCore.js\";\nimport { OdspDriverUrlResolver } from \"../odspDriverUrlResolver.js\";\nimport {\n\tcreateOdspLogger,\n\tgetOdspResolvedUrl,\n\ttoInstrumentedOdspStorageTokenFetcher,\n} from \"../odspUtils.js\";\nimport {\n\tcreateOdspVersionManager,\n\ttype IOdspVersionManager,\n} from \"../odspVersionManager/index.js\";\n\nimport { OdspPointInTimeDocumentService } from \"./odspPointInTimeDocumentService.js\";\n\n/**\n * ODSP document service factory that additionally supports point-in-time (sequence-number-based)\n * loading.\n *\n * @remarks\n * This extends {@link OdspDocumentServiceFactoryCore} with the ability to materialize a read-only\n * document service at a requested Fluid sequence number. The loader detects this capability via the\n * presence of {@link OdspPointInTimeDocumentServiceFactory.createPointInTimeDocumentService}, so\n * hosts that want to load a container to a target sequence number must construct this factory\n * (rather than the legacy `OdspDocumentServiceFactory`) and pass it to the loader.\n *\n * @internal\n */\nexport class OdspPointInTimeDocumentServiceFactory extends OdspDocumentServiceFactoryCore {\n\t/**\n\t * The storage token fetcher, captured here because the base class keeps it private.\n\t */\n\tprivate readonly getStorageTokenForVersions: TokenFetcher<OdspResourceTokenFetchOptions>;\n\n\tconstructor(\n\t\tgetStorageToken: TokenFetcher<OdspResourceTokenFetchOptions>,\n\t\tgetWebsocketToken: TokenFetcher<OdspResourceTokenFetchOptions> | undefined,\n\t\tpersistedCache?: IPersistedCache,\n\t\thostPolicy?: HostStoragePolicy,\n\t) {\n\t\tsuper(getStorageToken, getWebsocketToken, persistedCache, hostPolicy);\n\t\tthis.getStorageTokenForVersions = getStorageToken;\n\t}\n\n\t/**\n\t * Creates a document service that reads its snapshot from the closest file version at or before\n\t * the target and its deltas from the live document, materializing a requested sequence number\n\t * through replay.\n\t */\n\tpublic async createPointInTimeDocumentService(\n\t\tresolvedUrl: IResolvedUrl,\n\t\ttargetSequenceNumber: number,\n\t\tlogger?: ITelemetryBaseLogger,\n\t\tclientIsSummarizer?: boolean,\n\t): Promise<IDocumentService> {\n\t\tconst versionManager = await this.createVersionManager(\n\t\t\tresolvedUrl,\n\t\t\tlogger,\n\t\t\tclientIsSummarizer,\n\t\t);\n\t\tconst baseResult = await versionManager.findBaseForSeq(targetSequenceNumber);\n\t\tif (baseResult.kind === \"noBaseVersion\") {\n\t\t\tconst oldestResolvedSequenceDetail =\n\t\t\t\tbaseResult.oldestResolvedSeq === undefined\n\t\t\t\t\t? \"\"\n\t\t\t\t\t: ` The oldest resolved file version is at sequence number ${baseResult.oldestResolvedSeq}.`;\n\t\t\tthrow new UsageError(\n\t\t\t\t`No ODSP file version is available at or before sequence number ${targetSequenceNumber}.${oldestResolvedSequenceDetail}`,\n\t\t\t);\n\t\t}\n\n\t\tconst recoverableResolvedUrl = await this.resolveFileVersion(\n\t\t\tresolvedUrl,\n\t\t\tbaseResult.base.versionId,\n\t\t);\n\t\tconst recoverableDocumentService = await this.createDocumentService(\n\t\t\trecoverableResolvedUrl,\n\t\t\tlogger,\n\t\t\tclientIsSummarizer,\n\t\t);\n\t\tconst liveDocumentService = await this.createDocumentService(\n\t\t\tresolvedUrl,\n\t\t\tlogger,\n\t\t\tclientIsSummarizer,\n\t\t);\n\t\treturn new OdspPointInTimeDocumentService(\n\t\t\trecoverableResolvedUrl,\n\t\t\trecoverableDocumentService,\n\t\t\tliveDocumentService,\n\t\t\ttargetSequenceNumber,\n\t\t);\n\t}\n\n\t/**\n\t * Builds an IOdspVersionManager for the given file, which enumerates the file's stored\n\t * versions and resolves the closest version at or before a target sequence number.\n\t *\n\t * @remarks\n\t * This wires up the plumbing the version manager needs to talk to ODSP: it resolves the URL to\n\t * its ODSP parts (site/drive/item), creates a scoped child logger, an epoch tracker (from a fresh\n\t * NonPersistentCache, since only the tracker is needed for consistency checks), and an\n\t * instrumented storage-token/auth-header fetcher. The resulting manager is used by\n\t * {@link OdspPointInTimeDocumentServiceFactory.createPointInTimeDocumentService} to pick the base\n\t * snapshot for point-in-time loading.\n\t */\n\tprivate async createVersionManager(\n\t\tresolvedUrl: IResolvedUrl,\n\t\tlogger?: ITelemetryBaseLogger,\n\t\tclientIsSummarizer?: boolean,\n\t): Promise<IOdspVersionManager> {\n\t\tconst odspLogger = createOdspLogger(logger);\n\t\tconst extLogger = createChildLogger({ logger: odspLogger });\n\t\tconst odspResolvedUrl = getOdspResolvedUrl(resolvedUrl);\n\t\tconst urlParts: IOdspUrlParts = {\n\t\t\tsiteUrl: odspResolvedUrl.siteUrl,\n\t\t\tdriveId: odspResolvedUrl.driveId,\n\t\t\titemId: odspResolvedUrl.itemId,\n\t\t};\n\t\t// Only the epochTracker from the returned cacheAndTracker is used below, so a fresh\n\t\t// NonPersistentCache is sufficient here.\n\t\tconst cacheAndTracker = createOdspCacheAndTracker(\n\t\t\tthis.persistedCache,\n\t\t\tnew NonPersistentCache(),\n\t\t\t{\n\t\t\t\tresolvedUrl: odspResolvedUrl,\n\t\t\t\tdocId: odspResolvedUrl.hashedDocumentId,\n\t\t\t\tfileVersion: odspResolvedUrl.fileVersion,\n\t\t\t},\n\t\t\textLogger,\n\t\t\tclientIsSummarizer,\n\t\t);\n\t\tconst getAuthHeader = toInstrumentedOdspStorageTokenFetcher(\n\t\t\textLogger,\n\t\t\turlParts,\n\t\t\tthis.getStorageTokenForVersions,\n\t\t);\n\t\treturn createOdspVersionManager({\n\t\t\turlParts,\n\t\t\tgetAuthHeader,\n\t\t\tepochTracker: cacheAndTracker.epochTracker,\n\t\t\tlogger: extLogger,\n\t\t});\n\t}\n\n\tprivate async resolveFileVersion(\n\t\tresolvedUrl: IResolvedUrl,\n\t\tfileVersion: string,\n\t): Promise<IResolvedUrl> {\n\t\tconst odspResolvedUrl = getOdspResolvedUrl(resolvedUrl);\n\t\tconst query = new URLSearchParams({\n\t\t\tdriveId: odspResolvedUrl.driveId,\n\t\t\titemId: odspResolvedUrl.itemId,\n\t\t\tfileVersion,\n\t\t});\n\t\tif (odspResolvedUrl.dataStorePath !== undefined) {\n\t\t\tquery.set(\"path\", odspResolvedUrl.dataStorePath);\n\t\t}\n\t\tif (odspResolvedUrl.codeHint?.containerPackageName !== undefined) {\n\t\t\tquery.set(\"containerPackageName\", odspResolvedUrl.codeHint.containerPackageName);\n\t\t}\n\t\treturn new OdspDriverUrlResolver().resolve({\n\t\t\turl: `${odspResolvedUrl.siteUrl}?${query.toString()}`,\n\t\t});\n\t}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/odsp-driver",
3
- "version": "2.112.0",
3
+ "version": "2.113.0-411909",
4
4
  "description": "Socket storage implementation for SPO and ODC",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -69,22 +69,22 @@
69
69
  "temp-directory": "nyc/.nyc_output"
70
70
  },
71
71
  "dependencies": {
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",
72
+ "@fluid-internal/client-utils": "2.113.0-411909",
73
+ "@fluidframework/core-interfaces": "2.113.0-411909",
74
+ "@fluidframework/core-utils": "2.113.0-411909",
75
+ "@fluidframework/driver-base": "2.113.0-411909",
76
+ "@fluidframework/driver-definitions": "2.113.0-411909",
77
+ "@fluidframework/driver-utils": "2.113.0-411909",
78
+ "@fluidframework/odsp-doclib-utils": "2.113.0-411909",
79
+ "@fluidframework/odsp-driver-definitions": "2.113.0-411909",
80
+ "@fluidframework/telemetry-utils": "2.113.0-411909",
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.112.0",
87
+ "@fluid-internal/mocha-test-setup": "2.113.0-411909",
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",
package/src/index.ts CHANGED
@@ -29,6 +29,8 @@ export {
29
29
  OdspDocumentServiceFactory,
30
30
  } from "./odspDocumentServiceFactory.js";
31
31
  export { OdspDocumentServiceFactoryCore } from "./odspDocumentServiceFactoryCore.js";
32
+ // eslint-disable-next-line import-x/no-internal-modules
33
+ export { OdspPointInTimeDocumentServiceFactory } from "./pointInTimeDriver/odspPointInTimeDocumentServiceFactory.js";
32
34
 
33
35
  // File creation
34
36
  export { createOdspCreateContainerRequest } from "./createOdspCreateContainerRequest.js";
@@ -35,8 +35,11 @@ Part 1 is built in three components:
35
35
  - **Component A — the version manager**: choose which file version to load or replay from. **This
36
36
  folder is Component A**, and this document is mostly about it.
37
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.
38
+ target. **Built** in `../pointInTimeDriver/` (`OdspPointInTimeDocumentServiceFactory` /
39
+ `OdspPointInTimeDocumentService`)see [Part V](#part-v--components-b--c-as-built).
40
+ - **Component C — the loader hookup**: expose Component B through the container loader. **Built** in
41
+ `@fluidframework/container-loader` (`loadContainerToSequenceNumber`) — see
42
+ [Part V](#part-v--components-b--c-as-built).
40
43
 
41
44
  ## Part I — Foundations
42
45
 
@@ -79,10 +82,13 @@ driver's normal (`application/json` or `application/ms-fluid`) framing. The driv
79
82
  parser reads it, and the sequence number is `trees[0].sequenceNumber`. `blobs=2` inlines blob contents
80
83
  so the parser has everything it needs.
81
84
 
82
- ### What is deliberately not built here?
85
+ ### What is deliberately not built in *this folder*?
83
86
 
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).
87
+ Components B and C now exist, but elsewhere: the recomposed driver in `../pointInTimeDriver/` and the
88
+ loader hookup in `@fluidframework/container-loader` (see
89
+ [Part V](#part-v--components-b--c-as-built)). This folder (Component A) still owns only base selection.
90
+ Not built anywhere yet: bridging a trimmed op range via an intermediate snapshot, and a test against a
91
+ live ODSP file. See [Part IV](#part-iv--directional).
86
92
 
87
93
  ## Part II — The Version Manager
88
94
 
@@ -215,25 +221,13 @@ comparison. It also allows locating a version by time when no sequence number is
215
221
 
216
222
 
217
223
 
218
- ### Should there be a component that loads the base and replays ops to the exact target? (Component B)
224
+ ### How would Component B bridge a *trimmed* op range between snapshots?
219
225
 
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?
226
+ Component B is built (see [Part V](#part-v--components-b--c-as-built)), but the version it ships makes
227
+ one simplifying assumption: it loads a single base file version and replays the ops in `(base, target]`
228
+ from the **live** document's delta storage. That assumption holds only while those ops are still
229
+ retained. Bridging a *trimmed* range by starting from a newer intermediate snapshot is the part that is
230
+ not built yet the rest of this answer is its design.
237
231
 
238
232
  A snapshot already contains the full accumulated state at its sequence number — every op at or below it
239
233
  is baked in. So to reach a target `T`, Component B loads the closest base snapshot (`seq ≤ T`) and
@@ -259,12 +253,6 @@ baked into a snapshot, used when a snapshot is loaded, not an indicator of which
259
253
  retains. Op availability is determined by asking the op stream for the range, not by a version's minimum
260
254
  sequence number.
261
255
 
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
256
  ### Should there be an end-to-end test against a real ODSP file?
269
257
 
270
258
  The fetcher is covered by stubbed-`fetch` integration tests, but not against a live file (which needs
@@ -275,3 +263,87 @@ tenant credentials). An end-to-end test would exercise the real endpoints.
275
263
  The `/content` download also contains a version's snapshot, but wrapped in a container framing the
276
264
  snapshot parser does not read directly. If the version-scoped snapshot endpoint is ever unavailable,
277
265
  unwrapping `/content` could be a fallback path.
266
+
267
+ ## Part V — Components B & C, as built
268
+
269
+ Component A (this folder) only selects the base. Components B and C — which materialize the document at
270
+ the target and expose it through the loader — are now built, in other files. They carry no catechism
271
+ code IDs here because their are no tests at the moment; these are
272
+ conceptual answers in the spirit of [Part I](#part-i--foundations). The one still-directional gap is
273
+ bridging a *trimmed* op range via an intermediate snapshot (see
274
+ [Part IV](#part-iv--directional)); everything below is what ships today.
275
+
276
+ ### Component B — how does the recomposed driver materialize the target?
277
+
278
+ `OdspPointInTimeDocumentServiceFactory` (in `../pointInTimeDriver/`) extends
279
+ `OdspDocumentServiceFactoryCore` and adds `createPointInTimeDocumentService(resolvedUrl, targetSequenceNumber)`:
280
+
281
+ 1. Build a version manager (Component A) and call `findBaseForSeq(target)`. A `noBaseVersion` result
282
+ becomes a `UsageError` naming the target and the oldest resolved sequence number.
283
+ 2. Resolve the chosen file version into a version-scoped resolved URL, then create two ordinary ODSP
284
+ document services: a **recoverable** one bound to that base version (its storage is the base
285
+ snapshot) and a **live** one (its delta storage supplies the ops to replay).
286
+ 3. Return an `OdspPointInTimeDocumentService` composing the two.
287
+
288
+ It lives in this package rather than a generic wrapping driver (e.g. `@fluidframework/replay-driver`)
289
+ because loading a historical file version is a storage-layer concern: it needs the version-scoped
290
+ snapshot fetch, the epoch tracker, and authentication — all internal to this driver — and it consumes
291
+ the version manager in-package, so the manager itself needs no exported surface.
292
+
293
+ ### Component B — which `IDocumentService` method drives the replay?
294
+
295
+ `OdspPointInTimeDocumentService` is read-only and advertises the `storageOnly` document-service policy.
296
+ Its three `IDocumentService` methods:
297
+
298
+ - `connectToStorage` → the recoverable (base-version) service's storage: the base snapshot.
299
+ - `connectToDeltaStorage` → wraps the **live** service's delta storage and clamps every
300
+ **`fetchMessages(from, to, …)`** call to an exclusive upper bound of `targetSequenceNumber + 1`, so no
301
+ op past the target is ever fetched. **`fetchMessages` is the method that drives the bounded replay.**
302
+ - `connectToDeltaStream` → throws: under `storageOnly` the connection manager synthesizes a frozen,
303
+ read-only delta stream instead of opening a live socket, so this is never called under normal flow.
304
+
305
+ The `storageOnly` policy is the key mechanism: it forces the container read-only and reuses the loader's
306
+ existing "frozen" delta stream, and the delta manager then catches up from the base snapshot's sequence
307
+ number through delta storage — the bounded `fetchMessages` replay — up to and including the target op.
308
+
309
+ ### Component B — what request does the bounded `fetchMessages` actually make?
310
+
311
+ The point-in-time service builds no URL of its own: `connectToDeltaStorage` wraps the **live** service's
312
+ delta storage and only clamps the `to` argument (`Math.min(to, targetSequenceNumber + 1)`). Everything
313
+ below is the ordinary ODSP delta path (`OdspDeltaStorageWithCache` → `OdspDeltaStorageService`), just
314
+ range-constrained by that clamp.
315
+
316
+ `OdspDeltaStorageWithCache.fetchMessages` is a **paged stream**, not a single request: via `requestOps`
317
+ it walks the requested `[from, to)` in batches, checking three sources in order — ops bundled with the
318
+ base snapshot, then the cache, then network storage — so the clamp guarantees no page is ever requested
319
+ past the target.
320
+
321
+ The network leg (`OdspDeltaStorageService.get`) is where the request is constructed:
322
+
323
+ - **URL** (`buildUrl`): `${deltaStorageUrl}?ump=1&filter=` + `encodeURIComponent("sequenceNumber ge {from} and sequenceNumber le {to - 1}")`.
324
+ `deltaStorageUrl` is `.../drives/{driveId}/items/{itemId}/opStream`. Because `from` is inclusive and
325
+ `to` exclusive, the filter is `ge {from} and le {to - 1}`; with the clamped `to = target + 1` the
326
+ effective server bound is `sequenceNumber le target` — the target op is included, nothing beyond it.
327
+ - **Method & body**: despite fetching ops it issues a **`POST`** carrying `X-HTTP-Method-Override: GET`,
328
+ encoded as `multipart/form-data` (the `ump=1` "unified multipart" framing). The auth token rides in the
329
+ form body (`Authorization: {authHeader}` / `_post: 1`), not a header.
330
+ - **Plumbing**: the call goes through the `epochTracker` (epoch/consistency checks) and
331
+ `getWithRetryForTokenRefresh` (one token-refresh retry), with a 30s `AbortController` timeout as a
332
+ hang mitigation.
333
+
334
+ So the target bound flows `target + 1` → `Math.min` clamp → stream page `to` → `le {to - 1}` filter,
335
+ and the `opStream` endpoint is queried for exactly `[from, target]`.
336
+
337
+ ### Component C — how is this exposed through the loader?
338
+
339
+ `loadContainerToSequenceNumber` (in `@fluidframework/container-loader`):
340
+
341
+ 1. Validates `loadToSequenceNumber` is a non-negative integer (`UsageError` otherwise).
342
+ 2. Detects the point-in-time capability with `asPointInTimeCapableFactory`, which checks the passed
343
+ `documentServiceFactory` exposes `createPointInTimeDocumentService`. A plain factory is a
344
+ `UsageError` — the caller must pass `OdspPointInTimeDocumentServiceFactory` directly, with no wrapping.
345
+ 3. Wraps it in a `PointInTimeDocumentServiceFactory` adapter so the container's normal
346
+ `createDocumentService(resolvedUrl)` routes to `createPointInTimeDocumentService(resolvedUrl, target)`.
347
+ (`createContainer` throws — the adapter is load-only.)
348
+ 4. Delegates to `loadContainerPaused(...)` with inbound/outbound processing paused, returning a
349
+ disconnected, read-only historical view of the container at the target sequence number.
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/odsp-driver";
9
- export const pkgVersion = "2.112.0";
9
+ export const pkgVersion = "2.113.0-411909";
@@ -0,0 +1,94 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { TypedEventEmitter } from "@fluid-internal/client-utils";
7
+ import type {
8
+ IClient,
9
+ IDocumentDeltaConnection,
10
+ IDocumentDeltaStorageService,
11
+ IDocumentService,
12
+ IDocumentServiceEvents,
13
+ IDocumentServicePolicies,
14
+ IDocumentStorageService,
15
+ IResolvedUrl,
16
+ } from "@fluidframework/driver-definitions/internal";
17
+
18
+ /**
19
+ * A read-only document service that materializes a document at a target sequence number by combining
20
+ * a recoverable snapshot with a bounded replay of live ops.
21
+ *
22
+ * @remarks
23
+ * Storage (the snapshot) is served from the closest file version at or before the target sequence
24
+ * number. The ops needed to advance from that snapshot to the target are read from the live
25
+ * document's delta storage, bounded so that no op past the target is ever fetched.
26
+ *
27
+ * The service advertises the {@link IDocumentServicePolicies.storageOnly} policy. This reuses the
28
+ * loader's "frozen" load mechanism: the connection manager synthesizes a read-only frozen delta
29
+ * stream instead of opening a live socket, and forces the container read-only. The delta manager
30
+ * still catches up from the snapshot's sequence number through delta storage, which is exactly the
31
+ * bounded replay we want. As a result no live delta-stream connection is ever established.
32
+ *
33
+ * @internal
34
+ */
35
+ export class OdspPointInTimeDocumentService
36
+ extends TypedEventEmitter<IDocumentServiceEvents>
37
+ implements IDocumentService
38
+ {
39
+ public constructor(
40
+ public readonly resolvedUrl: IResolvedUrl,
41
+ // Serves the snapshot: a read-only document service bound to the closest file version at or
42
+ // before the target sequence number. Its storage is the base we replay ops on top of.
43
+ private readonly recoverableDocumentService: IDocumentService,
44
+ private readonly liveDocumentService: IDocumentService,
45
+ private readonly targetSequenceNumber: number,
46
+ ) {
47
+ super();
48
+ this.liveDocumentService.on("metadataUpdate", this.metadataUpdateHandler);
49
+ }
50
+
51
+ // storageOnly makes the connection manager synthesize a read-only frozen delta stream (no live
52
+ // socket) and force the container read-only - see the class remarks.
53
+ public readonly policies: IDocumentServicePolicies = { storageOnly: true };
54
+
55
+ public dispose(): void {
56
+ this.liveDocumentService.off("metadataUpdate", this.metadataUpdateHandler);
57
+ this.recoverableDocumentService.dispose();
58
+ this.liveDocumentService.dispose();
59
+ }
60
+
61
+ public async connectToStorage(): Promise<IDocumentStorageService> {
62
+ return this.recoverableDocumentService.connectToStorage();
63
+ }
64
+
65
+ public async connectToDeltaStorage(): Promise<IDocumentDeltaStorageService> {
66
+ const liveDeltaStorage = await this.liveDocumentService.connectToDeltaStorage();
67
+ // The exclusive upper bound needed to include the target op itself.
68
+ const boundedTo = this.targetSequenceNumber + 1;
69
+ return {
70
+ fetchMessages: (from, to, abortSignal, cachedOnly, fetchReason) => {
71
+ return liveDeltaStorage.fetchMessages(
72
+ from,
73
+ to === undefined ? boundedTo : Math.min(to, boundedTo),
74
+ abortSignal,
75
+ cachedOnly,
76
+ fetchReason,
77
+ );
78
+ },
79
+ };
80
+ }
81
+
82
+ public async connectToDeltaStream(_client: IClient): Promise<IDocumentDeltaConnection> {
83
+ // Unreachable under normal flow: the connection manager short-circuits on the storageOnly
84
+ // policy and synthesizes a frozen delta stream before ever calling connectToDeltaStream.
85
+ // Reaching here indicates a regression of that short-circuit.
86
+ throw new Error(
87
+ "OdspPointInTimeDocumentService is storage-only; connectToDeltaStream should not be called",
88
+ );
89
+ }
90
+
91
+ private readonly metadataUpdateHandler = (metadata: Record<string, string>): void => {
92
+ this.emit("metadataUpdate", metadata);
93
+ };
94
+ }