@fluidframework/odsp-driver 2.112.0 → 2.113.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/pointInTimeDriver/odspPointInTimeDocumentService.d.ts +37 -0
- package/dist/pointInTimeDriver/odspPointInTimeDocumentService.d.ts.map +1 -0
- package/dist/pointInTimeDriver/odspPointInTimeDocumentService.js +70 -0
- package/dist/pointInTimeDriver/odspPointInTimeDocumentService.js.map +1 -0
- package/dist/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.d.ts +49 -0
- package/dist/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.d.ts.map +1 -0
- package/dist/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.js +108 -0
- package/dist/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pointInTimeDriver/odspPointInTimeDocumentService.d.ts +37 -0
- package/lib/pointInTimeDriver/odspPointInTimeDocumentService.d.ts.map +1 -0
- package/lib/pointInTimeDriver/odspPointInTimeDocumentService.js +66 -0
- package/lib/pointInTimeDriver/odspPointInTimeDocumentService.js.map +1 -0
- package/lib/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.d.ts +49 -0
- package/lib/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.d.ts.map +1 -0
- package/lib/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.js +104 -0
- package/lib/pointInTimeDriver/odspPointInTimeDocumentServiceFactory.js.map +1 -0
- package/package.json +11 -11
- package/src/index.ts +2 -0
- package/src/odspVersionManager/DEV.md +101 -29
- package/src/packageVersion.ts +1 -1
- package/src/pointInTimeDriver/odspPointInTimeDocumentService.ts +94 -0
- 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":"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","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.
|
|
3
|
+
"version": "2.113.1",
|
|
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.
|
|
73
|
-
"@fluidframework/core-interfaces": "~2.
|
|
74
|
-
"@fluidframework/core-utils": "~2.
|
|
75
|
-
"@fluidframework/driver-base": "~2.
|
|
76
|
-
"@fluidframework/driver-definitions": "~2.
|
|
77
|
-
"@fluidframework/driver-utils": "~2.
|
|
78
|
-
"@fluidframework/odsp-doclib-utils": "~2.
|
|
79
|
-
"@fluidframework/odsp-driver-definitions": "~2.
|
|
80
|
-
"@fluidframework/telemetry-utils": "~2.
|
|
72
|
+
"@fluid-internal/client-utils": "~2.113.1",
|
|
73
|
+
"@fluidframework/core-interfaces": "~2.113.1",
|
|
74
|
+
"@fluidframework/core-utils": "~2.113.1",
|
|
75
|
+
"@fluidframework/driver-base": "~2.113.1",
|
|
76
|
+
"@fluidframework/driver-definitions": "~2.113.1",
|
|
77
|
+
"@fluidframework/driver-utils": "~2.113.1",
|
|
78
|
+
"@fluidframework/odsp-doclib-utils": "~2.113.1",
|
|
79
|
+
"@fluidframework/odsp-driver-definitions": "~2.113.1",
|
|
80
|
+
"@fluidframework/telemetry-utils": "~2.113.1",
|
|
81
81
|
"socket.io-client": "^4.8.3",
|
|
82
82
|
"uuid": "^11.1.0"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@arethetypeswrong/cli": "^0.18.2",
|
|
86
86
|
"@biomejs/biome": "~2.4.5",
|
|
87
|
-
"@fluid-internal/mocha-test-setup": "~2.
|
|
87
|
+
"@fluid-internal/mocha-test-setup": "~2.113.1",
|
|
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.
|
|
39
|
-
|
|
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
|
|
85
|
+
### What is deliberately not built in *this folder*?
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
###
|
|
224
|
+
### How would Component B bridge a *trimmed* op range between snapshots?
|
|
219
225
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
the live
|
|
223
|
-
|
|
224
|
-
|
|
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.
|
package/src/packageVersion.ts
CHANGED
|
@@ -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
|
+
}
|