@fluidframework/container-loader 2.93.0 → 2.101.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 (118) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/api-report/container-loader.legacy.alpha.api.md +13 -1
  3. package/dist/captureReferencedContents.d.ts +154 -0
  4. package/dist/captureReferencedContents.d.ts.map +1 -0
  5. package/dist/captureReferencedContents.js +349 -0
  6. package/dist/captureReferencedContents.js.map +1 -0
  7. package/dist/connectionManager.d.ts +2 -1
  8. package/dist/connectionManager.d.ts.map +1 -1
  9. package/dist/connectionManager.js +39 -8
  10. package/dist/connectionManager.js.map +1 -1
  11. package/dist/connectionStateHandler.d.ts.map +1 -1
  12. package/dist/connectionStateHandler.js +3 -1
  13. package/dist/connectionStateHandler.js.map +1 -1
  14. package/dist/container.d.ts.map +1 -1
  15. package/dist/container.js +13 -4
  16. package/dist/container.js.map +1 -1
  17. package/dist/containerStorageAdapter.d.ts +20 -2
  18. package/dist/containerStorageAdapter.d.ts.map +1 -1
  19. package/dist/containerStorageAdapter.js +2 -2
  20. package/dist/containerStorageAdapter.js.map +1 -1
  21. package/dist/createAndLoadContainerUtils.d.ts +95 -0
  22. package/dist/createAndLoadContainerUtils.d.ts.map +1 -1
  23. package/dist/createAndLoadContainerUtils.js +137 -11
  24. package/dist/createAndLoadContainerUtils.js.map +1 -1
  25. package/dist/frozenServices.d.ts +113 -30
  26. package/dist/frozenServices.d.ts.map +1 -1
  27. package/dist/frozenServices.js +236 -58
  28. package/dist/frozenServices.js.map +1 -1
  29. package/dist/index.d.ts +2 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +5 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/legacyAlpha.d.ts +2 -0
  34. package/dist/loader.d.ts +1 -1
  35. package/dist/loader.d.ts.map +1 -1
  36. package/dist/loader.js +1 -1
  37. package/dist/loader.js.map +1 -1
  38. package/dist/loaderLayerCompatState.d.ts +1 -1
  39. package/dist/packageVersion.d.ts +1 -1
  40. package/dist/packageVersion.d.ts.map +1 -1
  41. package/dist/packageVersion.js +1 -1
  42. package/dist/packageVersion.js.map +1 -1
  43. package/dist/pendingLocalStateStore.d.ts.map +1 -1
  44. package/dist/pendingLocalStateStore.js +9 -3
  45. package/dist/pendingLocalStateStore.js.map +1 -1
  46. package/dist/retriableDocumentStorageService.d.ts +2 -1
  47. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  48. package/dist/retriableDocumentStorageService.js +3 -2
  49. package/dist/retriableDocumentStorageService.js.map +1 -1
  50. package/dist/serializedStateManager.d.ts +16 -1
  51. package/dist/serializedStateManager.d.ts.map +1 -1
  52. package/dist/serializedStateManager.js +11 -1
  53. package/dist/serializedStateManager.js.map +1 -1
  54. package/lib/captureReferencedContents.d.ts +154 -0
  55. package/lib/captureReferencedContents.d.ts.map +1 -0
  56. package/lib/captureReferencedContents.js +338 -0
  57. package/lib/captureReferencedContents.js.map +1 -0
  58. package/lib/connectionManager.d.ts +2 -1
  59. package/lib/connectionManager.d.ts.map +1 -1
  60. package/lib/connectionManager.js +40 -9
  61. package/lib/connectionManager.js.map +1 -1
  62. package/lib/connectionStateHandler.d.ts.map +1 -1
  63. package/lib/connectionStateHandler.js +3 -1
  64. package/lib/connectionStateHandler.js.map +1 -1
  65. package/lib/container.d.ts.map +1 -1
  66. package/lib/container.js +14 -5
  67. package/lib/container.js.map +1 -1
  68. package/lib/containerStorageAdapter.d.ts +20 -2
  69. package/lib/containerStorageAdapter.d.ts.map +1 -1
  70. package/lib/containerStorageAdapter.js +2 -2
  71. package/lib/containerStorageAdapter.js.map +1 -1
  72. package/lib/createAndLoadContainerUtils.d.ts +95 -0
  73. package/lib/createAndLoadContainerUtils.d.ts.map +1 -1
  74. package/lib/createAndLoadContainerUtils.js +128 -3
  75. package/lib/createAndLoadContainerUtils.js.map +1 -1
  76. package/lib/frozenServices.d.ts +113 -30
  77. package/lib/frozenServices.d.ts.map +1 -1
  78. package/lib/frozenServices.js +233 -57
  79. package/lib/frozenServices.js.map +1 -1
  80. package/lib/index.d.ts +2 -1
  81. package/lib/index.d.ts.map +1 -1
  82. package/lib/index.js +2 -1
  83. package/lib/index.js.map +1 -1
  84. package/lib/legacyAlpha.d.ts +2 -0
  85. package/lib/loader.d.ts +1 -1
  86. package/lib/loader.d.ts.map +1 -1
  87. package/lib/loader.js +2 -2
  88. package/lib/loader.js.map +1 -1
  89. package/lib/loaderLayerCompatState.d.ts +1 -1
  90. package/lib/packageVersion.d.ts +1 -1
  91. package/lib/packageVersion.d.ts.map +1 -1
  92. package/lib/packageVersion.js +1 -1
  93. package/lib/packageVersion.js.map +1 -1
  94. package/lib/pendingLocalStateStore.d.ts.map +1 -1
  95. package/lib/pendingLocalStateStore.js +9 -3
  96. package/lib/pendingLocalStateStore.js.map +1 -1
  97. package/lib/retriableDocumentStorageService.d.ts +2 -1
  98. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  99. package/lib/retriableDocumentStorageService.js +3 -2
  100. package/lib/retriableDocumentStorageService.js.map +1 -1
  101. package/lib/serializedStateManager.d.ts +16 -1
  102. package/lib/serializedStateManager.d.ts.map +1 -1
  103. package/lib/serializedStateManager.js +11 -1
  104. package/lib/serializedStateManager.js.map +1 -1
  105. package/package.json +13 -13
  106. package/src/captureReferencedContents.ts +446 -0
  107. package/src/connectionManager.ts +47 -8
  108. package/src/connectionStateHandler.ts +14 -9
  109. package/src/container.ts +18 -4
  110. package/src/containerStorageAdapter.ts +22 -2
  111. package/src/createAndLoadContainerUtils.ts +229 -2
  112. package/src/frozenServices.ts +285 -64
  113. package/src/index.ts +7 -0
  114. package/src/loader.ts +4 -2
  115. package/src/packageVersion.ts +1 -1
  116. package/src/pendingLocalStateStore.ts +8 -1
  117. package/src/retriableDocumentStorageService.ts +11 -4
  118. package/src/serializedStateManager.ts +28 -1
@@ -63,7 +63,11 @@ import {
63
63
  ReconnectMode,
64
64
  } from "./contracts.js";
65
65
  import { DeltaQueue } from "./deltaQueue.js";
66
- import { FrozenDeltaStream, isFrozenDeltaStreamConnection } from "./frozenServices.js";
66
+ import {
67
+ FrozenDeltaStream,
68
+ isFrozenDeltaStreamConnection,
69
+ isWritableFrozenDeltaStreamConnection,
70
+ } from "./frozenServices.js";
67
71
  import { SignalType } from "./protocol.js";
68
72
  import { isDeltaStreamConnectionForbiddenError } from "./utils.js";
69
73
 
@@ -344,6 +348,7 @@ export class ConnectionManager implements IConnectionManager {
344
348
  reconnectAllowed: boolean,
345
349
  private readonly logger: ITelemetryLoggerExt,
346
350
  private readonly props: IConnectionManagerFactoryArgs,
351
+ private maxInitialConnectionAttempts?: number,
347
352
  ) {
348
353
  this.clientDetails = this.client.details;
349
354
  this.defaultReconnectionMode = this.client.mode;
@@ -581,9 +586,9 @@ export class ConnectionManager implements IConnectionManager {
581
586
  LogLevel.verbose,
582
587
  );
583
588
  if (isDeltaStreamConnectionForbiddenError(origError)) {
584
- connection = new FrozenDeltaStream(origError.storageOnlyReason, {
585
- text: origError.message,
586
- error: origError,
589
+ connection = new FrozenDeltaStream({
590
+ storageOnlyReason: origError.storageOnlyReason,
591
+ readonlyConnectionReason: { text: origError.message, error: origError },
587
592
  });
588
593
  requestedMode = "read";
589
594
  break;
@@ -591,11 +596,10 @@ export class ConnectionManager implements IConnectionManager {
591
596
  isFluidError(origError) &&
592
597
  origError.errorType === DriverErrorTypes.outOfStorageError
593
598
  ) {
594
- // If we get out of storage error from calling joinsession, then use the NoDeltaStream object so
599
+ // If we get out of storage error from calling joinsession, then use the FrozenDeltaStream object so
595
600
  // that user can at least load the container.
596
- connection = new FrozenDeltaStream(undefined, {
597
- text: origError.message,
598
- error: origError,
601
+ connection = new FrozenDeltaStream({
602
+ readonlyConnectionReason: { text: origError.message, error: origError },
599
603
  });
600
604
  requestedMode = "read";
601
605
  break;
@@ -622,6 +626,17 @@ export class ConnectionManager implements IConnectionManager {
622
626
 
623
627
  lastError = origError;
624
628
 
629
+ // When maxInitialConnectionAttempts is set, do not retry beyond the allowed attempts.
630
+ // The consumer will own the retry policy.
631
+ if (
632
+ this.maxInitialConnectionAttempts !== undefined &&
633
+ connectRepeatCount >= this.maxInitialConnectionAttempts
634
+ ) {
635
+ const error = normalizeError(origError, { props: fatalConnectErrorProp });
636
+ this.props.closeHandler(error);
637
+ throw error;
638
+ }
639
+
625
640
  // We will not perform retries if the container disconnected and the ReconnectMode is set to Disabled or Never
626
641
  // so break out of the re-connecting while-loop after first attempt
627
642
  if (this.reconnectMode !== ReconnectMode.Enabled) {
@@ -688,6 +703,11 @@ export class ConnectionManager implements IConnectionManager {
688
703
  return;
689
704
  }
690
705
 
706
+ // Clear the max connection attempts limit now that a connection has been established.
707
+ // The limit is only intended to scope initial connection retries;
708
+ // once connected, normal reconnect behavior should apply.
709
+ this.maxInitialConnectionAttempts = undefined;
710
+
691
711
  this.setupNewSuccessfulConnection(connection, requestedMode, reason);
692
712
  }
693
713
 
@@ -1072,6 +1092,25 @@ export class ConnectionManager implements IConnectionManager {
1072
1092
 
1073
1093
  public sendMessages(messages: IDocumentMessage[]): void {
1074
1094
  assert(this.connected, 0x2b4 /* "not connected on sending ops!" */);
1095
+ // WritableFrozenDeltaStream short-circuit: writable-frozen containers
1096
+ // (`loadFrozenContainerFromPendingState({ readOnly: false })`) attach a
1097
+ // WritableFrozenDeltaStream as the live connection. Its `mode` is "read" (advertising
1098
+ // "write" would imply quorum membership we cannot honor), so a runtime submit
1099
+ // would otherwise fall into the read-mode reconnect branch below. That branch
1100
+ // schedules `reconnect("write")`, which under `ReconnectMode.Never`
1101
+ // (`allowReconnect: false`) calls `closeHandler` and closes the container — the
1102
+ // opposite of what writable-frozen wants. Drop the messages here: the runtime's
1103
+ // outbox keeps them in `pendingStateManager` so `getPendingLocalState()` can
1104
+ // capture them, which is the entire point of the writable-frozen flow.
1105
+ //
1106
+ // Match only the writable variant (a sibling class, not a subclass) so the read-only
1107
+ // `FrozenDeltaStream` retains its `submit` 403-nack tripwire — a stray submit on a
1108
+ // storage-only frozen connection signals an upstream invariant break and should
1109
+ // remain observable. The read-only variant shouldn't reach here in normal flow anyway
1110
+ // (its `storageOnly` policy keeps the runtime from submitting).
1111
+ if (isWritableFrozenDeltaStreamConnection(this.connection)) {
1112
+ return;
1113
+ }
1075
1114
  // If connection is "read" or implicit "read" (got leave op for "write" connection),
1076
1115
  // then op can't make it through - we will get a nack if op is sent.
1077
1116
  // We can short-circuit this process.
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { IDeltaManager } from "@fluidframework/container-definitions/internal";
7
7
  import type { ITelemetryBaseProperties } from "@fluidframework/core-interfaces";
8
+ import { LogLevel } from "@fluidframework/core-interfaces";
8
9
  import { assert, Timer } from "@fluidframework/core-utils/internal";
9
10
  import type { IClient, ISequencedClient } from "@fluidframework/driver-definitions";
10
11
  import type { IAnyDriverError } from "@fluidframework/driver-definitions/internal";
@@ -687,15 +688,19 @@ export class ConnectionStateHandler implements IConnectionStateHandler {
687
688
  this.prevClientLeftTimer.restart();
688
689
  } else {
689
690
  // Adding this event temporarily so that we can get help debugging if something goes wrong.
690
- this.handler.logger.sendTelemetryEvent({
691
- eventName: "noWaitOnDisconnected",
692
- details: JSON.stringify({
693
- clientId: this._clientId,
694
- inQuorum: currentClientInQuorum,
695
- waitingForLeaveOp: this.waitingForLeaveOp,
696
- hadOutstandingOps: this.handler.shouldClientJoinWrite(),
697
- }),
698
- });
691
+ this.handler.logger.sendTelemetryEvent(
692
+ {
693
+ eventName: "noWaitOnDisconnected",
694
+ details: JSON.stringify({
695
+ clientId: this._clientId,
696
+ inQuorum: currentClientInQuorum,
697
+ waitingForLeaveOp: this.waitingForLeaveOp,
698
+ hadOutstandingOps: this.handler.shouldClientJoinWrite(),
699
+ }),
700
+ },
701
+ undefined, // error
702
+ LogLevel.info,
703
+ );
699
704
  }
700
705
  }
701
706
 
package/src/container.ts CHANGED
@@ -84,7 +84,7 @@ import {
84
84
  } from "@fluidframework/driver-utils/internal";
85
85
  import {
86
86
  type TelemetryEventCategory,
87
- type ITelemetryLoggerExt,
87
+ type TelemetryLoggerExt,
88
88
  EventEmitterWithErrorHandling,
89
89
  GenericError,
90
90
  type IFluidErrorBase,
@@ -94,6 +94,7 @@ import {
94
94
  connectedEventName,
95
95
  createChildLogger,
96
96
  createChildMonitoringContext,
97
+ extractTelemetryLoggerExt,
97
98
  formatTick,
98
99
  normalizeError,
99
100
  raiseConnectedEvent,
@@ -429,7 +430,7 @@ export class Container
429
430
  private readonly codeLoader: ICodeDetailsLoader;
430
431
  private readonly options: ILoaderOptions;
431
432
  private readonly scope: FluidObject;
432
- private readonly subLogger: ITelemetryLoggerExt;
433
+ private readonly subLogger: TelemetryLoggerExt;
433
434
  private readonly detachedBlobStorage: MemoryDetachedBlobStorage | undefined;
434
435
  private readonly protocolHandlerBuilder: InternalProtocolHandlerBuilder;
435
436
  private readonly signalAudience = new Audience();
@@ -858,7 +859,7 @@ export class Container
858
859
  logger: this.mc.logger,
859
860
  // WARNING: logger on this context should not including getters like containerConnectionState above (on this.subLogger),
860
861
  // as that will result in attempt to dereference this.connectionStateHandler from this call while it's still undefined.
861
- mc: loggerToMonitoringContext(subLogger),
862
+ mc: loggerToMonitoringContext(extractTelemetryLoggerExt(subLogger)),
862
863
  connectionStateChanged: (value, oldState, reason) => {
863
864
  this.logConnectionStateChangeTelemetry(value, oldState, reason);
864
865
  if (this.loaded) {
@@ -1069,6 +1070,7 @@ export class Container
1069
1070
 
1070
1071
  this.connectionStateHandler.dispose();
1071
1072
  this.serializedStateManager.dispose();
1073
+ this._runtime?.close?.();
1072
1074
  } catch (newError) {
1073
1075
  this.mc.logger.sendErrorEvent({ eventName: "ContainerCloseException" }, newError);
1074
1076
  }
@@ -1103,6 +1105,8 @@ export class Container
1103
1105
  eventName: "ContainerDispose",
1104
1106
  // Only log error if container isn't closed
1105
1107
  category: !this.closed && error !== undefined ? "error" : "generic",
1108
+ isDirty: this.isDirty,
1109
+ lastSequenceNumber: this._deltaManager.lastSequenceNumber,
1106
1110
  },
1107
1111
  error,
1108
1112
  );
@@ -1609,7 +1613,11 @@ export class Container
1609
1613
  this.connectToDeltaStream(connectionArgs);
1610
1614
  }
1611
1615
 
1612
- this.storageAdapter.connectToService(this.service);
1616
+ // When DisableLoadConnectionRetries is enabled, use no internal retries.
1617
+ // The consumer will own the retry policy.
1618
+ const disableLoadRetries =
1619
+ this.mc.config.getBoolean("Fluid.Container.DisableLoadConnectionRetries") === true;
1620
+ this.storageAdapter.connectToService(this.service, disableLoadRetries ? 0 : undefined);
1613
1621
 
1614
1622
  this.attachmentData = {
1615
1623
  state: AttachState.Attached,
@@ -1994,6 +2002,8 @@ export class Container
1994
2002
 
1995
2003
  private createDeltaManager(): DeltaManager<ConnectionManager> {
1996
2004
  const serviceProvider = (): IDocumentService | undefined => this.service;
2005
+ const disableLoadConnectionRetries =
2006
+ this.mc.config.getBoolean("Fluid.Container.DisableLoadConnectionRetries") === true;
1997
2007
  const deltaManager = new DeltaManager<ConnectionManager>(
1998
2008
  serviceProvider,
1999
2009
  createChildLogger({ logger: this.subLogger, namespace: "DeltaManager" }),
@@ -2006,6 +2016,7 @@ export class Container
2006
2016
  this._canReconnect,
2007
2017
  createChildLogger({ logger: this.subLogger, namespace: "ConnectionManager" }),
2008
2018
  props,
2019
+ disableLoadConnectionRetries ? 1 : undefined /* maxInitialConnectionAttempts */,
2009
2020
  ),
2010
2021
  );
2011
2022
 
@@ -2386,6 +2397,9 @@ export class Container
2386
2397
  this.subLogger,
2387
2398
  { eventName: "CodeLoad" },
2388
2399
  async () => this.codeLoader.load(codeDetails),
2400
+ undefined, // markers
2401
+ undefined, // sampleThreshold
2402
+ LogLevel.info,
2389
2403
  );
2390
2404
 
2391
2405
  this._loadedModule = {
@@ -36,13 +36,32 @@ import type {
36
36
  import { convertSnapshotInfoToSnapshot } from "./utils.js";
37
37
 
38
38
  /**
39
- * Stringified blobs from a summary/snapshot tree.
39
+ * Stringified blobs from a summary/snapshot tree, keyed by blob id.
40
+ * Values are **UTF-8-encoded** — this is the right encoding for JSON or
41
+ * other text the runtime authors and consumes through this map. For
42
+ * arbitrary binary payloads (e.g. attachment blob contents), use
43
+ * {@link IBase64BlobContents} instead; a UTF-8 round-trip silently
44
+ * corrupts non-UTF-8 byte sequences with replacement characters.
40
45
  * @internal
41
46
  */
42
47
  export interface ISerializableBlobContents {
43
48
  [id: string]: string;
44
49
  }
45
50
 
51
+ /**
52
+ * Stringified blobs inlined in a summary/snapshot tree, keyed by blob id.
53
+ * Values are **base64-encoded** raw bytes. Used for attachment-blob
54
+ * payloads, which may carry arbitrary binary data (images, encrypted
55
+ * blobs, etc.). Mirrors the encoding used by the runtime's own
56
+ * pending-blob serializer in `BlobManager`. Structurally identical to
57
+ * {@link ISerializableBlobContents}; the two types exist to keep the
58
+ * encoding contract visible at every call site.
59
+ * @internal
60
+ */
61
+ export interface IBase64BlobContents {
62
+ [id: string]: string;
63
+ }
64
+
46
65
  /**
47
66
  * This class wraps the actual storage and make sure no wrong apis are called according to
48
67
  * container attach state.
@@ -104,7 +123,7 @@ export class ContainerStorageAdapter
104
123
  this.disposed = true;
105
124
  }
106
125
 
107
- public connectToService(service: IDocumentService): void {
126
+ public connectToService(service: IDocumentService, maxRetries?: number): void {
108
127
  if (!(this._storageService instanceof BlobOnlyStorage)) {
109
128
  return;
110
129
  }
@@ -113,6 +132,7 @@ export class ContainerStorageAdapter
113
132
  const retriableStorage = (this._storageService = new RetriableDocumentStorageService(
114
133
  storageServiceP,
115
134
  this.logger,
135
+ maxRetries,
116
136
  ));
117
137
 
118
138
  // A storage service wrapper which intercept calls to uploadSummaryWithContext and ensure they include
@@ -19,11 +19,16 @@ import type {
19
19
  import type { IClientDetails } from "@fluidframework/driver-definitions";
20
20
  import type {
21
21
  IDocumentServiceFactory,
22
+ ISequencedDocumentMessage,
23
+ ISnapshot,
24
+ ISnapshotTree,
22
25
  IUrlResolver,
23
26
  } from "@fluidframework/driver-definitions/internal";
24
- import { DriverHeader } from "@fluidframework/driver-definitions/internal";
27
+ import { DriverHeader, FetchSource } from "@fluidframework/driver-definitions/internal";
28
+ import { getSnapshotTree } from "@fluidframework/driver-utils/internal";
25
29
  import {
26
30
  GenericError,
31
+ UsageError,
27
32
  normalizeError,
28
33
  createChildMonitoringContext,
29
34
  mixinMonitoringContext,
@@ -33,16 +38,28 @@ import {
33
38
  } from "@fluidframework/telemetry-utils/internal";
34
39
  import { v4 as uuid } from "uuid";
35
40
 
41
+ import {
42
+ captureReferencedAttachmentBlobs,
43
+ extractBlobAttachReferences,
44
+ inlineAttachmentBlobsByReference,
45
+ parseGcSnapshotData,
46
+ readReferencedSnapshotBlobs,
47
+ snapshotHasLoadingGroups,
48
+ unreferencedAttachmentBlobLocalIds,
49
+ type IBlobAttachReference,
50
+ } from "./captureReferencedContents.js";
36
51
  import { DebugLogger } from "./debugLogger.js";
37
52
  import { createFrozenDocumentServiceFactory } from "./frozenServices.js";
38
53
  import { Loader } from "./loader.js";
39
54
  import { pkgVersion } from "./packageVersion.js";
40
55
  import type { ProtocolHandlerBuilder } from "./protocol.js";
56
+ import type { IPendingContainerState } from "./serializedStateManager.js";
41
57
  import type {
42
58
  LoadSummarizerSummaryResult,
43
59
  OnDemandSummaryResults,
44
60
  SummarizeOnDemandResults,
45
61
  } from "./summarizerResultTypes.js";
62
+ import { getDocumentAttributes } from "./utils.js";
46
63
 
47
64
  interface OnDemandSummarizeResultsPromises {
48
65
  readonly summarySubmitted: Promise<SummarizeOnDemandResults["summarySubmitted"]>;
@@ -229,6 +246,34 @@ export interface ILoadFrozenContainerFromPendingStateProps
229
246
  * Pending local state to be applied to the container.
230
247
  */
231
248
  readonly pendingLocalState: string;
249
+
250
+ /**
251
+ * Controls whether the frozen container is surfaced as read-only.
252
+ *
253
+ * Defaults to `true`. When `true`, the container reports `readOnlyInfo.readonly === true`
254
+ * with `storageOnly === true`, matching the historical behavior of frozen loads.
255
+ *
256
+ * When `false`, the container loads as writable so the runtime will accept DDS submissions.
257
+ * The connection itself stays `Connected`: the connection manager recognizes the synthetic
258
+ * frozen delta stream and drops outbound messages at the network layer, so no read→write
259
+ * reconnect is attempted. Local DDS state continues to update via optimistic apply, and
260
+ * submitted ops accumulate in the runtime's pending-state manager. Use this when callers
261
+ * want to accrue and capture pending state without publishing it.
262
+ *
263
+ * @remarks
264
+ * The flag uses negative polarity (`readOnly`) rather than a positive opt-in (`writable`)
265
+ * to align with `IContainer.readOnlyInfo.readonly`, which is the established surface for
266
+ * read/write state on a loaded container. A future positive-polarity option can layer on
267
+ * top of this without breaking callers, but flipping the polarity now would split readers
268
+ * between two conventions for the same concept.
269
+ *
270
+ * Subsystem behavior is unchanged from the read-only frozen path regardless of `readOnly`:
271
+ * storage operations still throw (only `readBlob` is supported); summarizer / id-compressor
272
+ * never fire because no acks arrive; the quorum is whatever was captured in pending state
273
+ * and gains no members during the writable-frozen lifetime. The only behavioral delta is
274
+ * that the runtime accepts DDS submissions and accumulates them in `pendingStateManager`.
275
+ */
276
+ readonly readOnly?: boolean;
232
277
  }
233
278
 
234
279
  /**
@@ -241,10 +286,192 @@ export async function loadFrozenContainerFromPendingState(
241
286
  ): Promise<IContainer> {
242
287
  return loadExistingContainer({
243
288
  ...props,
244
- documentServiceFactory: createFrozenDocumentServiceFactory(props.documentServiceFactory),
289
+ documentServiceFactory: createFrozenDocumentServiceFactory(
290
+ props.documentServiceFactory,
291
+ props.readOnly,
292
+ ),
245
293
  });
246
294
  }
247
295
 
296
+ /**
297
+ * Properties for {@link captureFullContainerState}.
298
+ * @legacy @alpha
299
+ */
300
+ export interface ICaptureFullContainerStateProps {
301
+ /**
302
+ * The url resolver used to resolve the request into a Fluid resolved url.
303
+ */
304
+ readonly urlResolver: IUrlResolver;
305
+ /**
306
+ * The document service factory used to construct the driver services
307
+ * against which the state is captured.
308
+ */
309
+ readonly documentServiceFactory: IDocumentServiceFactory;
310
+ /**
311
+ * The request identifying the container whose state is to be captured.
312
+ */
313
+ readonly request: IRequest;
314
+ /**
315
+ * Optional logger for driver-side telemetry.
316
+ */
317
+ readonly logger?: ITelemetryBaseLogger | undefined;
318
+ }
319
+
320
+ /**
321
+ * Captures the current state of an attached container using only driver-level
322
+ * services, without instantiating a runtime or loading a full container. The
323
+ * returned string is a serialized pending container state in the same wire
324
+ * format produced by a live container's pending-state serialization, and can
325
+ * be handed to {@link loadExistingContainer} as `pendingLocalState`.
326
+ *
327
+ * The output is a self-contained view of the container's referenced graph:
328
+ * the latest snapshot, inlined contents of every blob reachable through
329
+ * referenced subtrees, inlined contents of every referenced attachment blob
330
+ * keyed by storage id, and all ops with sequence numbers after the base
331
+ * snapshot's sequence number (as read from its attributes blob).
332
+ *
333
+ * Reachability respects GC. Snapshot subtrees flagged `unreferenced: true`
334
+ * are skipped (their contents are not inlined). Attachment blobs that GC has
335
+ * marked unreferenced, tombstoned, or deleted are skipped. When the snapshot
336
+ * has no GC tree (GC disabled or pre-GC document), no filtering is applied.
337
+ *
338
+ * Blob reads on load hit the `ContainerStorageAdapter` cache populated from
339
+ * the captured `snapshotBlobs` map, so a frozen loader can serve the full
340
+ * referenced graph without a live storage service.
341
+ *
342
+ * `pendingRuntimeState` is `undefined` — no runtime is instantiated — so the
343
+ * output cannot carry DDS-level in-flight changes. It is intended for state
344
+ * relay, inspection, and durable-state snapshot use cases.
345
+ *
346
+ * Containers that declare loading groups are not yet supported: the function
347
+ * throws `UsageError` if any referenced subtree carries a `groupId`. Group
348
+ * snapshots would need a separate prefetch + serialization path; until there
349
+ * is a known consumer and end-to-end coverage, the capture refuses rather
350
+ * than silently producing pending state that omits group data.
351
+ *
352
+ * Note: if a new snapshot lands between the snapshot fetch and the ops fetch,
353
+ * the returned state may not reflect the very latest snapshot, but remains
354
+ * internally consistent: ops are anchored to the snapshot that was captured.
355
+ *
356
+ * No `mixinMonitoringContext` / `configProvider` is wired here, deliberately
357
+ * diverging from the sibling entry points in this file. The function reads
358
+ * no feature flags and instantiates no runtime, so there is nothing for a
359
+ * monitoring context to gate or attribute. If a future change introduces
360
+ * config-gated behavior or runtime-attributed telemetry, add the wiring
361
+ * back together with that change.
362
+ * @legacy @alpha
363
+ */
364
+ export async function captureFullContainerState({
365
+ urlResolver,
366
+ documentServiceFactory,
367
+ request,
368
+ logger,
369
+ }: ICaptureFullContainerStateProps): Promise<string> {
370
+ const resolvedUrl = await urlResolver.resolve(request);
371
+ if (resolvedUrl === undefined) {
372
+ throw new UsageError("Failed to resolve request to a Fluid URL");
373
+ }
374
+
375
+ const documentService = await documentServiceFactory.createDocumentService(
376
+ resolvedUrl,
377
+ logger,
378
+ );
379
+ try {
380
+ const storage = await documentService.connectToStorage();
381
+
382
+ const versions = await storage.getVersions(
383
+ // `null` signals "latest"
384
+ // eslint-disable-next-line unicorn/no-null
385
+ null,
386
+ 1,
387
+ "captureFullContainerState",
388
+ FetchSource.noCache,
389
+ );
390
+ const version = versions[0];
391
+ const snapshot: ISnapshot | ISnapshotTree | undefined =
392
+ storage.getSnapshot === undefined
393
+ ? ((await storage.getSnapshotTree(version, "captureFullContainerState")) ?? undefined)
394
+ : await storage.getSnapshot({
395
+ cacheSnapshot: false,
396
+ versionId: version?.id,
397
+ scenarioName: "captureFullContainerState",
398
+ });
399
+ if (snapshot === undefined) {
400
+ throw new GenericError("Failed to fetch snapshot for captureFullContainerState");
401
+ }
402
+
403
+ const baseSnapshot = getSnapshotTree(snapshot);
404
+ if (snapshotHasLoadingGroups(baseSnapshot)) {
405
+ throw new UsageError(
406
+ "captureFullContainerState does not yet support containers with loading groups",
407
+ );
408
+ }
409
+ const attributes = await getDocumentAttributes(storage, baseSnapshot);
410
+ const gcData = await parseGcSnapshotData(baseSnapshot, storage);
411
+ // Structural snapshot blobs (JSON/text the runtime authored) are
412
+ // UTF-8-encoded; attachment blobs may carry arbitrary binary bytes
413
+ // and are base64-encoded. Keep them on separate fields of the
414
+ // pending state so the load side can apply the matching decoder
415
+ // without ambiguity. See IPendingContainerState.attachmentBlobContents.
416
+ const [snapshotBlobs, attachmentBlobContents] = await Promise.all([
417
+ readReferencedSnapshotBlobs(snapshot, storage), // utf8 encoded
418
+ captureReferencedAttachmentBlobs(baseSnapshot, storage, gcData), // base64 encoded
419
+ ]);
420
+
421
+ const deltaStorage = await documentService.connectToDeltaStorage();
422
+ const opsStream = deltaStorage.fetchMessages(
423
+ attributes.sequenceNumber + 1,
424
+ undefined,
425
+ undefined,
426
+ false,
427
+ "captureFullContainerState",
428
+ );
429
+ const savedOps: ISequencedDocumentMessage[] = [];
430
+ const postSnapshotBlobReferences: IBlobAttachReference[] = [];
431
+ let opsResult = await opsStream.read();
432
+ while (!opsResult.done) {
433
+ for (const op of opsResult.value) {
434
+ savedOps.push(op);
435
+ // Blobs uploaded after the base snapshot are not in its
436
+ // `.blobs` redirect table, so `captureReferencedAttachmentBlobs`
437
+ // did not see them. The wire-format BlobAttach op carries
438
+ // `(localId, storageId)` in its metadata; collect those here so
439
+ // we can backfill the bytes before sealing the artifact.
440
+ const refs = extractBlobAttachReferences(op);
441
+ if (refs.length > 0) {
442
+ postSnapshotBlobReferences.push(...refs);
443
+ }
444
+ }
445
+ opsResult = await opsStream.read();
446
+ }
447
+
448
+ if (postSnapshotBlobReferences.length > 0) {
449
+ const added = await inlineAttachmentBlobsByReference(
450
+ postSnapshotBlobReferences,
451
+ storage,
452
+ unreferencedAttachmentBlobLocalIds(gcData),
453
+ attachmentBlobContents,
454
+ );
455
+ Object.assign(attachmentBlobContents, added);
456
+ }
457
+
458
+ const pendingState: IPendingContainerState = {
459
+ attached: true,
460
+ baseSnapshot,
461
+ snapshotBlobs,
462
+ attachmentBlobContents:
463
+ Object.keys(attachmentBlobContents).length === 0 ? undefined : attachmentBlobContents,
464
+ loadedGroupIdSnapshots: undefined,
465
+ pendingRuntimeState: undefined,
466
+ savedOps,
467
+ url: resolvedUrl.url,
468
+ };
469
+ return JSON.stringify(pendingState);
470
+ } finally {
471
+ documentService.dispose();
472
+ }
473
+ }
474
+
248
475
  /**
249
476
  * Loads a summarizer container with the required headers, triggers an on-demand summary, and then closes it.
250
477
  * Returns success/failure and an optional error for host-side handling.