@fluidframework/container-loader 2.0.0-dev-rc.3.0.0.254866 → 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/lib/container.js CHANGED
@@ -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.
@@ -189,14 +188,33 @@ export class Container extends EventEmitterWithErrorHandling {
189
188
  // It's conceivable the container could be closed when this is called
190
189
  // Only transition states if currently loading
191
190
  if (this._lifecycleState === "loading") {
192
- // Propagate current connection state through the system.
193
- this.propagateConnectionState(true /* initial transition */);
194
191
  this._lifecycleState = "loaded";
192
+ // Connections transitions are delayed till we are loaded.
193
+ // This is done by holding ops and signals until the end of load sequence
194
+ // (calling this.handleDeltaConnectionArg() after setLoaded() call)
195
+ // If this assert fires, it means our logic managing connection flow is wrong, and the logic below is also wrong.
196
+ assert(this.connectionState !== ConnectionState.Connected, "not connected yet");
197
+ // Propagate current connection state through the system.
198
+ const readonly = this.readOnlyInfo.readonly ?? false;
199
+ // This call does not look like needed any more, with delaying all connection-related events past loaded phase.
200
+ // Yet, there could be some customer code that would break if we do not deliver it.
201
+ // Will be removed in further PRs with proper changeset.
202
+ this.setContextConnectedState(false /* connected */, readonly);
203
+ // Deliver delayed calls to DeltaManager - we ignored "connect" events while loading.
204
+ const cm = this._deltaManager.connectionManager;
205
+ if (cm.connected) {
206
+ const details = cm.connectionDetails;
207
+ assert(details !== undefined, "should have details if connected");
208
+ this.connectionStateHandler.receivedConnectEvent(details);
209
+ }
195
210
  }
196
211
  }
197
212
  get closed() {
198
213
  return (this._lifecycleState === "closing" || this._lifecycleState === "closed" || this.disposed);
199
214
  }
215
+ get loaded() {
216
+ return this._lifecycleState === "loaded";
217
+ }
200
218
  get disposed() {
201
219
  return this._lifecycleState === "disposing" || this._lifecycleState === "disposed";
202
220
  }
@@ -265,11 +283,12 @@ export class Container extends EventEmitterWithErrorHandling {
265
283
  return this.connectionStateHandler.connectionState === ConnectionState.Connected;
266
284
  }
267
285
  /**
268
- * The server provided id of the client.
269
- * Set once this.connected is true, otherwise undefined
286
+ * clientId of the latest connection. Changes only once client is connected, caught up and fully loaded.
287
+ * Changes to clientId are delayed through container loading sequence and delived once container is fully loaded.
288
+ * clientId does not reset on lost connection - old value persists until new connection is fully established.
270
289
  */
271
290
  get clientId() {
272
- return this._clientId;
291
+ return this.protocolHandler.audience.getSelf()?.clientId;
273
292
  }
274
293
  get isInteractiveClient() {
275
294
  return this.deltaManager.clientDetails.capabilities.interactive;
@@ -333,6 +352,7 @@ export class Container extends EventEmitterWithErrorHandling {
333
352
  eventName: "ContainerEventHandlerException",
334
353
  name: typeof name === "string" ? name : undefined,
335
354
  }, error);
355
+ this.close(normalizeError(error));
336
356
  });
337
357
  /**
338
358
  * Lifecycle state of the container, used mainly to prevent re-entrancy and telemetry
@@ -430,10 +450,10 @@ export class Container extends EventEmitterWithErrorHandling {
430
450
  const snapshotWithBlobs = await attachP;
431
451
  this.serializedStateManager.setInitialSnapshot(snapshotWithBlobs);
432
452
  if (!this.closed) {
433
- this.handleDeltaConnectionArg({
453
+ this.handleDeltaConnectionArg(attachProps?.deltaConnection, {
434
454
  fetchOpsFromStorage: false,
435
455
  reason: { text: "createDetached" },
436
- }, attachProps?.deltaConnection);
456
+ });
437
457
  }
438
458
  }, { start: true, end: true, cancel: "generic" });
439
459
  });
@@ -457,7 +477,6 @@ export class Container extends EventEmitterWithErrorHandling {
457
477
  const { canReconnect, clientDetailsOverride, urlResolver, documentServiceFactory, codeLoader, options, scope, subLogger, detachedBlobStorage, protocolHandlerBuilder, } = createProps;
458
478
  this.connectionTransitionTimes[ConnectionState.Disconnected] = performance.now();
459
479
  const pendingLocalState = loadProps?.pendingLocalState;
460
- this._clientId = pendingLocalState?.clientId;
461
480
  this._canReconnect = canReconnect ?? true;
462
481
  this.clientDetailsOverride = clientDetailsOverride;
463
482
  this.urlResolver = urlResolver;
@@ -527,12 +546,9 @@ export class Container extends EventEmitterWithErrorHandling {
527
546
  this.connectionStateHandler = createConnectionStateHandler({
528
547
  logger: this.mc.logger,
529
548
  connectionStateChanged: (value, oldState, reason) => {
530
- if (value === ConnectionState.Connected) {
531
- this._clientId = this.connectionStateHandler.pendingClientId;
532
- }
533
549
  this.logConnectionStateChangeTelemetry(value, oldState, reason);
534
- if (this._lifecycleState === "loaded") {
535
- this.propagateConnectionState(false /* initial transition */, value === ConnectionState.Disconnected
550
+ if (this.loaded) {
551
+ this.propagateConnectionState(value === ConnectionState.Disconnected
536
552
  ? reason
537
553
  : undefined /* disconnectedReason */);
538
554
  }
@@ -554,15 +570,22 @@ export class Container extends EventEmitterWithErrorHandling {
554
570
  this.connectionTransitionTimes[ConnectionState.CatchingUp],
555
571
  ...(details === undefined ? {} : { details: JSON.stringify(details) }),
556
572
  });
573
+ // This assert is important for many reasons:
574
+ // 1) Cosmetic / OCE burden: It's useless to raise NoJoinOp error events, if we are loading, as that's most
575
+ // likely to happen if snapshot loading takes too long. During this time we are not processing ops so there is no
576
+ // way to move to "connected" state, and thus "NoJoin" timer would fire (see
577
+ // IConnectionStateHandler.logConnectionIssue() callback and related code in ConnectStateHandler class implementation).
578
+ // But these events do not tell us anything about connectivity pipeline / op processing pipeline,
579
+ // only that boot is slow, and we have events for that.
580
+ // 2) Doing recovery below is useless in loading mode, for the reasons described above. At the same time we can't
581
+ // not do it, as maybe we lost JoinSignal for "self", and when loading is done, we never move to connected
582
+ // state. So we would have to do (in most cases) useless infinite reconnect loop while we are loading.
583
+ assert(this.loaded, "connection issues can be raised only after container is loaded");
557
584
  // If this is "write" connection, it took too long to receive join op. But in most cases that's due
558
585
  // 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") {
586
+ // For "read" connections, we get here due to join signal for "self" not arriving on time.
587
+ // Attempt to recover by reconnecting.
588
+ if (mode === "read" && category === "error") {
566
589
  const reason = { text: "NoJoinSignal" };
567
590
  this.disconnectInternal(reason);
568
591
  this.connectInternal({ reason, fetchOpsFromStorage: false });
@@ -571,6 +594,9 @@ export class Container extends EventEmitterWithErrorHandling {
571
594
  clientShouldHaveLeft: (clientId) => {
572
595
  this.clientsWhoShouldHaveLeft.add(clientId);
573
596
  },
597
+ onCriticalError: (error) => {
598
+ this.close(normalizeError(error));
599
+ },
574
600
  }, this.deltaManager, pendingLocalState?.clientId);
575
601
  this.on(savedContainerEvent, () => {
576
602
  this.connectionStateHandler.containerSaved();
@@ -585,11 +611,11 @@ export class Container extends EventEmitterWithErrorHandling {
585
611
  // Even if not forced on via this flag, combined summaries may still be enabled by service policy.
586
612
  const forceEnableSummarizeProtocolTree = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2") ??
587
613
  options.summarizeProtocolTree;
588
- this.storageAdapter = new ContainerStorageAdapter(detachedBlobStorage, this.mc.logger, pendingLocalState?.snapshotBlobs, addProtocolSummaryIfMissing, forceEnableSummarizeProtocolTree);
614
+ this.storageAdapter = new ContainerStorageAdapter(detachedBlobStorage, this.mc.logger, pendingLocalState?.snapshotBlobs, pendingLocalState?.loadedGroupIdSnapshots, addProtocolSummaryIfMissing, forceEnableSummarizeProtocolTree);
589
615
  const offlineLoadEnabled = (this.isInteractiveClient &&
590
616
  this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad")) ??
591
617
  options.enableOfflineLoad === true;
592
- this.serializedStateManager = new SerializedStateManager(pendingLocalState, this.subLogger, this.storageAdapter, offlineLoadEnabled);
618
+ this.serializedStateManager = new SerializedStateManager(pendingLocalState, this.subLogger, this.storageAdapter, offlineLoadEnabled, this, () => this.isDirty);
593
619
  const isDomAvailable = typeof document === "object" &&
594
620
  document !== null &&
595
621
  typeof document.addEventListener === "function" &&
@@ -794,11 +820,11 @@ export class Container extends EventEmitterWithErrorHandling {
794
820
  connectInternal(args) {
795
821
  assert(!this.closed, 0x2c5 /* "Attempting to connect() a closed Container" */);
796
822
  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
823
  // Set Auto Reconnect Mode
800
824
  const mode = ReconnectMode.Enabled;
801
825
  this.setAutoReconnectInternal(mode, args.reason);
826
+ // Resume processing ops and connect to delta stream
827
+ this.resumeInternal(args);
802
828
  }
803
829
  disconnect() {
804
830
  if (this.closed) {
@@ -818,6 +844,11 @@ export class Container extends EventEmitterWithErrorHandling {
818
844
  assert(!this.closed, 0x0d9 /* "Attempting to connect() a closed DeltaManager" */);
819
845
  // Resume processing ops
820
846
  if (this.inboundQueuePausedFromInit) {
847
+ // This assert guards against possibility of ops/signals showing up too soon, while
848
+ // container is not ready yet to receive them. We can hit it only if some internal code call into here,
849
+ // as public API like Container.connect() can be only called when user got back container object, i.e.
850
+ // it is already fully loaded.
851
+ assert(this.loaded, "connect() can be called only in fully loaded state");
821
852
  this.inboundQueuePausedFromInit = false;
822
853
  this._deltaManager.inbound.resume();
823
854
  this._deltaManager.inboundSignal.resume();
@@ -906,7 +937,7 @@ export class Container extends EventEmitterWithErrorHandling {
906
937
  *
907
938
  * @param specifiedVersion - Version SHA to load snapshot. If not specified, will fetch the latest snapshot.
908
939
  */
909
- async load(specifiedVersion, loadMode, resolvedUrl, pendingLocalState, loadToSequenceNumber) {
940
+ async load(specifiedVersion, loadMode, resolvedUrl, pendingLocalState) {
910
941
  const timings = { phase1: performance.now() };
911
942
  this.service = await this.createDocumentService(async () => this.serviceFactory.createDocumentService(resolvedUrl, this.subLogger, this.client.details.type === summarizerClientType));
912
943
  // Except in cases where it has stashed ops or requested by feature gate, the container will connect in "read" mode
@@ -921,7 +952,7 @@ export class Container extends EventEmitterWithErrorHandling {
921
952
  };
922
953
  // Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
923
954
  // DeltaManager is resilient to this and will wait to start processing ops until after it is attached.
924
- if (loadMode.deltaConnection === undefined && !pendingLocalState) {
955
+ if (loadMode.deltaConnection === undefined) {
925
956
  this.connectToDeltaStream(connectionArgs);
926
957
  }
927
958
  this.storageAdapter.connectToService(this.service);
@@ -939,48 +970,6 @@ export class Container extends EventEmitterWithErrorHandling {
939
970
  const lastProcessedSequenceNumber = pendingLocalState?.savedOps[pendingLocalState.savedOps.length - 1]?.sequenceNumber ??
940
971
  attributes.sequenceNumber;
941
972
  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
973
  // Attach op handlers to finish initialization and be able to start processing ops
985
974
  // Kick off any ops fetching if required.
986
975
  switch (loadMode.opsBeforeReturn) {
@@ -989,7 +978,6 @@ export class Container extends EventEmitterWithErrorHandling {
989
978
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
990
979
  this.attachDeltaManagerOpHandler(attributes, loadMode.deltaConnection !== "none" ? "all" : "none", lastProcessedSequenceNumber);
991
980
  break;
992
- case "sequenceNumber":
993
981
  case "cached":
994
982
  case "all":
995
983
  opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, loadMode.opsBeforeReturn, lastProcessedSequenceNumber);
@@ -1000,6 +988,12 @@ export class Container extends EventEmitterWithErrorHandling {
1000
988
  // ...load in the existing quorum
1001
989
  // Initialize the protocol handler
1002
990
  await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, baseSnapshot);
991
+ // If we are loading from pending state, we start with old clientId.
992
+ // We switch to latest connection clientId only after setLoaded().
993
+ assert(this.clientId === undefined, "there should be no clientId yet");
994
+ if (pendingLocalState?.clientId !== undefined) {
995
+ this.protocolHandler.audience.setCurrentClientId(pendingLocalState?.clientId);
996
+ }
1003
997
  timings.phase3 = performance.now();
1004
998
  const codeDetails = this.getCodeDetailsFromQuorum();
1005
999
  await this.instantiateRuntime(codeDetails, baseSnapshot,
@@ -1016,6 +1010,7 @@ export class Container extends EventEmitterWithErrorHandling {
1016
1010
  await this.runtime.notifyOpReplay?.(message);
1017
1011
  }
1018
1012
  pendingLocalState.savedOps = [];
1013
+ this.storageAdapter.clearPendingState();
1019
1014
  }
1020
1015
  // We might have hit some failure that did not manifest itself in exception in this flow,
1021
1016
  // do not start op processing in such case - static version of Container.load() will handle it correctly.
@@ -1027,20 +1022,11 @@ export class Container extends EventEmitterWithErrorHandling {
1027
1022
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1028
1023
  this._deltaManager.inbound.pause();
1029
1024
  }
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
- });
1025
+ // Internal context is fully loaded at this point
1026
+ // Move to loaded before calling this.handleDeltaConnectionArg() - latter allows ops & signals in, which
1027
+ // may result in container moving to "connected" state. Such transitions are allowed only in loaded state.
1028
+ this.setLoaded();
1029
+ this.handleDeltaConnectionArg(loadMode.deltaConnection);
1044
1030
  }
1045
1031
  // Safety net: static version of Container.load() should have learned about it through "closed" handler.
1046
1032
  // But if that did not happen for some reason, fail load for sure.
@@ -1050,8 +1036,6 @@ export class Container extends EventEmitterWithErrorHandling {
1050
1036
  if (this.closed) {
1051
1037
  throw new Error("Container was closed while load()");
1052
1038
  }
1053
- // Internal context is fully loaded at this point
1054
- this.setLoaded();
1055
1039
  timings.end = performance.now();
1056
1040
  this.subLogger.sendTelemetryEvent({
1057
1041
  eventName: "LoadStagesTimings",
@@ -1231,7 +1215,21 @@ export class Container extends EventEmitterWithErrorHandling {
1231
1215
  deltaManager.inboundSignal.pause();
1232
1216
  deltaManager.on("connect", (details, _opsBehind) => {
1233
1217
  assert(this.connectionMode === details.mode, 0x4b7 /* mismatch */);
1234
- this.connectionStateHandler.receivedConnectEvent(details);
1218
+ // Delay raising events until setLoaded()
1219
+ // Here are some of the reasons why this design is chosen:
1220
+ // 1. Various processes track speed of connection. But we are not processing ops or signal while container is loading,
1221
+ // and thus we can't move forward across connection modes. This results in telemetry errors (like NoJoinOp) that
1222
+ // have nothing to do with connection flow itself
1223
+ // 2. This also makes it hard to reason about recovery (like reconnection) in case we might have lost JoinSignal. Reconnecting
1224
+ // in loading phase is useless (get back to same state), but at the same time not doing it may result in broken connection
1225
+ // without recovery (after we loaded).
1226
+ // 3. We expose non-consistent view. ContainerRuntime may start loading in non-connected state, but end in connected, with
1227
+ // no events telling about it (until we loaded). Most of the code relies on a fact that state changes when events fire.
1228
+ // This will not delay any processes (as observed by the user). I.e. once container moves to loaded phase,
1229
+ // we immediately would transition across all phases, if we have proper signals / ops ready.
1230
+ if (this.loaded) {
1231
+ this.connectionStateHandler.receivedConnectEvent(details);
1232
+ }
1235
1233
  });
1236
1234
  deltaManager.on("establishingConnection", (reason) => {
1237
1235
  this.connectionStateHandler.establishingConnection(reason);
@@ -1241,8 +1239,14 @@ export class Container extends EventEmitterWithErrorHandling {
1241
1239
  });
1242
1240
  deltaManager.on("disconnect", (text, error) => {
1243
1241
  this.noopHeuristic?.notifyDisconnect();
1244
- if (!this.closed) {
1245
- this.connectionStateHandler.receivedDisconnectEvent({ text, error });
1242
+ const reason = { text, error };
1243
+ // Symmetry with "connect" events
1244
+ if (this.loaded) {
1245
+ this.connectionStateHandler.receivedDisconnectEvent(reason);
1246
+ }
1247
+ else if (!this.closed) {
1248
+ // Raise cancellation to get state machine back to initial state
1249
+ this.connectionStateHandler.cancelEstablishingConnection(reason);
1246
1250
  }
1247
1251
  });
1248
1252
  deltaManager.on("throttled", (warning) => {
@@ -1255,7 +1259,9 @@ export class Container extends EventEmitterWithErrorHandling {
1255
1259
  this.emit("warning", warn);
1256
1260
  });
1257
1261
  deltaManager.on("readonly", (readonly) => {
1258
- this.setContextConnectedState(this.connectionState === ConnectionState.Connected, readonly);
1262
+ if (this.loaded) {
1263
+ this.setContextConnectedState(this.connectionState === ConnectionState.Connected, readonly);
1264
+ }
1259
1265
  this.emit("readonly", readonly);
1260
1266
  });
1261
1267
  deltaManager.on("closed", (error) => {
@@ -1297,8 +1303,7 @@ export class Container extends EventEmitterWithErrorHandling {
1297
1303
  // This info is of most interesting while Catching Up.
1298
1304
  checkpointSequenceNumber = this.deltaManager.lastKnownSeqNumber;
1299
1305
  // Need to check that we have already loaded and fetched the snapshot.
1300
- if (this.deltaManager.hasCheckpointSequenceNumber &&
1301
- this._lifecycleState === "loaded") {
1306
+ if (this.deltaManager.hasCheckpointSequenceNumber && this.loaded) {
1302
1307
  opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
1303
1308
  }
1304
1309
  }
@@ -1312,7 +1317,7 @@ export class Container extends EventEmitterWithErrorHandling {
1312
1317
  reason: reason?.text,
1313
1318
  connectionInitiationReason,
1314
1319
  pendingClientId: this.connectionStateHandler.pendingClientId,
1315
- clientId: this.clientId,
1320
+ clientId: this.connectionStateHandler.clientId,
1316
1321
  autoReconnect,
1317
1322
  opsBehind,
1318
1323
  online: OnlineStatus[isOnline()],
@@ -1328,20 +1333,23 @@ export class Container extends EventEmitterWithErrorHandling {
1328
1333
  this.firstConnection = false;
1329
1334
  }
1330
1335
  }
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.
1336
+ propagateConnectionState(disconnectedReason) {
1337
+ const connected = this.connectionState === ConnectionState.Connected;
1338
+ if (connected) {
1339
+ const clientId = this.connectionStateHandler.clientId;
1340
+ assert(clientId !== undefined, "there has to be clientId");
1341
+ this.protocolHandler.audience.setCurrentClientId(clientId);
1342
+ }
1343
+ // We communicate only transitions to Connected & Disconnected states, skipping all other states.
1334
1344
  // 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 &&
1345
+ if (this.connectionState !== ConnectionState.Connected &&
1337
1346
  this.connectionState !== ConnectionState.Disconnected) {
1338
1347
  return;
1339
1348
  }
1340
- const state = this.connectionState === ConnectionState.Connected;
1341
1349
  // 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);
1350
+ this.setContextConnectedState(connected, this.readOnlyInfo.readonly ?? false);
1351
+ this.protocolHandler.setConnectionState(connected, this.clientId);
1352
+ raiseConnectedEvent(this.mc.logger, this, connected, this.clientId, disconnectedReason?.text);
1345
1353
  }
1346
1354
  // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
1347
1355
  submitContainerMessage(type, contents, batch, metadata) {
@@ -1462,24 +1470,29 @@ export class Container extends EventEmitterWithErrorHandling {
1462
1470
  /**
1463
1471
  * Set the connected state of the ContainerContext
1464
1472
  * This controls the "connected" state of the ContainerRuntime as well
1465
- * @param state - Is the container currently connected?
1473
+ * @param connected - Is the container currently connected?
1466
1474
  * @param readonly - Is the container in readonly mode?
1467
1475
  */
1468
- setContextConnectedState(state, readonly) {
1469
- if (this._runtime?.disposed === false) {
1476
+ setContextConnectedState(connected, readonly) {
1477
+ if (this._runtime?.disposed === false && this.loaded) {
1470
1478
  /**
1471
1479
  * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
1472
1480
  * ops getting through to the DeltaManager.
1473
1481
  * The ContainerRuntime's "connected" state simply means it is ok to send ops
1474
1482
  * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
1475
1483
  */
1476
- this.runtime.setConnectionState(state && !readonly, this.clientId);
1484
+ this.runtime.setConnectionState(connected && !readonly, this.clientId);
1477
1485
  }
1478
1486
  }
1479
- handleDeltaConnectionArg(connectionArgs, deltaConnectionArg, canConnect = true) {
1487
+ handleDeltaConnectionArg(deltaConnectionArg, connectionArgs) {
1488
+ // This ensures that we allow transitions to "connected" state only after container has been fully loaded
1489
+ // and we propagate such events to container runtime. All events prior to being loaded are ignored.
1490
+ // This means if we get here in non-loaded state, we might not deliver proper events to container runtime,
1491
+ // and runtime implementation may miss such events.
1492
+ assert(this.loaded, "has to be called after container transitions to loaded state");
1480
1493
  switch (deltaConnectionArg) {
1481
1494
  case undefined:
1482
- if (canConnect) {
1495
+ if (connectionArgs) {
1483
1496
  // connect to delta stream now since we did not before
1484
1497
  this.connectToDeltaStream(connectionArgs);
1485
1498
  }