@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/src/container.ts CHANGED
@@ -75,8 +75,9 @@ import {
75
75
  MessageType,
76
76
  SummaryType,
77
77
  } from "@fluidframework/protocol-definitions";
78
- import { ITelemetryLoggerExt, type TelemetryEventCategory } from "@fluidframework/telemetry-utils";
79
78
  import {
79
+ type TelemetryEventCategory,
80
+ ITelemetryLoggerExt,
80
81
  EventEmitterWithErrorHandling,
81
82
  GenericError,
82
83
  IFluidErrorBase,
@@ -163,11 +164,6 @@ export interface IContainerLoadProps {
163
164
  * The pending state serialized from a pervious container instance
164
165
  */
165
166
  readonly pendingLocalState?: IPendingContainerState;
166
-
167
- /**
168
- * Load the container to at least this sequence number.
169
- */
170
- readonly loadToSequenceNumber?: number;
171
167
  }
172
168
 
173
169
  /**
@@ -361,18 +357,13 @@ export class Container
361
357
  loadProps: IContainerLoadProps,
362
358
  createProps: IContainerCreateProps,
363
359
  ): Promise<Container> {
364
- const { version, pendingLocalState, loadMode, resolvedUrl, loadToSequenceNumber } =
365
- loadProps;
360
+ const { version, pendingLocalState, loadMode, resolvedUrl } = loadProps;
366
361
 
367
362
  const container = new Container(createProps, loadProps);
368
363
 
369
- const disableRecordHeapSize = container.mc.config.getBoolean(
370
- "Fluid.Loader.DisableRecordHeapSize",
371
- );
372
-
373
364
  return PerformanceEvent.timedExecAsync(
374
365
  container.mc.logger,
375
- { eventName: "Load" },
366
+ { eventName: "Load", ...loadMode },
376
367
  async (event) =>
377
368
  new Promise<Container>((resolve, reject) => {
378
369
  const defaultMode: IContainerLoadMode = { opsBeforeReturn: "cached" };
@@ -391,13 +382,13 @@ export class Container
391
382
  container.on("closed", onClosed);
392
383
 
393
384
  container
394
- .load(version, mode, resolvedUrl, pendingLocalState, loadToSequenceNumber)
385
+ .load(version, mode, resolvedUrl, pendingLocalState)
395
386
  .finally(() => {
396
387
  container.removeListener("closed", onClosed);
397
388
  })
398
389
  .then(
399
390
  (props) => {
400
- event.end({ ...props, ...loadMode });
391
+ event.end({ ...props });
401
392
  resolve(container);
402
393
  },
403
394
  (error) => {
@@ -415,7 +406,6 @@ export class Container
415
406
  );
416
407
  }),
417
408
  { start: true, end: true, cancel: "generic" },
418
- disableRecordHeapSize !== true /* recordHeapSize */,
419
409
  );
420
410
  }
421
411
 
@@ -513,9 +503,27 @@ export class Container
513
503
  // It's conceivable the container could be closed when this is called
514
504
  // Only transition states if currently loading
515
505
  if (this._lifecycleState === "loading") {
516
- // Propagate current connection state through the system.
517
- this.propagateConnectionState(true /* initial transition */);
518
506
  this._lifecycleState = "loaded";
507
+
508
+ // Connections transitions are delayed till we are loaded.
509
+ // This is done by holding ops and signals until the end of load sequence
510
+ // (calling this.handleDeltaConnectionArg() after setLoaded() call)
511
+ // If this assert fires, it means our logic managing connection flow is wrong, and the logic below is also wrong.
512
+ assert(this.connectionState !== ConnectionState.Connected, "not connected yet");
513
+
514
+ // Propagate current connection state through the system.
515
+ const readonly = this.readOnlyInfo.readonly ?? false;
516
+ // This call does not look like needed any more, with delaying all connection-related events past loaded phase.
517
+ // Yet, there could be some customer code that would break if we do not deliver it.
518
+ // Will be removed in further PRs with proper changeset.
519
+ this.setContextConnectedState(false /* connected */, readonly);
520
+ // Deliver delayed calls to DeltaManager - we ignored "connect" events while loading.
521
+ const cm = this._deltaManager.connectionManager;
522
+ if (cm.connected) {
523
+ const details = cm.connectionDetails;
524
+ assert(details !== undefined, "should have details if connected");
525
+ this.connectionStateHandler.receivedConnectEvent(details);
526
+ }
519
527
  }
520
528
  }
521
529
 
@@ -525,6 +533,10 @@ export class Container
525
533
  );
526
534
  }
527
535
 
536
+ protected get loaded(): boolean {
537
+ return this._lifecycleState === "loaded";
538
+ }
539
+
528
540
  public get disposed(): boolean {
529
541
  return this._lifecycleState === "disposing" || this._lifecycleState === "disposed";
530
542
  }
@@ -629,14 +641,13 @@ export class Container
629
641
  return this.connectionStateHandler.connectionState === ConnectionState.Connected;
630
642
  }
631
643
 
632
- private _clientId: string | undefined;
633
-
634
644
  /**
635
- * The server provided id of the client.
636
- * Set once this.connected is true, otherwise undefined
645
+ * clientId of the latest connection. Changes only once client is connected, caught up and fully loaded.
646
+ * Changes to clientId are delayed through container loading sequence and delived once container is fully loaded.
647
+ * clientId does not reset on lost connection - old value persists until new connection is fully established.
637
648
  */
638
649
  public get clientId(): string | undefined {
639
- return this._clientId;
650
+ return this.protocolHandler.audience.getSelf()?.clientId;
640
651
  }
641
652
 
642
653
  private get isInteractiveClient(): boolean {
@@ -721,6 +732,7 @@ export class Container
721
732
  },
722
733
  error,
723
734
  );
735
+ this.close(normalizeError(error));
724
736
  });
725
737
 
726
738
  const {
@@ -738,7 +750,6 @@ export class Container
738
750
 
739
751
  this.connectionTransitionTimes[ConnectionState.Disconnected] = performance.now();
740
752
  const pendingLocalState = loadProps?.pendingLocalState;
741
- this._clientId = pendingLocalState?.clientId;
742
753
 
743
754
  this._canReconnect = canReconnect ?? true;
744
755
  this.clientDetailsOverride = clientDetailsOverride;
@@ -840,13 +851,9 @@ export class Container
840
851
  {
841
852
  logger: this.mc.logger,
842
853
  connectionStateChanged: (value, oldState, reason) => {
843
- if (value === ConnectionState.Connected) {
844
- this._clientId = this.connectionStateHandler.pendingClientId;
845
- }
846
854
  this.logConnectionStateChangeTelemetry(value, oldState, reason);
847
- if (this._lifecycleState === "loaded") {
855
+ if (this.loaded) {
848
856
  this.propagateConnectionState(
849
- false /* initial transition */,
850
857
  value === ConnectionState.Disconnected
851
858
  ? reason
852
859
  : undefined /* disconnectedReason */,
@@ -876,15 +883,26 @@ export class Container
876
883
  ...(details === undefined ? {} : { details: JSON.stringify(details) }),
877
884
  });
878
885
 
886
+ // This assert is important for many reasons:
887
+ // 1) Cosmetic / OCE burden: It's useless to raise NoJoinOp error events, if we are loading, as that's most
888
+ // likely to happen if snapshot loading takes too long. During this time we are not processing ops so there is no
889
+ // way to move to "connected" state, and thus "NoJoin" timer would fire (see
890
+ // IConnectionStateHandler.logConnectionIssue() callback and related code in ConnectStateHandler class implementation).
891
+ // But these events do not tell us anything about connectivity pipeline / op processing pipeline,
892
+ // only that boot is slow, and we have events for that.
893
+ // 2) Doing recovery below is useless in loading mode, for the reasons described above. At the same time we can't
894
+ // not do it, as maybe we lost JoinSignal for "self", and when loading is done, we never move to connected
895
+ // state. So we would have to do (in most cases) useless infinite reconnect loop while we are loading.
896
+ assert(
897
+ this.loaded,
898
+ "connection issues can be raised only after container is loaded",
899
+ );
900
+
879
901
  // If this is "write" connection, it took too long to receive join op. But in most cases that's due
880
902
  // to very slow op fetches and we will eventually get there.
881
- // For "read" connections, we get here due to self join signal not arriving on time. We will need to
882
- // better understand when and why it may happen.
883
- // For now, attempt to recover by reconnecting. In future, maybe we can query relay service for
884
- // current state of audience.
885
- // Other possible recovery path - move to connected state (i.e. ConnectionStateHandler.joinOpTimer
886
- // to call this.applyForConnectedState("addMemberEvent") for "read" connections)
887
- if (mode === "read") {
903
+ // For "read" connections, we get here due to join signal for "self" not arriving on time.
904
+ // Attempt to recover by reconnecting.
905
+ if (mode === "read" && category === "error") {
888
906
  const reason = { text: "NoJoinSignal" };
889
907
  this.disconnectInternal(reason);
890
908
  this.connectInternal({ reason, fetchOpsFromStorage: false });
@@ -893,6 +911,9 @@ export class Container
893
911
  clientShouldHaveLeft: (clientId: string) => {
894
912
  this.clientsWhoShouldHaveLeft.add(clientId);
895
913
  },
914
+ onCriticalError: (error: unknown) => {
915
+ this.close(normalizeError(error));
916
+ },
896
917
  },
897
918
  this.deltaManager,
898
919
  pendingLocalState?.clientId,
@@ -920,6 +941,7 @@ export class Container
920
941
  detachedBlobStorage,
921
942
  this.mc.logger,
922
943
  pendingLocalState?.snapshotBlobs,
944
+ pendingLocalState?.loadedGroupIdSnapshots,
923
945
  addProtocolSummaryIfMissing,
924
946
  forceEnableSummarizeProtocolTree,
925
947
  );
@@ -933,6 +955,8 @@ export class Container
933
955
  this.subLogger,
934
956
  this.storageAdapter,
935
957
  offlineLoadEnabled,
958
+ this,
959
+ () => this.isDirty,
936
960
  );
937
961
 
938
962
  const isDomAvailable =
@@ -1284,13 +1308,10 @@ export class Container
1284
1308
  const snapshotWithBlobs = await attachP;
1285
1309
  this.serializedStateManager.setInitialSnapshot(snapshotWithBlobs);
1286
1310
  if (!this.closed) {
1287
- this.handleDeltaConnectionArg(
1288
- {
1289
- fetchOpsFromStorage: false,
1290
- reason: { text: "createDetached" },
1291
- },
1292
- attachProps?.deltaConnection,
1293
- );
1311
+ this.handleDeltaConnectionArg(attachProps?.deltaConnection, {
1312
+ fetchOpsFromStorage: false,
1313
+ reason: { text: "createDetached" },
1314
+ });
1294
1315
  }
1295
1316
  },
1296
1317
  { start: true, end: true, cancel: "generic" },
@@ -1343,12 +1364,12 @@ export class Container
1343
1364
  0x2c6 /* "Attempting to connect() a container that is not attached" */,
1344
1365
  );
1345
1366
 
1346
- // Resume processing ops and connect to delta stream
1347
- this.resumeInternal(args);
1348
-
1349
1367
  // Set Auto Reconnect Mode
1350
1368
  const mode = ReconnectMode.Enabled;
1351
1369
  this.setAutoReconnectInternal(mode, args.reason);
1370
+
1371
+ // Resume processing ops and connect to delta stream
1372
+ this.resumeInternal(args);
1352
1373
  }
1353
1374
 
1354
1375
  public disconnect() {
@@ -1372,6 +1393,12 @@ export class Container
1372
1393
 
1373
1394
  // Resume processing ops
1374
1395
  if (this.inboundQueuePausedFromInit) {
1396
+ // This assert guards against possibility of ops/signals showing up too soon, while
1397
+ // container is not ready yet to receive them. We can hit it only if some internal code call into here,
1398
+ // as public API like Container.connect() can be only called when user got back container object, i.e.
1399
+ // it is already fully loaded.
1400
+ assert(this.loaded, "connect() can be called only in fully loaded state");
1401
+
1375
1402
  this.inboundQueuePausedFromInit = false;
1376
1403
  this._deltaManager.inbound.resume();
1377
1404
  this._deltaManager.inboundSignal.resume();
@@ -1509,7 +1536,6 @@ export class Container
1509
1536
  loadMode: IContainerLoadMode,
1510
1537
  resolvedUrl: IResolvedUrl,
1511
1538
  pendingLocalState: IPendingContainerState | undefined,
1512
- loadToSequenceNumber: number | undefined,
1513
1539
  ) {
1514
1540
  const timings: Record<string, number> = { phase1: performance.now() };
1515
1541
  this.service = await this.createDocumentService(async () =>
@@ -1534,7 +1560,7 @@ export class Container
1534
1560
 
1535
1561
  // Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
1536
1562
  // DeltaManager is resilient to this and will wait to start processing ops until after it is attached.
1537
- if (loadMode.deltaConnection === undefined && !pendingLocalState) {
1563
+ if (loadMode.deltaConnection === undefined) {
1538
1564
  this.connectToDeltaStream(connectionArgs);
1539
1565
  }
1540
1566
 
@@ -1566,57 +1592,6 @@ export class Container
1566
1592
  attributes.sequenceNumber;
1567
1593
  let opsBeforeReturnP: Promise<void> | undefined;
1568
1594
 
1569
- if (loadMode.pauseAfterLoad === true) {
1570
- // If we are trying to pause at a specific sequence number, ensure the latest snapshot is not newer than the desired sequence number.
1571
- if (loadMode.opsBeforeReturn === "sequenceNumber") {
1572
- assert(
1573
- loadToSequenceNumber !== undefined,
1574
- 0x727 /* sequenceNumber should be defined */,
1575
- );
1576
- // Note: It is possible that we think the latest snapshot is newer than the specified sequence number
1577
- // due to saved ops that may be replayed after the snapshot.
1578
- // https://dev.azure.com/fluidframework/internal/_workitems/edit/5055
1579
- if (lastProcessedSequenceNumber > loadToSequenceNumber) {
1580
- throw new Error(
1581
- "Cannot satisfy request to pause the container at the specified sequence number. Most recent snapshot is newer than the specified sequence number.",
1582
- );
1583
- }
1584
- }
1585
-
1586
- // Force readonly mode - this will ensure we don't receive an error for the lack of join op
1587
- this.forceReadonly(true);
1588
-
1589
- // We need to setup a listener to stop op processing once we reach the desired sequence number (if specified).
1590
- const opHandler = () => {
1591
- if (loadToSequenceNumber === undefined) {
1592
- // If there is no specified sequence number, pause after the inbound queue is empty.
1593
- if (this.deltaManager.inbound.length !== 0) {
1594
- return;
1595
- }
1596
- } else {
1597
- // If there is a specified sequence number, keep processing until we reach it.
1598
- if (this.deltaManager.lastSequenceNumber < loadToSequenceNumber) {
1599
- return;
1600
- }
1601
- }
1602
-
1603
- // Pause op processing once we have processed the desired number of ops.
1604
- void this.deltaManager.inbound.pause();
1605
- void this.deltaManager.outbound.pause();
1606
- this.off("op", opHandler);
1607
- };
1608
- if (
1609
- (loadToSequenceNumber === undefined && this.deltaManager.inbound.length === 0) ||
1610
- this.deltaManager.lastSequenceNumber === loadToSequenceNumber
1611
- ) {
1612
- // If we have already reached the desired sequence number, call opHandler() to pause immediately.
1613
- opHandler();
1614
- } else {
1615
- // If we have not yet reached the desired sequence number, setup a listener to pause once we reach it.
1616
- this.on("op", opHandler);
1617
- }
1618
- }
1619
-
1620
1595
  // Attach op handlers to finish initialization and be able to start processing ops
1621
1596
  // Kick off any ops fetching if required.
1622
1597
  switch (loadMode.opsBeforeReturn) {
@@ -1629,7 +1604,6 @@ export class Container
1629
1604
  lastProcessedSequenceNumber,
1630
1605
  );
1631
1606
  break;
1632
- case "sequenceNumber":
1633
1607
  case "cached":
1634
1608
  case "all":
1635
1609
  opsBeforeReturnP = this.attachDeltaManagerOpHandler(
@@ -1650,6 +1624,13 @@ export class Container
1650
1624
  baseSnapshot,
1651
1625
  );
1652
1626
 
1627
+ // If we are loading from pending state, we start with old clientId.
1628
+ // We switch to latest connection clientId only after setLoaded().
1629
+ assert(this.clientId === undefined, "there should be no clientId yet");
1630
+ if (pendingLocalState?.clientId !== undefined) {
1631
+ this.protocolHandler.audience.setCurrentClientId(pendingLocalState?.clientId);
1632
+ }
1633
+
1653
1634
  timings.phase3 = performance.now();
1654
1635
  const codeDetails = this.getCodeDetailsFromQuorum();
1655
1636
  await this.instantiateRuntime(
@@ -1672,6 +1653,7 @@ export class Container
1672
1653
  await this.runtime.notifyOpReplay?.(message);
1673
1654
  }
1674
1655
  pendingLocalState.savedOps = [];
1656
+ this.storageAdapter.clearPendingState();
1675
1657
  }
1676
1658
 
1677
1659
  // We might have hit some failure that did not manifest itself in exception in this flow,
@@ -1695,27 +1677,12 @@ export class Container
1695
1677
  this._deltaManager.inbound.pause();
1696
1678
  }
1697
1679
 
1698
- this.handleDeltaConnectionArg(
1699
- connectionArgs,
1700
- loadMode.deltaConnection,
1701
- pendingLocalState !== undefined,
1702
- );
1703
- }
1680
+ // Internal context is fully loaded at this point
1681
+ // Move to loaded before calling this.handleDeltaConnectionArg() - latter allows ops & signals in, which
1682
+ // may result in container moving to "connected" state. Such transitions are allowed only in loaded state.
1683
+ this.setLoaded();
1704
1684
 
1705
- // If we have not yet reached `loadToSequenceNumber`, we will wait for ops to arrive until we reach it
1706
- if (
1707
- loadToSequenceNumber !== undefined &&
1708
- this.deltaManager.lastSequenceNumber < loadToSequenceNumber
1709
- ) {
1710
- await new Promise<void>((resolve, reject) => {
1711
- const opHandler = (message: ISequencedDocumentMessage) => {
1712
- if (message.sequenceNumber >= loadToSequenceNumber) {
1713
- resolve();
1714
- this.off("op", opHandler);
1715
- }
1716
- };
1717
- this.on("op", opHandler);
1718
- });
1685
+ this.handleDeltaConnectionArg(loadMode.deltaConnection);
1719
1686
  }
1720
1687
 
1721
1688
  // Safety net: static version of Container.load() should have learned about it through "closed" handler.
@@ -1727,8 +1694,6 @@ export class Container
1727
1694
  throw new Error("Container was closed while load()");
1728
1695
  }
1729
1696
 
1730
- // Internal context is fully loaded at this point
1731
- this.setLoaded();
1732
1697
  timings.end = performance.now();
1733
1698
  this.subLogger.sendTelemetryEvent(
1734
1699
  {
@@ -2006,7 +1971,22 @@ export class Container
2006
1971
 
2007
1972
  deltaManager.on("connect", (details: IConnectionDetailsInternal, _opsBehind?: number) => {
2008
1973
  assert(this.connectionMode === details.mode, 0x4b7 /* mismatch */);
2009
- this.connectionStateHandler.receivedConnectEvent(details);
1974
+
1975
+ // Delay raising events until setLoaded()
1976
+ // Here are some of the reasons why this design is chosen:
1977
+ // 1. Various processes track speed of connection. But we are not processing ops or signal while container is loading,
1978
+ // and thus we can't move forward across connection modes. This results in telemetry errors (like NoJoinOp) that
1979
+ // have nothing to do with connection flow itself
1980
+ // 2. This also makes it hard to reason about recovery (like reconnection) in case we might have lost JoinSignal. Reconnecting
1981
+ // in loading phase is useless (get back to same state), but at the same time not doing it may result in broken connection
1982
+ // without recovery (after we loaded).
1983
+ // 3. We expose non-consistent view. ContainerRuntime may start loading in non-connected state, but end in connected, with
1984
+ // no events telling about it (until we loaded). Most of the code relies on a fact that state changes when events fire.
1985
+ // This will not delay any processes (as observed by the user). I.e. once container moves to loaded phase,
1986
+ // we immediately would transition across all phases, if we have proper signals / ops ready.
1987
+ if (this.loaded) {
1988
+ this.connectionStateHandler.receivedConnectEvent(details);
1989
+ }
2010
1990
  });
2011
1991
 
2012
1992
  deltaManager.on("establishingConnection", (reason: IConnectionStateChangeReason) => {
@@ -2019,8 +1999,13 @@ export class Container
2019
1999
 
2020
2000
  deltaManager.on("disconnect", (text, error) => {
2021
2001
  this.noopHeuristic?.notifyDisconnect();
2022
- if (!this.closed) {
2023
- this.connectionStateHandler.receivedDisconnectEvent({ text, error });
2002
+ const reason = { text, error };
2003
+ // Symmetry with "connect" events
2004
+ if (this.loaded) {
2005
+ this.connectionStateHandler.receivedDisconnectEvent(reason);
2006
+ } else if (!this.closed) {
2007
+ // Raise cancellation to get state machine back to initial state
2008
+ this.connectionStateHandler.cancelEstablishingConnection(reason);
2024
2009
  }
2025
2010
  });
2026
2011
 
@@ -2035,10 +2020,12 @@ export class Container
2035
2020
  });
2036
2021
 
2037
2022
  deltaManager.on("readonly", (readonly) => {
2038
- this.setContextConnectedState(
2039
- this.connectionState === ConnectionState.Connected,
2040
- readonly,
2041
- );
2023
+ if (this.loaded) {
2024
+ this.setContextConnectedState(
2025
+ this.connectionState === ConnectionState.Connected,
2026
+ readonly,
2027
+ );
2028
+ }
2042
2029
  this.emit("readonly", readonly);
2043
2030
  });
2044
2031
 
@@ -2055,7 +2042,7 @@ export class Container
2055
2042
 
2056
2043
  private async attachDeltaManagerOpHandler(
2057
2044
  attributes: IDocumentAttributes,
2058
- prefetchType?: "sequenceNumber" | "cached" | "all" | "none",
2045
+ prefetchType?: "cached" | "all" | "none",
2059
2046
  lastProcessedSequenceNumber?: number,
2060
2047
  ) {
2061
2048
  return this._deltaManager.attachOpHandler(
@@ -2098,10 +2085,7 @@ export class Container
2098
2085
  // This info is of most interesting while Catching Up.
2099
2086
  checkpointSequenceNumber = this.deltaManager.lastKnownSeqNumber;
2100
2087
  // Need to check that we have already loaded and fetched the snapshot.
2101
- if (
2102
- this.deltaManager.hasCheckpointSequenceNumber &&
2103
- this._lifecycleState === "loaded"
2104
- ) {
2088
+ if (this.deltaManager.hasCheckpointSequenceNumber && this.loaded) {
2105
2089
  opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
2106
2090
  }
2107
2091
  }
@@ -2117,7 +2101,7 @@ export class Container
2117
2101
  reason: reason?.text,
2118
2102
  connectionInitiationReason,
2119
2103
  pendingClientId: this.connectionStateHandler.pendingClientId,
2120
- clientId: this.clientId,
2104
+ clientId: this.connectionStateHandler.clientId,
2121
2105
  autoReconnect,
2122
2106
  opsBehind,
2123
2107
  online: OnlineStatus[isOnline()],
@@ -2138,27 +2122,35 @@ export class Container
2138
2122
  }
2139
2123
  }
2140
2124
 
2141
- private propagateConnectionState(
2142
- initialTransition: boolean,
2143
- disconnectedReason?: IConnectionStateChangeReason,
2144
- ) {
2145
- // When container loaded, we want to propagate initial connection state.
2146
- // After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
2125
+ private propagateConnectionState(disconnectedReason?: IConnectionStateChangeReason) {
2126
+ const connected = this.connectionState === ConnectionState.Connected;
2127
+
2128
+ if (connected) {
2129
+ const clientId = this.connectionStateHandler.clientId;
2130
+ assert(clientId !== undefined, "there has to be clientId");
2131
+ this.protocolHandler.audience.setCurrentClientId(clientId);
2132
+ }
2133
+
2134
+ // We communicate only transitions to Connected & Disconnected states, skipping all other states.
2147
2135
  // This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
2148
2136
  if (
2149
- !initialTransition &&
2150
2137
  this.connectionState !== ConnectionState.Connected &&
2151
2138
  this.connectionState !== ConnectionState.Disconnected
2152
2139
  ) {
2153
2140
  return;
2154
2141
  }
2155
- const state = this.connectionState === ConnectionState.Connected;
2156
2142
 
2157
2143
  // Both protocol and context should not be undefined if we got so far.
2158
2144
 
2159
- this.setContextConnectedState(state, this.readOnlyInfo.readonly ?? false);
2160
- this.protocolHandler.setConnectionState(state, this.clientId);
2161
- raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason?.text);
2145
+ this.setContextConnectedState(connected, this.readOnlyInfo.readonly ?? false);
2146
+ this.protocolHandler.setConnectionState(connected, this.clientId);
2147
+ raiseConnectedEvent(
2148
+ this.mc.logger,
2149
+ this,
2150
+ connected,
2151
+ this.clientId,
2152
+ disconnectedReason?.text,
2153
+ );
2162
2154
  }
2163
2155
 
2164
2156
  // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
@@ -2396,29 +2388,34 @@ export class Container
2396
2388
  /**
2397
2389
  * Set the connected state of the ContainerContext
2398
2390
  * This controls the "connected" state of the ContainerRuntime as well
2399
- * @param state - Is the container currently connected?
2391
+ * @param connected - Is the container currently connected?
2400
2392
  * @param readonly - Is the container in readonly mode?
2401
2393
  */
2402
- private setContextConnectedState(state: boolean, readonly: boolean): void {
2403
- if (this._runtime?.disposed === false) {
2394
+ private setContextConnectedState(connected: boolean, readonly: boolean): void {
2395
+ if (this._runtime?.disposed === false && this.loaded) {
2404
2396
  /**
2405
2397
  * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
2406
2398
  * ops getting through to the DeltaManager.
2407
2399
  * The ContainerRuntime's "connected" state simply means it is ok to send ops
2408
2400
  * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
2409
2401
  */
2410
- this.runtime.setConnectionState(state && !readonly, this.clientId);
2402
+ this.runtime.setConnectionState(connected && !readonly, this.clientId);
2411
2403
  }
2412
2404
  }
2413
2405
 
2414
2406
  private handleDeltaConnectionArg(
2415
- connectionArgs: IConnectionArgs,
2416
2407
  deltaConnectionArg?: "none" | "delayed",
2417
- canConnect: boolean = true,
2408
+ connectionArgs?: IConnectionArgs,
2418
2409
  ) {
2410
+ // This ensures that we allow transitions to "connected" state only after container has been fully loaded
2411
+ // and we propagate such events to container runtime. All events prior to being loaded are ignored.
2412
+ // This means if we get here in non-loaded state, we might not deliver proper events to container runtime,
2413
+ // and runtime implementation may miss such events.
2414
+ assert(this.loaded, "has to be called after container transitions to loaded state");
2415
+
2419
2416
  switch (deltaConnectionArg) {
2420
2417
  case undefined:
2421
- if (canConnect) {
2418
+ if (connectionArgs) {
2422
2419
  // connect to delta stream now since we did not before
2423
2420
  this.connectToDeltaStream(connectionArgs);
2424
2421
  }
@@ -28,7 +28,7 @@ import {
28
28
  IVersion,
29
29
  MessageType,
30
30
  } from "@fluidframework/protocol-definitions";
31
- import { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils";
31
+ import { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal";
32
32
 
33
33
  /**
34
34
  * {@inheritDoc @fluidframework/container-definitions#IContainerContext}