@fluidframework/container-loader 2.102.0 → 2.110.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/api-report/container-loader.legacy.alpha.api.md +21 -8
- package/api-report/container-loader.legacy.beta.api.md +19 -6
- package/dist/connectionManager.d.ts +2 -2
- package/dist/connectionManager.d.ts.map +1 -1
- package/dist/connectionManager.js.map +1 -1
- package/dist/connectionStateHandler.d.ts +3 -3
- package/dist/connectionStateHandler.d.ts.map +1 -1
- package/dist/connectionStateHandler.js.map +1 -1
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +7 -3
- package/dist/container.js.map +1 -1
- package/dist/containerContext.d.ts +3 -3
- package/dist/containerContext.d.ts.map +1 -1
- package/dist/containerContext.js.map +1 -1
- package/dist/containerStorageAdapter.d.ts +2 -2
- package/dist/containerStorageAdapter.d.ts.map +1 -1
- package/dist/containerStorageAdapter.js.map +1 -1
- package/dist/createAndLoadContainerUtils.d.ts +111 -19
- package/dist/createAndLoadContainerUtils.d.ts.map +1 -1
- package/dist/createAndLoadContainerUtils.js +101 -19
- package/dist/createAndLoadContainerUtils.js.map +1 -1
- package/dist/debugLogger.d.ts +2 -2
- package/dist/debugLogger.d.ts.map +1 -1
- package/dist/debugLogger.js.map +1 -1
- package/dist/deltaManager.d.ts +2 -2
- package/dist/deltaManager.d.ts.map +1 -1
- package/dist/deltaManager.js.map +1 -1
- package/dist/error.d.ts +2 -2
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js.map +1 -1
- package/dist/frozenServices.d.ts.map +1 -1
- package/dist/frozenServices.js +17 -4
- package/dist/frozenServices.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/legacy.d.ts +3 -0
- package/dist/legacyAlpha.d.ts +3 -0
- package/dist/loaderLayerCompatState.d.ts +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/retriableDocumentStorageService.d.ts +2 -2
- package/dist/retriableDocumentStorageService.d.ts.map +1 -1
- package/dist/retriableDocumentStorageService.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +13 -1
- package/dist/utils.js.map +1 -1
- package/lib/connectionManager.d.ts +2 -2
- package/lib/connectionManager.d.ts.map +1 -1
- package/lib/connectionManager.js +1 -1
- package/lib/connectionManager.js.map +1 -1
- package/lib/connectionStateHandler.d.ts +3 -3
- package/lib/connectionStateHandler.d.ts.map +1 -1
- package/lib/connectionStateHandler.js.map +1 -1
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +7 -3
- package/lib/container.js.map +1 -1
- package/lib/containerContext.d.ts +3 -3
- package/lib/containerContext.d.ts.map +1 -1
- package/lib/containerContext.js.map +1 -1
- package/lib/containerStorageAdapter.d.ts +2 -2
- package/lib/containerStorageAdapter.d.ts.map +1 -1
- package/lib/containerStorageAdapter.js.map +1 -1
- package/lib/createAndLoadContainerUtils.d.ts +111 -19
- package/lib/createAndLoadContainerUtils.d.ts.map +1 -1
- package/lib/createAndLoadContainerUtils.js +85 -3
- package/lib/createAndLoadContainerUtils.js.map +1 -1
- package/lib/debugLogger.d.ts +2 -2
- package/lib/debugLogger.d.ts.map +1 -1
- package/lib/debugLogger.js.map +1 -1
- package/lib/deltaManager.d.ts +2 -2
- package/lib/deltaManager.d.ts.map +1 -1
- package/lib/deltaManager.js +1 -1
- package/lib/deltaManager.js.map +1 -1
- package/lib/error.d.ts +2 -2
- package/lib/error.d.ts.map +1 -1
- package/lib/error.js.map +1 -1
- package/lib/frozenServices.d.ts.map +1 -1
- package/lib/frozenServices.js +17 -4
- package/lib/frozenServices.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/legacy.d.ts +3 -0
- package/lib/legacyAlpha.d.ts +3 -0
- package/lib/loaderLayerCompatState.d.ts +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/retriableDocumentStorageService.d.ts +2 -2
- package/lib/retriableDocumentStorageService.d.ts.map +1 -1
- package/lib/retriableDocumentStorageService.js.map +1 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +13 -1
- package/lib/utils.js.map +1 -1
- package/package.json +42 -14
- package/src/connectionManager.ts +4 -4
- package/src/connectionStateHandler.ts +4 -4
- package/src/container.ts +8 -3
- package/src/containerContext.ts +3 -3
- package/src/containerStorageAdapter.ts +3 -3
- package/src/createAndLoadContainerUtils.ts +227 -24
- package/src/debugLogger.ts +3 -3
- package/src/deltaManager.ts +7 -7
- package/src/error.ts +2 -2
- package/src/frozenServices.ts +22 -8
- package/src/index.ts +3 -0
- package/src/packageVersion.ts +1 -1
- package/src/retriableDocumentStorageService.ts +2 -2
- package/src/utils.ts +12 -1
package/src/container.ts
CHANGED
|
@@ -963,9 +963,7 @@ export class Container
|
|
|
963
963
|
|
|
964
964
|
const offlineLoadEnabled =
|
|
965
965
|
this.isInteractiveClient &&
|
|
966
|
-
(this.mc.config.getBoolean("Fluid.Container.
|
|
967
|
-
this.mc.config.getBoolean("Fluid.Container.enableOfflineFull") ??
|
|
968
|
-
options.enableOfflineLoad !== false);
|
|
966
|
+
(this.mc.config.getBoolean("Fluid.Container.enableOfflineFull") ?? true);
|
|
969
967
|
this.serializedStateManager = new SerializedStateManager(
|
|
970
968
|
this.subLogger,
|
|
971
969
|
this.storageAdapter,
|
|
@@ -1592,6 +1590,7 @@ export class Container
|
|
|
1592
1590
|
version: string | undefined;
|
|
1593
1591
|
dmLastProcessedSeqNumber: number;
|
|
1594
1592
|
dmLastKnownSeqNumber: number;
|
|
1593
|
+
numUnsummarizedOps: number;
|
|
1595
1594
|
}> {
|
|
1596
1595
|
const timings: Record<string, number> = { phase1: performanceNow() };
|
|
1597
1596
|
this.service = await this.createDocumentService(resolvedUrl, { mode: "load" });
|
|
@@ -1759,6 +1758,12 @@ export class Container
|
|
|
1759
1758
|
version: version?.id,
|
|
1760
1759
|
dmLastProcessedSeqNumber: this._deltaManager.lastSequenceNumber,
|
|
1761
1760
|
dmLastKnownSeqNumber: this._deltaManager.lastKnownSeqNumber,
|
|
1761
|
+
// Ops known since the last summary (including queued/unprocessed). The loaded snapshot's
|
|
1762
|
+
// sequence number corresponds to the last summary's reference sequence number under normal
|
|
1763
|
+
// "latest" load paths (the runtime asserts this match unless pendingLocalState is used or
|
|
1764
|
+
// sequence number verification is bypassed), so this approximates numUnsummarizedOps at
|
|
1765
|
+
// the loader level without requiring access to runtime summary metadata.
|
|
1766
|
+
numUnsummarizedOps: this._deltaManager.lastKnownSeqNumber - attributes.sequenceNumber,
|
|
1762
1767
|
};
|
|
1763
1768
|
}
|
|
1764
1769
|
|
package/src/containerContext.ts
CHANGED
|
@@ -32,7 +32,7 @@ import type {
|
|
|
32
32
|
MessageType,
|
|
33
33
|
ISequencedDocumentMessage,
|
|
34
34
|
} from "@fluidframework/driver-definitions/internal";
|
|
35
|
-
import type {
|
|
35
|
+
import type { TelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal";
|
|
36
36
|
|
|
37
37
|
import type { ConnectionState } from "./connectionState.js";
|
|
38
38
|
import { loaderCompatDetailsForRuntime } from "./loaderLayerCompatState.js";
|
|
@@ -69,7 +69,7 @@ export interface IContainerContextConfig
|
|
|
69
69
|
readonly getAttachState: () => AttachState;
|
|
70
70
|
readonly getConnected: () => boolean;
|
|
71
71
|
readonly existing: boolean;
|
|
72
|
-
readonly taggedLogger:
|
|
72
|
+
readonly taggedLogger: TelemetryLoggerExt;
|
|
73
73
|
// This "overrides" IContainerContext.snapshotWithContents to be required but allow `undefined`.
|
|
74
74
|
readonly snapshotWithContents: IContainerContext["snapshotWithContents"] | undefined;
|
|
75
75
|
}
|
|
@@ -136,7 +136,7 @@ export class ContainerContext
|
|
|
136
136
|
public readonly getAbsoluteUrl: (relativeUrl: string) => Promise<string | undefined>;
|
|
137
137
|
public readonly clientDetails: IClientDetails;
|
|
138
138
|
public readonly existing: boolean;
|
|
139
|
-
public readonly taggedLogger:
|
|
139
|
+
public readonly taggedLogger: TelemetryLoggerExt;
|
|
140
140
|
public readonly pendingLocalState: unknown;
|
|
141
141
|
public readonly snapshotWithContents?: ISnapshot;
|
|
142
142
|
|
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
IVersion,
|
|
25
25
|
} from "@fluidframework/driver-definitions/internal";
|
|
26
26
|
import { isInstanceOfISnapshot, UsageError } from "@fluidframework/driver-utils/internal";
|
|
27
|
-
import type {
|
|
27
|
+
import type { TelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal";
|
|
28
28
|
|
|
29
29
|
import type { MemoryDetachedBlobStorage } from "./memoryBlobStorage.js";
|
|
30
30
|
import { ProtocolTreeStorageService } from "./protocolTreeDocumentStorageService.js";
|
|
@@ -107,7 +107,7 @@ export class ContainerStorageAdapter
|
|
|
107
107
|
*/
|
|
108
108
|
public constructor(
|
|
109
109
|
detachedBlobStorage: MemoryDetachedBlobStorage | undefined,
|
|
110
|
-
private readonly logger:
|
|
110
|
+
private readonly logger: TelemetryLoggerExt,
|
|
111
111
|
private loadingGroupIdSnapshotsFromPendingState:
|
|
112
112
|
| Record<string, SerializedSnapshotInfo>
|
|
113
113
|
| undefined,
|
|
@@ -276,7 +276,7 @@ export class ContainerStorageAdapter
|
|
|
276
276
|
class BlobOnlyStorage implements IDocumentStorageService {
|
|
277
277
|
constructor(
|
|
278
278
|
private readonly detachedStorage: MemoryDetachedBlobStorage | undefined,
|
|
279
|
-
private readonly logger:
|
|
279
|
+
private readonly logger: TelemetryLoggerExt,
|
|
280
280
|
) {}
|
|
281
281
|
|
|
282
282
|
public async createBlob(content: ArrayBufferLike): Promise<ICreateBlobResponse> {
|
|
@@ -16,9 +16,12 @@ import type {
|
|
|
16
16
|
IRequest,
|
|
17
17
|
ITelemetryBaseLogger,
|
|
18
18
|
} from "@fluidframework/core-interfaces";
|
|
19
|
+
import type { AllOrNone } from "@fluidframework/core-interfaces/internal";
|
|
20
|
+
import { validateAllOrNone } from "@fluidframework/core-utils/internal";
|
|
19
21
|
import type { IClientDetails } from "@fluidframework/driver-definitions";
|
|
20
22
|
import type {
|
|
21
23
|
IDocumentServiceFactory,
|
|
24
|
+
IResolvedUrl,
|
|
22
25
|
ISequencedDocumentMessage,
|
|
23
26
|
ISnapshot,
|
|
24
27
|
ISnapshotTree,
|
|
@@ -59,7 +62,11 @@ import type {
|
|
|
59
62
|
OnDemandSummaryResults,
|
|
60
63
|
SummarizeOnDemandResults,
|
|
61
64
|
} from "./summarizerResultTypes.js";
|
|
62
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
getAttachedContainerStateFromSerializedContainer,
|
|
67
|
+
getDocumentAttributes,
|
|
68
|
+
tryParseCompatibleResolvedUrl,
|
|
69
|
+
} from "./utils.js";
|
|
63
70
|
|
|
64
71
|
interface OnDemandSummarizeResultsPromises {
|
|
65
72
|
readonly summarySubmitted: Promise<SummarizeOnDemandResults["summarySubmitted"]>;
|
|
@@ -78,22 +85,17 @@ interface SummarizerLike {
|
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
/**
|
|
81
|
-
*
|
|
88
|
+
* Host-level container loader properties — the code loader plus all the
|
|
89
|
+
* optional policy / observability fields that aren't tied to driver wiring.
|
|
90
|
+
*
|
|
91
|
+
* @remarks
|
|
92
|
+
* Extracted as a reusable building block so other props types (create,
|
|
93
|
+
* rehydrate, load, frozen-load) can compose it without duplicating the
|
|
94
|
+
* optional-fields surface.
|
|
95
|
+
*
|
|
82
96
|
* @legacy @beta
|
|
83
97
|
*/
|
|
84
|
-
export interface
|
|
85
|
-
/**
|
|
86
|
-
* The url resolver used by the loader for resolving external urls
|
|
87
|
-
* into Fluid urls such that the container specified by the
|
|
88
|
-
* external url can be loaded.
|
|
89
|
-
*/
|
|
90
|
-
readonly urlResolver: IUrlResolver;
|
|
91
|
-
/**
|
|
92
|
-
* The document service factory take the Fluid url provided
|
|
93
|
-
* by the resolved url and constructs all the necessary services
|
|
94
|
-
* for communication with the container's server.
|
|
95
|
-
*/
|
|
96
|
-
readonly documentServiceFactory: IDocumentServiceFactory;
|
|
98
|
+
export interface IContainerHostProps {
|
|
97
99
|
/**
|
|
98
100
|
* The code loader handles loading the necessary code
|
|
99
101
|
* for running a container once it is loaded.
|
|
@@ -139,11 +141,75 @@ export interface ICreateAndLoadContainerProps {
|
|
|
139
141
|
readonly clientDetailsOverride?: IClientDetails | undefined;
|
|
140
142
|
}
|
|
141
143
|
|
|
144
|
+
/**
|
|
145
|
+
* The driver-services pair — `urlResolver` plus `documentServiceFactory` —
|
|
146
|
+
* required to wire a container to a real driver at create or load time.
|
|
147
|
+
*
|
|
148
|
+
* @remarks
|
|
149
|
+
* Extracted as a reusable building block so the `request` field can be
|
|
150
|
+
* added on top for load-time props (see {@link IContainerLoadDriverProps})
|
|
151
|
+
* while create-time props that don't carry a request can compose just this
|
|
152
|
+
* pair.
|
|
153
|
+
*
|
|
154
|
+
* @legacy @beta
|
|
155
|
+
*/
|
|
156
|
+
export interface IContainerDriverServices {
|
|
157
|
+
/**
|
|
158
|
+
* The url resolver used by the loader for resolving external urls
|
|
159
|
+
* into Fluid urls such that the container specified by the
|
|
160
|
+
* external url can be loaded.
|
|
161
|
+
*/
|
|
162
|
+
readonly urlResolver: IUrlResolver;
|
|
163
|
+
/**
|
|
164
|
+
* The document service factory take the Fluid url provided
|
|
165
|
+
* by the resolved url and constructs all the necessary services
|
|
166
|
+
* for communication with the container's server.
|
|
167
|
+
*/
|
|
168
|
+
readonly documentServiceFactory: IDocumentServiceFactory;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The load-time driver wiring trio — `request`, `urlResolver`, and
|
|
173
|
+
* `documentServiceFactory` together.
|
|
174
|
+
*
|
|
175
|
+
* @remarks
|
|
176
|
+
* Reused as the all-or-nothing group for entry points (e.g. the frozen-load
|
|
177
|
+
* entry point) that accept either a full driver wiring (online form) or none
|
|
178
|
+
* of it (offline form). See `AllOrNone` in `@fluidframework/core-interfaces`.
|
|
179
|
+
*
|
|
180
|
+
* @legacy @beta
|
|
181
|
+
*/
|
|
182
|
+
export interface IContainerLoadDriverProps extends IContainerDriverServices {
|
|
183
|
+
/**
|
|
184
|
+
* The request to resolve the container.
|
|
185
|
+
*/
|
|
186
|
+
readonly request: IRequest;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Properties necessary for creating and loading a container.
|
|
191
|
+
*
|
|
192
|
+
* @deprecated
|
|
193
|
+
* Use the composable building blocks instead: extend
|
|
194
|
+
* {@link IContainerHostProps} for the code-loader / policy / observability
|
|
195
|
+
* surface and {@link IContainerDriverServices} for the
|
|
196
|
+
* `urlResolver` / `documentServiceFactory` pair, or compose them inline as
|
|
197
|
+
* `IContainerHostProps & IContainerDriverServices`. This interface is kept
|
|
198
|
+
* as an alias for back-compat and will be removed in a future release.
|
|
199
|
+
*
|
|
200
|
+
* @legacy @beta
|
|
201
|
+
*/
|
|
202
|
+
export interface ICreateAndLoadContainerProps
|
|
203
|
+
extends IContainerHostProps,
|
|
204
|
+
IContainerDriverServices {}
|
|
205
|
+
|
|
142
206
|
/**
|
|
143
207
|
* Props used to load a container.
|
|
144
208
|
* @legacy @beta
|
|
145
209
|
*/
|
|
146
|
-
export interface ILoadExistingContainerProps
|
|
210
|
+
export interface ILoadExistingContainerProps
|
|
211
|
+
extends IContainerHostProps,
|
|
212
|
+
IContainerDriverServices {
|
|
147
213
|
/**
|
|
148
214
|
* The request to resolve the container.
|
|
149
215
|
*/
|
|
@@ -168,7 +234,9 @@ export type ILoadSummarizerContainerProps = Omit<
|
|
|
168
234
|
* Props used to create a detached container.
|
|
169
235
|
* @legacy @beta
|
|
170
236
|
*/
|
|
171
|
-
export interface ICreateDetachedContainerProps
|
|
237
|
+
export interface ICreateDetachedContainerProps
|
|
238
|
+
extends IContainerHostProps,
|
|
239
|
+
IContainerDriverServices {
|
|
172
240
|
/**
|
|
173
241
|
* The code details for the container to be created.
|
|
174
242
|
*/
|
|
@@ -179,7 +247,9 @@ export interface ICreateDetachedContainerProps extends ICreateAndLoadContainerPr
|
|
|
179
247
|
* Props used to rehydrate a detached container.
|
|
180
248
|
* @legacy @beta
|
|
181
249
|
*/
|
|
182
|
-
export interface IRehydrateDetachedContainerProps
|
|
250
|
+
export interface IRehydrateDetachedContainerProps
|
|
251
|
+
extends IContainerHostProps,
|
|
252
|
+
IContainerDriverServices {
|
|
183
253
|
/**
|
|
184
254
|
* The serialized state returned by calling serialize on another container
|
|
185
255
|
*/
|
|
@@ -238,10 +308,47 @@ export async function loadExistingContainer(
|
|
|
238
308
|
|
|
239
309
|
/**
|
|
240
310
|
* Properties required to load a frozen container from pending state.
|
|
311
|
+
*
|
|
312
|
+
* @remarks
|
|
313
|
+
* Two forms are supported and selected by the presence of the driver-wiring
|
|
314
|
+
* fields. **Online** form supplies `request`, `urlResolver`, and
|
|
315
|
+
* `documentServiceFactory`; the supplied factory is wrapped in a frozen
|
|
316
|
+
* factory and the resolver is used to re-resolve the request URL just as with
|
|
317
|
+
* {@link loadExistingContainer}. **Offline** form omits all three driver
|
|
318
|
+
* fields; the captured URL stored in `pendingLocalState` is used to
|
|
319
|
+
* synthesize a resolved URL, no real driver is contacted, and
|
|
320
|
+
* `IContainer.getAbsoluteUrl` throws on the returned container because the
|
|
321
|
+
* resolver's external URL format is unknown without a real `IUrlResolver`.
|
|
322
|
+
*
|
|
323
|
+
* **Offline form precondition.** With no driver wiring there is no live
|
|
324
|
+
* storage to read attachment blobs from, so any blob the runtime dereferences
|
|
325
|
+
* during load must already be inlined into `pendingLocalState`. The pending
|
|
326
|
+
* state must therefore be produced by {@link captureFullContainerState}
|
|
327
|
+
* (which inlines all referenced attachment blobs) rather than
|
|
328
|
+
* `IContainer.getPendingLocalState` / `getRequiredPendingLocalState` (which
|
|
329
|
+
* do not). If the runtime needs an attachment blob that is not inlined, the
|
|
330
|
+
* load fails with `UsageError` from the synthesized storage service.
|
|
331
|
+
*
|
|
332
|
+
* **URL shape requirement.** In the offline form the captured
|
|
333
|
+
* `pendingLocalState.url` is the only URL available; it is parsed in place of
|
|
334
|
+
* a real `IUrlResolver.resolve()` call, so it must satisfy
|
|
335
|
+
* {@link tryParseCompatibleResolvedUrl}'s contract — a resolved URL of shape
|
|
336
|
+
* `protocol://<string>/.../..?<querystring>`. This is the format that
|
|
337
|
+
* Fluid-shipped drivers emit; drivers that emit a non-standard resolved-URL
|
|
338
|
+
* shape will surface as a `UsageError` at load time. The online form has no
|
|
339
|
+
* such constraint because the supplied resolver controls URL parsing.
|
|
340
|
+
*
|
|
341
|
+
* The offline form supports `readOnly: false` for the same capture-and-relay
|
|
342
|
+
* use case as the online form: local DDS submissions accrue in the runtime's
|
|
343
|
+
* pending-state manager and can be captured via `getPendingLocalState` for
|
|
344
|
+
* a later online replay. Nothing is published from an offline container.
|
|
345
|
+
*
|
|
346
|
+
* Mixing the two forms (some driver fields supplied, others omitted) is a
|
|
347
|
+
* compile-time error courtesy of the `AllOrNone` modifier from
|
|
348
|
+
* `@fluidframework/core-interfaces`.
|
|
241
349
|
* @legacy @alpha
|
|
242
350
|
*/
|
|
243
|
-
export
|
|
244
|
-
extends ILoadExistingContainerProps {
|
|
351
|
+
export type ILoadFrozenContainerFromPendingStateProps = IContainerHostProps & {
|
|
245
352
|
/**
|
|
246
353
|
* Pending local state to be applied to the container.
|
|
247
354
|
*/
|
|
@@ -274,7 +381,17 @@ export interface ILoadFrozenContainerFromPendingStateProps
|
|
|
274
381
|
* that the runtime accepts DDS submissions and accumulates them in `pendingStateManager`.
|
|
275
382
|
*/
|
|
276
383
|
readonly readOnly?: boolean;
|
|
277
|
-
}
|
|
384
|
+
} & AllOrNone<IContainerLoadDriverProps>;
|
|
385
|
+
|
|
386
|
+
const driverPropKeys = ["request", "urlResolver", "documentServiceFactory"] as const;
|
|
387
|
+
|
|
388
|
+
// Recognizable opaque placeholder used as `request.url` for the offline form of
|
|
389
|
+
// `loadFrozenContainerFromPendingState`. The synthesized `IUrlResolver` ignores
|
|
390
|
+
// its argument and returns a `IResolvedUrl` derived from `pendingLocalState.url`,
|
|
391
|
+
// so this string is never interpreted as a real URL by any downstream stage; it
|
|
392
|
+
// only needs to be a fixed sentinel that won't collide with real request URLs.
|
|
393
|
+
const offlineFrozenRequestPlaceholderUrl =
|
|
394
|
+
"frozen-load-from-pending-state://offline-placeholder";
|
|
278
395
|
|
|
279
396
|
/**
|
|
280
397
|
* Loads a frozen container from pending local state.
|
|
@@ -284,11 +401,84 @@ export interface ILoadFrozenContainerFromPendingStateProps
|
|
|
284
401
|
export async function loadFrozenContainerFromPendingState(
|
|
285
402
|
props: ILoadFrozenContainerFromPendingStateProps,
|
|
286
403
|
): Promise<IContainer> {
|
|
404
|
+
const fnName = loadFrozenContainerFromPendingState.name;
|
|
405
|
+
const { readOnly, pendingLocalState } = props;
|
|
406
|
+
|
|
407
|
+
const driverWiring = validateAllOrNone<IContainerLoadDriverProps>(props, driverPropKeys);
|
|
408
|
+
|
|
409
|
+
if (driverWiring === "mixed") {
|
|
410
|
+
throw new UsageError(
|
|
411
|
+
`${fnName}: ${driverPropKeys.join(", ")} must all be provided or all omitted`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (driverWiring === "none") {
|
|
416
|
+
// Offline: synthesize the driver wiring from the URL captured in pending state.
|
|
417
|
+
// The container's load pipeline is reused unchanged — the synthesized resolver
|
|
418
|
+
// returns a resolved URL whose `url` equals `pendingLocalState.url`, so the
|
|
419
|
+
// identity guard in `Loader.resolveCore` is trivially satisfied.
|
|
420
|
+
const pending = getAttachedContainerStateFromSerializedContainer(pendingLocalState);
|
|
421
|
+
const parsed = tryParseCompatibleResolvedUrl(pending.url);
|
|
422
|
+
if (parsed === undefined) {
|
|
423
|
+
throw new UsageError(
|
|
424
|
+
`${fnName}: pending state URL is not in a parseable form (${pending.url})`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
// `tryParseCompatibleResolvedUrl` returns `parsed.id` as the first two
|
|
428
|
+
// path segments joined by `/` (i.e. `tenantId/docId`). Production
|
|
429
|
+
// resolvers populate `IResolvedUrl.id` with the single doc-id segment
|
|
430
|
+
// (see `localResolver.ts`, `insecureUrlResolver.ts`,
|
|
431
|
+
// `routerlicious-urlResolver/urlResolver.ts`, `odspDriverUrlResolver.ts`),
|
|
432
|
+
// so downstream consumers reading `IContainer.resolvedUrl.id` for
|
|
433
|
+
// telemetry / cache keys expect the single-segment shape. Take the
|
|
434
|
+
// trailing segment so offline-loaded containers match that contract.
|
|
435
|
+
const parsedIdSegments = parsed.id.split("/");
|
|
436
|
+
const synthesizedId = parsedIdSegments[parsedIdSegments.length - 1] ?? parsed.id;
|
|
437
|
+
const synthesizedResolvedUrl: IResolvedUrl = {
|
|
438
|
+
type: "fluid",
|
|
439
|
+
id: synthesizedId,
|
|
440
|
+
url: pending.url,
|
|
441
|
+
// `tokens` and `endpoints` are empty because no real driver is contacted; the
|
|
442
|
+
// frozen factory never reads them. A future offline mode that needs them would
|
|
443
|
+
// require carrying them in the pending-state format.
|
|
444
|
+
tokens: {},
|
|
445
|
+
endpoints: {},
|
|
446
|
+
};
|
|
447
|
+
const synthesizedUrlResolver: IUrlResolver = {
|
|
448
|
+
// The argument is ignored: in offline mode the only URL in the system is the
|
|
449
|
+
// one captured in pending state. Any request is mapped to the same resolved URL.
|
|
450
|
+
resolve: async () => synthesizedResolvedUrl,
|
|
451
|
+
// External (request-form) URLs cannot be reconstructed from the captured
|
|
452
|
+
// resolved URL alone, so we cannot honor `getAbsoluteUrl`. Surface the misuse
|
|
453
|
+
// loudly rather than fabricate a string in the wrong format.
|
|
454
|
+
getAbsoluteUrl: async () => {
|
|
455
|
+
throw new UsageError(`${fnName}: getAbsoluteUrl requires ${driverPropKeys.join("/")}`);
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
return loadExistingContainer({
|
|
459
|
+
...props,
|
|
460
|
+
// `request.url` is unused: `synthesizedUrlResolver.resolve()` returns
|
|
461
|
+
// `synthesizedResolvedUrl` regardless of input, and the identity
|
|
462
|
+
// guard in `Loader.resolveCore` compares the resolver's output URL
|
|
463
|
+
// against `pendingLocalState.url` — both equal `pending.url` here.
|
|
464
|
+
// Using a recognizable opaque placeholder instead of the resolved-form
|
|
465
|
+
// URL avoids implying that any downstream stage interprets it as a
|
|
466
|
+
// request-form URL.
|
|
467
|
+
request: { url: offlineFrozenRequestPlaceholderUrl, headers: {} },
|
|
468
|
+
urlResolver: synthesizedUrlResolver,
|
|
469
|
+
documentServiceFactory: createFrozenDocumentServiceFactory(undefined, readOnly),
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Online: wrap the caller-supplied factory so the live driver only serves blob reads.
|
|
474
|
+
// `driverWiring === "all"` narrows `props` to the form that carries the driver fields.
|
|
475
|
+
const onlineProps = props as ILoadFrozenContainerFromPendingStateProps &
|
|
476
|
+
IContainerLoadDriverProps;
|
|
287
477
|
return loadExistingContainer({
|
|
288
|
-
...
|
|
478
|
+
...onlineProps,
|
|
289
479
|
documentServiceFactory: createFrozenDocumentServiceFactory(
|
|
290
|
-
|
|
291
|
-
|
|
480
|
+
onlineProps.documentServiceFactory,
|
|
481
|
+
readOnly,
|
|
292
482
|
),
|
|
293
483
|
});
|
|
294
484
|
}
|
|
@@ -372,6 +562,19 @@ export async function captureFullContainerState({
|
|
|
372
562
|
throw new UsageError("Failed to resolve request to a Fluid URL");
|
|
373
563
|
}
|
|
374
564
|
|
|
565
|
+
// Validate the resolver's URL shape at capture time. The captured pending
|
|
566
|
+
// state is rehydrated later (possibly in a different process) via the
|
|
567
|
+
// offline form of `loadFrozenContainerFromPendingState`, which requires
|
|
568
|
+
// `tryParseCompatibleResolvedUrl` to succeed on `pendingLocalState.url`.
|
|
569
|
+
// Failing fast here turns "your captured artifact silently isn't
|
|
570
|
+
// rehydratable" into a same-call error a partner can act on, instead of
|
|
571
|
+
// surfacing as a `UsageError` at offline-load time in a different process.
|
|
572
|
+
if (tryParseCompatibleResolvedUrl(resolvedUrl.url) === undefined) {
|
|
573
|
+
throw new UsageError(
|
|
574
|
+
`${captureFullContainerState.name}: resolved URL is not in the shape required by tryParseCompatibleResolvedUrl (protocol://<string>/<tenantId>/<docId>?<querystring>); captured state would not rehydrate offline (${resolvedUrl.url})`,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
375
578
|
const documentService = await documentServiceFactory.createDocumentService(
|
|
376
579
|
resolvedUrl,
|
|
377
580
|
logger,
|
package/src/debugLogger.ts
CHANGED
|
@@ -10,11 +10,11 @@ import type {
|
|
|
10
10
|
ITelemetryBaseProperties,
|
|
11
11
|
} from "@fluidframework/core-interfaces";
|
|
12
12
|
import {
|
|
13
|
-
type ITelemetryLoggerExt,
|
|
14
|
-
type ITelemetryLoggerPropertyBags,
|
|
15
13
|
createMultiSinkLogger,
|
|
16
14
|
eventNamespaceSeparator,
|
|
17
15
|
formatTick,
|
|
16
|
+
type ITelemetryLoggerPropertyBags,
|
|
17
|
+
type TelemetryLoggerExt,
|
|
18
18
|
} from "@fluidframework/telemetry-utils/internal";
|
|
19
19
|
// This import style is necessary to ensure the emitted JS code works in both CJS and ESM.
|
|
20
20
|
import debugPkg from "debug";
|
|
@@ -38,7 +38,7 @@ export class DebugLogger implements ITelemetryBaseLogger {
|
|
|
38
38
|
namespace: string,
|
|
39
39
|
baseLogger?: ITelemetryBaseLogger,
|
|
40
40
|
properties?: ITelemetryLoggerPropertyBags,
|
|
41
|
-
):
|
|
41
|
+
): TelemetryLoggerExt {
|
|
42
42
|
// Setup base logger upfront, such that host can disable it (if needed)
|
|
43
43
|
const debug = registerDebug(namespace);
|
|
44
44
|
|
package/src/deltaManager.ts
CHANGED
|
@@ -34,17 +34,17 @@ import {
|
|
|
34
34
|
} from "@fluidframework/driver-definitions/internal";
|
|
35
35
|
import { NonRetryableError, isRuntimeMessage } from "@fluidframework/driver-utils/internal";
|
|
36
36
|
import {
|
|
37
|
-
type ITelemetryErrorEventExt,
|
|
38
|
-
type ITelemetryGenericEventExt,
|
|
39
|
-
type ITelemetryLoggerExt,
|
|
40
37
|
DataCorruptionError,
|
|
41
38
|
DataProcessingError,
|
|
42
|
-
|
|
39
|
+
EventEmitterWithErrorHandling,
|
|
43
40
|
extractSafePropertiesFromMessage,
|
|
44
41
|
isFluidError,
|
|
42
|
+
type ITelemetryErrorEventExt,
|
|
43
|
+
type ITelemetryGenericEventExt,
|
|
45
44
|
normalizeError,
|
|
46
45
|
safeRaiseEvent,
|
|
47
|
-
|
|
46
|
+
type TelemetryLoggerExt,
|
|
47
|
+
UsageError,
|
|
48
48
|
} from "@fluidframework/telemetry-utils/internal";
|
|
49
49
|
import { v4 as uuid } from "uuid";
|
|
50
50
|
|
|
@@ -133,7 +133,7 @@ function isClientMessage(message: ISequencedDocumentMessage | IDocumentMessage):
|
|
|
133
133
|
*/
|
|
134
134
|
function logIfFalse(
|
|
135
135
|
condition: boolean,
|
|
136
|
-
logger:
|
|
136
|
+
logger: TelemetryLoggerExt,
|
|
137
137
|
event: string | ITelemetryGenericEventExt,
|
|
138
138
|
): condition is true {
|
|
139
139
|
if (condition) {
|
|
@@ -420,7 +420,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
420
420
|
|
|
421
421
|
constructor(
|
|
422
422
|
private readonly serviceProvider: () => IDocumentService | undefined,
|
|
423
|
-
private readonly logger:
|
|
423
|
+
private readonly logger: TelemetryLoggerExt,
|
|
424
424
|
private readonly _active: () => boolean,
|
|
425
425
|
createConnectionManager: (props: IConnectionManagerFactoryArgs) => TConnectionManager,
|
|
426
426
|
) {
|
package/src/error.ts
CHANGED
|
@@ -8,8 +8,8 @@ import type { ITelemetryBaseProperties } from "@fluidframework/core-interfaces";
|
|
|
8
8
|
import type { IThrottlingWarning } from "@fluidframework/core-interfaces/internal";
|
|
9
9
|
import {
|
|
10
10
|
type IFluidErrorBase,
|
|
11
|
-
type ITelemetryLoggerExt,
|
|
12
11
|
LoggingError,
|
|
12
|
+
type TelemetryLoggerExt,
|
|
13
13
|
wrapErrorAndLog,
|
|
14
14
|
} from "@fluidframework/telemetry-utils/internal";
|
|
15
15
|
|
|
@@ -40,7 +40,7 @@ export class ThrottlingWarning
|
|
|
40
40
|
public static wrap(
|
|
41
41
|
error: unknown,
|
|
42
42
|
retryAfterSeconds: number,
|
|
43
|
-
logger:
|
|
43
|
+
logger: TelemetryLoggerExt,
|
|
44
44
|
): IThrottlingWarning {
|
|
45
45
|
const newErrorFn = (errMsg: string): ThrottlingWarning =>
|
|
46
46
|
new ThrottlingWarning(errMsg, retryAfterSeconds);
|
package/src/frozenServices.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type ISignalMessage,
|
|
27
27
|
type ITokenClaims,
|
|
28
28
|
} from "@fluidframework/driver-definitions/internal";
|
|
29
|
+
import { UsageError } from "@fluidframework/telemetry-utils/internal";
|
|
29
30
|
import { v4 as uuid } from "uuid";
|
|
30
31
|
|
|
31
32
|
import type { IConnectionStateChangeReason } from "./contracts.js";
|
|
@@ -165,7 +166,22 @@ class FrozenDocumentService
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
const frozenDocumentStorageServiceHandler = (): never => {
|
|
168
|
-
throw new
|
|
169
|
+
throw new UsageError("Operations are not supported on the FrozenDocumentStorageService.");
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Distinct from {@link frozenDocumentStorageServiceHandler} because callers
|
|
174
|
+
* that hit this path are almost always exercising a fully-offline frozen load
|
|
175
|
+
* whose pending state was produced by {@link getPendingLocalState} (which omits
|
|
176
|
+
* inlined attachment blobs) rather than {@link captureFullContainerState}. A
|
|
177
|
+
* generic "operation not supported" is technically true but unhelpful; this
|
|
178
|
+
* message names the missing precondition and the API that produces it.
|
|
179
|
+
*/
|
|
180
|
+
const frozenReadBlobOfflineHandler = async (): Promise<never> => {
|
|
181
|
+
throw new UsageError(
|
|
182
|
+
"Attempted to read an attachment blob from a frozen-loaded container without a live storage service. " +
|
|
183
|
+
"Fully-offline frozen loads must use pending state produced by `captureFullContainerState`, which inlines all referenced attachment blobs.",
|
|
184
|
+
);
|
|
169
185
|
};
|
|
170
186
|
|
|
171
187
|
class FrozenDocumentStorageService implements IDocumentStorageService, IDisposable {
|
|
@@ -184,10 +200,7 @@ class FrozenDocumentStorageService implements IDocumentStorageService, IDisposab
|
|
|
184
200
|
return this._disposed;
|
|
185
201
|
}
|
|
186
202
|
|
|
187
|
-
constructor(
|
|
188
|
-
readOnly: boolean,
|
|
189
|
-
private readonly documentStorageService?: IDocumentStorageService,
|
|
190
|
-
) {
|
|
203
|
+
constructor(readOnly: boolean, documentStorageService?: IDocumentStorageService) {
|
|
191
204
|
let rejectFn!: (error: Error) => void;
|
|
192
205
|
const promise = new Promise<never>((_, reject) => {
|
|
193
206
|
rejectFn = reject;
|
|
@@ -209,15 +222,16 @@ class FrozenDocumentStorageService implements IDocumentStorageService, IDisposab
|
|
|
209
222
|
this.createBlob = readOnly
|
|
210
223
|
? frozenDocumentStorageServiceHandler
|
|
211
224
|
: async () => this.disposalDeferred.promise;
|
|
225
|
+
this.readBlob =
|
|
226
|
+
documentStorageService?.readBlob.bind(documentStorageService) ??
|
|
227
|
+
frozenReadBlobOfflineHandler;
|
|
212
228
|
}
|
|
213
229
|
|
|
214
230
|
getSnapshotTree = frozenDocumentStorageServiceHandler;
|
|
215
231
|
getSnapshot = frozenDocumentStorageServiceHandler;
|
|
216
232
|
getVersions = frozenDocumentStorageServiceHandler;
|
|
217
233
|
createBlob: IDocumentStorageService["createBlob"];
|
|
218
|
-
readBlob
|
|
219
|
-
this.documentStorageService?.readBlob.bind(this.documentStorageService) ??
|
|
220
|
-
frozenDocumentStorageServiceHandler;
|
|
234
|
+
readBlob: IDocumentStorageService["readBlob"];
|
|
221
235
|
uploadSummaryWithContext = frozenDocumentStorageServiceHandler;
|
|
222
236
|
downloadSummary = frozenDocumentStorageServiceHandler;
|
|
223
237
|
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,9 @@ export {
|
|
|
14
14
|
loadFrozenContainerFromPendingState,
|
|
15
15
|
loadSummarizerContainerAndMakeSummary,
|
|
16
16
|
type ICaptureFullContainerStateProps,
|
|
17
|
+
type IContainerDriverServices,
|
|
18
|
+
type IContainerHostProps,
|
|
19
|
+
type IContainerLoadDriverProps,
|
|
17
20
|
type ICreateAndLoadContainerProps,
|
|
18
21
|
type ICreateDetachedContainerProps,
|
|
19
22
|
type ILoadExistingContainerProps,
|
package/src/packageVersion.ts
CHANGED
|
@@ -19,8 +19,8 @@ import type {
|
|
|
19
19
|
} from "@fluidframework/driver-definitions/internal";
|
|
20
20
|
import { runWithRetry } from "@fluidframework/driver-utils/internal";
|
|
21
21
|
import {
|
|
22
|
-
type ITelemetryLoggerExt,
|
|
23
22
|
GenericError,
|
|
23
|
+
type TelemetryLoggerExt,
|
|
24
24
|
UsageError,
|
|
25
25
|
} from "@fluidframework/telemetry-utils/internal";
|
|
26
26
|
|
|
@@ -29,7 +29,7 @@ export class RetriableDocumentStorageService implements IDocumentStorageService,
|
|
|
29
29
|
private internalStorageService: IDocumentStorageService | undefined;
|
|
30
30
|
constructor(
|
|
31
31
|
private readonly internalStorageServiceP: Promise<IDocumentStorageService>,
|
|
32
|
-
private readonly logger:
|
|
32
|
+
private readonly logger: TelemetryLoggerExt,
|
|
33
33
|
private readonly maxRetries?: number,
|
|
34
34
|
) {
|
|
35
35
|
this.internalStorageServiceP
|
package/src/utils.ts
CHANGED
|
@@ -81,7 +81,18 @@ export interface IParsedUrl {
|
|
|
81
81
|
* @legacy @beta
|
|
82
82
|
*/
|
|
83
83
|
export function tryParseCompatibleResolvedUrl(url: string): IParsedUrl | undefined {
|
|
84
|
-
|
|
84
|
+
// `new URL(...)` throws `TypeError` for any string that isn't a well-formed
|
|
85
|
+
// absolute URL. The `try*` name in this function's identifier implies a
|
|
86
|
+
// non-throwing contract on bad input, and callers all gate on `=== undefined`
|
|
87
|
+
// to surface their own errors — letting the built-in throw escape here would
|
|
88
|
+
// bypass those caller-supplied error messages for the broadest class of bad
|
|
89
|
+
// URLs ("not absolute", "invalid characters", etc.).
|
|
90
|
+
let parsed: URL;
|
|
91
|
+
try {
|
|
92
|
+
parsed = new URL(url);
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
85
96
|
if (typeof parsed.pathname !== "string") {
|
|
86
97
|
throw new LoggingError("Failed to parse pathname");
|
|
87
98
|
}
|