@fluidframework/container-loader 2.0.0-rc.3.0.2 → 2.0.0-rc.4.0.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 (188) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/api-report/container-loader.api.md +5 -1
  3. package/dist/attachment.d.ts +3 -2
  4. package/dist/attachment.d.ts.map +1 -1
  5. package/dist/attachment.js +5 -5
  6. package/dist/attachment.js.map +1 -1
  7. package/dist/audience.d.ts +6 -4
  8. package/dist/audience.d.ts.map +1 -1
  9. package/dist/audience.js +18 -3
  10. package/dist/audience.js.map +1 -1
  11. package/dist/catchUpMonitor.d.ts +1 -1
  12. package/dist/catchUpMonitor.d.ts.map +1 -1
  13. package/dist/catchUpMonitor.js.map +1 -1
  14. package/dist/connectionManager.d.ts +7 -3
  15. package/dist/connectionManager.d.ts.map +1 -1
  16. package/dist/connectionManager.js +57 -38
  17. package/dist/connectionManager.js.map +1 -1
  18. package/dist/connectionStateHandler.d.ts +31 -10
  19. package/dist/connectionStateHandler.d.ts.map +1 -1
  20. package/dist/connectionStateHandler.js +49 -36
  21. package/dist/connectionStateHandler.js.map +1 -1
  22. package/dist/container.d.ts +22 -13
  23. package/dist/container.d.ts.map +1 -1
  24. package/dist/container.js +145 -117
  25. package/dist/container.js.map +1 -1
  26. package/dist/containerContext.d.ts +3 -3
  27. package/dist/containerContext.d.ts.map +1 -1
  28. package/dist/containerContext.js.map +1 -1
  29. package/dist/containerStorageAdapter.d.ts +12 -3
  30. package/dist/containerStorageAdapter.d.ts.map +1 -1
  31. package/dist/containerStorageAdapter.js +42 -4
  32. package/dist/containerStorageAdapter.js.map +1 -1
  33. package/dist/contracts.d.ts +2 -2
  34. package/dist/contracts.d.ts.map +1 -1
  35. package/dist/contracts.js.map +1 -1
  36. package/dist/debugLogger.d.ts +1 -2
  37. package/dist/debugLogger.d.ts.map +1 -1
  38. package/dist/debugLogger.js.map +1 -1
  39. package/dist/deltaManager.d.ts +5 -6
  40. package/dist/deltaManager.d.ts.map +1 -1
  41. package/dist/deltaManager.js +29 -24
  42. package/dist/deltaManager.js.map +1 -1
  43. package/dist/deltaQueue.d.ts +1 -1
  44. package/dist/deltaQueue.d.ts.map +1 -1
  45. package/dist/deltaQueue.js.map +1 -1
  46. package/dist/error.d.ts +1 -2
  47. package/dist/error.d.ts.map +1 -1
  48. package/dist/error.js.map +1 -1
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +3 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/legacy.d.ts +2 -2
  54. package/dist/loadPaused.d.ts +35 -0
  55. package/dist/loadPaused.d.ts.map +1 -0
  56. package/dist/loadPaused.js +115 -0
  57. package/dist/loadPaused.js.map +1 -0
  58. package/dist/loader.d.ts +1 -1
  59. package/dist/loader.d.ts.map +1 -1
  60. package/dist/loader.js +1 -14
  61. package/dist/loader.js.map +1 -1
  62. package/dist/location-redirection-utilities/resolveWithLocationRedirection.d.ts.map +1 -1
  63. package/dist/location-redirection-utilities/resolveWithLocationRedirection.js +4 -4
  64. package/dist/location-redirection-utilities/resolveWithLocationRedirection.js.map +1 -1
  65. package/dist/packageVersion.d.ts +1 -1
  66. package/dist/packageVersion.js +1 -1
  67. package/dist/packageVersion.js.map +1 -1
  68. package/dist/protocol.d.ts.map +1 -1
  69. package/dist/protocol.js +3 -0
  70. package/dist/protocol.js.map +1 -1
  71. package/dist/public.d.ts +1 -1
  72. package/dist/retriableDocumentStorageService.d.ts +1 -1
  73. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  74. package/dist/retriableDocumentStorageService.js.map +1 -1
  75. package/dist/serializedStateManager.d.ts +89 -9
  76. package/dist/serializedStateManager.d.ts.map +1 -1
  77. package/dist/serializedStateManager.js +150 -34
  78. package/dist/serializedStateManager.js.map +1 -1
  79. package/dist/utils.d.ts +11 -1
  80. package/dist/utils.d.ts.map +1 -1
  81. package/dist/utils.js +29 -14
  82. package/dist/utils.js.map +1 -1
  83. package/lib/attachment.d.ts +3 -2
  84. package/lib/attachment.d.ts.map +1 -1
  85. package/lib/attachment.js +5 -5
  86. package/lib/attachment.js.map +1 -1
  87. package/lib/audience.d.ts +6 -4
  88. package/lib/audience.d.ts.map +1 -1
  89. package/lib/audience.js +19 -4
  90. package/lib/audience.js.map +1 -1
  91. package/lib/catchUpMonitor.d.ts +1 -1
  92. package/lib/catchUpMonitor.d.ts.map +1 -1
  93. package/lib/catchUpMonitor.js.map +1 -1
  94. package/lib/connectionManager.d.ts +7 -3
  95. package/lib/connectionManager.d.ts.map +1 -1
  96. package/lib/connectionManager.js +36 -17
  97. package/lib/connectionManager.js.map +1 -1
  98. package/lib/connectionStateHandler.d.ts +31 -10
  99. package/lib/connectionStateHandler.d.ts.map +1 -1
  100. package/lib/connectionStateHandler.js +49 -36
  101. package/lib/connectionStateHandler.js.map +1 -1
  102. package/lib/container.d.ts +22 -13
  103. package/lib/container.d.ts.map +1 -1
  104. package/lib/container.js +146 -118
  105. package/lib/container.js.map +1 -1
  106. package/lib/containerContext.d.ts +3 -3
  107. package/lib/containerContext.d.ts.map +1 -1
  108. package/lib/containerContext.js.map +1 -1
  109. package/lib/containerStorageAdapter.d.ts +12 -3
  110. package/lib/containerStorageAdapter.d.ts.map +1 -1
  111. package/lib/containerStorageAdapter.js +42 -4
  112. package/lib/containerStorageAdapter.js.map +1 -1
  113. package/lib/contracts.d.ts +2 -2
  114. package/lib/contracts.d.ts.map +1 -1
  115. package/lib/contracts.js +1 -1
  116. package/lib/contracts.js.map +1 -1
  117. package/lib/debugLogger.d.ts +1 -2
  118. package/lib/debugLogger.d.ts.map +1 -1
  119. package/lib/debugLogger.js.map +1 -1
  120. package/lib/deltaManager.d.ts +5 -6
  121. package/lib/deltaManager.d.ts.map +1 -1
  122. package/lib/deltaManager.js +10 -5
  123. package/lib/deltaManager.js.map +1 -1
  124. package/lib/deltaQueue.d.ts +1 -1
  125. package/lib/deltaQueue.d.ts.map +1 -1
  126. package/lib/deltaQueue.js.map +1 -1
  127. package/lib/error.d.ts +1 -2
  128. package/lib/error.d.ts.map +1 -1
  129. package/lib/error.js.map +1 -1
  130. package/lib/index.d.ts +1 -0
  131. package/lib/index.d.ts.map +1 -1
  132. package/lib/index.js +1 -0
  133. package/lib/index.js.map +1 -1
  134. package/lib/legacy.d.ts +2 -2
  135. package/lib/loadPaused.d.ts +35 -0
  136. package/lib/loadPaused.d.ts.map +1 -0
  137. package/lib/loadPaused.js +111 -0
  138. package/lib/loadPaused.js.map +1 -0
  139. package/lib/loader.d.ts +1 -1
  140. package/lib/loader.d.ts.map +1 -1
  141. package/lib/loader.js +3 -16
  142. package/lib/loader.js.map +1 -1
  143. package/lib/location-redirection-utilities/resolveWithLocationRedirection.d.ts.map +1 -1
  144. package/lib/location-redirection-utilities/resolveWithLocationRedirection.js +1 -1
  145. package/lib/location-redirection-utilities/resolveWithLocationRedirection.js.map +1 -1
  146. package/lib/packageVersion.d.ts +1 -1
  147. package/lib/packageVersion.js +1 -1
  148. package/lib/packageVersion.js.map +1 -1
  149. package/lib/protocol.d.ts.map +1 -1
  150. package/lib/protocol.js +3 -0
  151. package/lib/protocol.js.map +1 -1
  152. package/lib/public.d.ts +1 -1
  153. package/lib/retriableDocumentStorageService.d.ts +1 -1
  154. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  155. package/lib/retriableDocumentStorageService.js +1 -1
  156. package/lib/retriableDocumentStorageService.js.map +1 -1
  157. package/lib/serializedStateManager.d.ts +89 -9
  158. package/lib/serializedStateManager.d.ts.map +1 -1
  159. package/lib/serializedStateManager.js +146 -30
  160. package/lib/serializedStateManager.js.map +1 -1
  161. package/lib/tsdoc-metadata.json +1 -1
  162. package/lib/utils.d.ts +11 -1
  163. package/lib/utils.d.ts.map +1 -1
  164. package/lib/utils.js +15 -1
  165. package/lib/utils.js.map +1 -1
  166. package/package.json +24 -21
  167. package/src/attachment.ts +12 -13
  168. package/src/audience.ts +30 -9
  169. package/src/catchUpMonitor.ts +1 -1
  170. package/src/connectionManager.ts +45 -22
  171. package/src/connectionStateHandler.ts +78 -45
  172. package/src/container.ts +181 -160
  173. package/src/containerContext.ts +2 -2
  174. package/src/containerStorageAdapter.ts +61 -6
  175. package/src/contracts.ts +5 -4
  176. package/src/debugLogger.ts +1 -1
  177. package/src/deltaManager.ts +15 -8
  178. package/src/deltaQueue.ts +1 -1
  179. package/src/error.ts +1 -1
  180. package/src/index.ts +1 -0
  181. package/src/loadPaused.ts +140 -0
  182. package/src/loader.ts +6 -23
  183. package/src/location-redirection-utilities/resolveWithLocationRedirection.ts +1 -1
  184. package/src/packageVersion.ts +1 -1
  185. package/src/protocol.ts +4 -0
  186. package/src/retriableDocumentStorageService.ts +5 -2
  187. package/src/serializedStateManager.ts +215 -48
  188. package/src/utils.ts +19 -1
package/lib/container.js CHANGED
@@ -7,7 +7,7 @@ import { AttachState, } from "@fluidframework/container-definitions";
7
7
  import { isFluidCodeDetails, } from "@fluidframework/container-definitions/internal";
8
8
  import { LogLevel, } from "@fluidframework/core-interfaces";
9
9
  import { assert, isPromiseLike, unreachableCase } from "@fluidframework/core-utils/internal";
10
- import { MessageType2, OnlineStatus, isCombinedAppAndProtocolSummary, isInstanceOfISnapshot, isOnline, readAndParse, runWithRetry, } from "@fluidframework/driver-utils/internal";
10
+ import { getSnapshotTree, MessageType2, OnlineStatus, isCombinedAppAndProtocolSummary, isInstanceOfISnapshot, isOnline, readAndParse, runWithRetry, } from "@fluidframework/driver-utils/internal";
11
11
  import { MessageType, SummaryType, } from "@fluidframework/protocol-definitions";
12
12
  import { EventEmitterWithErrorHandling, GenericError, PerformanceEvent, UsageError, connectedEventName, createChildLogger, createChildMonitoringContext, formatTick, normalizeError, raiseConnectedEvent, wrapError, } from "@fluidframework/telemetry-utils/internal";
13
13
  import structuredClone from "@ungap/structured-clone";
@@ -126,10 +126,9 @@ export class Container extends EventEmitterWithErrorHandling {
126
126
  * Load an existing container.
127
127
  */
128
128
  static async load(loadProps, createProps) {
129
- const { version, pendingLocalState, loadMode, resolvedUrl, loadToSequenceNumber } = loadProps;
129
+ const { version, pendingLocalState, loadMode, resolvedUrl } = loadProps;
130
130
  const container = new Container(createProps, loadProps);
131
- const disableRecordHeapSize = container.mc.config.getBoolean("Fluid.Loader.DisableRecordHeapSize");
132
- return PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "Load" }, async (event) => new Promise((resolve, reject) => {
131
+ return PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "Load", ...loadMode }, async (event) => new Promise((resolve, reject) => {
133
132
  const defaultMode = { opsBeforeReturn: "cached" };
134
133
  // if we have pendingLocalState, anything we cached is not useful and we shouldn't wait for connection
135
134
  // to return container, so ignore this value and use undefined for opsBeforeReturn
@@ -142,12 +141,12 @@ export class Container extends EventEmitterWithErrorHandling {
142
141
  };
143
142
  container.on("closed", onClosed);
144
143
  container
145
- .load(version, mode, resolvedUrl, pendingLocalState, loadToSequenceNumber)
144
+ .load(version, mode, resolvedUrl, pendingLocalState)
146
145
  .finally(() => {
147
146
  container.removeListener("closed", onClosed);
148
147
  })
149
148
  .then((props) => {
150
- event.end({ ...props, ...loadMode });
149
+ event.end({ ...props });
151
150
  resolve(container);
152
151
  }, (error) => {
153
152
  const err = normalizeError(error);
@@ -161,7 +160,7 @@ export class Container extends EventEmitterWithErrorHandling {
161
160
  container.dispose(err);
162
161
  onClosed(err);
163
162
  });
164
- }), { start: true, end: true, cancel: "generic" }, disableRecordHeapSize !== true /* recordHeapSize */);
163
+ }), { start: true, end: true, cancel: "generic" });
165
164
  }
166
165
  /**
167
166
  * Create a new container in a detached state.
@@ -176,6 +175,8 @@ export class Container extends EventEmitterWithErrorHandling {
176
175
  /**
177
176
  * Create a new container in a detached state that is initialized with a
178
177
  * snapshot from a previous detached container.
178
+ * @param createProps - Config options for this new container instance
179
+ * @param snapshot - A stringified {@link IPendingDetachedContainerState}, e.g. generated via {@link serialize}
179
180
  */
180
181
  static async rehydrateDetachedFromSnapshot(createProps, snapshot) {
181
182
  const container = new Container(createProps);
@@ -189,14 +190,33 @@ export class Container extends EventEmitterWithErrorHandling {
189
190
  // It's conceivable the container could be closed when this is called
190
191
  // Only transition states if currently loading
191
192
  if (this._lifecycleState === "loading") {
192
- // Propagate current connection state through the system.
193
- this.propagateConnectionState(true /* initial transition */);
194
193
  this._lifecycleState = "loaded";
194
+ // Connections transitions are delayed till we are loaded.
195
+ // This is done by holding ops and signals until the end of load sequence
196
+ // (calling this.handleDeltaConnectionArg() after setLoaded() call)
197
+ // If this assert fires, it means our logic managing connection flow is wrong, and the logic below is also wrong.
198
+ assert(this.connectionState !== ConnectionState.Connected, 0x969 /* not connected yet */);
199
+ // Propagate current connection state through the system.
200
+ const readonly = this.readOnlyInfo.readonly ?? false;
201
+ // This call does not look like needed any more, with delaying all connection-related events past loaded phase.
202
+ // Yet, there could be some customer code that would break if we do not deliver it.
203
+ // Will be removed in further PRs with proper changeset.
204
+ this.setContextConnectedState(false /* connected */, readonly);
205
+ // Deliver delayed calls to DeltaManager - we ignored "connect" events while loading.
206
+ const cm = this._deltaManager.connectionManager;
207
+ if (cm.connected) {
208
+ const details = cm.connectionDetails;
209
+ assert(details !== undefined, 0x96a /* should have details if connected */);
210
+ this.connectionStateHandler.receivedConnectEvent(details);
211
+ }
195
212
  }
196
213
  }
197
214
  get closed() {
198
215
  return (this._lifecycleState === "closing" || this._lifecycleState === "closed" || this.disposed);
199
216
  }
217
+ get loaded() {
218
+ return this._lifecycleState === "loaded";
219
+ }
200
220
  get disposed() {
201
221
  return this._lifecycleState === "disposing" || this._lifecycleState === "disposed";
202
222
  }
@@ -265,11 +285,12 @@ export class Container extends EventEmitterWithErrorHandling {
265
285
  return this.connectionStateHandler.connectionState === ConnectionState.Connected;
266
286
  }
267
287
  /**
268
- * The server provided id of the client.
269
- * Set once this.connected is true, otherwise undefined
288
+ * clientId of the latest connection. Changes only once client is connected, caught up and fully loaded.
289
+ * Changes to clientId are delayed through container loading sequence and delived once container is fully loaded.
290
+ * clientId does not reset on lost connection - old value persists until new connection is fully established.
270
291
  */
271
292
  get clientId() {
272
- return this._clientId;
293
+ return this.protocolHandler.audience.getSelf()?.clientId;
273
294
  }
274
295
  get isInteractiveClient() {
275
296
  return this.deltaManager.clientDetails.capabilities.interactive;
@@ -333,6 +354,7 @@ export class Container extends EventEmitterWithErrorHandling {
333
354
  eventName: "ContainerEventHandlerException",
334
355
  name: typeof name === "string" ? name : undefined,
335
356
  }, error);
357
+ this.close(normalizeError(error));
336
358
  });
337
359
  /**
338
360
  * Lifecycle state of the container, used mainly to prevent re-entrancy and telemetry
@@ -427,13 +449,14 @@ export class Container extends EventEmitterWithErrorHandling {
427
449
  throw normalizeErrorAndClose(error);
428
450
  });
429
451
  }
452
+ // If offline load is enabled, attachP will return the attach summary (in Snapshot format) so we can initialize SerializedStateManager
430
453
  const snapshotWithBlobs = await attachP;
431
454
  this.serializedStateManager.setInitialSnapshot(snapshotWithBlobs);
432
455
  if (!this.closed) {
433
- this.handleDeltaConnectionArg({
456
+ this.handleDeltaConnectionArg(attachProps?.deltaConnection, {
434
457
  fetchOpsFromStorage: false,
435
458
  reason: { text: "createDetached" },
436
- }, attachProps?.deltaConnection);
459
+ });
437
460
  }
438
461
  }, { start: true, end: true, cancel: "generic" });
439
462
  });
@@ -457,7 +480,6 @@ export class Container extends EventEmitterWithErrorHandling {
457
480
  const { canReconnect, clientDetailsOverride, urlResolver, documentServiceFactory, codeLoader, options, scope, subLogger, detachedBlobStorage, protocolHandlerBuilder, } = createProps;
458
481
  this.connectionTransitionTimes[ConnectionState.Disconnected] = performance.now();
459
482
  const pendingLocalState = loadProps?.pendingLocalState;
460
- this._clientId = pendingLocalState?.clientId;
461
483
  this._canReconnect = canReconnect ?? true;
462
484
  this.clientDetailsOverride = clientDetailsOverride;
463
485
  this.urlResolver = urlResolver;
@@ -527,12 +549,9 @@ export class Container extends EventEmitterWithErrorHandling {
527
549
  this.connectionStateHandler = createConnectionStateHandler({
528
550
  logger: this.mc.logger,
529
551
  connectionStateChanged: (value, oldState, reason) => {
530
- if (value === ConnectionState.Connected) {
531
- this._clientId = this.connectionStateHandler.pendingClientId;
532
- }
533
552
  this.logConnectionStateChangeTelemetry(value, oldState, reason);
534
- if (this._lifecycleState === "loaded") {
535
- this.propagateConnectionState(false /* initial transition */, value === ConnectionState.Disconnected
553
+ if (this.loaded) {
554
+ this.propagateConnectionState(value === ConnectionState.Disconnected
536
555
  ? reason
537
556
  : undefined /* disconnectedReason */);
538
557
  }
@@ -554,15 +573,22 @@ export class Container extends EventEmitterWithErrorHandling {
554
573
  this.connectionTransitionTimes[ConnectionState.CatchingUp],
555
574
  ...(details === undefined ? {} : { details: JSON.stringify(details) }),
556
575
  });
576
+ // This assert is important for many reasons:
577
+ // 1) Cosmetic / OCE burden: It's useless to raise NoJoinOp error events, if we are loading, as that's most
578
+ // likely to happen if snapshot loading takes too long. During this time we are not processing ops so there is no
579
+ // way to move to "connected" state, and thus "NoJoin" timer would fire (see
580
+ // IConnectionStateHandler.logConnectionIssue() callback and related code in ConnectStateHandler class implementation).
581
+ // But these events do not tell us anything about connectivity pipeline / op processing pipeline,
582
+ // only that boot is slow, and we have events for that.
583
+ // 2) Doing recovery below is useless in loading mode, for the reasons described above. At the same time we can't
584
+ // not do it, as maybe we lost JoinSignal for "self", and when loading is done, we never move to connected
585
+ // state. So we would have to do (in most cases) useless infinite reconnect loop while we are loading.
586
+ assert(this.loaded, 0x96b /* connection issues can be raised only after container is loaded */);
557
587
  // If this is "write" connection, it took too long to receive join op. But in most cases that's due
558
588
  // to very slow op fetches and we will eventually get there.
559
- // For "read" connections, we get here due to self join signal not arriving on time. We will need to
560
- // better understand when and why it may happen.
561
- // For now, attempt to recover by reconnecting. In future, maybe we can query relay service for
562
- // current state of audience.
563
- // Other possible recovery path - move to connected state (i.e. ConnectionStateHandler.joinOpTimer
564
- // to call this.applyForConnectedState("addMemberEvent") for "read" connections)
565
- if (mode === "read") {
589
+ // For "read" connections, we get here due to join signal for "self" not arriving on time.
590
+ // Attempt to recover by reconnecting.
591
+ if (mode === "read" && category === "error") {
566
592
  const reason = { text: "NoJoinSignal" };
567
593
  this.disconnectInternal(reason);
568
594
  this.connectInternal({ reason, fetchOpsFromStorage: false });
@@ -571,6 +597,9 @@ export class Container extends EventEmitterWithErrorHandling {
571
597
  clientShouldHaveLeft: (clientId) => {
572
598
  this.clientsWhoShouldHaveLeft.add(clientId);
573
599
  },
600
+ onCriticalError: (error) => {
601
+ this.close(normalizeError(error));
602
+ },
574
603
  }, this.deltaManager, pendingLocalState?.clientId);
575
604
  this.on(savedContainerEvent, () => {
576
605
  this.connectionStateHandler.containerSaved();
@@ -585,11 +614,11 @@ export class Container extends EventEmitterWithErrorHandling {
585
614
  // Even if not forced on via this flag, combined summaries may still be enabled by service policy.
586
615
  const forceEnableSummarizeProtocolTree = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2") ??
587
616
  options.summarizeProtocolTree;
588
- this.storageAdapter = new ContainerStorageAdapter(detachedBlobStorage, this.mc.logger, pendingLocalState?.snapshotBlobs, addProtocolSummaryIfMissing, forceEnableSummarizeProtocolTree);
617
+ this.storageAdapter = new ContainerStorageAdapter(detachedBlobStorage, this.mc.logger, pendingLocalState?.snapshotBlobs, pendingLocalState?.loadedGroupIdSnapshots, addProtocolSummaryIfMissing, forceEnableSummarizeProtocolTree);
589
618
  const offlineLoadEnabled = (this.isInteractiveClient &&
590
619
  this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad")) ??
591
620
  options.enableOfflineLoad === true;
592
- this.serializedStateManager = new SerializedStateManager(pendingLocalState, this.subLogger, this.storageAdapter, offlineLoadEnabled);
621
+ this.serializedStateManager = new SerializedStateManager(pendingLocalState, this.subLogger, this.storageAdapter, offlineLoadEnabled, this, () => this.isDirty);
593
622
  const isDomAvailable = typeof document === "object" &&
594
623
  document !== null &&
595
624
  typeof document.addEventListener === "function" &&
@@ -724,6 +753,11 @@ export class Container extends EventEmitterWithErrorHandling {
724
753
  this.close();
725
754
  return pendingState;
726
755
  }
756
+ /**
757
+ * Serialize current container state required to rehydrate to the same position without dataloss.
758
+ * Note: The container must already be attached. For detached containers use {@link serialize}
759
+ * @returns stringified {@link IPendingContainerState} for the container
760
+ */
727
761
  async getPendingLocalState() {
728
762
  return this.getPendingLocalStateCore({ notifyImminentClosure: false });
729
763
  }
@@ -733,12 +767,18 @@ export class Container extends EventEmitterWithErrorHandling {
733
767
  }
734
768
  assert(this.attachmentData.state === AttachState.Attached, 0x0d1 /* "Container should be attached before close" */);
735
769
  assert(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid", 0x0d2 /* "resolved url should be valid Fluid url" */);
736
- const pendingState = await this.serializedStateManager.getPendingLocalStateCore(props, this.clientId, this.runtime, this.resolvedUrl);
770
+ const pendingState = await this.serializedStateManager.getPendingLocalState(props, this.clientId, this.runtime, this.resolvedUrl);
737
771
  return pendingState;
738
772
  }
739
773
  get attachState() {
740
774
  return this.attachmentData.state;
741
775
  }
776
+ /**
777
+ * Serialize current container state required to rehydrate to the same position without dataloss.
778
+ * Note: The container must be detached and not closed. For attached containers use
779
+ * {@link getPendingLocalState} or {@link closeAndGetPendingLocalState}
780
+ * @returns stringified {@link IPendingDetachedContainerState} for the container
781
+ */
742
782
  serialize() {
743
783
  if (this.attachmentData.state === AttachState.Attached || this.closed) {
744
784
  throw new UsageError("Container must not be attached or closed.");
@@ -794,11 +834,11 @@ export class Container extends EventEmitterWithErrorHandling {
794
834
  connectInternal(args) {
795
835
  assert(!this.closed, 0x2c5 /* "Attempting to connect() a closed Container" */);
796
836
  assert(this.attachState === AttachState.Attached, 0x2c6 /* "Attempting to connect() a container that is not attached" */);
797
- // Resume processing ops and connect to delta stream
798
- this.resumeInternal(args);
799
837
  // Set Auto Reconnect Mode
800
838
  const mode = ReconnectMode.Enabled;
801
839
  this.setAutoReconnectInternal(mode, args.reason);
840
+ // Resume processing ops and connect to delta stream
841
+ this.resumeInternal(args);
802
842
  }
803
843
  disconnect() {
804
844
  if (this.closed) {
@@ -818,6 +858,11 @@ export class Container extends EventEmitterWithErrorHandling {
818
858
  assert(!this.closed, 0x0d9 /* "Attempting to connect() a closed DeltaManager" */);
819
859
  // Resume processing ops
820
860
  if (this.inboundQueuePausedFromInit) {
861
+ // This assert guards against possibility of ops/signals showing up too soon, while
862
+ // container is not ready yet to receive them. We can hit it only if some internal code call into here,
863
+ // as public API like Container.connect() can be only called when user got back container object, i.e.
864
+ // it is already fully loaded.
865
+ assert(this.loaded, 0x96c /* connect() can be called only in fully loaded state */);
821
866
  this.inboundQueuePausedFromInit = false;
822
867
  this._deltaManager.inbound.resume();
823
868
  this._deltaManager.inboundSignal.resume();
@@ -906,7 +951,7 @@ export class Container extends EventEmitterWithErrorHandling {
906
951
  *
907
952
  * @param specifiedVersion - Version SHA to load snapshot. If not specified, will fetch the latest snapshot.
908
953
  */
909
- async load(specifiedVersion, loadMode, resolvedUrl, pendingLocalState, loadToSequenceNumber) {
954
+ async load(specifiedVersion, loadMode, resolvedUrl, pendingLocalState) {
910
955
  const timings = { phase1: performance.now() };
911
956
  this.service = await this.createDocumentService(async () => this.serviceFactory.createDocumentService(resolvedUrl, this.subLogger, this.client.details.type === summarizerClientType));
912
957
  // Except in cases where it has stashed ops or requested by feature gate, the container will connect in "read" mode
@@ -921,7 +966,7 @@ export class Container extends EventEmitterWithErrorHandling {
921
966
  };
922
967
  // Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
923
968
  // DeltaManager is resilient to this and will wait to start processing ops until after it is attached.
924
- if (loadMode.deltaConnection === undefined && !pendingLocalState) {
969
+ if (loadMode.deltaConnection === undefined) {
925
970
  this.connectToDeltaStream(connectionArgs);
926
971
  }
927
972
  this.storageAdapter.connectToService(this.service);
@@ -933,54 +978,13 @@ export class Container extends EventEmitterWithErrorHandling {
933
978
  true && this.service?.policies?.supportGetSnapshotApi === true;
934
979
  // Fetch specified snapshot.
935
980
  const { baseSnapshot, version } = await this.serializedStateManager.fetchSnapshot(specifiedVersion, supportGetSnapshotApi);
981
+ const baseSnapshotTree = getSnapshotTree(baseSnapshot);
936
982
  this._loadedFromVersion = version;
937
- const attributes = await getDocumentAttributes(this.storageAdapter, baseSnapshot);
983
+ const attributes = await getDocumentAttributes(this.storageAdapter, baseSnapshotTree);
938
984
  // If we saved ops, we will replay them and don't need DeltaManager to fetch them
939
985
  const lastProcessedSequenceNumber = pendingLocalState?.savedOps[pendingLocalState.savedOps.length - 1]?.sequenceNumber ??
940
986
  attributes.sequenceNumber;
941
987
  let opsBeforeReturnP;
942
- if (loadMode.pauseAfterLoad === true) {
943
- // If we are trying to pause at a specific sequence number, ensure the latest snapshot is not newer than the desired sequence number.
944
- if (loadMode.opsBeforeReturn === "sequenceNumber") {
945
- assert(loadToSequenceNumber !== undefined, 0x727 /* sequenceNumber should be defined */);
946
- // Note: It is possible that we think the latest snapshot is newer than the specified sequence number
947
- // due to saved ops that may be replayed after the snapshot.
948
- // https://dev.azure.com/fluidframework/internal/_workitems/edit/5055
949
- if (lastProcessedSequenceNumber > loadToSequenceNumber) {
950
- throw new Error("Cannot satisfy request to pause the container at the specified sequence number. Most recent snapshot is newer than the specified sequence number.");
951
- }
952
- }
953
- // Force readonly mode - this will ensure we don't receive an error for the lack of join op
954
- this.forceReadonly(true);
955
- // We need to setup a listener to stop op processing once we reach the desired sequence number (if specified).
956
- const opHandler = () => {
957
- if (loadToSequenceNumber === undefined) {
958
- // If there is no specified sequence number, pause after the inbound queue is empty.
959
- if (this.deltaManager.inbound.length !== 0) {
960
- return;
961
- }
962
- }
963
- else {
964
- // If there is a specified sequence number, keep processing until we reach it.
965
- if (this.deltaManager.lastSequenceNumber < loadToSequenceNumber) {
966
- return;
967
- }
968
- }
969
- // Pause op processing once we have processed the desired number of ops.
970
- void this.deltaManager.inbound.pause();
971
- void this.deltaManager.outbound.pause();
972
- this.off("op", opHandler);
973
- };
974
- if ((loadToSequenceNumber === undefined && this.deltaManager.inbound.length === 0) ||
975
- this.deltaManager.lastSequenceNumber === loadToSequenceNumber) {
976
- // If we have already reached the desired sequence number, call opHandler() to pause immediately.
977
- opHandler();
978
- }
979
- else {
980
- // If we have not yet reached the desired sequence number, setup a listener to pause once we reach it.
981
- this.on("op", opHandler);
982
- }
983
- }
984
988
  // Attach op handlers to finish initialization and be able to start processing ops
985
989
  // Kick off any ops fetching if required.
986
990
  switch (loadMode.opsBeforeReturn) {
@@ -989,7 +993,6 @@ export class Container extends EventEmitterWithErrorHandling {
989
993
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
990
994
  this.attachDeltaManagerOpHandler(attributes, loadMode.deltaConnection !== "none" ? "all" : "none", lastProcessedSequenceNumber);
991
995
  break;
992
- case "sequenceNumber":
993
996
  case "cached":
994
997
  case "all":
995
998
  opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, loadMode.opsBeforeReturn, lastProcessedSequenceNumber);
@@ -999,10 +1002,16 @@ export class Container extends EventEmitterWithErrorHandling {
999
1002
  }
1000
1003
  // ...load in the existing quorum
1001
1004
  // Initialize the protocol handler
1002
- await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, baseSnapshot);
1005
+ await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, baseSnapshotTree);
1006
+ // If we are loading from pending state, we start with old clientId.
1007
+ // We switch to latest connection clientId only after setLoaded().
1008
+ assert(this.clientId === undefined, 0x96d /* there should be no clientId yet */);
1009
+ if (pendingLocalState?.clientId !== undefined) {
1010
+ this.protocolHandler.audience.setCurrentClientId(pendingLocalState?.clientId);
1011
+ }
1003
1012
  timings.phase3 = performance.now();
1004
1013
  const codeDetails = this.getCodeDetailsFromQuorum();
1005
- await this.instantiateRuntime(codeDetails, baseSnapshot,
1014
+ await this.instantiateRuntime(codeDetails, baseSnapshotTree,
1006
1015
  // give runtime a dummy value so it knows we're loading from a stash blob
1007
1016
  pendingLocalState ? pendingLocalState?.pendingRuntimeState ?? {} : undefined, isInstanceOfISnapshot(baseSnapshot) ? baseSnapshot : undefined);
1008
1017
  // replay saved ops
@@ -1016,6 +1025,7 @@ export class Container extends EventEmitterWithErrorHandling {
1016
1025
  await this.runtime.notifyOpReplay?.(message);
1017
1026
  }
1018
1027
  pendingLocalState.savedOps = [];
1028
+ this.storageAdapter.clearPendingState();
1019
1029
  }
1020
1030
  // We might have hit some failure that did not manifest itself in exception in this flow,
1021
1031
  // do not start op processing in such case - static version of Container.load() will handle it correctly.
@@ -1027,20 +1037,11 @@ export class Container extends EventEmitterWithErrorHandling {
1027
1037
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1028
1038
  this._deltaManager.inbound.pause();
1029
1039
  }
1030
- this.handleDeltaConnectionArg(connectionArgs, loadMode.deltaConnection, pendingLocalState !== undefined);
1031
- }
1032
- // If we have not yet reached `loadToSequenceNumber`, we will wait for ops to arrive until we reach it
1033
- if (loadToSequenceNumber !== undefined &&
1034
- this.deltaManager.lastSequenceNumber < loadToSequenceNumber) {
1035
- await new Promise((resolve, reject) => {
1036
- const opHandler = (message) => {
1037
- if (message.sequenceNumber >= loadToSequenceNumber) {
1038
- resolve();
1039
- this.off("op", opHandler);
1040
- }
1041
- };
1042
- this.on("op", opHandler);
1043
- });
1040
+ // Internal context is fully loaded at this point
1041
+ // Move to loaded before calling this.handleDeltaConnectionArg() - latter allows ops & signals in, which
1042
+ // may result in container moving to "connected" state. Such transitions are allowed only in loaded state.
1043
+ this.setLoaded();
1044
+ this.handleDeltaConnectionArg(loadMode.deltaConnection);
1044
1045
  }
1045
1046
  // Safety net: static version of Container.load() should have learned about it through "closed" handler.
1046
1047
  // But if that did not happen for some reason, fail load for sure.
@@ -1050,8 +1051,6 @@ export class Container extends EventEmitterWithErrorHandling {
1050
1051
  if (this.closed) {
1051
1052
  throw new Error("Container was closed while load()");
1052
1053
  }
1053
- // Internal context is fully loaded at this point
1054
- this.setLoaded();
1055
1054
  timings.end = performance.now();
1056
1055
  this.subLogger.sendTelemetryEvent({
1057
1056
  eventName: "LoadStagesTimings",
@@ -1231,7 +1230,21 @@ export class Container extends EventEmitterWithErrorHandling {
1231
1230
  deltaManager.inboundSignal.pause();
1232
1231
  deltaManager.on("connect", (details, _opsBehind) => {
1233
1232
  assert(this.connectionMode === details.mode, 0x4b7 /* mismatch */);
1234
- this.connectionStateHandler.receivedConnectEvent(details);
1233
+ // Delay raising events until setLoaded()
1234
+ // Here are some of the reasons why this design is chosen:
1235
+ // 1. Various processes track speed of connection. But we are not processing ops or signal while container is loading,
1236
+ // and thus we can't move forward across connection modes. This results in telemetry errors (like NoJoinOp) that
1237
+ // have nothing to do with connection flow itself
1238
+ // 2. This also makes it hard to reason about recovery (like reconnection) in case we might have lost JoinSignal. Reconnecting
1239
+ // in loading phase is useless (get back to same state), but at the same time not doing it may result in broken connection
1240
+ // without recovery (after we loaded).
1241
+ // 3. We expose non-consistent view. ContainerRuntime may start loading in non-connected state, but end in connected, with
1242
+ // no events telling about it (until we loaded). Most of the code relies on a fact that state changes when events fire.
1243
+ // This will not delay any processes (as observed by the user). I.e. once container moves to loaded phase,
1244
+ // we immediately would transition across all phases, if we have proper signals / ops ready.
1245
+ if (this.loaded) {
1246
+ this.connectionStateHandler.receivedConnectEvent(details);
1247
+ }
1235
1248
  });
1236
1249
  deltaManager.on("establishingConnection", (reason) => {
1237
1250
  this.connectionStateHandler.establishingConnection(reason);
@@ -1241,8 +1254,14 @@ export class Container extends EventEmitterWithErrorHandling {
1241
1254
  });
1242
1255
  deltaManager.on("disconnect", (text, error) => {
1243
1256
  this.noopHeuristic?.notifyDisconnect();
1244
- if (!this.closed) {
1245
- this.connectionStateHandler.receivedDisconnectEvent({ text, error });
1257
+ const reason = { text, error };
1258
+ // Symmetry with "connect" events
1259
+ if (this.loaded) {
1260
+ this.connectionStateHandler.receivedDisconnectEvent(reason);
1261
+ }
1262
+ else if (!this.closed) {
1263
+ // Raise cancellation to get state machine back to initial state
1264
+ this.connectionStateHandler.cancelEstablishingConnection(reason);
1246
1265
  }
1247
1266
  });
1248
1267
  deltaManager.on("throttled", (warning) => {
@@ -1255,7 +1274,9 @@ export class Container extends EventEmitterWithErrorHandling {
1255
1274
  this.emit("warning", warn);
1256
1275
  });
1257
1276
  deltaManager.on("readonly", (readonly) => {
1258
- this.setContextConnectedState(this.connectionState === ConnectionState.Connected, readonly);
1277
+ if (this.loaded) {
1278
+ this.setContextConnectedState(this.connectionState === ConnectionState.Connected, readonly);
1279
+ }
1259
1280
  this.emit("readonly", readonly);
1260
1281
  });
1261
1282
  deltaManager.on("closed", (error) => {
@@ -1297,8 +1318,7 @@ export class Container extends EventEmitterWithErrorHandling {
1297
1318
  // This info is of most interesting while Catching Up.
1298
1319
  checkpointSequenceNumber = this.deltaManager.lastKnownSeqNumber;
1299
1320
  // Need to check that we have already loaded and fetched the snapshot.
1300
- if (this.deltaManager.hasCheckpointSequenceNumber &&
1301
- this._lifecycleState === "loaded") {
1321
+ if (this.deltaManager.hasCheckpointSequenceNumber && this.loaded) {
1302
1322
  opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
1303
1323
  }
1304
1324
  }
@@ -1312,7 +1332,7 @@ export class Container extends EventEmitterWithErrorHandling {
1312
1332
  reason: reason?.text,
1313
1333
  connectionInitiationReason,
1314
1334
  pendingClientId: this.connectionStateHandler.pendingClientId,
1315
- clientId: this.clientId,
1335
+ clientId: this.connectionStateHandler.clientId,
1316
1336
  autoReconnect,
1317
1337
  opsBehind,
1318
1338
  online: OnlineStatus[isOnline()],
@@ -1328,20 +1348,23 @@ export class Container extends EventEmitterWithErrorHandling {
1328
1348
  this.firstConnection = false;
1329
1349
  }
1330
1350
  }
1331
- propagateConnectionState(initialTransition, disconnectedReason) {
1332
- // When container loaded, we want to propagate initial connection state.
1333
- // After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
1351
+ propagateConnectionState(disconnectedReason) {
1352
+ const connected = this.connectionState === ConnectionState.Connected;
1353
+ if (connected) {
1354
+ const clientId = this.connectionStateHandler.clientId;
1355
+ assert(clientId !== undefined, 0x96e /* there has to be clientId */);
1356
+ this.protocolHandler.audience.setCurrentClientId(clientId);
1357
+ }
1358
+ // We communicate only transitions to Connected & Disconnected states, skipping all other states.
1334
1359
  // This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
1335
- if (!initialTransition &&
1336
- this.connectionState !== ConnectionState.Connected &&
1360
+ if (this.connectionState !== ConnectionState.Connected &&
1337
1361
  this.connectionState !== ConnectionState.Disconnected) {
1338
1362
  return;
1339
1363
  }
1340
- const state = this.connectionState === ConnectionState.Connected;
1341
1364
  // Both protocol and context should not be undefined if we got so far.
1342
- this.setContextConnectedState(state, this.readOnlyInfo.readonly ?? false);
1343
- this.protocolHandler.setConnectionState(state, this.clientId);
1344
- raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason?.text);
1365
+ this.setContextConnectedState(connected, this.readOnlyInfo.readonly ?? false);
1366
+ this.protocolHandler.setConnectionState(connected, this.clientId);
1367
+ raiseConnectedEvent(this.mc.logger, this, connected, this.clientId, disconnectedReason?.text);
1345
1368
  }
1346
1369
  // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
1347
1370
  submitContainerMessage(type, contents, batch, metadata) {
@@ -1462,24 +1485,29 @@ export class Container extends EventEmitterWithErrorHandling {
1462
1485
  /**
1463
1486
  * Set the connected state of the ContainerContext
1464
1487
  * This controls the "connected" state of the ContainerRuntime as well
1465
- * @param state - Is the container currently connected?
1488
+ * @param connected - Is the container currently connected?
1466
1489
  * @param readonly - Is the container in readonly mode?
1467
1490
  */
1468
- setContextConnectedState(state, readonly) {
1469
- if (this._runtime?.disposed === false) {
1491
+ setContextConnectedState(connected, readonly) {
1492
+ if (this._runtime?.disposed === false && this.loaded) {
1470
1493
  /**
1471
1494
  * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
1472
1495
  * ops getting through to the DeltaManager.
1473
1496
  * The ContainerRuntime's "connected" state simply means it is ok to send ops
1474
1497
  * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
1475
1498
  */
1476
- this.runtime.setConnectionState(state && !readonly, this.clientId);
1499
+ this.runtime.setConnectionState(connected && !readonly, this.clientId);
1477
1500
  }
1478
1501
  }
1479
- handleDeltaConnectionArg(connectionArgs, deltaConnectionArg, canConnect = true) {
1502
+ handleDeltaConnectionArg(deltaConnectionArg, connectionArgs) {
1503
+ // This ensures that we allow transitions to "connected" state only after container has been fully loaded
1504
+ // and we propagate such events to container runtime. All events prior to being loaded are ignored.
1505
+ // This means if we get here in non-loaded state, we might not deliver proper events to container runtime,
1506
+ // and runtime implementation may miss such events.
1507
+ assert(this.loaded, 0x96f /* has to be called after container transitions to loaded state */);
1480
1508
  switch (deltaConnectionArg) {
1481
1509
  case undefined:
1482
- if (canConnect) {
1510
+ if (connectionArgs) {
1483
1511
  // connect to delta stream now since we did not before
1484
1512
  this.connectToDeltaStream(connectionArgs);
1485
1513
  }