@fluidframework/container-loader 1.2.2 → 2.0.0-internal.1.0.0.81589

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 (105) hide show
  1. package/dist/audience.d.ts +2 -2
  2. package/dist/audience.d.ts.map +1 -1
  3. package/dist/audience.js.map +1 -1
  4. package/dist/catchUpMonitor.d.ts +40 -0
  5. package/dist/catchUpMonitor.d.ts.map +1 -0
  6. package/dist/catchUpMonitor.js +74 -0
  7. package/dist/catchUpMonitor.js.map +1 -0
  8. package/dist/connectionManager.d.ts.map +1 -1
  9. package/dist/connectionManager.js +0 -1
  10. package/dist/connectionManager.js.map +1 -1
  11. package/dist/connectionState.d.ts +0 -5
  12. package/dist/connectionState.d.ts.map +1 -1
  13. package/dist/connectionState.js +0 -5
  14. package/dist/connectionState.js.map +1 -1
  15. package/dist/connectionStateHandler.d.ts +12 -4
  16. package/dist/connectionStateHandler.d.ts.map +1 -1
  17. package/dist/connectionStateHandler.js +47 -15
  18. package/dist/connectionStateHandler.js.map +1 -1
  19. package/dist/container.d.ts +8 -6
  20. package/dist/container.d.ts.map +1 -1
  21. package/dist/container.js +82 -46
  22. package/dist/container.js.map +1 -1
  23. package/dist/deltaManager.d.ts.map +1 -1
  24. package/dist/deltaManager.js +6 -6
  25. package/dist/deltaManager.js.map +1 -1
  26. package/dist/deltaManagerProxy.d.ts +4 -1
  27. package/dist/deltaManagerProxy.d.ts.map +1 -1
  28. package/dist/deltaQueue.d.ts +9 -2
  29. package/dist/deltaQueue.d.ts.map +1 -1
  30. package/dist/deltaQueue.js +31 -26
  31. package/dist/deltaQueue.js.map +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/loader.d.ts +7 -0
  36. package/dist/loader.d.ts.map +1 -1
  37. package/dist/loader.js +4 -3
  38. package/dist/loader.js.map +1 -1
  39. package/dist/packageVersion.d.ts +1 -1
  40. package/dist/packageVersion.d.ts.map +1 -1
  41. package/dist/packageVersion.js +1 -1
  42. package/dist/packageVersion.js.map +1 -1
  43. package/dist/protocol.d.ts +22 -0
  44. package/dist/protocol.d.ts.map +1 -0
  45. package/dist/protocol.js +52 -0
  46. package/dist/protocol.js.map +1 -0
  47. package/lib/audience.d.ts +2 -2
  48. package/lib/audience.d.ts.map +1 -1
  49. package/lib/audience.js.map +1 -1
  50. package/lib/catchUpMonitor.d.ts +40 -0
  51. package/lib/catchUpMonitor.d.ts.map +1 -0
  52. package/lib/catchUpMonitor.js +69 -0
  53. package/lib/catchUpMonitor.js.map +1 -0
  54. package/lib/connectionManager.d.ts.map +1 -1
  55. package/lib/connectionManager.js +0 -1
  56. package/lib/connectionManager.js.map +1 -1
  57. package/lib/connectionState.d.ts +0 -5
  58. package/lib/connectionState.d.ts.map +1 -1
  59. package/lib/connectionState.js +0 -5
  60. package/lib/connectionState.js.map +1 -1
  61. package/lib/connectionStateHandler.d.ts +12 -4
  62. package/lib/connectionStateHandler.d.ts.map +1 -1
  63. package/lib/connectionStateHandler.js +47 -15
  64. package/lib/connectionStateHandler.js.map +1 -1
  65. package/lib/container.d.ts +8 -6
  66. package/lib/container.d.ts.map +1 -1
  67. package/lib/container.js +82 -46
  68. package/lib/container.js.map +1 -1
  69. package/lib/deltaManager.d.ts.map +1 -1
  70. package/lib/deltaManager.js +6 -6
  71. package/lib/deltaManager.js.map +1 -1
  72. package/lib/deltaManagerProxy.d.ts +4 -1
  73. package/lib/deltaManagerProxy.d.ts.map +1 -1
  74. package/lib/deltaQueue.d.ts +9 -2
  75. package/lib/deltaQueue.d.ts.map +1 -1
  76. package/lib/deltaQueue.js +32 -27
  77. package/lib/deltaQueue.js.map +1 -1
  78. package/lib/index.d.ts +1 -0
  79. package/lib/index.d.ts.map +1 -1
  80. package/lib/index.js.map +1 -1
  81. package/lib/loader.d.ts +7 -0
  82. package/lib/loader.d.ts.map +1 -1
  83. package/lib/loader.js +4 -3
  84. package/lib/loader.js.map +1 -1
  85. package/lib/packageVersion.d.ts +1 -1
  86. package/lib/packageVersion.d.ts.map +1 -1
  87. package/lib/packageVersion.js +1 -1
  88. package/lib/packageVersion.js.map +1 -1
  89. package/lib/protocol.d.ts +22 -0
  90. package/lib/protocol.d.ts.map +1 -0
  91. package/lib/protocol.js +48 -0
  92. package/lib/protocol.js.map +1 -0
  93. package/package.json +23 -15
  94. package/src/audience.ts +2 -2
  95. package/src/catchUpMonitor.ts +99 -0
  96. package/src/connectionManager.ts +0 -1
  97. package/src/connectionState.ts +0 -6
  98. package/src/connectionStateHandler.ts +55 -15
  99. package/src/container.ts +115 -63
  100. package/src/deltaManager.ts +6 -4
  101. package/src/deltaQueue.ts +34 -28
  102. package/src/index.ts +4 -0
  103. package/src/loader.ts +13 -2
  104. package/src/packageVersion.ts +1 -1
  105. package/src/protocol.ts +96 -0
@@ -5,11 +5,12 @@
5
5
 
6
6
  import { ITelemetryLogger, ITelemetryProperties } from "@fluidframework/common-definitions";
7
7
  import { assert, Timer } from "@fluidframework/common-utils";
8
- import { IConnectionDetails } from "@fluidframework/container-definitions";
8
+ import { IConnectionDetails, IDeltaManager } from "@fluidframework/container-definitions";
9
9
  import { ILocalSequencedClient, IProtocolHandler } from "@fluidframework/protocol-base";
10
10
  import { ConnectionMode, IQuorumClients } from "@fluidframework/protocol-definitions";
11
11
  import { PerformanceEvent } from "@fluidframework/telemetry-utils";
12
12
  import { ConnectionState } from "./connectionState";
13
+ import { CatchUpMonitor, ICatchUpMonitor, ImmediateCatchUpMonitor } from "./catchUpMonitor";
13
14
 
14
15
  /** Constructor parameter type for passing in dependencies needed by the ConnectionStateHandler */
15
16
  export interface IConnectionStateHandlerInputs {
@@ -36,7 +37,7 @@ const JoinOpTimeoutMs = 45000;
36
37
  * sequenced or blocked by the server before emitting the new "connected" event and allowing runtime to resubmit ops.
37
38
  *
38
39
  * Each connection is assigned a clientId by the service, and the connection is book-ended by a Join and a Leave op
39
- * generated by the service. Due to the distributed nature of the ordering service, in the case of reconnect we cannot
40
+ * generated by the service. Due to the distributed nature of the Relay Service, in the case of reconnect we cannot
40
41
  * make any assumptions about ordering of operations between the old and new connections - i.e. new Join op could
41
42
  * be sequenced before old Leave op (and some acks from pending ops that were in flight when we disconnected).
42
43
  *
@@ -47,14 +48,17 @@ const JoinOpTimeoutMs = 45000;
47
48
  * pending ops can safely be submitted with the new clientId without fear of duplication in the sequenced op stream.
48
49
  * (B) We process the Join op for the new clientId (identified when the underlying connection was first established),
49
50
  * indicating the service is ready to sequence ops sent with the new clientId.
51
+ * (C) We process all ops known at the time the underlying connection was established (so we are "caught up")
50
52
  *
51
53
  * For (A) we give up waiting after some time (same timeout as server uses), and go ahead and transition to Connected.
52
54
  * For (B) we log telemetry if it takes too long, but still only transition to Connected when the Join op is processed
53
55
  * and we are added to the Quorum.
56
+ * For (C) this is optional behavior, controlled by the parameters of receivedConnectEvent
54
57
  */
55
58
  export class ConnectionStateHandler {
56
59
  private _connectionState = ConnectionState.Disconnected;
57
60
  private _pendingClientId: string | undefined;
61
+ private catchUpMonitor: ICatchUpMonitor | undefined;
58
62
  private readonly prevClientLeftTimer: Timer;
59
63
  private readonly joinOpTimer: Timer;
60
64
 
@@ -106,7 +110,7 @@ export class ConnectionStateHandler {
106
110
  const quorumClients = this.handler.quorumClients();
107
111
  const details = {
108
112
  quorumInitialized: quorumClients !== undefined,
109
- hasPendingClientId: this.pendingClientId !== undefined,
113
+ pendingClientId: this.pendingClientId,
110
114
  inQuorum: quorumClients?.getMember(this.pendingClientId ?? "") !== undefined,
111
115
  waitingForLeaveOp: this.waitingForLeaveOp,
112
116
  };
@@ -184,7 +188,10 @@ export class ConnectionStateHandler {
184
188
  && !this.waitingForLeaveOp
185
189
  ) {
186
190
  this.waitEvent?.end({ source });
187
- this.setConnectionState(ConnectionState.Connected);
191
+
192
+ assert(this.catchUpMonitor !== undefined,
193
+ "catchUpMonitor should always be set if pendingClientId is set");
194
+ this.catchUpMonitor.on("caughtUp", this.transitionToConnectedState);
188
195
  } else {
189
196
  // Adding this event temporarily so that we can get help debugging if something goes wrong.
190
197
  this.logger.sendTelemetryEvent({
@@ -210,32 +217,47 @@ export class ConnectionStateHandler {
210
217
  }
211
218
 
212
219
  public receivedDisconnectEvent(reason: string) {
213
- if (this.joinOpTimer.hasTimer) {
214
- this.stopJoinOpTimer();
215
- }
216
220
  this.setConnectionState(ConnectionState.Disconnected, reason);
217
221
  }
218
222
 
223
+ private readonly transitionToConnectedState = () => {
224
+ // Defensive measure, we should always be in CatchingUp state when this is called.
225
+ if (this._connectionState === ConnectionState.CatchingUp) {
226
+ this.setConnectionState(ConnectionState.Connected);
227
+ } else {
228
+ this.logger.sendTelemetryEvent({
229
+ eventName: "cannotTransitionToConnectedState",
230
+ connectionState: ConnectionState[this._connectionState],
231
+ });
232
+ }
233
+ };
234
+
219
235
  /**
220
236
  * The "connect" event indicates the connection to the Relay Service is live.
221
237
  * However, some additional conditions must be met before we can fully transition to
222
238
  * "Connected" state. This function handles that interim period, known as "Connecting" state.
223
239
  * @param connectionMode - Read or Write connection
224
- * @param details - Connection details returned from the ordering service
240
+ * @param details - Connection details returned from the Relay Service
241
+ * @param deltaManager - DeltaManager to be used for delaying Connected transition until caught up.
242
+ * If it's undefined, then don't delay and transition to Connected as soon as Leave/Join op are accounted for
225
243
  */
226
244
  public receivedConnectEvent(
227
245
  connectionMode: ConnectionMode,
228
246
  details: IConnectionDetails,
247
+ deltaManager?: IDeltaManager<any, any>,
229
248
  ) {
230
249
  const oldState = this._connectionState;
231
250
  this._connectionState = ConnectionState.CatchingUp;
232
251
 
233
252
  const writeConnection = connectionMode === "write";
234
- assert(writeConnection || !this.handler.shouldClientJoinWrite(),
253
+ assert(!this.handler.shouldClientJoinWrite() || writeConnection,
235
254
  0x30a /* shouldClientJoinWrite should imply this is a writeConnection */);
236
- assert(writeConnection || !this.waitingForLeaveOp,
255
+ assert(!this.waitingForLeaveOp || writeConnection,
237
256
  0x2a6 /* "waitingForLeaveOp should imply writeConnection (we need to be ready to flush pending ops)" */);
238
257
 
258
+ // Defensive measure in case catchUpMonitor from previous connection attempt wasn't already cleared
259
+ this.catchUpMonitor?.dispose();
260
+
239
261
  // Note that this may be undefined since the connection is established proactively on load
240
262
  // and the quorum may still be under initialization.
241
263
  const quorumClients: IQuorumClients | undefined = this.handler.quorumClients();
@@ -248,6 +270,11 @@ export class ConnectionStateHandler {
248
270
  // we know there can no longer be outstanding ops that we sent with the previous client id.
249
271
  this._pendingClientId = details.clientId;
250
272
 
273
+ // We may want to catch up to known ops as of now before transitioning to Connected state
274
+ this.catchUpMonitor = deltaManager !== undefined
275
+ ? new CatchUpMonitor(deltaManager)
276
+ : new ImmediateCatchUpMonitor();
277
+
251
278
  // IMPORTANT: Report telemetry after we set _pendingClientId, but before transitioning to Connected state
252
279
  this.handler.logConnectionStateChangeTelemetry(ConnectionState.CatchingUp, oldState);
253
280
 
@@ -264,9 +291,21 @@ export class ConnectionStateHandler {
264
291
  this.startJoinOpTimer();
265
292
  } else if (!this.waitingForLeaveOp) {
266
293
  // We're not waiting for Join or Leave op (if read-only connection those don't even apply),
267
- // go ahead and declare the state to be Connected!
268
- // If we are waiting for Leave op still, do nothing for now, we will transition to Connected later.
269
- this.setConnectionState(ConnectionState.Connected);
294
+ // but we do need to wait until we are caught up (to now-known ops) before transitioning to Connected state.
295
+ this.catchUpMonitor.on("caughtUp", this.transitionToConnectedState);
296
+ }
297
+ // else - We are waiting for Leave op still, do nothing for now, we will transition to Connected later
298
+ }
299
+
300
+ /** Clear all the state used during the Connecting phase (set in receivedConnectEvent) */
301
+ private clearPendingConnectionState() {
302
+ this._pendingClientId = undefined;
303
+
304
+ this.catchUpMonitor?.dispose();
305
+ this.catchUpMonitor = undefined;
306
+
307
+ if (this.joinOpTimer.hasTimer) {
308
+ this.stopJoinOpTimer();
270
309
  }
271
310
  }
272
311
 
@@ -295,8 +334,9 @@ export class ConnectionStateHandler {
295
334
  }
296
335
  this._clientId = this.pendingClientId;
297
336
  } else if (value === ConnectionState.Disconnected) {
298
- // Important as we process our own joinSession message through delta request
299
- this._pendingClientId = undefined;
337
+ // Clear pending state immediately to prepare for reconnect
338
+ this.clearPendingConnectionState();
339
+
300
340
  // Only wait for "leave" message if the connected client exists in the quorum because only the write
301
341
  // client will exist in the quorum and only for those clients we will receive "removeMember" event and
302
342
  // the client has some unacked ops.
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, ITelemetryProperties,
10
+ IDisposable, ITelemetryLogger, ITelemetryProperties,
11
11
  } from "@fluidframework/common-definitions";
12
12
  import { assert, performance, unreachableCase } from "@fluidframework/common-utils";
13
13
  import {
@@ -53,10 +53,7 @@ import {
53
53
  isRuntimeMessage,
54
54
  isUnpackedRuntimeMessage,
55
55
  } from "@fluidframework/driver-utils";
56
- import {
57
- IProtocolHandler,
58
- ProtocolOpHandlerWithClientValidation,
59
- } from "@fluidframework/protocol-base";
56
+ import { IQuorumSnapshot } from "@fluidframework/protocol-base";
60
57
  import {
61
58
  IClient,
62
59
  IClientConfiguration,
@@ -109,6 +106,11 @@ import { initQuorumValuesFromCodeDetails, getCodeDetailsFromQuorumValues, Quorum
109
106
  import { CollabWindowTracker } from "./collabWindowTracker";
110
107
  import { ConnectionManager } from "./connectionManager";
111
108
  import { ConnectionState } from "./connectionState";
109
+ import {
110
+ IProtocolHandler,
111
+ ProtocolHandler,
112
+ ProtocolHandlerBuilder,
113
+ } from "./protocol";
112
114
 
113
115
  const detachedContainerRefSeqNumber = 0;
114
116
 
@@ -179,6 +181,10 @@ export async function waitContainerToCatchUp(container: IContainer) {
179
181
  };
180
182
  container.on("closed", closedCallback);
181
183
 
184
+ // Depending on config, transition to "connected" state may include the guarantee
185
+ // that all known ops have been processed. If so, we may introduce additional wait here.
186
+ // Waiting for "connected" state in either case gets us at least to our own Join op
187
+ // which is a reasonable approximation of "caught up"
182
188
  const waitForOps = () => {
183
189
  assert(container.connectionState === ConnectionState.CatchingUp
184
190
  || container.connectionState === ConnectionState.Connected,
@@ -228,6 +234,24 @@ const getCodeProposal =
228
234
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
229
235
  (quorum: IQuorumProposals) => quorum.get("code") ?? quorum.get("code2");
230
236
 
237
+ /**
238
+ * Helper function to report to telemetry cases where operation takes longer than expected (1s)
239
+ * @param logger - logger to use
240
+ * @param eventName - event name
241
+ * @param action - functor to call and measure
242
+ */
243
+ async function ReportIfTooLong(
244
+ logger: ITelemetryLogger,
245
+ eventName: string,
246
+ action: () => Promise<ITelemetryProperties>,
247
+ ) {
248
+ const event = PerformanceEvent.start(logger, { eventName });
249
+ const props = await action();
250
+ if (event.duration > 1000) {
251
+ event.end(props);
252
+ }
253
+ }
254
+
231
255
  /**
232
256
  * State saved by a container at close time, to be used to load a new instance
233
257
  * of the container to the same state
@@ -252,6 +276,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
252
276
  loader: Loader,
253
277
  loadOptions: IContainerLoadOptions,
254
278
  pendingLocalState?: IPendingContainerState,
279
+ protocolHandlerBuilder?: ProtocolHandlerBuilder,
255
280
  ): Promise<Container> {
256
281
  const container = new Container(
257
282
  loader,
@@ -260,7 +285,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
260
285
  resolvedUrl: loadOptions.resolvedUrl,
261
286
  canReconnect: loadOptions.canReconnect,
262
287
  serializedContainerState: pendingLocalState,
263
- });
288
+ },
289
+ protocolHandlerBuilder);
264
290
 
265
291
  return PerformanceEvent.timedExecAsync(
266
292
  container.mc.logger,
@@ -308,10 +334,12 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
308
334
  public static async createDetached(
309
335
  loader: Loader,
310
336
  codeDetails: IFluidCodeDetails,
337
+ protocolHandlerBuilder?: ProtocolHandlerBuilder,
311
338
  ): Promise<Container> {
312
339
  const container = new Container(
313
340
  loader,
314
- {});
341
+ {},
342
+ protocolHandlerBuilder);
315
343
 
316
344
  return PerformanceEvent.timedExecAsync(
317
345
  container.mc.logger,
@@ -330,10 +358,13 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
330
358
  public static async rehydrateDetachedFromSnapshot(
331
359
  loader: Loader,
332
360
  snapshot: string,
361
+ protocolHandlerBuilder?: ProtocolHandlerBuilder,
333
362
  ): Promise<Container> {
334
363
  const container = new Container(
335
364
  loader,
336
- {});
365
+ {},
366
+ protocolHandlerBuilder);
367
+
337
368
  return PerformanceEvent.timedExecAsync(
338
369
  container.mc.logger,
339
370
  { eventName: "RehydrateDetachedFromSnapshot" },
@@ -387,7 +418,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
387
418
  private readonly clientDetailsOverride: IClientDetails | undefined;
388
419
  private readonly _deltaManager: DeltaManager<ConnectionManager>;
389
420
  private service: IDocumentService | undefined;
390
- private readonly _audience: Audience;
421
+ private _initialClients: ISignalClient[] | undefined;
391
422
 
392
423
  private _context: ContainerContext | undefined;
393
424
  private get context() {
@@ -510,13 +541,13 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
510
541
  * Retrieves the audience associated with the document
511
542
  */
512
543
  public get audience(): IAudience {
513
- return this._audience;
544
+ return this.protocolHandler.audience;
514
545
  }
515
546
 
516
547
  /**
517
548
  * Returns true if container is dirty.
518
549
  * Which means data loss if container is closed at that same moment
519
- * Most likely that happens when there is no network connection to ordering service
550
+ * Most likely that happens when there is no network connection to Relay Service
520
551
  */
521
552
  public get isDirty() {
522
553
  return this._dirtyContainer;
@@ -531,6 +562,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
531
562
  constructor(
532
563
  private readonly loader: Loader,
533
564
  config: IContainerConfig,
565
+ private readonly protocolHandlerBuilder?: ProtocolHandlerBuilder,
534
566
  ) {
535
567
  super((name, error) => {
536
568
  this.mc.logger.sendErrorEvent(
@@ -540,7 +572,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
540
572
  },
541
573
  error);
542
574
  });
543
- this._audience = new Audience();
544
575
 
545
576
  this.clientDetailsOverride = config.clientDetailsOverride;
546
577
  this._resolvedUrl = config.resolvedUrl;
@@ -566,6 +597,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
566
597
  containerAttachState: () => this._attachState,
567
598
  containerLifecycleState: () => this._lifecycleState,
568
599
  containerConnectionState: () => ConnectionState[this.connectionState],
600
+ serializedContainer: config.serializedContainerState !== undefined,
569
601
  },
570
602
  // we need to be judicious with our logging here to avoid generating too much data
571
603
  // all data logged here should be broadly applicable, and not specific to a
@@ -578,6 +610,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
578
610
  containerLoadedFromVersionId: () => this.loadedFromVersion?.id,
579
611
  containerLoadedFromVersionDate: () => this.loadedFromVersion?.date,
580
612
  // message information to associate errors with the specific execution state
613
+ // dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
581
614
  dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
582
615
  dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
583
616
  dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId,
@@ -772,8 +805,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
772
805
  assert(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid",
773
806
  0x0d2 /* "resolved url should be valid Fluid url" */);
774
807
  assert(!!this._protocolHandler, 0x2e3 /* "Must have a valid protocol handler instance" */);
775
- assert(this._protocolHandler.attributes.term !== undefined,
776
- 0x30b /* Must have a valid protocol handler instance */);
808
+ assert(this._protocolHandler.attributes.term !== undefined, "Must have a valid protocol handler instance");
777
809
  const pendingState: IPendingContainerState = {
778
810
  pendingRuntimeState: this.context.getPendingLocalState(),
779
811
  url: this.resolvedUrl.url,
@@ -1159,12 +1191,17 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1159
1191
  // ...load in the existing quorum
1160
1192
  // Initialize the protocol handler
1161
1193
  this._protocolHandler = pendingLocalState === undefined
1162
- ? await this.initializeProtocolStateFromSnapshot(attributes, this.storageService, snapshot)
1163
- : await this.initializeProtocolState(
1194
+ ? await this.initializeProtocolStateFromSnapshot(
1195
+ attributes,
1196
+ this.storageService,
1197
+ snapshot,
1198
+ ) : await this.initializeProtocolState(
1164
1199
  attributes,
1165
- pendingLocalState.protocol.members,
1166
- pendingLocalState.protocol.proposals,
1167
- pendingLocalState.protocol.values,
1200
+ {
1201
+ members: pendingLocalState.protocol.members,
1202
+ proposals: pendingLocalState.protocol.proposals,
1203
+ values: pendingLocalState.protocol.values,
1204
+ }, // pending IQuorumSnapshot
1168
1205
  );
1169
1206
 
1170
1207
  const codeDetails = this.getCodeDetailsFromQuorum();
@@ -1175,17 +1212,20 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1175
1212
  pendingLocalState?.pendingRuntimeState,
1176
1213
  );
1177
1214
 
1178
- // Internal context is fully loaded at this point
1179
- this.setLoaded();
1180
-
1181
1215
  // We might have hit some failure that did not manifest itself in exception in this flow,
1182
1216
  // do not start op processing in such case - static version of Container.load() will handle it correctly.
1183
1217
  if (!this.closed) {
1184
1218
  if (opsBeforeReturnP !== undefined) {
1185
1219
  this._deltaManager.inbound.resume();
1186
1220
 
1187
- await opsBeforeReturnP;
1188
- await this._deltaManager.inbound.waitTillProcessingDone();
1221
+ await ReportIfTooLong(
1222
+ this.mc.logger,
1223
+ "WaitOps",
1224
+ async () => { await opsBeforeReturnP; return {}; });
1225
+ await ReportIfTooLong(
1226
+ this.mc.logger,
1227
+ "WaitOpProcessing",
1228
+ async () => this._deltaManager.inbound.waitTillProcessingDone());
1189
1229
 
1190
1230
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1191
1231
  this._deltaManager.inbound.pause();
@@ -1215,9 +1255,14 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1215
1255
  throw new Error("Container was closed while load()");
1216
1256
  }
1217
1257
 
1258
+ // Internal context is fully loaded at this point
1259
+ this.setLoaded();
1260
+
1218
1261
  return {
1219
1262
  sequenceNumber: attributes.sequenceNumber,
1220
1263
  version: versionId,
1264
+ dmLastProcessedSeqNumber: this._deltaManager.lastSequenceNumber,
1265
+ dmLastKnownSeqNumber: this._deltaManager.lastKnownSeqNumber,
1221
1266
  };
1222
1267
  }
1223
1268
 
@@ -1234,9 +1279,11 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1234
1279
  const qValues = initQuorumValuesFromCodeDetails(source);
1235
1280
  this._protocolHandler = await this.initializeProtocolState(
1236
1281
  attributes,
1237
- [], // members
1238
- [], // proposals
1239
- qValues,
1282
+ {
1283
+ members: [],
1284
+ proposals: [],
1285
+ values: qValues,
1286
+ }, // IQuorumSnapShot
1240
1287
  );
1241
1288
 
1242
1289
  // The load context - given we seeded the quorum - will be great
@@ -1270,9 +1317,12 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1270
1317
  this._protocolHandler =
1271
1318
  await this.initializeProtocolState(
1272
1319
  attributes,
1273
- [], // members
1274
- [], // proposals
1275
- codeDetails !== undefined ? initQuorumValuesFromCodeDetails(codeDetails) : []);
1320
+ {
1321
+ members: [],
1322
+ proposals: [],
1323
+ values: codeDetails !== undefined ? initQuorumValuesFromCodeDetails(codeDetails) : [],
1324
+ }, // IQuorumSnapShot
1325
+ );
1276
1326
 
1277
1327
  await this.instantiateContextDetached(
1278
1328
  true, // existing
@@ -1336,44 +1386,40 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1336
1386
  storage: IDocumentStorageService,
1337
1387
  snapshot: ISnapshotTree | undefined,
1338
1388
  ): Promise<IProtocolHandler> {
1339
- let members: [string, ISequencedClient][] = [];
1340
- let proposals: [number, ISequencedProposal, string[]][] = [];
1341
- let values: [string, any][] = [];
1389
+ const quorumSnapshot: IQuorumSnapshot = {
1390
+ members: [],
1391
+ proposals: [],
1392
+ values: [],
1393
+ };
1342
1394
 
1343
1395
  if (snapshot !== undefined) {
1344
1396
  const baseTree = getProtocolSnapshotTree(snapshot);
1345
- [members, proposals, values] = await Promise.all([
1397
+ [quorumSnapshot.members, quorumSnapshot.proposals, quorumSnapshot.values] = await Promise.all([
1346
1398
  readAndParse<[string, ISequencedClient][]>(storage, baseTree.blobs.quorumMembers),
1347
1399
  readAndParse<[number, ISequencedProposal, string[]][]>(storage, baseTree.blobs.quorumProposals),
1348
1400
  readAndParse<[string, ICommittedProposal][]>(storage, baseTree.blobs.quorumValues),
1349
1401
  ]);
1350
1402
  }
1351
1403
 
1352
- const protocolHandler = await this.initializeProtocolState(
1353
- attributes,
1354
- members,
1355
- proposals,
1356
- values);
1357
-
1404
+ const protocolHandler = await this.initializeProtocolState(attributes, quorumSnapshot);
1358
1405
  return protocolHandler;
1359
1406
  }
1360
1407
 
1361
1408
  private async initializeProtocolState(
1362
1409
  attributes: IDocumentAttributes,
1363
- members: [string, ISequencedClient][],
1364
- proposals: [number, ISequencedProposal, string[]][],
1365
- values: [string, any][],
1410
+ quorumSnapshot: IQuorumSnapshot,
1366
1411
  ): Promise<IProtocolHandler> {
1367
- const protocol = new ProtocolOpHandlerWithClientValidation(
1368
- attributes.minimumSequenceNumber,
1369
- attributes.sequenceNumber,
1370
- attributes.term,
1371
- members,
1372
- proposals,
1373
- values,
1412
+ const protocolHandlerBuilder =
1413
+ this.protocolHandlerBuilder ?? ((...args) => new ProtocolHandler(...args, new Audience()));
1414
+ const protocol = protocolHandlerBuilder(
1415
+ attributes,
1416
+ quorumSnapshot,
1374
1417
  (key, value) => this.submitMessage(MessageType.Propose, { key, value }),
1418
+ this._initialClients ?? [],
1375
1419
  );
1376
1420
 
1421
+ this._initialClients = undefined;
1422
+
1377
1423
  const protocolLogger = ChildLogger.create(this.subLogger, "ProtocolHandler");
1378
1424
 
1379
1425
  protocol.quorum.on("error", (error) => {
@@ -1494,17 +1540,30 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1494
1540
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1495
1541
  deltaManager.inboundSignal.pause();
1496
1542
 
1497
- deltaManager.on("connect", (details: IConnectionDetails, opsBehind?: number) => {
1498
- // Back-compat for new client and old server.
1499
- this._audience.clear();
1543
+ deltaManager.on("connect", (details: IConnectionDetails, _opsBehind?: number) => {
1544
+ if (this._protocolHandler === undefined) {
1545
+ // Store the initial clients so that they can be submitted to the
1546
+ // protocol handler when it is created.
1547
+ this._initialClients = details.initialClients;
1548
+ } else {
1549
+ // When reconnecting, the protocol handler is already created,
1550
+ // so we can update the audience right now.
1551
+ this._protocolHandler.audience.clear();
1500
1552
 
1501
- for (const priorClient of details.initialClients ?? []) {
1502
- this._audience.addMember(priorClient.clientId, priorClient.client);
1553
+ for (const priorClient of details.initialClients ?? []) {
1554
+ this._protocolHandler.audience.addMember(priorClient.clientId, priorClient.client);
1555
+ }
1503
1556
  }
1504
1557
 
1558
+ const deltaManagerForCatchingUp =
1559
+ this.mc.config.getBoolean("Fluid.Container.CatchUpBeforeDeclaringConnected") === true ?
1560
+ this.deltaManager
1561
+ : undefined;
1562
+
1505
1563
  this.connectionStateHandler.receivedConnectEvent(
1506
1564
  this.connectionMode,
1507
1565
  details,
1566
+ deltaManagerForCatchingUp,
1508
1567
  );
1509
1568
  });
1510
1569
 
@@ -1730,14 +1789,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1730
1789
  private processSignal(message: ISignalMessage) {
1731
1790
  // No clientId indicates a system signal message.
1732
1791
  if (message.clientId === null) {
1733
- const innerContent = message.content as { content: any; type: string; };
1734
- if (innerContent.type === MessageType.ClientJoin) {
1735
- const newClient = innerContent.content as ISignalClient;
1736
- this._audience.addMember(newClient.clientId, newClient.client);
1737
- } else if (innerContent.type === MessageType.ClientLeave) {
1738
- const leftClientId = innerContent.content as string;
1739
- this._audience.removeMember(leftClientId);
1740
- }
1792
+ this.protocolHandler.processSignal(message);
1741
1793
  } else {
1742
1794
  const local = this.clientId === message.clientId;
1743
1795
  this.context.processSignal(message, local);
@@ -410,7 +410,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
410
410
 
411
411
  if (prefetchType !== "none") {
412
412
  const cacheOnly = prefetchType === "cached";
413
- await this.fetchMissingDeltasCore("DocumentOpen", cacheOnly, this.lastQueuedSequenceNumber);
413
+ await this.fetchMissingDeltasCore(`DocumentOpen_${prefetchType}`, cacheOnly);
414
414
 
415
415
  // Keep going with fetching ops from storage once we have all cached ops in.
416
416
  // But do not block load and make this request async / not blocking this api.
@@ -418,7 +418,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
418
418
  // (which in most cases will happen when we are done processing cached ops)
419
419
  if (cacheOnly) {
420
420
  // fire and forget
421
- this.fetchMissingDeltas("DocumentOpen");
421
+ this.fetchMissingDeltas("PostDocumentOpen");
422
422
  }
423
423
  }
424
424
 
@@ -453,6 +453,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
453
453
  private async getDeltas(
454
454
  from: number, // inclusive
455
455
  to: number | undefined, // exclusive
456
+ fetchReason: string,
456
457
  callback: (messages: ISequencedDocumentMessage[]) => void,
457
458
  cacheOnly: boolean) {
458
459
  const docService = this.serviceProvider();
@@ -473,7 +474,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
473
474
  // received through delta stream. Validate that before moving forward.
474
475
  if (this.lastQueuedSequenceNumber >= lastExpectedOp) {
475
476
  this.logger.sendPerformanceEvent({
476
- reason: this.fetchReason,
477
+ reason: fetchReason,
477
478
  eventName: "ExtraStorageCall",
478
479
  early: true,
479
480
  from,
@@ -521,7 +522,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
521
522
  to, // exclusive
522
523
  controller.signal,
523
524
  cacheOnly,
524
- this.fetchReason);
525
+ fetchReason);
525
526
 
526
527
  // eslint-disable-next-line no-constant-condition
527
528
  while (true) {
@@ -876,6 +877,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
876
877
  await this.getDeltas(
877
878
  from,
878
879
  to,
880
+ fetchReason,
879
881
  (messages) => {
880
882
  this.refreshDelayInfo(this.deltaStorageDelayId);
881
883
  this.enqueueMessages(messages, fetchReason);