@fluidframework/container-loader 2.0.0-rc.3.0.3 → 2.0.0-rc.4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/api-report/container-loader.api.md +5 -1
  3. package/dist/attachment.d.ts +3 -2
  4. package/dist/attachment.d.ts.map +1 -1
  5. package/dist/attachment.js +5 -5
  6. package/dist/attachment.js.map +1 -1
  7. package/dist/audience.d.ts +6 -4
  8. package/dist/audience.d.ts.map +1 -1
  9. package/dist/audience.js +18 -3
  10. package/dist/audience.js.map +1 -1
  11. package/dist/catchUpMonitor.d.ts +1 -1
  12. package/dist/catchUpMonitor.d.ts.map +1 -1
  13. package/dist/catchUpMonitor.js.map +1 -1
  14. package/dist/connectionManager.d.ts +7 -3
  15. package/dist/connectionManager.d.ts.map +1 -1
  16. package/dist/connectionManager.js +57 -38
  17. package/dist/connectionManager.js.map +1 -1
  18. package/dist/connectionStateHandler.d.ts +31 -10
  19. package/dist/connectionStateHandler.d.ts.map +1 -1
  20. package/dist/connectionStateHandler.js +49 -36
  21. package/dist/connectionStateHandler.js.map +1 -1
  22. package/dist/container.d.ts +22 -13
  23. package/dist/container.d.ts.map +1 -1
  24. package/dist/container.js +145 -117
  25. package/dist/container.js.map +1 -1
  26. package/dist/containerContext.d.ts +3 -3
  27. package/dist/containerContext.d.ts.map +1 -1
  28. package/dist/containerContext.js.map +1 -1
  29. package/dist/containerStorageAdapter.d.ts +12 -3
  30. package/dist/containerStorageAdapter.d.ts.map +1 -1
  31. package/dist/containerStorageAdapter.js +42 -4
  32. package/dist/containerStorageAdapter.js.map +1 -1
  33. package/dist/contracts.d.ts +2 -2
  34. package/dist/contracts.d.ts.map +1 -1
  35. package/dist/contracts.js.map +1 -1
  36. package/dist/debugLogger.d.ts +1 -2
  37. package/dist/debugLogger.d.ts.map +1 -1
  38. package/dist/debugLogger.js.map +1 -1
  39. package/dist/deltaManager.d.ts +5 -6
  40. package/dist/deltaManager.d.ts.map +1 -1
  41. package/dist/deltaManager.js +29 -24
  42. package/dist/deltaManager.js.map +1 -1
  43. package/dist/deltaQueue.d.ts +1 -1
  44. package/dist/deltaQueue.d.ts.map +1 -1
  45. package/dist/deltaQueue.js.map +1 -1
  46. package/dist/error.d.ts +1 -2
  47. package/dist/error.d.ts.map +1 -1
  48. package/dist/error.js.map +1 -1
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +3 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/legacy.d.ts +2 -2
  54. package/dist/loadPaused.d.ts +35 -0
  55. package/dist/loadPaused.d.ts.map +1 -0
  56. package/dist/loadPaused.js +115 -0
  57. package/dist/loadPaused.js.map +1 -0
  58. package/dist/loader.d.ts +1 -1
  59. package/dist/loader.d.ts.map +1 -1
  60. package/dist/loader.js +1 -14
  61. package/dist/loader.js.map +1 -1
  62. package/dist/location-redirection-utilities/resolveWithLocationRedirection.d.ts.map +1 -1
  63. package/dist/location-redirection-utilities/resolveWithLocationRedirection.js +4 -4
  64. package/dist/location-redirection-utilities/resolveWithLocationRedirection.js.map +1 -1
  65. package/dist/packageVersion.d.ts +1 -1
  66. package/dist/packageVersion.js +1 -1
  67. package/dist/packageVersion.js.map +1 -1
  68. package/dist/protocol.d.ts.map +1 -1
  69. package/dist/protocol.js +3 -0
  70. package/dist/protocol.js.map +1 -1
  71. package/dist/public.d.ts +1 -1
  72. package/dist/retriableDocumentStorageService.d.ts +1 -1
  73. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  74. package/dist/retriableDocumentStorageService.js.map +1 -1
  75. package/dist/serializedStateManager.d.ts +89 -9
  76. package/dist/serializedStateManager.d.ts.map +1 -1
  77. package/dist/serializedStateManager.js +150 -34
  78. package/dist/serializedStateManager.js.map +1 -1
  79. package/dist/utils.d.ts +11 -1
  80. package/dist/utils.d.ts.map +1 -1
  81. package/dist/utils.js +29 -14
  82. package/dist/utils.js.map +1 -1
  83. package/lib/attachment.d.ts +3 -2
  84. package/lib/attachment.d.ts.map +1 -1
  85. package/lib/attachment.js +5 -5
  86. package/lib/attachment.js.map +1 -1
  87. package/lib/audience.d.ts +6 -4
  88. package/lib/audience.d.ts.map +1 -1
  89. package/lib/audience.js +19 -4
  90. package/lib/audience.js.map +1 -1
  91. package/lib/catchUpMonitor.d.ts +1 -1
  92. package/lib/catchUpMonitor.d.ts.map +1 -1
  93. package/lib/catchUpMonitor.js.map +1 -1
  94. package/lib/connectionManager.d.ts +7 -3
  95. package/lib/connectionManager.d.ts.map +1 -1
  96. package/lib/connectionManager.js +36 -17
  97. package/lib/connectionManager.js.map +1 -1
  98. package/lib/connectionStateHandler.d.ts +31 -10
  99. package/lib/connectionStateHandler.d.ts.map +1 -1
  100. package/lib/connectionStateHandler.js +49 -36
  101. package/lib/connectionStateHandler.js.map +1 -1
  102. package/lib/container.d.ts +22 -13
  103. package/lib/container.d.ts.map +1 -1
  104. package/lib/container.js +146 -118
  105. package/lib/container.js.map +1 -1
  106. package/lib/containerContext.d.ts +3 -3
  107. package/lib/containerContext.d.ts.map +1 -1
  108. package/lib/containerContext.js.map +1 -1
  109. package/lib/containerStorageAdapter.d.ts +12 -3
  110. package/lib/containerStorageAdapter.d.ts.map +1 -1
  111. package/lib/containerStorageAdapter.js +42 -4
  112. package/lib/containerStorageAdapter.js.map +1 -1
  113. package/lib/contracts.d.ts +2 -2
  114. package/lib/contracts.d.ts.map +1 -1
  115. package/lib/contracts.js +1 -1
  116. package/lib/contracts.js.map +1 -1
  117. package/lib/debugLogger.d.ts +1 -2
  118. package/lib/debugLogger.d.ts.map +1 -1
  119. package/lib/debugLogger.js.map +1 -1
  120. package/lib/deltaManager.d.ts +5 -6
  121. package/lib/deltaManager.d.ts.map +1 -1
  122. package/lib/deltaManager.js +10 -5
  123. package/lib/deltaManager.js.map +1 -1
  124. package/lib/deltaQueue.d.ts +1 -1
  125. package/lib/deltaQueue.d.ts.map +1 -1
  126. package/lib/deltaQueue.js.map +1 -1
  127. package/lib/error.d.ts +1 -2
  128. package/lib/error.d.ts.map +1 -1
  129. package/lib/error.js.map +1 -1
  130. package/lib/index.d.ts +1 -0
  131. package/lib/index.d.ts.map +1 -1
  132. package/lib/index.js +1 -0
  133. package/lib/index.js.map +1 -1
  134. package/lib/legacy.d.ts +2 -2
  135. package/lib/loadPaused.d.ts +35 -0
  136. package/lib/loadPaused.d.ts.map +1 -0
  137. package/lib/loadPaused.js +111 -0
  138. package/lib/loadPaused.js.map +1 -0
  139. package/lib/loader.d.ts +1 -1
  140. package/lib/loader.d.ts.map +1 -1
  141. package/lib/loader.js +3 -16
  142. package/lib/loader.js.map +1 -1
  143. package/lib/location-redirection-utilities/resolveWithLocationRedirection.d.ts.map +1 -1
  144. package/lib/location-redirection-utilities/resolveWithLocationRedirection.js +1 -1
  145. package/lib/location-redirection-utilities/resolveWithLocationRedirection.js.map +1 -1
  146. package/lib/packageVersion.d.ts +1 -1
  147. package/lib/packageVersion.js +1 -1
  148. package/lib/packageVersion.js.map +1 -1
  149. package/lib/protocol.d.ts.map +1 -1
  150. package/lib/protocol.js +3 -0
  151. package/lib/protocol.js.map +1 -1
  152. package/lib/public.d.ts +1 -1
  153. package/lib/retriableDocumentStorageService.d.ts +1 -1
  154. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  155. package/lib/retriableDocumentStorageService.js +1 -1
  156. package/lib/retriableDocumentStorageService.js.map +1 -1
  157. package/lib/serializedStateManager.d.ts +89 -9
  158. package/lib/serializedStateManager.d.ts.map +1 -1
  159. package/lib/serializedStateManager.js +146 -30
  160. package/lib/serializedStateManager.js.map +1 -1
  161. package/lib/tsdoc-metadata.json +1 -1
  162. package/lib/utils.d.ts +11 -1
  163. package/lib/utils.d.ts.map +1 -1
  164. package/lib/utils.js +15 -1
  165. package/lib/utils.js.map +1 -1
  166. package/package.json +24 -21
  167. package/src/attachment.ts +12 -13
  168. package/src/audience.ts +30 -9
  169. package/src/catchUpMonitor.ts +1 -1
  170. package/src/connectionManager.ts +45 -22
  171. package/src/connectionStateHandler.ts +78 -45
  172. package/src/container.ts +181 -160
  173. package/src/containerContext.ts +2 -2
  174. package/src/containerStorageAdapter.ts +61 -6
  175. package/src/contracts.ts +5 -4
  176. package/src/debugLogger.ts +1 -1
  177. package/src/deltaManager.ts +15 -8
  178. package/src/deltaQueue.ts +1 -1
  179. package/src/error.ts +1 -1
  180. package/src/index.ts +1 -0
  181. package/src/loadPaused.ts +140 -0
  182. package/src/loader.ts +6 -23
  183. package/src/location-redirection-utilities/resolveWithLocationRedirection.ts +1 -1
  184. package/src/packageVersion.ts +1 -1
  185. package/src/protocol.ts +4 -0
  186. package/src/retriableDocumentStorageService.ts +5 -2
  187. package/src/serializedStateManager.ts +215 -48
  188. package/src/utils.ts +19 -1
package/src/container.ts CHANGED
@@ -8,8 +8,6 @@ import {
8
8
  AttachState,
9
9
  IAudience,
10
10
  ICriticalContainerError,
11
- IDeltaManager,
12
- ReadOnlyInfo,
13
11
  } from "@fluidframework/container-definitions";
14
12
  import {
15
13
  ContainerWarning,
@@ -27,6 +25,8 @@ import {
27
25
  IProvideRuntimeFactory,
28
26
  IRuntime,
29
27
  isFluidCodeDetails,
28
+ IDeltaManager,
29
+ ReadOnlyInfo,
30
30
  } from "@fluidframework/container-definitions/internal";
31
31
  import {
32
32
  FluidObject,
@@ -47,6 +47,7 @@ import {
47
47
  IUrlResolver,
48
48
  } from "@fluidframework/driver-definitions/internal";
49
49
  import {
50
+ getSnapshotTree,
50
51
  MessageType2,
51
52
  OnlineStatus,
52
53
  isCombinedAppAndProtocolSummary,
@@ -75,8 +76,9 @@ import {
75
76
  MessageType,
76
77
  SummaryType,
77
78
  } from "@fluidframework/protocol-definitions";
78
- import { ITelemetryLoggerExt, type TelemetryEventCategory } from "@fluidframework/telemetry-utils";
79
79
  import {
80
+ type TelemetryEventCategory,
81
+ ITelemetryLoggerExt,
80
82
  EventEmitterWithErrorHandling,
81
83
  GenericError,
82
84
  IFluidErrorBase,
@@ -160,14 +162,9 @@ export interface IContainerLoadProps {
160
162
  readonly loadMode?: IContainerLoadMode;
161
163
 
162
164
  /**
163
- * The pending state serialized from a pervious container instance
165
+ * The pending state serialized from a previous container instance
164
166
  */
165
167
  readonly pendingLocalState?: IPendingContainerState;
166
-
167
- /**
168
- * Load the container to at least this sequence number.
169
- */
170
- readonly loadToSequenceNumber?: number;
171
168
  }
172
169
 
173
170
  /**
@@ -361,18 +358,13 @@ export class Container
361
358
  loadProps: IContainerLoadProps,
362
359
  createProps: IContainerCreateProps,
363
360
  ): Promise<Container> {
364
- const { version, pendingLocalState, loadMode, resolvedUrl, loadToSequenceNumber } =
365
- loadProps;
361
+ const { version, pendingLocalState, loadMode, resolvedUrl } = loadProps;
366
362
 
367
363
  const container = new Container(createProps, loadProps);
368
364
 
369
- const disableRecordHeapSize = container.mc.config.getBoolean(
370
- "Fluid.Loader.DisableRecordHeapSize",
371
- );
372
-
373
365
  return PerformanceEvent.timedExecAsync(
374
366
  container.mc.logger,
375
- { eventName: "Load" },
367
+ { eventName: "Load", ...loadMode },
376
368
  async (event) =>
377
369
  new Promise<Container>((resolve, reject) => {
378
370
  const defaultMode: IContainerLoadMode = { opsBeforeReturn: "cached" };
@@ -391,13 +383,13 @@ export class Container
391
383
  container.on("closed", onClosed);
392
384
 
393
385
  container
394
- .load(version, mode, resolvedUrl, pendingLocalState, loadToSequenceNumber)
386
+ .load(version, mode, resolvedUrl, pendingLocalState)
395
387
  .finally(() => {
396
388
  container.removeListener("closed", onClosed);
397
389
  })
398
390
  .then(
399
391
  (props) => {
400
- event.end({ ...props, ...loadMode });
392
+ event.end({ ...props });
401
393
  resolve(container);
402
394
  },
403
395
  (error) => {
@@ -415,7 +407,6 @@ export class Container
415
407
  );
416
408
  }),
417
409
  { start: true, end: true, cancel: "generic" },
418
- disableRecordHeapSize !== true /* recordHeapSize */,
419
410
  );
420
411
  }
421
412
 
@@ -442,6 +433,8 @@ export class Container
442
433
  /**
443
434
  * Create a new container in a detached state that is initialized with a
444
435
  * snapshot from a previous detached container.
436
+ * @param createProps - Config options for this new container instance
437
+ * @param snapshot - A stringified {@link IPendingDetachedContainerState}, e.g. generated via {@link serialize}
445
438
  */
446
439
  public static async rehydrateDetachedFromSnapshot(
447
440
  createProps: IContainerCreateProps,
@@ -513,9 +506,30 @@ export class Container
513
506
  // It's conceivable the container could be closed when this is called
514
507
  // Only transition states if currently loading
515
508
  if (this._lifecycleState === "loading") {
516
- // Propagate current connection state through the system.
517
- this.propagateConnectionState(true /* initial transition */);
518
509
  this._lifecycleState = "loaded";
510
+
511
+ // Connections transitions are delayed till we are loaded.
512
+ // This is done by holding ops and signals until the end of load sequence
513
+ // (calling this.handleDeltaConnectionArg() after setLoaded() call)
514
+ // If this assert fires, it means our logic managing connection flow is wrong, and the logic below is also wrong.
515
+ assert(
516
+ this.connectionState !== ConnectionState.Connected,
517
+ 0x969 /* not connected yet */,
518
+ );
519
+
520
+ // Propagate current connection state through the system.
521
+ const readonly = this.readOnlyInfo.readonly ?? false;
522
+ // This call does not look like needed any more, with delaying all connection-related events past loaded phase.
523
+ // Yet, there could be some customer code that would break if we do not deliver it.
524
+ // Will be removed in further PRs with proper changeset.
525
+ this.setContextConnectedState(false /* connected */, readonly);
526
+ // Deliver delayed calls to DeltaManager - we ignored "connect" events while loading.
527
+ const cm = this._deltaManager.connectionManager;
528
+ if (cm.connected) {
529
+ const details = cm.connectionDetails;
530
+ assert(details !== undefined, 0x96a /* should have details if connected */);
531
+ this.connectionStateHandler.receivedConnectEvent(details);
532
+ }
519
533
  }
520
534
  }
521
535
 
@@ -525,6 +539,10 @@ export class Container
525
539
  );
526
540
  }
527
541
 
542
+ protected get loaded(): boolean {
543
+ return this._lifecycleState === "loaded";
544
+ }
545
+
528
546
  public get disposed(): boolean {
529
547
  return this._lifecycleState === "disposing" || this._lifecycleState === "disposed";
530
548
  }
@@ -629,14 +647,13 @@ export class Container
629
647
  return this.connectionStateHandler.connectionState === ConnectionState.Connected;
630
648
  }
631
649
 
632
- private _clientId: string | undefined;
633
-
634
650
  /**
635
- * The server provided id of the client.
636
- * Set once this.connected is true, otherwise undefined
651
+ * clientId of the latest connection. Changes only once client is connected, caught up and fully loaded.
652
+ * Changes to clientId are delayed through container loading sequence and delived once container is fully loaded.
653
+ * clientId does not reset on lost connection - old value persists until new connection is fully established.
637
654
  */
638
655
  public get clientId(): string | undefined {
639
- return this._clientId;
656
+ return this.protocolHandler.audience.getSelf()?.clientId;
640
657
  }
641
658
 
642
659
  private get isInteractiveClient(): boolean {
@@ -721,6 +738,7 @@ export class Container
721
738
  },
722
739
  error,
723
740
  );
741
+ this.close(normalizeError(error));
724
742
  });
725
743
 
726
744
  const {
@@ -738,7 +756,6 @@ export class Container
738
756
 
739
757
  this.connectionTransitionTimes[ConnectionState.Disconnected] = performance.now();
740
758
  const pendingLocalState = loadProps?.pendingLocalState;
741
- this._clientId = pendingLocalState?.clientId;
742
759
 
743
760
  this._canReconnect = canReconnect ?? true;
744
761
  this.clientDetailsOverride = clientDetailsOverride;
@@ -840,13 +857,9 @@ export class Container
840
857
  {
841
858
  logger: this.mc.logger,
842
859
  connectionStateChanged: (value, oldState, reason) => {
843
- if (value === ConnectionState.Connected) {
844
- this._clientId = this.connectionStateHandler.pendingClientId;
845
- }
846
860
  this.logConnectionStateChangeTelemetry(value, oldState, reason);
847
- if (this._lifecycleState === "loaded") {
861
+ if (this.loaded) {
848
862
  this.propagateConnectionState(
849
- false /* initial transition */,
850
863
  value === ConnectionState.Disconnected
851
864
  ? reason
852
865
  : undefined /* disconnectedReason */,
@@ -876,15 +889,26 @@ export class Container
876
889
  ...(details === undefined ? {} : { details: JSON.stringify(details) }),
877
890
  });
878
891
 
892
+ // This assert is important for many reasons:
893
+ // 1) Cosmetic / OCE burden: It's useless to raise NoJoinOp error events, if we are loading, as that's most
894
+ // likely to happen if snapshot loading takes too long. During this time we are not processing ops so there is no
895
+ // way to move to "connected" state, and thus "NoJoin" timer would fire (see
896
+ // IConnectionStateHandler.logConnectionIssue() callback and related code in ConnectStateHandler class implementation).
897
+ // But these events do not tell us anything about connectivity pipeline / op processing pipeline,
898
+ // only that boot is slow, and we have events for that.
899
+ // 2) Doing recovery below is useless in loading mode, for the reasons described above. At the same time we can't
900
+ // not do it, as maybe we lost JoinSignal for "self", and when loading is done, we never move to connected
901
+ // state. So we would have to do (in most cases) useless infinite reconnect loop while we are loading.
902
+ assert(
903
+ this.loaded,
904
+ 0x96b /* connection issues can be raised only after container is loaded */,
905
+ );
906
+
879
907
  // If this is "write" connection, it took too long to receive join op. But in most cases that's due
880
908
  // 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") {
909
+ // For "read" connections, we get here due to join signal for "self" not arriving on time.
910
+ // Attempt to recover by reconnecting.
911
+ if (mode === "read" && category === "error") {
888
912
  const reason = { text: "NoJoinSignal" };
889
913
  this.disconnectInternal(reason);
890
914
  this.connectInternal({ reason, fetchOpsFromStorage: false });
@@ -893,6 +917,9 @@ export class Container
893
917
  clientShouldHaveLeft: (clientId: string) => {
894
918
  this.clientsWhoShouldHaveLeft.add(clientId);
895
919
  },
920
+ onCriticalError: (error: unknown) => {
921
+ this.close(normalizeError(error));
922
+ },
896
923
  },
897
924
  this.deltaManager,
898
925
  pendingLocalState?.clientId,
@@ -920,6 +947,7 @@ export class Container
920
947
  detachedBlobStorage,
921
948
  this.mc.logger,
922
949
  pendingLocalState?.snapshotBlobs,
950
+ pendingLocalState?.loadedGroupIdSnapshots,
923
951
  addProtocolSummaryIfMissing,
924
952
  forceEnableSummarizeProtocolTree,
925
953
  );
@@ -933,6 +961,8 @@ export class Container
933
961
  this.subLogger,
934
962
  this.storageAdapter,
935
963
  offlineLoadEnabled,
964
+ this,
965
+ () => this.isDirty,
936
966
  );
937
967
 
938
968
  const isDomAvailable =
@@ -1110,6 +1140,11 @@ export class Container
1110
1140
  return pendingState;
1111
1141
  }
1112
1142
 
1143
+ /**
1144
+ * Serialize current container state required to rehydrate to the same position without dataloss.
1145
+ * Note: The container must already be attached. For detached containers use {@link serialize}
1146
+ * @returns stringified {@link IPendingContainerState} for the container
1147
+ */
1113
1148
  public async getPendingLocalState(): Promise<string> {
1114
1149
  return this.getPendingLocalStateCore({ notifyImminentClosure: false });
1115
1150
  }
@@ -1128,7 +1163,7 @@ export class Container
1128
1163
  this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid",
1129
1164
  0x0d2 /* "resolved url should be valid Fluid url" */,
1130
1165
  );
1131
- const pendingState = await this.serializedStateManager.getPendingLocalStateCore(
1166
+ const pendingState = await this.serializedStateManager.getPendingLocalState(
1132
1167
  props,
1133
1168
  this.clientId,
1134
1169
  this.runtime,
@@ -1141,6 +1176,12 @@ export class Container
1141
1176
  return this.attachmentData.state;
1142
1177
  }
1143
1178
 
1179
+ /**
1180
+ * Serialize current container state required to rehydrate to the same position without dataloss.
1181
+ * Note: The container must be detached and not closed. For attached containers use
1182
+ * {@link getPendingLocalState} or {@link closeAndGetPendingLocalState}
1183
+ * @returns stringified {@link IPendingDetachedContainerState} for the container
1184
+ */
1144
1185
  public serialize(): string {
1145
1186
  if (this.attachmentData.state === AttachState.Attached || this.closed) {
1146
1187
  throw new UsageError("Container must not be attached or closed.");
@@ -1281,16 +1322,16 @@ export class Container
1281
1322
  throw normalizeErrorAndClose(error);
1282
1323
  });
1283
1324
  }
1325
+
1326
+ // If offline load is enabled, attachP will return the attach summary (in Snapshot format) so we can initialize SerializedStateManager
1284
1327
  const snapshotWithBlobs = await attachP;
1285
1328
  this.serializedStateManager.setInitialSnapshot(snapshotWithBlobs);
1329
+
1286
1330
  if (!this.closed) {
1287
- this.handleDeltaConnectionArg(
1288
- {
1289
- fetchOpsFromStorage: false,
1290
- reason: { text: "createDetached" },
1291
- },
1292
- attachProps?.deltaConnection,
1293
- );
1331
+ this.handleDeltaConnectionArg(attachProps?.deltaConnection, {
1332
+ fetchOpsFromStorage: false,
1333
+ reason: { text: "createDetached" },
1334
+ });
1294
1335
  }
1295
1336
  },
1296
1337
  { start: true, end: true, cancel: "generic" },
@@ -1343,12 +1384,12 @@ export class Container
1343
1384
  0x2c6 /* "Attempting to connect() a container that is not attached" */,
1344
1385
  );
1345
1386
 
1346
- // Resume processing ops and connect to delta stream
1347
- this.resumeInternal(args);
1348
-
1349
1387
  // Set Auto Reconnect Mode
1350
1388
  const mode = ReconnectMode.Enabled;
1351
1389
  this.setAutoReconnectInternal(mode, args.reason);
1390
+
1391
+ // Resume processing ops and connect to delta stream
1392
+ this.resumeInternal(args);
1352
1393
  }
1353
1394
 
1354
1395
  public disconnect() {
@@ -1372,6 +1413,12 @@ export class Container
1372
1413
 
1373
1414
  // Resume processing ops
1374
1415
  if (this.inboundQueuePausedFromInit) {
1416
+ // This assert guards against possibility of ops/signals showing up too soon, while
1417
+ // container is not ready yet to receive them. We can hit it only if some internal code call into here,
1418
+ // as public API like Container.connect() can be only called when user got back container object, i.e.
1419
+ // it is already fully loaded.
1420
+ assert(this.loaded, 0x96c /* connect() can be called only in fully loaded state */);
1421
+
1375
1422
  this.inboundQueuePausedFromInit = false;
1376
1423
  this._deltaManager.inbound.resume();
1377
1424
  this._deltaManager.inboundSignal.resume();
@@ -1509,7 +1556,6 @@ export class Container
1509
1556
  loadMode: IContainerLoadMode,
1510
1557
  resolvedUrl: IResolvedUrl,
1511
1558
  pendingLocalState: IPendingContainerState | undefined,
1512
- loadToSequenceNumber: number | undefined,
1513
1559
  ) {
1514
1560
  const timings: Record<string, number> = { phase1: performance.now() };
1515
1561
  this.service = await this.createDocumentService(async () =>
@@ -1534,7 +1580,7 @@ export class Container
1534
1580
 
1535
1581
  // Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
1536
1582
  // DeltaManager is resilient to this and will wait to start processing ops until after it is attached.
1537
- if (loadMode.deltaConnection === undefined && !pendingLocalState) {
1583
+ if (loadMode.deltaConnection === undefined) {
1538
1584
  this.connectToDeltaStream(connectionArgs);
1539
1585
  }
1540
1586
 
@@ -1554,10 +1600,11 @@ export class Container
1554
1600
  specifiedVersion,
1555
1601
  supportGetSnapshotApi,
1556
1602
  );
1603
+ const baseSnapshotTree: ISnapshotTree | undefined = getSnapshotTree(baseSnapshot);
1557
1604
  this._loadedFromVersion = version;
1558
1605
  const attributes: IDocumentAttributes = await getDocumentAttributes(
1559
1606
  this.storageAdapter,
1560
- baseSnapshot,
1607
+ baseSnapshotTree,
1561
1608
  );
1562
1609
 
1563
1610
  // If we saved ops, we will replay them and don't need DeltaManager to fetch them
@@ -1566,57 +1613,6 @@ export class Container
1566
1613
  attributes.sequenceNumber;
1567
1614
  let opsBeforeReturnP: Promise<void> | undefined;
1568
1615
 
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
1616
  // Attach op handlers to finish initialization and be able to start processing ops
1621
1617
  // Kick off any ops fetching if required.
1622
1618
  switch (loadMode.opsBeforeReturn) {
@@ -1629,7 +1625,6 @@ export class Container
1629
1625
  lastProcessedSequenceNumber,
1630
1626
  );
1631
1627
  break;
1632
- case "sequenceNumber":
1633
1628
  case "cached":
1634
1629
  case "all":
1635
1630
  opsBeforeReturnP = this.attachDeltaManagerOpHandler(
@@ -1647,14 +1642,21 @@ export class Container
1647
1642
  await this.initializeProtocolStateFromSnapshot(
1648
1643
  attributes,
1649
1644
  this.storageAdapter,
1650
- baseSnapshot,
1645
+ baseSnapshotTree,
1651
1646
  );
1652
1647
 
1648
+ // If we are loading from pending state, we start with old clientId.
1649
+ // We switch to latest connection clientId only after setLoaded().
1650
+ assert(this.clientId === undefined, 0x96d /* there should be no clientId yet */);
1651
+ if (pendingLocalState?.clientId !== undefined) {
1652
+ this.protocolHandler.audience.setCurrentClientId(pendingLocalState?.clientId);
1653
+ }
1654
+
1653
1655
  timings.phase3 = performance.now();
1654
1656
  const codeDetails = this.getCodeDetailsFromQuorum();
1655
1657
  await this.instantiateRuntime(
1656
1658
  codeDetails,
1657
- baseSnapshot,
1659
+ baseSnapshotTree,
1658
1660
  // give runtime a dummy value so it knows we're loading from a stash blob
1659
1661
  pendingLocalState ? pendingLocalState?.pendingRuntimeState ?? {} : undefined,
1660
1662
  isInstanceOfISnapshot(baseSnapshot) ? baseSnapshot : undefined,
@@ -1672,6 +1674,7 @@ export class Container
1672
1674
  await this.runtime.notifyOpReplay?.(message);
1673
1675
  }
1674
1676
  pendingLocalState.savedOps = [];
1677
+ this.storageAdapter.clearPendingState();
1675
1678
  }
1676
1679
 
1677
1680
  // We might have hit some failure that did not manifest itself in exception in this flow,
@@ -1695,27 +1698,12 @@ export class Container
1695
1698
  this._deltaManager.inbound.pause();
1696
1699
  }
1697
1700
 
1698
- this.handleDeltaConnectionArg(
1699
- connectionArgs,
1700
- loadMode.deltaConnection,
1701
- pendingLocalState !== undefined,
1702
- );
1703
- }
1701
+ // Internal context is fully loaded at this point
1702
+ // Move to loaded before calling this.handleDeltaConnectionArg() - latter allows ops & signals in, which
1703
+ // may result in container moving to "connected" state. Such transitions are allowed only in loaded state.
1704
+ this.setLoaded();
1704
1705
 
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
- });
1706
+ this.handleDeltaConnectionArg(loadMode.deltaConnection);
1719
1707
  }
1720
1708
 
1721
1709
  // Safety net: static version of Container.load() should have learned about it through "closed" handler.
@@ -1727,8 +1715,6 @@ export class Container
1727
1715
  throw new Error("Container was closed while load()");
1728
1716
  }
1729
1717
 
1730
- // Internal context is fully loaded at this point
1731
- this.setLoaded();
1732
1718
  timings.end = performance.now();
1733
1719
  this.subLogger.sendTelemetryEvent(
1734
1720
  {
@@ -2006,7 +1992,22 @@ export class Container
2006
1992
 
2007
1993
  deltaManager.on("connect", (details: IConnectionDetailsInternal, _opsBehind?: number) => {
2008
1994
  assert(this.connectionMode === details.mode, 0x4b7 /* mismatch */);
2009
- this.connectionStateHandler.receivedConnectEvent(details);
1995
+
1996
+ // Delay raising events until setLoaded()
1997
+ // Here are some of the reasons why this design is chosen:
1998
+ // 1. Various processes track speed of connection. But we are not processing ops or signal while container is loading,
1999
+ // and thus we can't move forward across connection modes. This results in telemetry errors (like NoJoinOp) that
2000
+ // have nothing to do with connection flow itself
2001
+ // 2. This also makes it hard to reason about recovery (like reconnection) in case we might have lost JoinSignal. Reconnecting
2002
+ // in loading phase is useless (get back to same state), but at the same time not doing it may result in broken connection
2003
+ // without recovery (after we loaded).
2004
+ // 3. We expose non-consistent view. ContainerRuntime may start loading in non-connected state, but end in connected, with
2005
+ // no events telling about it (until we loaded). Most of the code relies on a fact that state changes when events fire.
2006
+ // This will not delay any processes (as observed by the user). I.e. once container moves to loaded phase,
2007
+ // we immediately would transition across all phases, if we have proper signals / ops ready.
2008
+ if (this.loaded) {
2009
+ this.connectionStateHandler.receivedConnectEvent(details);
2010
+ }
2010
2011
  });
2011
2012
 
2012
2013
  deltaManager.on("establishingConnection", (reason: IConnectionStateChangeReason) => {
@@ -2019,8 +2020,13 @@ export class Container
2019
2020
 
2020
2021
  deltaManager.on("disconnect", (text, error) => {
2021
2022
  this.noopHeuristic?.notifyDisconnect();
2022
- if (!this.closed) {
2023
- this.connectionStateHandler.receivedDisconnectEvent({ text, error });
2023
+ const reason = { text, error };
2024
+ // Symmetry with "connect" events
2025
+ if (this.loaded) {
2026
+ this.connectionStateHandler.receivedDisconnectEvent(reason);
2027
+ } else if (!this.closed) {
2028
+ // Raise cancellation to get state machine back to initial state
2029
+ this.connectionStateHandler.cancelEstablishingConnection(reason);
2024
2030
  }
2025
2031
  });
2026
2032
 
@@ -2035,10 +2041,12 @@ export class Container
2035
2041
  });
2036
2042
 
2037
2043
  deltaManager.on("readonly", (readonly) => {
2038
- this.setContextConnectedState(
2039
- this.connectionState === ConnectionState.Connected,
2040
- readonly,
2041
- );
2044
+ if (this.loaded) {
2045
+ this.setContextConnectedState(
2046
+ this.connectionState === ConnectionState.Connected,
2047
+ readonly,
2048
+ );
2049
+ }
2042
2050
  this.emit("readonly", readonly);
2043
2051
  });
2044
2052
 
@@ -2055,7 +2063,7 @@ export class Container
2055
2063
 
2056
2064
  private async attachDeltaManagerOpHandler(
2057
2065
  attributes: IDocumentAttributes,
2058
- prefetchType?: "sequenceNumber" | "cached" | "all" | "none",
2066
+ prefetchType?: "cached" | "all" | "none",
2059
2067
  lastProcessedSequenceNumber?: number,
2060
2068
  ) {
2061
2069
  return this._deltaManager.attachOpHandler(
@@ -2098,10 +2106,7 @@ export class Container
2098
2106
  // This info is of most interesting while Catching Up.
2099
2107
  checkpointSequenceNumber = this.deltaManager.lastKnownSeqNumber;
2100
2108
  // Need to check that we have already loaded and fetched the snapshot.
2101
- if (
2102
- this.deltaManager.hasCheckpointSequenceNumber &&
2103
- this._lifecycleState === "loaded"
2104
- ) {
2109
+ if (this.deltaManager.hasCheckpointSequenceNumber && this.loaded) {
2105
2110
  opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
2106
2111
  }
2107
2112
  }
@@ -2117,7 +2122,7 @@ export class Container
2117
2122
  reason: reason?.text,
2118
2123
  connectionInitiationReason,
2119
2124
  pendingClientId: this.connectionStateHandler.pendingClientId,
2120
- clientId: this.clientId,
2125
+ clientId: this.connectionStateHandler.clientId,
2121
2126
  autoReconnect,
2122
2127
  opsBehind,
2123
2128
  online: OnlineStatus[isOnline()],
@@ -2138,27 +2143,35 @@ export class Container
2138
2143
  }
2139
2144
  }
2140
2145
 
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.
2146
+ private propagateConnectionState(disconnectedReason?: IConnectionStateChangeReason) {
2147
+ const connected = this.connectionState === ConnectionState.Connected;
2148
+
2149
+ if (connected) {
2150
+ const clientId = this.connectionStateHandler.clientId;
2151
+ assert(clientId !== undefined, 0x96e /* there has to be clientId */);
2152
+ this.protocolHandler.audience.setCurrentClientId(clientId);
2153
+ }
2154
+
2155
+ // We communicate only transitions to Connected & Disconnected states, skipping all other states.
2147
2156
  // This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
2148
2157
  if (
2149
- !initialTransition &&
2150
2158
  this.connectionState !== ConnectionState.Connected &&
2151
2159
  this.connectionState !== ConnectionState.Disconnected
2152
2160
  ) {
2153
2161
  return;
2154
2162
  }
2155
- const state = this.connectionState === ConnectionState.Connected;
2156
2163
 
2157
2164
  // Both protocol and context should not be undefined if we got so far.
2158
2165
 
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);
2166
+ this.setContextConnectedState(connected, this.readOnlyInfo.readonly ?? false);
2167
+ this.protocolHandler.setConnectionState(connected, this.clientId);
2168
+ raiseConnectedEvent(
2169
+ this.mc.logger,
2170
+ this,
2171
+ connected,
2172
+ this.clientId,
2173
+ disconnectedReason?.text,
2174
+ );
2162
2175
  }
2163
2176
 
2164
2177
  // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
@@ -2396,29 +2409,37 @@ export class Container
2396
2409
  /**
2397
2410
  * Set the connected state of the ContainerContext
2398
2411
  * This controls the "connected" state of the ContainerRuntime as well
2399
- * @param state - Is the container currently connected?
2412
+ * @param connected - Is the container currently connected?
2400
2413
  * @param readonly - Is the container in readonly mode?
2401
2414
  */
2402
- private setContextConnectedState(state: boolean, readonly: boolean): void {
2403
- if (this._runtime?.disposed === false) {
2415
+ private setContextConnectedState(connected: boolean, readonly: boolean): void {
2416
+ if (this._runtime?.disposed === false && this.loaded) {
2404
2417
  /**
2405
2418
  * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
2406
2419
  * ops getting through to the DeltaManager.
2407
2420
  * The ContainerRuntime's "connected" state simply means it is ok to send ops
2408
2421
  * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
2409
2422
  */
2410
- this.runtime.setConnectionState(state && !readonly, this.clientId);
2423
+ this.runtime.setConnectionState(connected && !readonly, this.clientId);
2411
2424
  }
2412
2425
  }
2413
2426
 
2414
2427
  private handleDeltaConnectionArg(
2415
- connectionArgs: IConnectionArgs,
2416
2428
  deltaConnectionArg?: "none" | "delayed",
2417
- canConnect: boolean = true,
2429
+ connectionArgs?: IConnectionArgs,
2418
2430
  ) {
2431
+ // This ensures that we allow transitions to "connected" state only after container has been fully loaded
2432
+ // and we propagate such events to container runtime. All events prior to being loaded are ignored.
2433
+ // This means if we get here in non-loaded state, we might not deliver proper events to container runtime,
2434
+ // and runtime implementation may miss such events.
2435
+ assert(
2436
+ this.loaded,
2437
+ 0x96f /* has to be called after container transitions to loaded state */,
2438
+ );
2439
+
2419
2440
  switch (deltaConnectionArg) {
2420
2441
  case undefined:
2421
- if (canConnect) {
2442
+ if (connectionArgs) {
2422
2443
  // connect to delta stream now since we did not before
2423
2444
  this.connectToDeltaStream(connectionArgs);
2424
2445
  }