@fluidframework/container-loader 2.0.0-dev-rc.3.0.0.254674 → 2.0.0-dev-rc.4.0.0.261659

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 (151) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/api-report/container-loader.api.md +7 -3
  3. package/dist/audience.d.ts +6 -4
  4. package/dist/audience.d.ts.map +1 -1
  5. package/dist/audience.js +18 -3
  6. package/dist/audience.js.map +1 -1
  7. package/dist/connectionManager.d.ts +6 -2
  8. package/dist/connectionManager.d.ts.map +1 -1
  9. package/dist/connectionManager.js +40 -16
  10. package/dist/connectionManager.js.map +1 -1
  11. package/dist/connectionStateHandler.d.ts +29 -8
  12. package/dist/connectionStateHandler.d.ts.map +1 -1
  13. package/dist/connectionStateHandler.js +49 -36
  14. package/dist/connectionStateHandler.js.map +1 -1
  15. package/dist/container.d.ts +6 -10
  16. package/dist/container.d.ts.map +1 -1
  17. package/dist/container.js +126 -113
  18. package/dist/container.js.map +1 -1
  19. package/dist/containerContext.d.ts +1 -1
  20. package/dist/containerContext.d.ts.map +1 -1
  21. package/dist/containerContext.js.map +1 -1
  22. package/dist/containerStorageAdapter.d.ts +12 -3
  23. package/dist/containerStorageAdapter.d.ts.map +1 -1
  24. package/dist/containerStorageAdapter.js +42 -4
  25. package/dist/containerStorageAdapter.js.map +1 -1
  26. package/dist/debugLogger.d.ts +1 -2
  27. package/dist/debugLogger.d.ts.map +1 -1
  28. package/dist/debugLogger.js.map +1 -1
  29. package/dist/deltaManager.d.ts +3 -4
  30. package/dist/deltaManager.d.ts.map +1 -1
  31. package/dist/deltaManager.js +8 -3
  32. package/dist/deltaManager.js.map +1 -1
  33. package/dist/error.d.ts +1 -2
  34. package/dist/error.d.ts.map +1 -1
  35. package/dist/error.js.map +1 -1
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +3 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/{alpha.d.ts → legacy.d.ts} +5 -2
  41. package/dist/loadPaused.d.ts +35 -0
  42. package/dist/loadPaused.d.ts.map +1 -0
  43. package/dist/loadPaused.js +115 -0
  44. package/dist/loadPaused.js.map +1 -0
  45. package/dist/loader.d.ts +1 -1
  46. package/dist/loader.d.ts.map +1 -1
  47. package/dist/loader.js +0 -13
  48. package/dist/loader.js.map +1 -1
  49. package/dist/packageVersion.d.ts +1 -1
  50. package/dist/packageVersion.js +1 -1
  51. package/dist/packageVersion.js.map +1 -1
  52. package/dist/protocol.d.ts.map +1 -1
  53. package/dist/protocol.js +3 -0
  54. package/dist/protocol.js.map +1 -1
  55. package/dist/public.d.ts +2 -1
  56. package/dist/retriableDocumentStorageService.d.ts +1 -1
  57. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  58. package/dist/retriableDocumentStorageService.js.map +1 -1
  59. package/dist/serializedStateManager.d.ts +23 -5
  60. package/dist/serializedStateManager.d.ts.map +1 -1
  61. package/dist/serializedStateManager.js +72 -22
  62. package/dist/serializedStateManager.js.map +1 -1
  63. package/dist/utils.d.ts +2 -2
  64. package/dist/utils.d.ts.map +1 -1
  65. package/dist/utils.js +2 -3
  66. package/dist/utils.js.map +1 -1
  67. package/{dist/beta.d.ts → internal.d.ts} +2 -4
  68. package/{lib/beta.d.ts → legacy.d.ts} +2 -4
  69. package/lib/audience.d.ts +6 -4
  70. package/lib/audience.d.ts.map +1 -1
  71. package/lib/audience.js +19 -4
  72. package/lib/audience.js.map +1 -1
  73. package/lib/connectionManager.d.ts +6 -2
  74. package/lib/connectionManager.d.ts.map +1 -1
  75. package/lib/connectionManager.js +41 -17
  76. package/lib/connectionManager.js.map +1 -1
  77. package/lib/connectionStateHandler.d.ts +29 -8
  78. package/lib/connectionStateHandler.d.ts.map +1 -1
  79. package/lib/connectionStateHandler.js +49 -36
  80. package/lib/connectionStateHandler.js.map +1 -1
  81. package/lib/container.d.ts +6 -10
  82. package/lib/container.d.ts.map +1 -1
  83. package/lib/container.js +126 -113
  84. package/lib/container.js.map +1 -1
  85. package/lib/containerContext.d.ts +1 -1
  86. package/lib/containerContext.d.ts.map +1 -1
  87. package/lib/containerContext.js.map +1 -1
  88. package/lib/containerStorageAdapter.d.ts +12 -3
  89. package/lib/containerStorageAdapter.d.ts.map +1 -1
  90. package/lib/containerStorageAdapter.js +42 -4
  91. package/lib/containerStorageAdapter.js.map +1 -1
  92. package/lib/debugLogger.d.ts +1 -2
  93. package/lib/debugLogger.d.ts.map +1 -1
  94. package/lib/debugLogger.js.map +1 -1
  95. package/lib/deltaManager.d.ts +3 -4
  96. package/lib/deltaManager.d.ts.map +1 -1
  97. package/lib/deltaManager.js +9 -4
  98. package/lib/deltaManager.js.map +1 -1
  99. package/lib/error.d.ts +1 -2
  100. package/lib/error.d.ts.map +1 -1
  101. package/lib/error.js.map +1 -1
  102. package/lib/index.d.ts +1 -0
  103. package/lib/index.d.ts.map +1 -1
  104. package/lib/index.js +1 -0
  105. package/lib/index.js.map +1 -1
  106. package/lib/{alpha.d.ts → legacy.d.ts} +5 -2
  107. package/lib/loadPaused.d.ts +35 -0
  108. package/lib/loadPaused.d.ts.map +1 -0
  109. package/lib/loadPaused.js +111 -0
  110. package/lib/loadPaused.js.map +1 -0
  111. package/lib/loader.d.ts +1 -1
  112. package/lib/loader.d.ts.map +1 -1
  113. package/lib/loader.js +1 -14
  114. package/lib/loader.js.map +1 -1
  115. package/lib/packageVersion.d.ts +1 -1
  116. package/lib/packageVersion.js +1 -1
  117. package/lib/packageVersion.js.map +1 -1
  118. package/lib/protocol.d.ts.map +1 -1
  119. package/lib/protocol.js +3 -0
  120. package/lib/protocol.js.map +1 -1
  121. package/lib/public.d.ts +2 -1
  122. package/lib/retriableDocumentStorageService.d.ts +1 -1
  123. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  124. package/lib/retriableDocumentStorageService.js +1 -1
  125. package/lib/retriableDocumentStorageService.js.map +1 -1
  126. package/lib/serializedStateManager.d.ts +23 -5
  127. package/lib/serializedStateManager.d.ts.map +1 -1
  128. package/lib/serializedStateManager.js +66 -16
  129. package/lib/serializedStateManager.js.map +1 -1
  130. package/lib/utils.d.ts +2 -2
  131. package/lib/utils.d.ts.map +1 -1
  132. package/lib/utils.js +2 -3
  133. package/lib/utils.js.map +1 -1
  134. package/package.json +29 -27
  135. package/src/audience.ts +30 -9
  136. package/src/connectionManager.ts +50 -21
  137. package/src/connectionStateHandler.ts +76 -43
  138. package/src/container.ts +150 -153
  139. package/src/containerContext.ts +1 -1
  140. package/src/containerStorageAdapter.ts +59 -7
  141. package/src/debugLogger.ts +1 -1
  142. package/src/deltaManager.ts +13 -6
  143. package/src/error.ts +1 -1
  144. package/src/index.ts +1 -0
  145. package/src/loadPaused.ts +140 -0
  146. package/src/loader.ts +1 -21
  147. package/src/packageVersion.ts +1 -1
  148. package/src/protocol.ts +4 -0
  149. package/src/retriableDocumentStorageService.ts +5 -2
  150. package/src/serializedStateManager.ts +107 -31
  151. package/src/utils.ts +3 -4
package/dist/container.js CHANGED
@@ -134,10 +134,9 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
134
134
  * Load an existing container.
135
135
  */
136
136
  static async load(loadProps, createProps) {
137
- const { version, pendingLocalState, loadMode, resolvedUrl, loadToSequenceNumber } = loadProps;
137
+ const { version, pendingLocalState, loadMode, resolvedUrl } = loadProps;
138
138
  const container = new Container(createProps, loadProps);
139
- const disableRecordHeapSize = container.mc.config.getBoolean("Fluid.Loader.DisableRecordHeapSize");
140
- return internal_4.PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "Load" }, async (event) => new Promise((resolve, reject) => {
139
+ return internal_4.PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "Load", ...loadMode }, async (event) => new Promise((resolve, reject) => {
141
140
  const defaultMode = { opsBeforeReturn: "cached" };
142
141
  // if we have pendingLocalState, anything we cached is not useful and we shouldn't wait for connection
143
142
  // to return container, so ignore this value and use undefined for opsBeforeReturn
@@ -150,12 +149,12 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
150
149
  };
151
150
  container.on("closed", onClosed);
152
151
  container
153
- .load(version, mode, resolvedUrl, pendingLocalState, loadToSequenceNumber)
152
+ .load(version, mode, resolvedUrl, pendingLocalState)
154
153
  .finally(() => {
155
154
  container.removeListener("closed", onClosed);
156
155
  })
157
156
  .then((props) => {
158
- event.end({ ...props, ...loadMode });
157
+ event.end({ ...props });
159
158
  resolve(container);
160
159
  }, (error) => {
161
160
  const err = (0, internal_4.normalizeError)(error);
@@ -169,7 +168,7 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
169
168
  container.dispose(err);
170
169
  onClosed(err);
171
170
  });
172
- }), { start: true, end: true, cancel: "generic" }, disableRecordHeapSize !== true /* recordHeapSize */);
171
+ }), { start: true, end: true, cancel: "generic" });
173
172
  }
174
173
  /**
175
174
  * Create a new container in a detached state.
@@ -197,14 +196,33 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
197
196
  // It's conceivable the container could be closed when this is called
198
197
  // Only transition states if currently loading
199
198
  if (this._lifecycleState === "loading") {
200
- // Propagate current connection state through the system.
201
- this.propagateConnectionState(true /* initial transition */);
202
199
  this._lifecycleState = "loaded";
200
+ // Connections transitions are delayed till we are loaded.
201
+ // This is done by holding ops and signals until the end of load sequence
202
+ // (calling this.handleDeltaConnectionArg() after setLoaded() call)
203
+ // If this assert fires, it means our logic managing connection flow is wrong, and the logic below is also wrong.
204
+ (0, internal_2.assert)(this.connectionState !== connectionState_js_1.ConnectionState.Connected, "not connected yet");
205
+ // Propagate current connection state through the system.
206
+ const readonly = this.readOnlyInfo.readonly ?? false;
207
+ // This call does not look like needed any more, with delaying all connection-related events past loaded phase.
208
+ // Yet, there could be some customer code that would break if we do not deliver it.
209
+ // Will be removed in further PRs with proper changeset.
210
+ this.setContextConnectedState(false /* connected */, readonly);
211
+ // Deliver delayed calls to DeltaManager - we ignored "connect" events while loading.
212
+ const cm = this._deltaManager.connectionManager;
213
+ if (cm.connected) {
214
+ const details = cm.connectionDetails;
215
+ (0, internal_2.assert)(details !== undefined, "should have details if connected");
216
+ this.connectionStateHandler.receivedConnectEvent(details);
217
+ }
203
218
  }
204
219
  }
205
220
  get closed() {
206
221
  return (this._lifecycleState === "closing" || this._lifecycleState === "closed" || this.disposed);
207
222
  }
223
+ get loaded() {
224
+ return this._lifecycleState === "loaded";
225
+ }
208
226
  get disposed() {
209
227
  return this._lifecycleState === "disposing" || this._lifecycleState === "disposed";
210
228
  }
@@ -273,11 +291,12 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
273
291
  return this.connectionStateHandler.connectionState === connectionState_js_1.ConnectionState.Connected;
274
292
  }
275
293
  /**
276
- * The server provided id of the client.
277
- * Set once this.connected is true, otherwise undefined
294
+ * clientId of the latest connection. Changes only once client is connected, caught up and fully loaded.
295
+ * Changes to clientId are delayed through container loading sequence and delived once container is fully loaded.
296
+ * clientId does not reset on lost connection - old value persists until new connection is fully established.
278
297
  */
279
298
  get clientId() {
280
- return this._clientId;
299
+ return this.protocolHandler.audience.getSelf()?.clientId;
281
300
  }
282
301
  get isInteractiveClient() {
283
302
  return this.deltaManager.clientDetails.capabilities.interactive;
@@ -341,6 +360,7 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
341
360
  eventName: "ContainerEventHandlerException",
342
361
  name: typeof name === "string" ? name : undefined,
343
362
  }, error);
363
+ this.close((0, internal_4.normalizeError)(error));
344
364
  });
345
365
  /**
346
366
  * Lifecycle state of the container, used mainly to prevent re-entrancy and telemetry
@@ -438,10 +458,10 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
438
458
  const snapshotWithBlobs = await attachP;
439
459
  this.serializedStateManager.setInitialSnapshot(snapshotWithBlobs);
440
460
  if (!this.closed) {
441
- this.handleDeltaConnectionArg({
461
+ this.handleDeltaConnectionArg(attachProps?.deltaConnection, {
442
462
  fetchOpsFromStorage: false,
443
463
  reason: { text: "createDetached" },
444
- }, attachProps?.deltaConnection);
464
+ });
445
465
  }
446
466
  }, { start: true, end: true, cancel: "generic" });
447
467
  });
@@ -465,7 +485,6 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
465
485
  const { canReconnect, clientDetailsOverride, urlResolver, documentServiceFactory, codeLoader, options, scope, subLogger, detachedBlobStorage, protocolHandlerBuilder, } = createProps;
466
486
  this.connectionTransitionTimes[connectionState_js_1.ConnectionState.Disconnected] = client_utils_1.performance.now();
467
487
  const pendingLocalState = loadProps?.pendingLocalState;
468
- this._clientId = pendingLocalState?.clientId;
469
488
  this._canReconnect = canReconnect ?? true;
470
489
  this.clientDetailsOverride = clientDetailsOverride;
471
490
  this.urlResolver = urlResolver;
@@ -535,12 +554,9 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
535
554
  this.connectionStateHandler = (0, connectionStateHandler_js_1.createConnectionStateHandler)({
536
555
  logger: this.mc.logger,
537
556
  connectionStateChanged: (value, oldState, reason) => {
538
- if (value === connectionState_js_1.ConnectionState.Connected) {
539
- this._clientId = this.connectionStateHandler.pendingClientId;
540
- }
541
557
  this.logConnectionStateChangeTelemetry(value, oldState, reason);
542
- if (this._lifecycleState === "loaded") {
543
- this.propagateConnectionState(false /* initial transition */, value === connectionState_js_1.ConnectionState.Disconnected
558
+ if (this.loaded) {
559
+ this.propagateConnectionState(value === connectionState_js_1.ConnectionState.Disconnected
544
560
  ? reason
545
561
  : undefined /* disconnectedReason */);
546
562
  }
@@ -562,15 +578,22 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
562
578
  this.connectionTransitionTimes[connectionState_js_1.ConnectionState.CatchingUp],
563
579
  ...(details === undefined ? {} : { details: JSON.stringify(details) }),
564
580
  });
581
+ // This assert is important for many reasons:
582
+ // 1) Cosmetic / OCE burden: It's useless to raise NoJoinOp error events, if we are loading, as that's most
583
+ // likely to happen if snapshot loading takes too long. During this time we are not processing ops so there is no
584
+ // way to move to "connected" state, and thus "NoJoin" timer would fire (see
585
+ // IConnectionStateHandler.logConnectionIssue() callback and related code in ConnectStateHandler class implementation).
586
+ // But these events do not tell us anything about connectivity pipeline / op processing pipeline,
587
+ // only that boot is slow, and we have events for that.
588
+ // 2) Doing recovery below is useless in loading mode, for the reasons described above. At the same time we can't
589
+ // not do it, as maybe we lost JoinSignal for "self", and when loading is done, we never move to connected
590
+ // state. So we would have to do (in most cases) useless infinite reconnect loop while we are loading.
591
+ (0, internal_2.assert)(this.loaded, "connection issues can be raised only after container is loaded");
565
592
  // If this is "write" connection, it took too long to receive join op. But in most cases that's due
566
593
  // to very slow op fetches and we will eventually get there.
567
- // For "read" connections, we get here due to self join signal not arriving on time. We will need to
568
- // better understand when and why it may happen.
569
- // For now, attempt to recover by reconnecting. In future, maybe we can query relay service for
570
- // current state of audience.
571
- // Other possible recovery path - move to connected state (i.e. ConnectionStateHandler.joinOpTimer
572
- // to call this.applyForConnectedState("addMemberEvent") for "read" connections)
573
- if (mode === "read") {
594
+ // For "read" connections, we get here due to join signal for "self" not arriving on time.
595
+ // Attempt to recover by reconnecting.
596
+ if (mode === "read" && category === "error") {
574
597
  const reason = { text: "NoJoinSignal" };
575
598
  this.disconnectInternal(reason);
576
599
  this.connectInternal({ reason, fetchOpsFromStorage: false });
@@ -579,6 +602,9 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
579
602
  clientShouldHaveLeft: (clientId) => {
580
603
  this.clientsWhoShouldHaveLeft.add(clientId);
581
604
  },
605
+ onCriticalError: (error) => {
606
+ this.close((0, internal_4.normalizeError)(error));
607
+ },
582
608
  }, this.deltaManager, pendingLocalState?.clientId);
583
609
  this.on(savedContainerEvent, () => {
584
610
  this.connectionStateHandler.containerSaved();
@@ -593,11 +619,11 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
593
619
  // Even if not forced on via this flag, combined summaries may still be enabled by service policy.
594
620
  const forceEnableSummarizeProtocolTree = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2") ??
595
621
  options.summarizeProtocolTree;
596
- this.storageAdapter = new containerStorageAdapter_js_1.ContainerStorageAdapter(detachedBlobStorage, this.mc.logger, pendingLocalState?.snapshotBlobs, addProtocolSummaryIfMissing, forceEnableSummarizeProtocolTree);
622
+ this.storageAdapter = new containerStorageAdapter_js_1.ContainerStorageAdapter(detachedBlobStorage, this.mc.logger, pendingLocalState?.snapshotBlobs, pendingLocalState?.loadedGroupIdSnapshots, addProtocolSummaryIfMissing, forceEnableSummarizeProtocolTree);
597
623
  const offlineLoadEnabled = (this.isInteractiveClient &&
598
624
  this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad")) ??
599
625
  options.enableOfflineLoad === true;
600
- this.serializedStateManager = new serializedStateManager_js_1.SerializedStateManager(pendingLocalState, this.subLogger, this.storageAdapter, offlineLoadEnabled);
626
+ this.serializedStateManager = new serializedStateManager_js_1.SerializedStateManager(pendingLocalState, this.subLogger, this.storageAdapter, offlineLoadEnabled, this, () => this.isDirty);
601
627
  const isDomAvailable = typeof document === "object" &&
602
628
  document !== null &&
603
629
  typeof document.addEventListener === "function" &&
@@ -802,11 +828,11 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
802
828
  connectInternal(args) {
803
829
  (0, internal_2.assert)(!this.closed, 0x2c5 /* "Attempting to connect() a closed Container" */);
804
830
  (0, internal_2.assert)(this.attachState === container_definitions_1.AttachState.Attached, 0x2c6 /* "Attempting to connect() a container that is not attached" */);
805
- // Resume processing ops and connect to delta stream
806
- this.resumeInternal(args);
807
831
  // Set Auto Reconnect Mode
808
832
  const mode = contracts_js_1.ReconnectMode.Enabled;
809
833
  this.setAutoReconnectInternal(mode, args.reason);
834
+ // Resume processing ops and connect to delta stream
835
+ this.resumeInternal(args);
810
836
  }
811
837
  disconnect() {
812
838
  if (this.closed) {
@@ -826,6 +852,11 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
826
852
  (0, internal_2.assert)(!this.closed, 0x0d9 /* "Attempting to connect() a closed DeltaManager" */);
827
853
  // Resume processing ops
828
854
  if (this.inboundQueuePausedFromInit) {
855
+ // This assert guards against possibility of ops/signals showing up too soon, while
856
+ // container is not ready yet to receive them. We can hit it only if some internal code call into here,
857
+ // as public API like Container.connect() can be only called when user got back container object, i.e.
858
+ // it is already fully loaded.
859
+ (0, internal_2.assert)(this.loaded, "connect() can be called only in fully loaded state");
829
860
  this.inboundQueuePausedFromInit = false;
830
861
  this._deltaManager.inbound.resume();
831
862
  this._deltaManager.inboundSignal.resume();
@@ -914,7 +945,7 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
914
945
  *
915
946
  * @param specifiedVersion - Version SHA to load snapshot. If not specified, will fetch the latest snapshot.
916
947
  */
917
- async load(specifiedVersion, loadMode, resolvedUrl, pendingLocalState, loadToSequenceNumber) {
948
+ async load(specifiedVersion, loadMode, resolvedUrl, pendingLocalState) {
918
949
  const timings = { phase1: client_utils_1.performance.now() };
919
950
  this.service = await this.createDocumentService(async () => this.serviceFactory.createDocumentService(resolvedUrl, this.subLogger, this.client.details.type === summarizerClientType));
920
951
  // Except in cases where it has stashed ops or requested by feature gate, the container will connect in "read" mode
@@ -929,7 +960,7 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
929
960
  };
930
961
  // Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
931
962
  // DeltaManager is resilient to this and will wait to start processing ops until after it is attached.
932
- if (loadMode.deltaConnection === undefined && !pendingLocalState) {
963
+ if (loadMode.deltaConnection === undefined) {
933
964
  this.connectToDeltaStream(connectionArgs);
934
965
  }
935
966
  this.storageAdapter.connectToService(this.service);
@@ -947,48 +978,6 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
947
978
  const lastProcessedSequenceNumber = pendingLocalState?.savedOps[pendingLocalState.savedOps.length - 1]?.sequenceNumber ??
948
979
  attributes.sequenceNumber;
949
980
  let opsBeforeReturnP;
950
- if (loadMode.pauseAfterLoad === true) {
951
- // If we are trying to pause at a specific sequence number, ensure the latest snapshot is not newer than the desired sequence number.
952
- if (loadMode.opsBeforeReturn === "sequenceNumber") {
953
- (0, internal_2.assert)(loadToSequenceNumber !== undefined, 0x727 /* sequenceNumber should be defined */);
954
- // Note: It is possible that we think the latest snapshot is newer than the specified sequence number
955
- // due to saved ops that may be replayed after the snapshot.
956
- // https://dev.azure.com/fluidframework/internal/_workitems/edit/5055
957
- if (lastProcessedSequenceNumber > loadToSequenceNumber) {
958
- 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.");
959
- }
960
- }
961
- // Force readonly mode - this will ensure we don't receive an error for the lack of join op
962
- this.forceReadonly(true);
963
- // We need to setup a listener to stop op processing once we reach the desired sequence number (if specified).
964
- const opHandler = () => {
965
- if (loadToSequenceNumber === undefined) {
966
- // If there is no specified sequence number, pause after the inbound queue is empty.
967
- if (this.deltaManager.inbound.length !== 0) {
968
- return;
969
- }
970
- }
971
- else {
972
- // If there is a specified sequence number, keep processing until we reach it.
973
- if (this.deltaManager.lastSequenceNumber < loadToSequenceNumber) {
974
- return;
975
- }
976
- }
977
- // Pause op processing once we have processed the desired number of ops.
978
- void this.deltaManager.inbound.pause();
979
- void this.deltaManager.outbound.pause();
980
- this.off("op", opHandler);
981
- };
982
- if ((loadToSequenceNumber === undefined && this.deltaManager.inbound.length === 0) ||
983
- this.deltaManager.lastSequenceNumber === loadToSequenceNumber) {
984
- // If we have already reached the desired sequence number, call opHandler() to pause immediately.
985
- opHandler();
986
- }
987
- else {
988
- // If we have not yet reached the desired sequence number, setup a listener to pause once we reach it.
989
- this.on("op", opHandler);
990
- }
991
- }
992
981
  // Attach op handlers to finish initialization and be able to start processing ops
993
982
  // Kick off any ops fetching if required.
994
983
  switch (loadMode.opsBeforeReturn) {
@@ -997,7 +986,6 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
997
986
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
998
987
  this.attachDeltaManagerOpHandler(attributes, loadMode.deltaConnection !== "none" ? "all" : "none", lastProcessedSequenceNumber);
999
988
  break;
1000
- case "sequenceNumber":
1001
989
  case "cached":
1002
990
  case "all":
1003
991
  opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, loadMode.opsBeforeReturn, lastProcessedSequenceNumber);
@@ -1008,6 +996,12 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
1008
996
  // ...load in the existing quorum
1009
997
  // Initialize the protocol handler
1010
998
  await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, baseSnapshot);
999
+ // If we are loading from pending state, we start with old clientId.
1000
+ // We switch to latest connection clientId only after setLoaded().
1001
+ (0, internal_2.assert)(this.clientId === undefined, "there should be no clientId yet");
1002
+ if (pendingLocalState?.clientId !== undefined) {
1003
+ this.protocolHandler.audience.setCurrentClientId(pendingLocalState?.clientId);
1004
+ }
1011
1005
  timings.phase3 = client_utils_1.performance.now();
1012
1006
  const codeDetails = this.getCodeDetailsFromQuorum();
1013
1007
  await this.instantiateRuntime(codeDetails, baseSnapshot,
@@ -1024,6 +1018,7 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
1024
1018
  await this.runtime.notifyOpReplay?.(message);
1025
1019
  }
1026
1020
  pendingLocalState.savedOps = [];
1021
+ this.storageAdapter.clearPendingState();
1027
1022
  }
1028
1023
  // We might have hit some failure that did not manifest itself in exception in this flow,
1029
1024
  // do not start op processing in such case - static version of Container.load() will handle it correctly.
@@ -1035,20 +1030,11 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
1035
1030
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1036
1031
  this._deltaManager.inbound.pause();
1037
1032
  }
1038
- this.handleDeltaConnectionArg(connectionArgs, loadMode.deltaConnection, pendingLocalState !== undefined);
1039
- }
1040
- // If we have not yet reached `loadToSequenceNumber`, we will wait for ops to arrive until we reach it
1041
- if (loadToSequenceNumber !== undefined &&
1042
- this.deltaManager.lastSequenceNumber < loadToSequenceNumber) {
1043
- await new Promise((resolve, reject) => {
1044
- const opHandler = (message) => {
1045
- if (message.sequenceNumber >= loadToSequenceNumber) {
1046
- resolve();
1047
- this.off("op", opHandler);
1048
- }
1049
- };
1050
- this.on("op", opHandler);
1051
- });
1033
+ // Internal context is fully loaded at this point
1034
+ // Move to loaded before calling this.handleDeltaConnectionArg() - latter allows ops & signals in, which
1035
+ // may result in container moving to "connected" state. Such transitions are allowed only in loaded state.
1036
+ this.setLoaded();
1037
+ this.handleDeltaConnectionArg(loadMode.deltaConnection);
1052
1038
  }
1053
1039
  // Safety net: static version of Container.load() should have learned about it through "closed" handler.
1054
1040
  // But if that did not happen for some reason, fail load for sure.
@@ -1058,8 +1044,6 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
1058
1044
  if (this.closed) {
1059
1045
  throw new Error("Container was closed while load()");
1060
1046
  }
1061
- // Internal context is fully loaded at this point
1062
- this.setLoaded();
1063
1047
  timings.end = client_utils_1.performance.now();
1064
1048
  this.subLogger.sendTelemetryEvent({
1065
1049
  eventName: "LoadStagesTimings",
@@ -1239,7 +1223,21 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
1239
1223
  deltaManager.inboundSignal.pause();
1240
1224
  deltaManager.on("connect", (details, _opsBehind) => {
1241
1225
  (0, internal_2.assert)(this.connectionMode === details.mode, 0x4b7 /* mismatch */);
1242
- this.connectionStateHandler.receivedConnectEvent(details);
1226
+ // Delay raising events until setLoaded()
1227
+ // Here are some of the reasons why this design is chosen:
1228
+ // 1. Various processes track speed of connection. But we are not processing ops or signal while container is loading,
1229
+ // and thus we can't move forward across connection modes. This results in telemetry errors (like NoJoinOp) that
1230
+ // have nothing to do with connection flow itself
1231
+ // 2. This also makes it hard to reason about recovery (like reconnection) in case we might have lost JoinSignal. Reconnecting
1232
+ // in loading phase is useless (get back to same state), but at the same time not doing it may result in broken connection
1233
+ // without recovery (after we loaded).
1234
+ // 3. We expose non-consistent view. ContainerRuntime may start loading in non-connected state, but end in connected, with
1235
+ // no events telling about it (until we loaded). Most of the code relies on a fact that state changes when events fire.
1236
+ // This will not delay any processes (as observed by the user). I.e. once container moves to loaded phase,
1237
+ // we immediately would transition across all phases, if we have proper signals / ops ready.
1238
+ if (this.loaded) {
1239
+ this.connectionStateHandler.receivedConnectEvent(details);
1240
+ }
1243
1241
  });
1244
1242
  deltaManager.on("establishingConnection", (reason) => {
1245
1243
  this.connectionStateHandler.establishingConnection(reason);
@@ -1249,8 +1247,14 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
1249
1247
  });
1250
1248
  deltaManager.on("disconnect", (text, error) => {
1251
1249
  this.noopHeuristic?.notifyDisconnect();
1252
- if (!this.closed) {
1253
- this.connectionStateHandler.receivedDisconnectEvent({ text, error });
1250
+ const reason = { text, error };
1251
+ // Symmetry with "connect" events
1252
+ if (this.loaded) {
1253
+ this.connectionStateHandler.receivedDisconnectEvent(reason);
1254
+ }
1255
+ else if (!this.closed) {
1256
+ // Raise cancellation to get state machine back to initial state
1257
+ this.connectionStateHandler.cancelEstablishingConnection(reason);
1254
1258
  }
1255
1259
  });
1256
1260
  deltaManager.on("throttled", (warning) => {
@@ -1263,7 +1267,9 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
1263
1267
  this.emit("warning", warn);
1264
1268
  });
1265
1269
  deltaManager.on("readonly", (readonly) => {
1266
- this.setContextConnectedState(this.connectionState === connectionState_js_1.ConnectionState.Connected, readonly);
1270
+ if (this.loaded) {
1271
+ this.setContextConnectedState(this.connectionState === connectionState_js_1.ConnectionState.Connected, readonly);
1272
+ }
1267
1273
  this.emit("readonly", readonly);
1268
1274
  });
1269
1275
  deltaManager.on("closed", (error) => {
@@ -1305,8 +1311,7 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
1305
1311
  // This info is of most interesting while Catching Up.
1306
1312
  checkpointSequenceNumber = this.deltaManager.lastKnownSeqNumber;
1307
1313
  // Need to check that we have already loaded and fetched the snapshot.
1308
- if (this.deltaManager.hasCheckpointSequenceNumber &&
1309
- this._lifecycleState === "loaded") {
1314
+ if (this.deltaManager.hasCheckpointSequenceNumber && this.loaded) {
1310
1315
  opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
1311
1316
  }
1312
1317
  }
@@ -1320,7 +1325,7 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
1320
1325
  reason: reason?.text,
1321
1326
  connectionInitiationReason,
1322
1327
  pendingClientId: this.connectionStateHandler.pendingClientId,
1323
- clientId: this.clientId,
1328
+ clientId: this.connectionStateHandler.clientId,
1324
1329
  autoReconnect,
1325
1330
  opsBehind,
1326
1331
  online: internal_3.OnlineStatus[(0, internal_3.isOnline)()],
@@ -1336,20 +1341,23 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
1336
1341
  this.firstConnection = false;
1337
1342
  }
1338
1343
  }
1339
- propagateConnectionState(initialTransition, disconnectedReason) {
1340
- // When container loaded, we want to propagate initial connection state.
1341
- // After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
1344
+ propagateConnectionState(disconnectedReason) {
1345
+ const connected = this.connectionState === connectionState_js_1.ConnectionState.Connected;
1346
+ if (connected) {
1347
+ const clientId = this.connectionStateHandler.clientId;
1348
+ (0, internal_2.assert)(clientId !== undefined, "there has to be clientId");
1349
+ this.protocolHandler.audience.setCurrentClientId(clientId);
1350
+ }
1351
+ // We communicate only transitions to Connected & Disconnected states, skipping all other states.
1342
1352
  // This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
1343
- if (!initialTransition &&
1344
- this.connectionState !== connectionState_js_1.ConnectionState.Connected &&
1353
+ if (this.connectionState !== connectionState_js_1.ConnectionState.Connected &&
1345
1354
  this.connectionState !== connectionState_js_1.ConnectionState.Disconnected) {
1346
1355
  return;
1347
1356
  }
1348
- const state = this.connectionState === connectionState_js_1.ConnectionState.Connected;
1349
1357
  // Both protocol and context should not be undefined if we got so far.
1350
- this.setContextConnectedState(state, this.readOnlyInfo.readonly ?? false);
1351
- this.protocolHandler.setConnectionState(state, this.clientId);
1352
- (0, internal_4.raiseConnectedEvent)(this.mc.logger, this, state, this.clientId, disconnectedReason?.text);
1358
+ this.setContextConnectedState(connected, this.readOnlyInfo.readonly ?? false);
1359
+ this.protocolHandler.setConnectionState(connected, this.clientId);
1360
+ (0, internal_4.raiseConnectedEvent)(this.mc.logger, this, connected, this.clientId, disconnectedReason?.text);
1353
1361
  }
1354
1362
  // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
1355
1363
  submitContainerMessage(type, contents, batch, metadata) {
@@ -1470,24 +1478,29 @@ class Container extends internal_4.EventEmitterWithErrorHandling {
1470
1478
  /**
1471
1479
  * Set the connected state of the ContainerContext
1472
1480
  * This controls the "connected" state of the ContainerRuntime as well
1473
- * @param state - Is the container currently connected?
1481
+ * @param connected - Is the container currently connected?
1474
1482
  * @param readonly - Is the container in readonly mode?
1475
1483
  */
1476
- setContextConnectedState(state, readonly) {
1477
- if (this._runtime?.disposed === false) {
1484
+ setContextConnectedState(connected, readonly) {
1485
+ if (this._runtime?.disposed === false && this.loaded) {
1478
1486
  /**
1479
1487
  * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
1480
1488
  * ops getting through to the DeltaManager.
1481
1489
  * The ContainerRuntime's "connected" state simply means it is ok to send ops
1482
1490
  * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
1483
1491
  */
1484
- this.runtime.setConnectionState(state && !readonly, this.clientId);
1492
+ this.runtime.setConnectionState(connected && !readonly, this.clientId);
1485
1493
  }
1486
1494
  }
1487
- handleDeltaConnectionArg(connectionArgs, deltaConnectionArg, canConnect = true) {
1495
+ handleDeltaConnectionArg(deltaConnectionArg, connectionArgs) {
1496
+ // This ensures that we allow transitions to "connected" state only after container has been fully loaded
1497
+ // and we propagate such events to container runtime. All events prior to being loaded are ignored.
1498
+ // This means if we get here in non-loaded state, we might not deliver proper events to container runtime,
1499
+ // and runtime implementation may miss such events.
1500
+ (0, internal_2.assert)(this.loaded, "has to be called after container transitions to loaded state");
1488
1501
  switch (deltaConnectionArg) {
1489
1502
  case undefined:
1490
- if (canConnect) {
1503
+ if (connectionArgs) {
1491
1504
  // connect to delta stream now since we did not before
1492
1505
  this.connectToDeltaStream(connectionArgs);
1493
1506
  }