@fluidframework/container-loader 0.59.4001 → 1.1.0-75972

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 (135) hide show
  1. package/.eslintrc.js +1 -1
  2. package/README.md +1 -1
  3. package/dist/connectionManager.d.ts.map +1 -1
  4. package/dist/connectionManager.js +3 -1
  5. package/dist/connectionManager.js.map +1 -1
  6. package/dist/connectionState.d.ts +15 -3
  7. package/dist/connectionState.d.ts.map +1 -1
  8. package/dist/connectionState.js +15 -3
  9. package/dist/connectionState.js.map +1 -1
  10. package/dist/connectionStateHandler.d.ts +47 -11
  11. package/dist/connectionStateHandler.d.ts.map +1 -1
  12. package/dist/connectionStateHandler.js +108 -38
  13. package/dist/connectionStateHandler.js.map +1 -1
  14. package/dist/container.d.ts +20 -28
  15. package/dist/container.d.ts.map +1 -1
  16. package/dist/container.js +97 -153
  17. package/dist/container.js.map +1 -1
  18. package/dist/containerContext.d.ts +6 -4
  19. package/dist/containerContext.d.ts.map +1 -1
  20. package/dist/containerContext.js +8 -7
  21. package/dist/containerContext.js.map +1 -1
  22. package/dist/containerStorageAdapter.d.ts +4 -5
  23. package/dist/containerStorageAdapter.d.ts.map +1 -1
  24. package/dist/containerStorageAdapter.js +4 -7
  25. package/dist/containerStorageAdapter.js.map +1 -1
  26. package/dist/contracts.d.ts +1 -1
  27. package/dist/contracts.js +1 -1
  28. package/dist/contracts.js.map +1 -1
  29. package/dist/deltaManager.d.ts.map +1 -1
  30. package/dist/deltaManager.js +9 -1
  31. package/dist/deltaManager.js.map +1 -1
  32. package/dist/deltaManagerProxy.d.ts +0 -1
  33. package/dist/deltaManagerProxy.d.ts.map +1 -1
  34. package/dist/deltaManagerProxy.js +0 -3
  35. package/dist/deltaManagerProxy.js.map +1 -1
  36. package/dist/deltaQueue.d.ts.map +1 -1
  37. package/dist/deltaQueue.js +8 -3
  38. package/dist/deltaQueue.js.map +1 -1
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/loader.d.ts +1 -13
  43. package/dist/loader.d.ts.map +1 -1
  44. package/dist/loader.js +2 -3
  45. package/dist/loader.js.map +1 -1
  46. package/dist/packageVersion.d.ts +1 -1
  47. package/dist/packageVersion.d.ts.map +1 -1
  48. package/dist/packageVersion.js +1 -1
  49. package/dist/packageVersion.js.map +1 -1
  50. package/dist/protocolTreeDocumentStorageService.d.ts +2 -3
  51. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  52. package/dist/protocolTreeDocumentStorageService.js +0 -1
  53. package/dist/protocolTreeDocumentStorageService.js.map +1 -1
  54. package/dist/retriableDocumentStorageService.d.ts +3 -4
  55. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  56. package/dist/retriableDocumentStorageService.js +4 -7
  57. package/dist/retriableDocumentStorageService.js.map +1 -1
  58. package/dist/utils.d.ts.map +1 -1
  59. package/dist/utils.js +3 -2
  60. package/dist/utils.js.map +1 -1
  61. package/lib/connectionManager.d.ts.map +1 -1
  62. package/lib/connectionManager.js +4 -2
  63. package/lib/connectionManager.js.map +1 -1
  64. package/lib/connectionState.d.ts +15 -3
  65. package/lib/connectionState.d.ts.map +1 -1
  66. package/lib/connectionState.js +15 -3
  67. package/lib/connectionState.js.map +1 -1
  68. package/lib/connectionStateHandler.d.ts +47 -11
  69. package/lib/connectionStateHandler.d.ts.map +1 -1
  70. package/lib/connectionStateHandler.js +108 -38
  71. package/lib/connectionStateHandler.js.map +1 -1
  72. package/lib/container.d.ts +20 -28
  73. package/lib/container.d.ts.map +1 -1
  74. package/lib/container.js +98 -154
  75. package/lib/container.js.map +1 -1
  76. package/lib/containerContext.d.ts +6 -4
  77. package/lib/containerContext.d.ts.map +1 -1
  78. package/lib/containerContext.js +8 -7
  79. package/lib/containerContext.js.map +1 -1
  80. package/lib/containerStorageAdapter.d.ts +4 -5
  81. package/lib/containerStorageAdapter.d.ts.map +1 -1
  82. package/lib/containerStorageAdapter.js +4 -7
  83. package/lib/containerStorageAdapter.js.map +1 -1
  84. package/lib/contracts.d.ts +1 -1
  85. package/lib/contracts.js +1 -1
  86. package/lib/contracts.js.map +1 -1
  87. package/lib/deltaManager.d.ts.map +1 -1
  88. package/lib/deltaManager.js +9 -1
  89. package/lib/deltaManager.js.map +1 -1
  90. package/lib/deltaManagerProxy.d.ts +0 -1
  91. package/lib/deltaManagerProxy.d.ts.map +1 -1
  92. package/lib/deltaManagerProxy.js +0 -3
  93. package/lib/deltaManagerProxy.js.map +1 -1
  94. package/lib/deltaQueue.d.ts.map +1 -1
  95. package/lib/deltaQueue.js +8 -3
  96. package/lib/deltaQueue.js.map +1 -1
  97. package/lib/index.d.ts +1 -1
  98. package/lib/index.d.ts.map +1 -1
  99. package/lib/index.js.map +1 -1
  100. package/lib/loader.d.ts +1 -13
  101. package/lib/loader.d.ts.map +1 -1
  102. package/lib/loader.js +2 -3
  103. package/lib/loader.js.map +1 -1
  104. package/lib/packageVersion.d.ts +1 -1
  105. package/lib/packageVersion.d.ts.map +1 -1
  106. package/lib/packageVersion.js +1 -1
  107. package/lib/packageVersion.js.map +1 -1
  108. package/lib/protocolTreeDocumentStorageService.d.ts +2 -3
  109. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  110. package/lib/protocolTreeDocumentStorageService.js +0 -1
  111. package/lib/protocolTreeDocumentStorageService.js.map +1 -1
  112. package/lib/retriableDocumentStorageService.d.ts +3 -4
  113. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  114. package/lib/retriableDocumentStorageService.js +4 -7
  115. package/lib/retriableDocumentStorageService.js.map +1 -1
  116. package/lib/utils.d.ts.map +1 -1
  117. package/lib/utils.js +3 -2
  118. package/lib/utils.js.map +1 -1
  119. package/package.json +14 -27
  120. package/src/connectionManager.ts +4 -6
  121. package/src/connectionState.ts +20 -6
  122. package/src/connectionStateHandler.ts +136 -56
  123. package/src/container.ts +139 -185
  124. package/src/containerContext.ts +10 -10
  125. package/src/containerStorageAdapter.ts +5 -10
  126. package/src/contracts.ts +1 -1
  127. package/src/deltaManager.ts +8 -2
  128. package/src/deltaManagerProxy.ts +0 -4
  129. package/src/deltaQueue.ts +7 -3
  130. package/src/index.ts +1 -0
  131. package/src/loader.ts +4 -21
  132. package/src/packageVersion.ts +1 -1
  133. package/src/protocolTreeDocumentStorageService.ts +0 -1
  134. package/src/retriableDocumentStorageService.ts +4 -12
  135. package/src/utils.ts +3 -2
package/src/container.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import merge from "lodash/merge";
8
8
  import { v4 as uuid } from "uuid";
9
9
  import {
10
- IDisposable,
10
+ IDisposable, ITelemetryProperties,
11
11
  } from "@fluidframework/common-definitions";
12
12
  import { assert, performance, unreachableCase } from "@fluidframework/common-utils";
13
13
  import {
@@ -25,7 +25,6 @@ import {
25
25
  ContainerWarning,
26
26
  AttachState,
27
27
  IThrottlingWarning,
28
- IPendingLocalState,
29
28
  ReadOnlyInfo,
30
29
  IContainerLoadMode,
31
30
  IFluidCodeDetails,
@@ -54,7 +53,8 @@ import {
54
53
  } from "@fluidframework/driver-utils";
55
54
  import {
56
55
  isSystemMessage,
57
- ProtocolOpHandler,
56
+ IProtocolHandler,
57
+ ProtocolOpHandlerWithClientValidation,
58
58
  } from "@fluidframework/protocol-base";
59
59
  import {
60
60
  IClient,
@@ -64,6 +64,7 @@ import {
64
64
  IDocumentAttributes,
65
65
  IDocumentMessage,
66
66
  IProcessMessageResult,
67
+ IProtocolState,
67
68
  IQuorumClients,
68
69
  IQuorumProposals,
69
70
  ISequencedClient,
@@ -98,7 +99,7 @@ import { DeltaManager, IConnectionArgs } from "./deltaManager";
98
99
  import { DeltaManagerProxy } from "./deltaManagerProxy";
99
100
  import { ILoaderOptions, Loader, RelativeLoader } from "./loader";
100
101
  import { pkgVersion } from "./packageVersion";
101
- import { ConnectionStateHandler, ILocalSequencedClient } from "./connectionStateHandler";
102
+ import { ConnectionStateHandler } from "./connectionStateHandler";
102
103
  import { RetriableDocumentStorageService } from "./retriableDocumentStorageService";
103
104
  import { ProtocolTreeStorageService } from "./protocolTreeDocumentStorageService";
104
105
  import { BlobOnlyStorage, ContainerStorageAdapter } from "./containerStorageAdapter";
@@ -140,6 +141,10 @@ export interface IContainerConfig {
140
141
  * Client details provided in the override will be merged over the default client.
141
142
  */
142
143
  clientDetailsOverride?: IClientDetails;
144
+ /**
145
+ * Serialized state from a previous instance of this container
146
+ */
147
+ serializedContainerState?: IPendingContainerState;
143
148
  }
144
149
 
145
150
  /**
@@ -147,7 +152,7 @@ export interface IContainerConfig {
147
152
  * Useful when resolving URIs and hitting 404, due to container being loaded from (stale) snapshot and not being
148
153
  * up to date. Host may chose to wait in such case and retry resolving URI.
149
154
  * Warning: Will wait infinitely for connection to establish if there is no connection.
150
- * May result in deadlock if Container.setAutoReconnect(false) is called and never switched back to auto-reconnect.
155
+ * May result in deadlock if Container.disconnect() is called and never followed by a call to Container.connect().
151
156
  * @returns true: container is up to date, it processed all the ops that were know at the time of first connection
152
157
  * false: storage does not provide indication of how far the client is. Container processed
153
158
  * all the ops known to it, but it maybe still behind.
@@ -174,7 +179,8 @@ export async function waitContainerToCatchUp(container: IContainer) {
174
179
  container.on("closed", closedCallback);
175
180
 
176
181
  const waitForOps = () => {
177
- assert(container.connectionState !== ConnectionState.Disconnected,
182
+ assert(container.connectionState === ConnectionState.CatchingUp
183
+ || container.connectionState === ConnectionState.Connected,
178
184
  0x0cd /* "Container disconnected while waiting for ops!" */);
179
185
  const hasCheckpointSequenceNumber = deltaManager.hasCheckpointSequenceNumber;
180
186
 
@@ -211,9 +217,7 @@ export async function waitContainerToCatchUp(container: IContainer) {
211
217
  };
212
218
  container.on(connectedEventName, callback);
213
219
 
214
- // TODO: Remove null check after next release #8523
215
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
216
- container.resume!();
220
+ container.connect();
217
221
  });
218
222
  }
219
223
 
@@ -221,6 +225,18 @@ const getCodeProposal =
221
225
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
222
226
  (quorum: IQuorumProposals) => quorum.get("code") ?? quorum.get("code2");
223
227
 
228
+ /**
229
+ * State saved by a container at close time, to be used to load a new instance
230
+ * of the container to the same state
231
+ */
232
+ export interface IPendingContainerState {
233
+ pendingRuntimeState: unknown;
234
+ url: string;
235
+ protocol: IProtocolState;
236
+ term: number;
237
+ clientId?: string;
238
+ }
239
+
224
240
  const summarizerClientType = "summarizer";
225
241
 
226
242
  export class Container extends EventEmitterWithErrorHandling<IContainerEvents> implements IContainer {
@@ -232,7 +248,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
232
248
  public static async load(
233
249
  loader: Loader,
234
250
  loadOptions: IContainerLoadOptions,
235
- pendingLocalState?: unknown,
251
+ pendingLocalState?: IPendingContainerState,
236
252
  ): Promise<Container> {
237
253
  const container = new Container(
238
254
  loader,
@@ -240,21 +256,21 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
240
256
  clientDetailsOverride: loadOptions.clientDetailsOverride,
241
257
  resolvedUrl: loadOptions.resolvedUrl,
242
258
  canReconnect: loadOptions.canReconnect,
259
+ serializedContainerState: pendingLocalState,
243
260
  });
244
261
 
245
262
  return PerformanceEvent.timedExecAsync(
246
263
  container.mc.logger,
247
264
  { eventName: "Load" },
248
265
  async (event) => new Promise<Container>((resolve, reject) => {
249
- container._lifecycleState = "loading";
250
266
  const version = loadOptions.version;
251
267
 
252
- // always load unpaused with pending ops!
253
- // It is also default mode in general.
254
268
  const defaultMode: IContainerLoadMode = { opsBeforeReturn: "cached" };
255
- assert(pendingLocalState === undefined || loadOptions.loadMode === undefined,
256
- 0x1e1 /* "pending state requires immediate connection!" */);
257
- const mode: IContainerLoadMode = loadOptions.loadMode ?? defaultMode;
269
+ // if we have pendingLocalState, anything we cached is not useful and we shouldn't wait for connection
270
+ // to return container, so ignore this value and use undefined for opsBeforeReturn
271
+ const mode: IContainerLoadMode = pendingLocalState
272
+ ? { ...(loadOptions.loadMode ?? defaultMode), opsBeforeReturn: undefined }
273
+ : loadOptions.loadMode ?? defaultMode;
258
274
 
259
275
  const onClosed = (err?: ICriticalContainerError) => {
260
276
  // pre-0.58 error message: containerClosedWithoutErrorDuringLoad
@@ -298,7 +314,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
298
314
  container.mc.logger,
299
315
  { eventName: "CreateDetached" },
300
316
  async (_event) => {
301
- container._lifecycleState = "loading";
302
317
  await container.createDetached(codeDetails);
303
318
  return container;
304
319
  },
@@ -321,7 +336,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
321
336
  { eventName: "RehydrateDetachedFromSnapshot" },
322
337
  async (_event) => {
323
338
  const deserializedSummary = JSON.parse(snapshot) as ISummaryTree;
324
- container._lifecycleState = "loading";
325
339
  await container.rehydrateDetachedFromSnapshot(deserializedSummary);
326
340
  return container;
327
341
  },
@@ -336,19 +350,14 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
336
350
 
337
351
  private readonly mc: MonitoringContext;
338
352
 
339
- private _lifecycleState: "created" | "loading" | "loaded" | "closing" | "closed" = "created";
340
-
341
- private get loaded(): boolean {
342
- return (this._lifecycleState !== "created" && this._lifecycleState !== "loading");
343
- }
344
-
345
- private set loaded(t: boolean) {
346
- assert(t, 0x27d /* "Setting loaded state to false is not supported" */);
347
- assert(this._lifecycleState !== "created", 0x27e /* "Must go through loading state before loaded" */);
353
+ private _lifecycleState: "loading" | "loaded" | "closing" | "closed" = "loading";
348
354
 
355
+ private setLoaded() {
349
356
  // It's conceivable the container could be closed when this is called
350
357
  // Only transition states if currently loading
351
358
  if (this._lifecycleState === "loading") {
359
+ // Propagate current connection state through the system.
360
+ this.propagateConnectionState();
352
361
  this._lifecycleState = "loaded";
353
362
  }
354
363
  }
@@ -384,7 +393,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
384
393
  }
385
394
  return this._context;
386
395
  }
387
- private _protocolHandler: ProtocolOpHandler | undefined;
396
+ private _protocolHandler: IProtocolHandler | undefined;
388
397
  private get protocolHandler() {
389
398
  if (this._protocolHandler === undefined) {
390
399
  throw new Error("Attempted to access protocolHandler before it was defined");
@@ -592,21 +601,24 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
592
601
  this.logConnectionStateChangeTelemetry(value, oldState, reason),
593
602
  shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
594
603
  maxClientLeaveWaitTime: this.loader.services.options.maxClientLeaveWaitTime,
595
- logConnectionIssue: (eventName: string) => {
604
+ logConnectionIssue: (eventName: string, details?: ITelemetryProperties) => {
596
605
  // We get here when socket does not receive any ops on "write" connection, including
597
606
  // its own join op. Attempt recovery option.
598
607
  this._deltaManager.logConnectionIssue({
599
608
  eventName,
600
- duration: performance.now() - this.connectionTransitionTimes[ConnectionState.Connecting],
609
+ duration: performance.now() - this.connectionTransitionTimes[ConnectionState.CatchingUp],
610
+ ...(details === undefined ? {} : { details: JSON.stringify(details) }),
601
611
  });
602
612
  },
603
613
  connectionStateChanged: () => {
604
- if (this.loaded) {
614
+ // Fire events only if container is fully loaded and not closed
615
+ if (this._lifecycleState === "loaded") {
605
616
  this.propagateConnectionState();
606
617
  }
607
618
  },
608
619
  },
609
620
  this.mc.logger,
621
+ config.serializedContainerState?.clientId,
610
622
  );
611
623
 
612
624
  this.on(savedContainerEvent, () => {
@@ -691,16 +703,34 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
691
703
  }
692
704
 
693
705
  public close(error?: ICriticalContainerError) {
694
- if (this.closed) {
695
- return;
696
- }
706
+ // 1. Ensure that close sequence is exactly the same no matter if it's initiated by host or by DeltaManager
707
+ // 2. We need to ensure that we deliver disconnect event to runtime properly. See connectionStateChanged
708
+ // handler. We only deliver events if container fully loaded. Transitioning from "loading" ->
709
+ // "closing" will lose that info (can also solve by tracking extra state).
710
+ this._deltaManager.close(error);
711
+ assert(this.connectionState === ConnectionState.Disconnected,
712
+ 0x0cf /* "disconnect event was not raised!" */);
697
713
 
698
- try {
699
- this._lifecycleState = "closing";
714
+ assert(this._lifecycleState === "closed", 0x314 /* Container properly closed */);
715
+ }
716
+
717
+ private closeCore(error?: ICriticalContainerError) {
718
+ assert(!this.closed, 0x315 /* re-entrancy */);
700
719
 
720
+ try {
701
721
  // Ensure that we raise all key events even if one of these throws
702
722
  try {
703
- this._deltaManager.close(error);
723
+ // Raise event first, to ensure we capture _lifecycleState before transition.
724
+ // This gives us a chance to know what errors happened on open vs. on fully loaded container.
725
+ this.mc.logger.sendTelemetryEvent(
726
+ {
727
+ eventName: "ContainerClose",
728
+ category: error === undefined ? "generic" : "error",
729
+ },
730
+ error,
731
+ );
732
+
733
+ this._lifecycleState = "closing";
704
734
 
705
735
  this._protocolHandler?.close();
706
736
 
@@ -708,9 +738,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
708
738
 
709
739
  this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
710
740
 
711
- assert(this.connectionState === ConnectionState.Disconnected,
712
- 0x0cf /* "disconnect event was not raised!" */);
713
-
714
741
  this._storageService?.dispose();
715
742
 
716
743
  // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
@@ -721,14 +748,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
721
748
  this.mc.logger.sendErrorEvent({ eventName: "ContainerCloseException" }, exception);
722
749
  }
723
750
 
724
- this.mc.logger.sendTelemetryEvent(
725
- {
726
- eventName: "ContainerClose",
727
- category: error === undefined ? "generic" : "error",
728
- },
729
- error,
730
- );
731
-
732
751
  this.emit("closed", error);
733
752
 
734
753
  this.removeAllListeners();
@@ -748,9 +767,15 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
748
767
  assert(this.attachState === AttachState.Attached, 0x0d1 /* "Container should be attached before close" */);
749
768
  assert(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid",
750
769
  0x0d2 /* "resolved url should be valid Fluid url" */);
751
- const pendingState: IPendingLocalState = {
770
+ assert(!!this._protocolHandler, 0x2e3 /* "Must have a valid protocol handler instance" */);
771
+ assert(this._protocolHandler.attributes.term !== undefined,
772
+ 0x30b /* Must have a valid protocol handler instance */);
773
+ const pendingState: IPendingContainerState = {
752
774
  pendingRuntimeState: this.context.getPendingLocalState(),
753
775
  url: this.resolvedUrl.url,
776
+ protocol: this.protocolHandler.getProtocolState(),
777
+ term: this._protocolHandler.attributes.term,
778
+ clientId: this.clientId,
754
779
  };
755
780
 
756
781
  this.close();
@@ -808,7 +833,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
808
833
  // starting to attach the container to storage.
809
834
  // Also, this should only be fired in detached container.
810
835
  this._attachState = AttachState.Attaching;
811
- this.context.notifyAttaching();
836
+ this.context.notifyAttaching(getSnapshotTreeFromSerializedContainer(summary));
812
837
  }
813
838
 
814
839
  // Actually go and create the resolved document
@@ -860,7 +885,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
860
885
  summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
861
886
 
862
887
  this._attachState = AttachState.Attaching;
863
- this.context.notifyAttaching();
888
+ this.context.notifyAttaching(getSnapshotTreeFromSerializedContainer(summary));
864
889
 
865
890
  await this.storageService.uploadSummaryWithContext(summary, {
866
891
  referenceSequenceNumber: 0,
@@ -900,30 +925,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
900
925
  );
901
926
  }
902
927
 
903
- /**
904
- * Dictates whether or not the current container will automatically attempt to reconnect to the delta stream
905
- * after receiving a disconnect event
906
- * @param reconnect - Boolean indicating if reconnect should automatically occur
907
- * @deprecated - 0.58, This API will be removed in 1.0
908
- * Use `connect()` and `disconnect()` instead of `setAutoReconnect(true)` and `setAutoReconnect(false)` respectively
909
- * See https://github.com/microsoft/FluidFramework/issues/9167 for context
910
- */
911
- public setAutoReconnect(reconnect: boolean) {
912
- if (this.closed) {
913
- throw new Error("Attempting to setAutoReconnect() a closed Container");
914
- }
915
-
916
- const mode = reconnect ? ReconnectMode.Enabled : ReconnectMode.Disabled;
917
- this.setAutoReconnectInternal(mode);
918
-
919
- // If container state is not attached and resumed, then don't connect to delta stream. Also don't set the
920
- // manual reconnection flag to true as we haven't made the initial connection yet.
921
- if (reconnect && this._attachState === AttachState.Attached && this.resumedOpProcessingAfterLoad) {
922
- // Ensure connection to web socket
923
- this.connectToDeltaStream({ reason: "autoReconnect" });
924
- }
925
- }
926
-
927
928
  private setAutoReconnectInternal(mode: ReconnectMode) {
928
929
  const currentMode = this._deltaManager.connectionManager.reconnectMode;
929
930
 
@@ -987,23 +988,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
987
988
  this.setAutoReconnectInternal(mode);
988
989
  }
989
990
 
990
- /**
991
- * Have the container attempt to resume processing ops
992
- * @deprecated - 0.58, This API will be removed in 1.0
993
- * Use `connect()` instead
994
- * See https://github.com/microsoft/FluidFramework/issues/9167 for context
995
- */
996
- public resume() {
997
- if (!this.closed) {
998
- // Note: no need to fetch ops as we do it preemptively as part of DeltaManager.attachOpHandler().
999
- // If there is gap, we will learn about it once connected, but the gap should be small (if any),
1000
- // assuming that resume() is called quickly after initial container boot.
1001
- this.resumeInternal({ reason: "DocumentOpenResume", fetchOpsFromStorage: false });
1002
- }
1003
- }
1004
-
1005
991
  private resumeInternal(args: IConnectionArgs) {
1006
- assert(!this.closed, 0x0d9 /* "Attempting to setAutoReconnect() a closed DeltaManager" */);
992
+ assert(!this.closed, 0x0d9 /* "Attempting to connect() a closed DeltaManager" */);
1007
993
 
1008
994
  // Resume processing ops
1009
995
  if (!this.resumedOpProcessingAfterLoad) {
@@ -1095,7 +1081,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1095
1081
  private async load(
1096
1082
  specifiedVersion: string | undefined,
1097
1083
  loadMode: IContainerLoadMode,
1098
- pendingLocalState?: unknown,
1084
+ pendingLocalState?: IPendingContainerState,
1099
1085
  ) {
1100
1086
  if (this._resolvedUrl === undefined) {
1101
1087
  throw new Error("Attempting to load without a resolved url");
@@ -1123,14 +1109,28 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1123
1109
  this.connectToDeltaStream(connectionArgs);
1124
1110
  }
1125
1111
 
1126
- await this.connectStorageService();
1112
+ if (!pendingLocalState) {
1113
+ await this.connectStorageService();
1114
+ } else {
1115
+ // if we have pendingLocalState we can load without storage; don't wait for connection
1116
+ this.connectStorageService().catch((error) => this.close(error));
1117
+ }
1118
+
1127
1119
  this._attachState = AttachState.Attached;
1128
1120
 
1129
1121
  // Fetch specified snapshot.
1130
- const { snapshot, versionId } = await this.fetchSnapshotTree(specifiedVersion);
1131
- assert(snapshot !== undefined, 0x237 /* "Snapshot should exist" */);
1122
+ const { snapshot, versionId } = pendingLocalState === undefined
1123
+ ? await this.fetchSnapshotTree(specifiedVersion)
1124
+ : { snapshot: undefined, versionId: undefined };
1125
+ assert(snapshot !== undefined || pendingLocalState !== undefined, 0x237 /* "Snapshot should exist" */);
1132
1126
 
1133
- const attributes = await this.getDocumentAttributes(this.storageService, snapshot);
1127
+ const attributes: IDocumentAttributes = pendingLocalState === undefined
1128
+ ? await this.getDocumentAttributes(this.storageService, snapshot)
1129
+ : {
1130
+ sequenceNumber: pendingLocalState.protocol.sequenceNumber,
1131
+ minimumSequenceNumber: pendingLocalState.protocol.minimumSequenceNumber,
1132
+ term: pendingLocalState.term,
1133
+ };
1134
1134
 
1135
1135
  let opsBeforeReturnP: Promise<void> | undefined;
1136
1136
 
@@ -1154,22 +1154,25 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1154
1154
 
1155
1155
  // ...load in the existing quorum
1156
1156
  // Initialize the protocol handler
1157
- this._protocolHandler =
1158
- await this.initializeProtocolStateFromSnapshot(attributes, this.storageService, snapshot);
1157
+ this._protocolHandler = pendingLocalState === undefined
1158
+ ? await this.initializeProtocolStateFromSnapshot(attributes, this.storageService, snapshot)
1159
+ : await this.initializeProtocolState(
1160
+ attributes,
1161
+ pendingLocalState.protocol.members,
1162
+ pendingLocalState.protocol.proposals,
1163
+ pendingLocalState.protocol.values,
1164
+ );
1159
1165
 
1160
1166
  const codeDetails = this.getCodeDetailsFromQuorum();
1161
1167
  await this.instantiateContext(
1162
1168
  true, // existing
1163
1169
  codeDetails,
1164
1170
  snapshot,
1165
- pendingLocalState,
1171
+ pendingLocalState?.pendingRuntimeState,
1166
1172
  );
1167
1173
 
1168
- // Propagate current connection state through the system.
1169
- this.propagateConnectionState();
1170
-
1171
1174
  // Internal context is fully loaded at this point
1172
- this.loaded = true;
1175
+ this.setLoaded();
1173
1176
 
1174
1177
  // We might have hit some failure that did not manifest itself in exception in this flow,
1175
1178
  // do not start op processing in such case - static version of Container.load() will handle it correctly.
@@ -1186,7 +1189,10 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1186
1189
 
1187
1190
  switch (loadMode.deltaConnection) {
1188
1191
  case undefined:
1189
- this.resume();
1192
+ // Note: no need to fetch ops as we do it preemptively as part of DeltaManager.attachOpHandler().
1193
+ // If there is gap, we will learn about it once connected, but the gap should be small (if any),
1194
+ // assuming that resumeInternal() is called quickly after initial container boot.
1195
+ this.resumeInternal({ reason: "DocumentLoad", fetchOpsFromStorage: false });
1190
1196
  break;
1191
1197
  case "delayed":
1192
1198
  this.resumedOpProcessingAfterLoad = true;
@@ -1238,9 +1244,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1238
1244
  false, // existing
1239
1245
  );
1240
1246
 
1241
- this.propagateConnectionState();
1242
-
1243
- this.loaded = true;
1247
+ this.setLoaded();
1244
1248
  }
1245
1249
 
1246
1250
  private async rehydrateDetachedFromSnapshot(detachedContainerSnapshot: ISummaryTree) {
@@ -1275,9 +1279,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1275
1279
  snapshotTree,
1276
1280
  );
1277
1281
 
1278
- this.loaded = true;
1279
-
1280
- this.propagateConnectionState();
1282
+ this.setLoaded();
1281
1283
  }
1282
1284
 
1283
1285
  private async connectStorageService(): Promise<void> {
@@ -1333,7 +1335,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1333
1335
  attributes: IDocumentAttributes,
1334
1336
  storage: IDocumentStorageService,
1335
1337
  snapshot: ISnapshotTree | undefined,
1336
- ): Promise<ProtocolOpHandler> {
1338
+ ): Promise<IProtocolHandler> {
1337
1339
  let members: [string, ISequencedClient][] = [];
1338
1340
  let proposals: [number, ISequencedProposal, string[]][] = [];
1339
1341
  let values: [string, any][] = [];
@@ -1361,8 +1363,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1361
1363
  members: [string, ISequencedClient][],
1362
1364
  proposals: [number, ISequencedProposal, string[]][],
1363
1365
  values: [string, any][],
1364
- ): Promise<ProtocolOpHandler> {
1365
- const protocol = new ProtocolOpHandler(
1366
+ ): Promise<IProtocolHandler> {
1367
+ const protocol = new ProtocolOpHandlerWithClientValidation(
1366
1368
  attributes.minimumSequenceNumber,
1367
1369
  attributes.sequenceNumber,
1368
1370
  attributes.term,
@@ -1379,13 +1381,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1379
1381
  });
1380
1382
 
1381
1383
  // Track membership changes and update connection state accordingly
1382
- protocol.quorum.on("addMember", (clientId, details) => {
1383
- this.connectionStateHandler.receivedAddMemberEvent(clientId);
1384
- });
1385
-
1386
- protocol.quorum.on("removeMember", (clientId) => {
1387
- this.connectionStateHandler.receivedRemoveMemberEvent(clientId);
1388
- });
1384
+ this.connectionStateHandler.initProtocol(protocol);
1389
1385
 
1390
1386
  protocol.quorum.on("addProposal", (proposal: ISequencedProposal) => {
1391
1387
  if (proposal.key === "code" || proposal.key === "code2") {
@@ -1413,19 +1409,11 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1413
1409
  }
1414
1410
 
1415
1411
  private captureProtocolSummary(): ISummaryTree {
1416
- const quorumSnapshot = this.protocolHandler.quorum.snapshot();
1417
-
1418
- // Save attributes for the document
1419
- const documentAttributes: IDocumentAttributes = {
1420
- minimumSequenceNumber: this.protocolHandler.minimumSequenceNumber,
1421
- sequenceNumber: this.protocolHandler.sequenceNumber,
1422
- term: this.protocolHandler.term,
1423
- };
1424
-
1412
+ const quorumSnapshot = this.protocolHandler.snapshot();
1425
1413
  const summary: ISummaryTree = {
1426
1414
  tree: {
1427
1415
  attributes: {
1428
- content: JSON.stringify(documentAttributes),
1416
+ content: JSON.stringify(this.protocolHandler.attributes),
1429
1417
  type: SummaryType.Blob,
1430
1418
  },
1431
1419
  quorumMembers: {
@@ -1540,7 +1528,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1540
1528
  });
1541
1529
 
1542
1530
  deltaManager.on("closed", (error?: ICriticalContainerError) => {
1543
- this.close(error);
1531
+ this.closeCore(error);
1544
1532
  });
1545
1533
 
1546
1534
  return deltaManager;
@@ -1611,6 +1599,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1611
1599
  online: OnlineStatus[isOnline()],
1612
1600
  lastVisible: this.lastVisible !== undefined ? performance.now() - this.lastVisible : undefined,
1613
1601
  checkpointSequenceNumber,
1602
+ quorumSize: this._protocolHandler?.quorum.getMembers().size,
1614
1603
  ...this._deltaManager.connectionProps,
1615
1604
  });
1616
1605
 
@@ -1629,11 +1618,13 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1629
1618
  }
1630
1619
 
1631
1620
  const state = this.connectionState === ConnectionState.Connected;
1632
- if (!this.context.disposed) {
1621
+
1622
+ // Both protocol and context should not be undefined if we got so far.
1623
+
1624
+ if (this._context?.disposed === false) {
1633
1625
  this.context.setConnectionState(state, this.clientId);
1634
1626
  }
1635
- assert(this.protocolHandler !== undefined, 0x0dc /* "Protocol handler should be set here" */);
1636
- this.protocolHandler.quorum.setConnectionState(state, this.clientId);
1627
+ this.protocolHandler.setConnectionState(state, this.clientId);
1637
1628
  raiseConnectedEvent(this.mc.logger, this, state, this.clientId);
1638
1629
 
1639
1630
  if (logOpsOnReconnect) {
@@ -1682,36 +1673,22 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1682
1673
  }
1683
1674
 
1684
1675
  private processRemoteMessage(message: ISequencedDocumentMessage): IProcessMessageResult {
1685
- // Check and report if we're getting messages from a clientId that we previously
1686
- // flagged as shouldHaveLeft, or from a client that's not in the quorum but should be
1687
- if (message.clientId != null) {
1688
- let errorMsg: string | undefined;
1689
- const client: ILocalSequencedClient | undefined =
1690
- this.getQuorum().getMember(message.clientId);
1691
- if (client === undefined && message.type !== MessageType.ClientJoin) {
1692
- // pre-0.58 error message: messageClientIdMissingFromQuorum
1693
- errorMsg = "Remote message's clientId is missing from the quorum";
1694
- } else if (client?.shouldHaveLeft === true && message.type !== MessageType.NoOp) {
1695
- // pre-0.58 error message: messageClientIdShouldHaveLeft
1696
- errorMsg = "Remote message's clientId already should have left";
1697
- }
1698
- if (errorMsg !== undefined) {
1699
- const error = new DataCorruptionError(
1700
- errorMsg,
1701
- extractSafePropertiesFromMessage(message));
1702
- this.close(normalizeError(error));
1703
- }
1704
- }
1705
-
1706
1676
  const local = this.clientId === message.clientId;
1707
1677
 
1678
+ // Allow the protocol handler to process the message
1679
+ let result: IProcessMessageResult = { immediateNoOp: false };
1680
+ try {
1681
+ result = this.protocolHandler.processMessage(message, local);
1682
+ } catch (error) {
1683
+ this.close(wrapError(error, (errorMessage) =>
1684
+ new DataCorruptionError(errorMessage, extractSafePropertiesFromMessage(message))));
1685
+ }
1686
+
1708
1687
  // Forward non system messages to the loaded runtime for processing
1709
1688
  if (!isSystemMessage(message)) {
1710
1689
  this.context.process(message, local, undefined);
1711
1690
  }
1712
1691
 
1713
- // Allow the protocol handler to process the message
1714
- const result = this.protocolHandler.processMessage(message, local);
1715
1692
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
1716
1693
  if (this.activeConnection()) {
1717
1694
  if (this.collabWindowTracker === undefined) {
@@ -1719,15 +1696,17 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1719
1696
  // That means that if relay service changes settings, such changes will impact only newly booted
1720
1697
  // clients.
1721
1698
  // All existing will continue to use settings they got earlier.
1722
- const [noopTimeFrequency, noopCountFrequency] = this.getNoopConfig();
1699
+ assert(
1700
+ this.serviceConfiguration !== undefined,
1701
+ 0x2e4 /* "there should be service config for active connection" */);
1723
1702
  this.collabWindowTracker = new CollabWindowTracker(
1724
1703
  (type, contents) => {
1725
1704
  assert(this.activeConnection(),
1726
1705
  0x241 /* "disconnect should result in stopSequenceNumberUpdate() call" */);
1727
1706
  this.submitMessage(type, contents);
1728
1707
  },
1729
- noopTimeFrequency,
1730
- noopCountFrequency,
1708
+ this.serviceConfiguration.noopTimeFrequency,
1709
+ this.serviceConfiguration.noopCountFrequency,
1731
1710
  );
1732
1711
  }
1733
1712
  this.collabWindowTracker.scheduleSequenceNumberUpdate(message, result.immediateNoOp === true);
@@ -1738,29 +1717,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1738
1717
  return result;
1739
1718
  }
1740
1719
 
1741
- /**
1742
- * #260 (ADO)
1743
- * back-compat: noopTimeFrequency & noopCountFrequency properties were added to
1744
- * IClientConfiguration in 0.59.3000. During the integration, we must read the
1745
- * available configuration from the loader options.
1746
- */
1747
- private getNoopConfig(): [number | undefined, number | undefined] {
1748
- assert(
1749
- this.serviceConfiguration !== undefined,
1750
- 0x2e2, /* "there should be service config for active connection" */
1751
- );
1752
-
1753
- if (this.serviceConfiguration.noopTimeFrequency !== undefined ||
1754
- this.serviceConfiguration.noopCountFrequency !== undefined) {
1755
- return [
1756
- this.serviceConfiguration.noopTimeFrequency as number,
1757
- this.serviceConfiguration.noopCountFrequency as number,
1758
- ];
1759
- }
1760
-
1761
- return [this.loader.services.options?.noopTimeFrequency, this.loader.services.options?.noopCountFrequency];
1762
- }
1763
-
1764
1720
  private submitSignal(message: any) {
1765
1721
  this._deltaManager.submitSignal(JSON.stringify(message));
1766
1722
  }
@@ -1807,7 +1763,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1807
1763
  private async instantiateContextDetached(
1808
1764
  existing: boolean,
1809
1765
  snapshot?: ISnapshotTree,
1810
- pendingLocalState?: unknown,
1811
1766
  ) {
1812
1767
  const codeDetails = this.getCodeDetailsFromQuorum();
1813
1768
  if (codeDetails === undefined) {
@@ -1818,7 +1773,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1818
1773
  existing,
1819
1774
  codeDetails,
1820
1775
  snapshot,
1821
- pendingLocalState,
1822
1776
  );
1823
1777
  }
1824
1778