@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.
Files changed (112) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/api-report/container-loader.legacy.alpha.api.md +21 -8
  3. package/api-report/container-loader.legacy.beta.api.md +19 -6
  4. package/dist/connectionManager.d.ts +2 -2
  5. package/dist/connectionManager.d.ts.map +1 -1
  6. package/dist/connectionManager.js.map +1 -1
  7. package/dist/connectionStateHandler.d.ts +3 -3
  8. package/dist/connectionStateHandler.d.ts.map +1 -1
  9. package/dist/connectionStateHandler.js.map +1 -1
  10. package/dist/container.d.ts.map +1 -1
  11. package/dist/container.js +7 -3
  12. package/dist/container.js.map +1 -1
  13. package/dist/containerContext.d.ts +3 -3
  14. package/dist/containerContext.d.ts.map +1 -1
  15. package/dist/containerContext.js.map +1 -1
  16. package/dist/containerStorageAdapter.d.ts +2 -2
  17. package/dist/containerStorageAdapter.d.ts.map +1 -1
  18. package/dist/containerStorageAdapter.js.map +1 -1
  19. package/dist/createAndLoadContainerUtils.d.ts +111 -19
  20. package/dist/createAndLoadContainerUtils.d.ts.map +1 -1
  21. package/dist/createAndLoadContainerUtils.js +101 -19
  22. package/dist/createAndLoadContainerUtils.js.map +1 -1
  23. package/dist/debugLogger.d.ts +2 -2
  24. package/dist/debugLogger.d.ts.map +1 -1
  25. package/dist/debugLogger.js.map +1 -1
  26. package/dist/deltaManager.d.ts +2 -2
  27. package/dist/deltaManager.d.ts.map +1 -1
  28. package/dist/deltaManager.js.map +1 -1
  29. package/dist/error.d.ts +2 -2
  30. package/dist/error.d.ts.map +1 -1
  31. package/dist/error.js.map +1 -1
  32. package/dist/frozenServices.d.ts.map +1 -1
  33. package/dist/frozenServices.js +17 -4
  34. package/dist/frozenServices.js.map +1 -1
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/legacy.d.ts +3 -0
  39. package/dist/legacyAlpha.d.ts +3 -0
  40. package/dist/loaderLayerCompatState.d.ts +1 -1
  41. package/dist/packageVersion.d.ts +1 -1
  42. package/dist/packageVersion.js +1 -1
  43. package/dist/packageVersion.js.map +1 -1
  44. package/dist/retriableDocumentStorageService.d.ts +2 -2
  45. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  46. package/dist/retriableDocumentStorageService.js.map +1 -1
  47. package/dist/utils.d.ts.map +1 -1
  48. package/dist/utils.js +13 -1
  49. package/dist/utils.js.map +1 -1
  50. package/lib/connectionManager.d.ts +2 -2
  51. package/lib/connectionManager.d.ts.map +1 -1
  52. package/lib/connectionManager.js +1 -1
  53. package/lib/connectionManager.js.map +1 -1
  54. package/lib/connectionStateHandler.d.ts +3 -3
  55. package/lib/connectionStateHandler.d.ts.map +1 -1
  56. package/lib/connectionStateHandler.js.map +1 -1
  57. package/lib/container.d.ts.map +1 -1
  58. package/lib/container.js +7 -3
  59. package/lib/container.js.map +1 -1
  60. package/lib/containerContext.d.ts +3 -3
  61. package/lib/containerContext.d.ts.map +1 -1
  62. package/lib/containerContext.js.map +1 -1
  63. package/lib/containerStorageAdapter.d.ts +2 -2
  64. package/lib/containerStorageAdapter.d.ts.map +1 -1
  65. package/lib/containerStorageAdapter.js.map +1 -1
  66. package/lib/createAndLoadContainerUtils.d.ts +111 -19
  67. package/lib/createAndLoadContainerUtils.d.ts.map +1 -1
  68. package/lib/createAndLoadContainerUtils.js +85 -3
  69. package/lib/createAndLoadContainerUtils.js.map +1 -1
  70. package/lib/debugLogger.d.ts +2 -2
  71. package/lib/debugLogger.d.ts.map +1 -1
  72. package/lib/debugLogger.js.map +1 -1
  73. package/lib/deltaManager.d.ts +2 -2
  74. package/lib/deltaManager.d.ts.map +1 -1
  75. package/lib/deltaManager.js +1 -1
  76. package/lib/deltaManager.js.map +1 -1
  77. package/lib/error.d.ts +2 -2
  78. package/lib/error.d.ts.map +1 -1
  79. package/lib/error.js.map +1 -1
  80. package/lib/frozenServices.d.ts.map +1 -1
  81. package/lib/frozenServices.js +17 -4
  82. package/lib/frozenServices.js.map +1 -1
  83. package/lib/index.d.ts +1 -1
  84. package/lib/index.d.ts.map +1 -1
  85. package/lib/index.js.map +1 -1
  86. package/lib/legacy.d.ts +3 -0
  87. package/lib/legacyAlpha.d.ts +3 -0
  88. package/lib/loaderLayerCompatState.d.ts +1 -1
  89. package/lib/packageVersion.d.ts +1 -1
  90. package/lib/packageVersion.js +1 -1
  91. package/lib/packageVersion.js.map +1 -1
  92. package/lib/retriableDocumentStorageService.d.ts +2 -2
  93. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  94. package/lib/retriableDocumentStorageService.js.map +1 -1
  95. package/lib/utils.d.ts.map +1 -1
  96. package/lib/utils.js +13 -1
  97. package/lib/utils.js.map +1 -1
  98. package/package.json +42 -14
  99. package/src/connectionManager.ts +4 -4
  100. package/src/connectionStateHandler.ts +4 -4
  101. package/src/container.ts +8 -3
  102. package/src/containerContext.ts +3 -3
  103. package/src/containerStorageAdapter.ts +3 -3
  104. package/src/createAndLoadContainerUtils.ts +227 -24
  105. package/src/debugLogger.ts +3 -3
  106. package/src/deltaManager.ts +7 -7
  107. package/src/error.ts +2 -2
  108. package/src/frozenServices.ts +22 -8
  109. package/src/index.ts +3 -0
  110. package/src/packageVersion.ts +1 -1
  111. package/src/retriableDocumentStorageService.ts +2 -2
  112. 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.enableOfflineLoad") ??
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
 
@@ -32,7 +32,7 @@ import type {
32
32
  MessageType,
33
33
  ISequencedDocumentMessage,
34
34
  } from "@fluidframework/driver-definitions/internal";
35
- import type { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal";
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: ITelemetryLoggerExt;
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: ITelemetryLoggerExt;
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 { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal";
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: ITelemetryLoggerExt,
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: ITelemetryLoggerExt,
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 { getDocumentAttributes } from "./utils.js";
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
- * Properties necessary for creating and loading a container.
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 ICreateAndLoadContainerProps {
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 extends ICreateAndLoadContainerProps {
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 extends ICreateAndLoadContainerProps {
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 extends ICreateAndLoadContainerProps {
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 interface ILoadFrozenContainerFromPendingStateProps
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
- ...props,
478
+ ...onlineProps,
289
479
  documentServiceFactory: createFrozenDocumentServiceFactory(
290
- props.documentServiceFactory,
291
- props.readOnly,
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,
@@ -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
- ): ITelemetryLoggerExt {
41
+ ): TelemetryLoggerExt {
42
42
  // Setup base logger upfront, such that host can disable it (if needed)
43
43
  const debug = registerDebug(namespace);
44
44
 
@@ -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
- UsageError,
39
+ EventEmitterWithErrorHandling,
43
40
  extractSafePropertiesFromMessage,
44
41
  isFluidError,
42
+ type ITelemetryErrorEventExt,
43
+ type ITelemetryGenericEventExt,
45
44
  normalizeError,
46
45
  safeRaiseEvent,
47
- EventEmitterWithErrorHandling,
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: ITelemetryLoggerExt,
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: ITelemetryLoggerExt,
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: ITelemetryLoggerExt,
43
+ logger: TelemetryLoggerExt,
44
44
  ): IThrottlingWarning {
45
45
  const newErrorFn = (errMsg: string): ThrottlingWarning =>
46
46
  new ThrottlingWarning(errMsg, retryAfterSeconds);
@@ -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 Error("Operations are not supported on the FrozenDocumentStorageService.");
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,
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-loader";
9
- export const pkgVersion = "2.102.0";
9
+ export const pkgVersion = "2.110.0";
@@ -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: ITelemetryLoggerExt,
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
- const parsed = new URL(url);
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
  }