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

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 (127) 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/containerStorageAdapter.d.ts +2 -2
  24. package/dist/containerStorageAdapter.d.ts.map +1 -1
  25. package/dist/containerStorageAdapter.js +2 -2
  26. package/dist/containerStorageAdapter.js.map +1 -1
  27. package/dist/deltaManager.d.ts.map +1 -1
  28. package/dist/deltaManager.js +6 -6
  29. package/dist/deltaManager.js.map +1 -1
  30. package/dist/deltaManagerProxy.d.ts +4 -1
  31. package/dist/deltaManagerProxy.d.ts.map +1 -1
  32. package/dist/deltaQueue.d.ts +9 -2
  33. package/dist/deltaQueue.d.ts.map +1 -1
  34. package/dist/deltaQueue.js +31 -26
  35. package/dist/deltaQueue.js.map +1 -1
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/loader.d.ts +7 -0
  40. package/dist/loader.d.ts.map +1 -1
  41. package/dist/loader.js +4 -3
  42. package/dist/loader.js.map +1 -1
  43. package/dist/packageVersion.d.ts +1 -1
  44. package/dist/packageVersion.d.ts.map +1 -1
  45. package/dist/packageVersion.js +1 -1
  46. package/dist/packageVersion.js.map +1 -1
  47. package/dist/protocol.d.ts +22 -0
  48. package/dist/protocol.d.ts.map +1 -0
  49. package/dist/protocol.js +52 -0
  50. package/dist/protocol.js.map +1 -0
  51. package/dist/protocolTreeDocumentStorageService.d.ts +1 -1
  52. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  53. package/dist/retriableDocumentStorageService.d.ts +2 -2
  54. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  55. package/dist/retriableDocumentStorageService.js +2 -2
  56. package/dist/retriableDocumentStorageService.js.map +1 -1
  57. package/lib/audience.d.ts +2 -2
  58. package/lib/audience.d.ts.map +1 -1
  59. package/lib/audience.js.map +1 -1
  60. package/lib/catchUpMonitor.d.ts +40 -0
  61. package/lib/catchUpMonitor.d.ts.map +1 -0
  62. package/lib/catchUpMonitor.js +69 -0
  63. package/lib/catchUpMonitor.js.map +1 -0
  64. package/lib/connectionManager.d.ts.map +1 -1
  65. package/lib/connectionManager.js +0 -1
  66. package/lib/connectionManager.js.map +1 -1
  67. package/lib/connectionState.d.ts +0 -5
  68. package/lib/connectionState.d.ts.map +1 -1
  69. package/lib/connectionState.js +0 -5
  70. package/lib/connectionState.js.map +1 -1
  71. package/lib/connectionStateHandler.d.ts +12 -4
  72. package/lib/connectionStateHandler.d.ts.map +1 -1
  73. package/lib/connectionStateHandler.js +47 -15
  74. package/lib/connectionStateHandler.js.map +1 -1
  75. package/lib/container.d.ts +8 -6
  76. package/lib/container.d.ts.map +1 -1
  77. package/lib/container.js +82 -46
  78. package/lib/container.js.map +1 -1
  79. package/lib/containerStorageAdapter.d.ts +2 -2
  80. package/lib/containerStorageAdapter.d.ts.map +1 -1
  81. package/lib/containerStorageAdapter.js +2 -2
  82. package/lib/containerStorageAdapter.js.map +1 -1
  83. package/lib/deltaManager.d.ts.map +1 -1
  84. package/lib/deltaManager.js +6 -6
  85. package/lib/deltaManager.js.map +1 -1
  86. package/lib/deltaManagerProxy.d.ts +4 -1
  87. package/lib/deltaManagerProxy.d.ts.map +1 -1
  88. package/lib/deltaQueue.d.ts +9 -2
  89. package/lib/deltaQueue.d.ts.map +1 -1
  90. package/lib/deltaQueue.js +32 -27
  91. package/lib/deltaQueue.js.map +1 -1
  92. package/lib/index.d.ts +1 -0
  93. package/lib/index.d.ts.map +1 -1
  94. package/lib/index.js.map +1 -1
  95. package/lib/loader.d.ts +7 -0
  96. package/lib/loader.d.ts.map +1 -1
  97. package/lib/loader.js +4 -3
  98. package/lib/loader.js.map +1 -1
  99. package/lib/packageVersion.d.ts +1 -1
  100. package/lib/packageVersion.d.ts.map +1 -1
  101. package/lib/packageVersion.js +1 -1
  102. package/lib/packageVersion.js.map +1 -1
  103. package/lib/protocol.d.ts +22 -0
  104. package/lib/protocol.d.ts.map +1 -0
  105. package/lib/protocol.js +48 -0
  106. package/lib/protocol.js.map +1 -0
  107. package/lib/protocolTreeDocumentStorageService.d.ts +1 -1
  108. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  109. package/lib/retriableDocumentStorageService.d.ts +2 -2
  110. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  111. package/lib/retriableDocumentStorageService.js +2 -2
  112. package/lib/retriableDocumentStorageService.js.map +1 -1
  113. package/package.json +23 -15
  114. package/src/audience.ts +2 -2
  115. package/src/catchUpMonitor.ts +99 -0
  116. package/src/connectionManager.ts +0 -1
  117. package/src/connectionState.ts +0 -6
  118. package/src/connectionStateHandler.ts +55 -15
  119. package/src/container.ts +115 -63
  120. package/src/containerStorageAdapter.ts +8 -2
  121. package/src/deltaManager.ts +6 -4
  122. package/src/deltaQueue.ts +34 -28
  123. package/src/index.ts +4 -0
  124. package/src/loader.ts +13 -2
  125. package/src/packageVersion.ts +1 -1
  126. package/src/protocol.ts +96 -0
  127. package/src/retriableDocumentStorageService.ts +8 -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, 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);
@@ -6,6 +6,7 @@
6
6
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
7
  import { ISnapshotTreeWithBlobContents } from "@fluidframework/container-definitions";
8
8
  import {
9
+ FetchSource,
9
10
  IDocumentStorageService,
10
11
  IDocumentStorageServicePolicies,
11
12
  ISummaryContext,
@@ -65,8 +66,13 @@ export class ContainerStorageAdapter implements IDocumentStorageService {
65
66
  return this.storageGetter().readBlob(id);
66
67
  }
67
68
 
68
- public async getVersions(versionId: string | null, count: number, scenarioName?: string): Promise<IVersion[]> {
69
- return this.storageGetter().getVersions(versionId, count, scenarioName);
69
+ public async getVersions(
70
+ versionId: string | null,
71
+ count: number,
72
+ scenarioName?: string,
73
+ fetchSource?: FetchSource,
74
+ ): Promise<IVersion[]> {
75
+ return this.storageGetter().getVersions(versionId, count, scenarioName, fetchSource);
70
76
  }
71
77
 
72
78
  public async uploadSummaryWithContext(summary: ISummaryTree, context: ISummaryContext): Promise<string> {
@@ -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);
package/src/deltaQueue.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { IDeltaQueue, IDeltaQueueEvents } from "@fluidframework/container-definitions";
7
- import { assert, performance, Deferred, TypedEventEmitter } from "@fluidframework/common-utils";
7
+ import { assert, performance, TypedEventEmitter } from "@fluidframework/common-utils";
8
8
  import Deque from "double-ended-queue";
9
9
 
10
10
  export interface IDeltaQueueWriter<T> {
@@ -30,7 +30,7 @@ export class DeltaQueue<T>
30
30
  * When processing is ongoing, holds a deferred that will resolve once processing stops.
31
31
  * Undefined when not processing.
32
32
  */
33
- private processingDeferred: Deferred<void> | undefined;
33
+ private processingPromise: Promise<{ count: number; duration: number; }> | undefined;
34
34
 
35
35
  public get disposed(): boolean {
36
36
  return this.isDisposed;
@@ -48,13 +48,11 @@ export class DeltaQueue<T>
48
48
  }
49
49
 
50
50
  public get idle(): boolean {
51
- return this.processingDeferred === undefined && this.q.length === 0;
51
+ return this.processingPromise === undefined && this.q.length === 0;
52
52
  }
53
53
 
54
- public async waitTillProcessingDone(): Promise<void> {
55
- if (this.processingDeferred !== undefined) {
56
- return this.processingDeferred.promise;
57
- }
54
+ public async waitTillProcessingDone() {
55
+ return this.processingPromise ?? { count: 0, duration: 0 };
58
56
  }
59
57
 
60
58
  /**
@@ -98,7 +96,7 @@ export class DeltaQueue<T>
98
96
  this.pauseCount++;
99
97
  // If called from within the processing loop, we are in the middle of processing an op. Return a promise
100
98
  // that will resolve when processing has actually stopped.
101
- return this.waitTillProcessingDone();
99
+ await this.waitTillProcessingDone();
102
100
  }
103
101
 
104
102
  public resume(): void {
@@ -113,20 +111,31 @@ export class DeltaQueue<T>
113
111
  * not already started.
114
112
  */
115
113
  private ensureProcessing() {
116
- if (!this.paused && this.processingDeferred === undefined) {
117
- this.processingDeferred = new Deferred<void>();
114
+ if (this.anythingToProcess() && this.processingPromise === undefined) {
118
115
  // Use a resolved promise to start the processing on a separate stack.
119
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
120
- Promise.resolve().then(() => {
121
- this.processDeltas();
122
- if (this.processingDeferred !== undefined) {
123
- this.processingDeferred.resolve();
124
- this.processingDeferred = undefined;
125
- }
116
+ this.processingPromise = Promise.resolve().then(() => {
117
+ assert(this.processingPromise !== undefined, "reentrancy?");
118
+ const result = this.processDeltas();
119
+ assert(this.processingPromise !== undefined, "reentrancy?");
120
+ // WARNING: Do not move next line to .finally() clause!
121
+ // It runs async and creates a race condition where incoming ensureProcessing() call observes
122
+ // from previous run while previous run is over (but finally clause was not scheduled yet)
123
+ this.processingPromise = undefined;
124
+ return result;
125
+ }).catch((error) => {
126
+ this.error = error;
127
+ this.processingPromise = undefined;
128
+ this.emit("error", error);
129
+ return { count: 0, duration: 0 };
126
130
  });
131
+ assert(this.processingPromise !== undefined, "processDeltas() should run async");
127
132
  }
128
133
  }
129
134
 
135
+ private anythingToProcess() {
136
+ return this.q.length !== 0 && !this.paused && this.error === undefined;
137
+ }
138
+
130
139
  /**
131
140
  * Executes the delta processing loop until a stop condition is reached.
132
141
  */
@@ -136,24 +145,21 @@ export class DeltaQueue<T>
136
145
 
137
146
  // For grouping to work we must process all local messages immediately and in the single turn.
138
147
  // So loop over them until no messages to process, we have become paused, or hit an error.
139
- while (!(this.q.length === 0 || this.paused || this.error !== undefined)) {
148
+ while (this.anythingToProcess()) {
140
149
  // Get the next message in the queue
141
150
  const next = this.q.shift();
142
151
  count++;
143
152
  // Process the message.
144
- try {
145
- // We know next is defined since we did a length check just prior to shifting.
146
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
147
- this.worker(next!);
148
- this.emit("op", next);
149
- } catch (error) {
150
- this.error = error;
151
- this.emit("error", error);
152
- }
153
+ // We know next is defined since we did a length check just prior to shifting.
154
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
155
+ this.worker(next!);
156
+ this.emit("op", next);
153
157
  }
154
158
 
159
+ const duration = performance.now() - start;
155
160
  if (this.q.length === 0) {
156
- this.emit("idle", count, performance.now() - start);
161
+ this.emit("idle", count, duration);
157
162
  }
163
+ return { count, duration };
158
164
  }
159
165
  }
package/src/index.ts CHANGED
@@ -21,3 +21,7 @@ export {
21
21
  Loader,
22
22
  RelativeLoader,
23
23
  } from "./loader";
24
+ export {
25
+ IProtocolHandler,
26
+ ProtocolHandlerBuilder,
27
+ } from "./protocol";
package/src/loader.ts CHANGED
@@ -48,6 +48,7 @@ import {
48
48
  import { Container, IPendingContainerState } from "./container";
49
49
  import { IParsedUrl, parseUrl } from "./utils";
50
50
  import { pkgVersion } from "./packageVersion";
51
+ import { ProtocolHandlerBuilder } from "./protocol";
51
52
 
52
53
  function canUseCache(request: IRequest): boolean {
53
54
  if (request.headers === undefined) {
@@ -211,7 +212,13 @@ export interface ILoaderProps {
211
212
  /**
212
213
  * The configuration provider which may be used to control features.
213
214
  */
214
- readonly configProvider?: IConfigProviderBase;
215
+ readonly configProvider?: IConfigProviderBase;
216
+
217
+ /**
218
+ * Optional property for allowing the container to use a custom
219
+ * protocol implementation for handling the quorum and/or the audience.
220
+ */
221
+ readonly protocolHandlerBuilder?: ProtocolHandlerBuilder;
215
222
  }
216
223
 
217
224
  /**
@@ -278,6 +285,7 @@ export class Loader implements IHostLoader {
278
285
  private readonly containers = new Map<string, Promise<Container>>();
279
286
  public readonly services: ILoaderServices;
280
287
  private readonly mc: MonitoringContext;
288
+ private readonly protocolHandlerBuilder: ProtocolHandlerBuilder | undefined;
281
289
 
282
290
  constructor(loaderProps: ILoaderProps) {
283
291
  const scope: FluidObject<ILoader> = { ...loaderProps.scope };
@@ -306,6 +314,7 @@ export class Loader implements IHostLoader {
306
314
  };
307
315
  this.mc = loggerToMonitoringContext(
308
316
  ChildLogger.create(this.services.subLogger, "Loader"));
317
+ this.protocolHandlerBuilder = loaderProps.protocolHandlerBuilder;
309
318
  }
310
319
 
311
320
  public get IFluidRouter(): IFluidRouter { return this; }
@@ -314,6 +323,7 @@ export class Loader implements IHostLoader {
314
323
  const container = await Container.createDetached(
315
324
  this,
316
325
  codeDetails,
326
+ this.protocolHandlerBuilder,
317
327
  );
318
328
 
319
329
  if (this.cachingEnabled) {
@@ -330,7 +340,7 @@ export class Loader implements IHostLoader {
330
340
  }
331
341
 
332
342
  public async rehydrateDetachedContainerFromSnapshot(snapshot: string): Promise<IContainer> {
333
- return Container.rehydrateDetachedFromSnapshot(this, snapshot);
343
+ return Container.rehydrateDetachedFromSnapshot(this, snapshot, this.protocolHandlerBuilder);
334
344
  }
335
345
 
336
346
  public async resolve(request: IRequest, pendingLocalState?: string): Promise<IContainer> {
@@ -482,6 +492,7 @@ export class Loader implements IHostLoader {
482
492
  loadMode: request.headers?.[LoaderHeader.loadMode],
483
493
  },
484
494
  pendingLocalState,
495
+ this.protocolHandlerBuilder,
485
496
  );
486
497
  }
487
498
  }
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-loader";
9
- export const pkgVersion = "1.2.2";
9
+ export const pkgVersion = "2.0.0-internal.1.0.0.82159";