@fluidframework/container-loader 2.0.0-internal.1.1.2 → 2.0.0-internal.1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/catchUpMonitor.d.ts +6 -17
  2. package/dist/catchUpMonitor.d.ts.map +1 -1
  3. package/dist/catchUpMonitor.js +5 -36
  4. package/dist/catchUpMonitor.js.map +1 -1
  5. package/dist/collabWindowTracker.d.ts +1 -1
  6. package/dist/collabWindowTracker.d.ts.map +1 -1
  7. package/dist/collabWindowTracker.js +2 -1
  8. package/dist/collabWindowTracker.js.map +1 -1
  9. package/dist/connectionManager.d.ts +1 -1
  10. package/dist/connectionManager.d.ts.map +1 -1
  11. package/dist/connectionManager.js +8 -11
  12. package/dist/connectionManager.js.map +1 -1
  13. package/dist/connectionStateHandler.d.ts +80 -26
  14. package/dist/connectionStateHandler.d.ts.map +1 -1
  15. package/dist/connectionStateHandler.js +170 -89
  16. package/dist/connectionStateHandler.js.map +1 -1
  17. package/dist/container.d.ts +22 -11
  18. package/dist/container.d.ts.map +1 -1
  19. package/dist/container.js +130 -142
  20. package/dist/container.js.map +1 -1
  21. package/dist/containerContext.d.ts +18 -7
  22. package/dist/containerContext.d.ts.map +1 -1
  23. package/dist/containerContext.js +18 -8
  24. package/dist/containerContext.js.map +1 -1
  25. package/dist/containerStorageAdapter.d.ts +10 -24
  26. package/dist/containerStorageAdapter.d.ts.map +1 -1
  27. package/dist/containerStorageAdapter.js +50 -16
  28. package/dist/containerStorageAdapter.js.map +1 -1
  29. package/dist/deltaManager.d.ts +1 -1
  30. package/dist/deltaManager.d.ts.map +1 -1
  31. package/dist/deltaManager.js +18 -6
  32. package/dist/deltaManager.js.map +1 -1
  33. package/dist/loader.d.ts +1 -1
  34. package/dist/loader.js.map +1 -1
  35. package/dist/packageVersion.d.ts +1 -1
  36. package/dist/packageVersion.js +1 -1
  37. package/dist/packageVersion.js.map +1 -1
  38. package/dist/protocol.d.ts.map +1 -1
  39. package/dist/protocol.js +2 -1
  40. package/dist/protocol.js.map +1 -1
  41. package/lib/catchUpMonitor.d.ts +6 -17
  42. package/lib/catchUpMonitor.d.ts.map +1 -1
  43. package/lib/catchUpMonitor.js +5 -35
  44. package/lib/catchUpMonitor.js.map +1 -1
  45. package/lib/collabWindowTracker.d.ts +1 -1
  46. package/lib/collabWindowTracker.d.ts.map +1 -1
  47. package/lib/collabWindowTracker.js +3 -2
  48. package/lib/collabWindowTracker.js.map +1 -1
  49. package/lib/connectionManager.d.ts +1 -1
  50. package/lib/connectionManager.d.ts.map +1 -1
  51. package/lib/connectionManager.js +9 -14
  52. package/lib/connectionManager.js.map +1 -1
  53. package/lib/connectionStateHandler.d.ts +80 -26
  54. package/lib/connectionStateHandler.d.ts.map +1 -1
  55. package/lib/connectionStateHandler.js +170 -90
  56. package/lib/connectionStateHandler.js.map +1 -1
  57. package/lib/container.d.ts +22 -11
  58. package/lib/container.d.ts.map +1 -1
  59. package/lib/container.js +133 -145
  60. package/lib/container.js.map +1 -1
  61. package/lib/containerContext.d.ts +18 -7
  62. package/lib/containerContext.d.ts.map +1 -1
  63. package/lib/containerContext.js +19 -9
  64. package/lib/containerContext.js.map +1 -1
  65. package/lib/containerStorageAdapter.d.ts +10 -24
  66. package/lib/containerStorageAdapter.d.ts.map +1 -1
  67. package/lib/containerStorageAdapter.js +50 -15
  68. package/lib/containerStorageAdapter.js.map +1 -1
  69. package/lib/deltaManager.d.ts +1 -1
  70. package/lib/deltaManager.d.ts.map +1 -1
  71. package/lib/deltaManager.js +18 -6
  72. package/lib/deltaManager.js.map +1 -1
  73. package/lib/loader.d.ts +1 -1
  74. package/lib/loader.js.map +1 -1
  75. package/lib/packageVersion.d.ts +1 -1
  76. package/lib/packageVersion.js +1 -1
  77. package/lib/packageVersion.js.map +1 -1
  78. package/lib/protocol.d.ts.map +1 -1
  79. package/lib/protocol.js +2 -1
  80. package/lib/protocol.js.map +1 -1
  81. package/package.json +13 -13
  82. package/src/catchUpMonitor.ts +7 -47
  83. package/src/collabWindowTracker.ts +4 -3
  84. package/src/connectionManager.ts +9 -11
  85. package/src/connectionStateHandler.ts +231 -106
  86. package/src/container.ts +156 -168
  87. package/src/containerContext.ts +22 -8
  88. package/src/containerStorageAdapter.ts +64 -15
  89. package/src/deltaManager.ts +20 -7
  90. package/src/loader.ts +1 -1
  91. package/src/packageVersion.ts +1 -1
  92. package/src/protocol.ts +2 -1
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, ITelemetryLogger, ITelemetryProperties,
10
+ ITelemetryLogger, ITelemetryProperties,
11
11
  } from "@fluidframework/common-definitions";
12
12
  import { assert, performance, unreachableCase } from "@fluidframework/common-utils";
13
13
  import {
@@ -29,10 +29,9 @@ import {
29
29
  IContainerLoadMode,
30
30
  IFluidCodeDetails,
31
31
  isFluidCodeDetails,
32
+ IBatchMessage,
32
33
  } from "@fluidframework/container-definitions";
33
34
  import {
34
- DataCorruptionError,
35
- extractSafePropertiesFromMessage,
36
35
  GenericError,
37
36
  UsageError,
38
37
  } from "@fluidframework/container-utils";
@@ -50,8 +49,6 @@ import {
50
49
  combineAppAndProtocolSummary,
51
50
  runWithRetry,
52
51
  isFluidResolvedUrl,
53
- isRuntimeMessage,
54
- isUnpackedRuntimeMessage,
55
52
  } from "@fluidframework/driver-utils";
56
53
  import { IQuorumSnapshot } from "@fluidframework/protocol-base";
57
54
  import {
@@ -61,7 +58,6 @@ import {
61
58
  ICommittedProposal,
62
59
  IDocumentAttributes,
63
60
  IDocumentMessage,
64
- IProcessMessageResult,
65
61
  IProtocolState,
66
62
  IQuorumClients,
67
63
  IQuorumProposals,
@@ -97,10 +93,11 @@ import { DeltaManager, IConnectionArgs } from "./deltaManager";
97
93
  import { DeltaManagerProxy } from "./deltaManagerProxy";
98
94
  import { ILoaderOptions, Loader, RelativeLoader } from "./loader";
99
95
  import { pkgVersion } from "./packageVersion";
100
- import { ConnectionStateHandler } from "./connectionStateHandler";
101
- import { RetriableDocumentStorageService } from "./retriableDocumentStorageService";
102
- import { ProtocolTreeStorageService } from "./protocolTreeDocumentStorageService";
103
- import { BlobOnlyStorage, ContainerStorageAdapter } from "./containerStorageAdapter";
96
+ import { ContainerStorageAdapter } from "./containerStorageAdapter";
97
+ import {
98
+ IConnectionStateHandler,
99
+ createConnectionStateHandler,
100
+ } from "./connectionStateHandler";
104
101
  import { getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer } from "./utils";
105
102
  import { initQuorumValuesFromCodeDetails, getCodeDetailsFromQuorumValues, QuorumProxy } from "./quorum";
106
103
  import { CollabWindowTracker } from "./collabWindowTracker";
@@ -151,14 +148,19 @@ export interface IContainerConfig {
151
148
  }
152
149
 
153
150
  /**
154
- * Waits until container connects to delta storage and gets up-to-date
151
+ * Waits until container connects to delta storage and gets up-to-date.
152
+ *
155
153
  * Useful when resolving URIs and hitting 404, due to container being loaded from (stale) snapshot and not being
156
154
  * up to date. Host may chose to wait in such case and retry resolving URI.
155
+ *
157
156
  * Warning: Will wait infinitely for connection to establish if there is no connection.
158
157
  * May result in deadlock if Container.disconnect() is called and never followed by a call to Container.connect().
159
- * @returns true: container is up to date, it processed all the ops that were know at the time of first connection
160
- * false: storage does not provide indication of how far the client is. Container processed
161
- * all the ops known to it, but it maybe still behind.
158
+ *
159
+ * @returns `true`: container is up to date, it processed all the ops that were know at the time of first connection.
160
+ *
161
+ * `false`: storage does not provide indication of how far the client is. Container processed all the ops known to it,
162
+ * but it maybe still behind.
163
+ *
162
164
  * @throws an error beginning with `"Container closed"` if the container is closed before it catches up.
163
165
  */
164
166
  export async function waitContainerToCatchUp(container: IContainer) {
@@ -391,7 +393,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
391
393
  // Only transition states if currently loading
392
394
  if (this._lifecycleState === "loading") {
393
395
  // Propagate current connection state through the system.
394
- this.propagateConnectionState();
396
+ this.propagateConnectionState(true /* initial transition */);
395
397
  this._lifecycleState = "loaded";
396
398
  }
397
399
  }
@@ -402,17 +404,9 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
402
404
 
403
405
  private _attachState = AttachState.Detached;
404
406
 
405
- private readonly _storage: ContainerStorageAdapter;
407
+ private readonly storageService: ContainerStorageAdapter;
406
408
  public get storage(): IDocumentStorageService {
407
- return this._storage;
408
- }
409
-
410
- private _storageService: IDocumentStorageService & IDisposable | undefined;
411
- private get storageService(): IDocumentStorageService {
412
- if (this._storageService === undefined) {
413
- throw new Error("Attempted to access storageService before it was defined");
414
- }
415
- return this._storageService;
409
+ return this.storageService;
416
410
  }
417
411
 
418
412
  private readonly clientDetailsOverride: IClientDetails | undefined;
@@ -447,7 +441,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
447
441
 
448
442
  private lastVisible: number | undefined;
449
443
  private readonly visibilityEventHandler: (() => void) | undefined;
450
- private readonly connectionStateHandler: ConnectionStateHandler;
444
+ private readonly connectionStateHandler: IConnectionStateHandler;
451
445
 
452
446
  private setAutoReconnectTime = performance.now();
453
447
 
@@ -489,7 +483,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
489
483
  }
490
484
 
491
485
  public get connected(): boolean {
492
- return this.connectionStateHandler.connected;
486
+ return this.connectionStateHandler.connectionState === ConnectionState.Connected;
493
487
  }
494
488
 
495
489
  /**
@@ -500,12 +494,14 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
500
494
  return this._deltaManager.serviceConfiguration;
501
495
  }
502
496
 
497
+ private _clientId: string | undefined;
498
+
503
499
  /**
504
500
  * The server provided id of the client.
505
501
  * Set once this.connected is true, otherwise undefined
506
502
  */
507
503
  public get clientId(): string | undefined {
508
- return this.connectionStateHandler.clientId;
504
+ return this._clientId;
509
505
  }
510
506
 
511
507
  /**
@@ -631,11 +627,21 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
631
627
  summarizeProtocolTree,
632
628
  };
633
629
 
634
- this.connectionStateHandler = new ConnectionStateHandler(
630
+ this._deltaManager = this.createDeltaManager();
631
+
632
+ this._clientId = config.serializedContainerState?.clientId;
633
+ this.connectionStateHandler = createConnectionStateHandler(
635
634
  {
636
- quorumClients: () => this._protocolHandler?.quorum,
637
- logConnectionStateChangeTelemetry: (value, oldState, reason) =>
638
- this.logConnectionStateChangeTelemetry(value, oldState, reason),
635
+ logger: this.mc.logger,
636
+ connectionStateChanged: (value, oldState, reason) => {
637
+ if (value === ConnectionState.Connected) {
638
+ this._clientId = this.connectionStateHandler.pendingClientId;
639
+ }
640
+ this.logConnectionStateChangeTelemetry(value, oldState, reason);
641
+ if (this._lifecycleState === "loaded") {
642
+ this.propagateConnectionState(false /* initial transition */);
643
+ }
644
+ },
639
645
  shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
640
646
  maxClientLeaveWaitTime: this.loader.services.options.maxClientLeaveWaitTime,
641
647
  logConnectionIssue: (eventName: string, details?: ITelemetryProperties) => {
@@ -647,35 +653,21 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
647
653
  ...(details === undefined ? {} : { details: JSON.stringify(details) }),
648
654
  });
649
655
  },
650
- connectionStateChanged: () => {
651
- // Fire events only if container is fully loaded and not closed
652
- if (this._lifecycleState === "loaded") {
653
- this.propagateConnectionState();
654
- }
655
- },
656
656
  },
657
- this.mc.logger,
658
- config.serializedContainerState?.clientId,
657
+ this.deltaManager,
658
+ this._clientId,
659
659
  );
660
660
 
661
661
  this.on(savedContainerEvent, () => {
662
662
  this.connectionStateHandler.containerSaved();
663
663
  });
664
664
 
665
- this._deltaManager = this.createDeltaManager();
666
- this._storage = new ContainerStorageAdapter(
667
- () => {
668
- if (this.attachState !== AttachState.Attached) {
669
- if (this.loader.services.detachedBlobStorage !== undefined) {
670
- return new BlobOnlyStorage(this.loader.services.detachedBlobStorage, this.mc.logger);
671
- }
672
- this.mc.logger.sendErrorEvent({
673
- eventName: "NoRealStorageInDetachedContainer",
674
- });
675
- throw new Error("Real storage calls not allowed in Unattached container");
676
- }
677
- return this.storageService;
678
- },
665
+ this.storageService = new ContainerStorageAdapter(
666
+ this.loader.services.detachedBlobStorage,
667
+ this.mc.logger,
668
+ this.options.summarizeProtocolTree === true
669
+ ? () => this.captureProtocolSummary()
670
+ : undefined,
679
671
  );
680
672
 
681
673
  const isDomAvailable = typeof document === "object" &&
@@ -775,7 +767,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
775
767
 
776
768
  this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
777
769
 
778
- this._storageService?.dispose();
770
+ this.storageService.dispose();
779
771
 
780
772
  // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
781
773
  // about file, like file being overwritten in storage, but client having stale local cache.
@@ -897,7 +889,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
897
889
  const resolvedUrl = this.service.resolvedUrl;
898
890
  ensureFluidResolvedUrl(resolvedUrl);
899
891
  this._resolvedUrl = resolvedUrl;
900
- await this.connectStorageService();
892
+ await this.storageService.connectToService(this.service);
901
893
 
902
894
  if (hasAttachmentBlobs) {
903
895
  // upload blobs to storage
@@ -935,8 +927,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
935
927
  this._attachState = AttachState.Attached;
936
928
  this.emit("attached");
937
929
 
938
- // Propagate current connection state through the system.
939
- this.propagateConnectionState();
940
930
  if (!this.closed) {
941
931
  this.resumeInternal({ fetchOpsFromStorage: false, reason: "createDetached" });
942
932
  }
@@ -1112,9 +1102,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1112
1102
  /**
1113
1103
  * Load container.
1114
1104
  *
1115
- * @param specifiedVersion - one of the following
1116
- * - undefined - fetch latest snapshot
1117
- * - otherwise, version sha to load snapshot
1105
+ * @param specifiedVersion - Version SHA to load snapshot. If not specified, will fetch the latest snapshot.
1118
1106
  */
1119
1107
  private async load(
1120
1108
  specifiedVersion: string | undefined,
@@ -1148,10 +1136,10 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1148
1136
  }
1149
1137
 
1150
1138
  if (!pendingLocalState) {
1151
- await this.connectStorageService();
1139
+ await this.storageService.connectToService(this.service);
1152
1140
  } else {
1153
1141
  // if we have pendingLocalState we can load without storage; don't wait for connection
1154
- this.connectStorageService().catch((error) => this.close(error));
1142
+ this.storageService.connectToService(this.service).catch((error) => this.close(error));
1155
1143
  }
1156
1144
 
1157
1145
  this._attachState = AttachState.Attached;
@@ -1192,12 +1180,13 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1192
1180
 
1193
1181
  // ...load in the existing quorum
1194
1182
  // Initialize the protocol handler
1195
- this._protocolHandler = pendingLocalState === undefined
1196
- ? await this.initializeProtocolStateFromSnapshot(
1183
+ if (pendingLocalState === undefined) {
1184
+ await this.initializeProtocolStateFromSnapshot(
1197
1185
  attributes,
1198
1186
  this.storageService,
1199
- snapshot,
1200
- ) : await this.initializeProtocolState(
1187
+ snapshot);
1188
+ } else {
1189
+ this.initializeProtocolState(
1201
1190
  attributes,
1202
1191
  {
1203
1192
  members: pendingLocalState.protocol.members,
@@ -1205,6 +1194,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1205
1194
  values: pendingLocalState.protocol.values,
1206
1195
  }, // pending IQuorumSnapshot
1207
1196
  );
1197
+ }
1208
1198
 
1209
1199
  const codeDetails = this.getCodeDetailsFromQuorum();
1210
1200
  await this.instantiateContext(
@@ -1279,7 +1269,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1279
1269
 
1280
1270
  // Need to just seed the source data in the code quorum. Quorum itself is empty
1281
1271
  const qValues = initQuorumValuesFromCodeDetails(source);
1282
- this._protocolHandler = await this.initializeProtocolState(
1272
+ this.initializeProtocolState(
1283
1273
  attributes,
1284
1274
  {
1285
1275
  members: [],
@@ -1304,27 +1294,26 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1304
1294
  }
1305
1295
 
1306
1296
  const snapshotTree = getSnapshotTreeFromSerializedContainer(detachedContainerSnapshot);
1307
- this._storage.loadSnapshotForRehydratingContainer(snapshotTree);
1308
- const attributes = await this.getDocumentAttributes(this._storage, snapshotTree);
1297
+ this.storageService.loadSnapshotForRehydratingContainer(snapshotTree);
1298
+ const attributes = await this.getDocumentAttributes(this.storageService, snapshotTree);
1309
1299
 
1310
1300
  await this.attachDeltaManagerOpHandler(attributes);
1311
1301
 
1312
1302
  // Initialize the protocol handler
1313
1303
  const baseTree = getProtocolSnapshotTree(snapshotTree);
1314
1304
  const qValues = await readAndParse<[string, ICommittedProposal][]>(
1315
- this._storage,
1305
+ this.storageService,
1316
1306
  baseTree.blobs.quorumValues,
1317
1307
  );
1318
1308
  const codeDetails = getCodeDetailsFromQuorumValues(qValues);
1319
- this._protocolHandler =
1320
- await this.initializeProtocolState(
1321
- attributes,
1322
- {
1323
- members: [],
1324
- proposals: [],
1325
- values: codeDetails !== undefined ? initQuorumValuesFromCodeDetails(codeDetails) : [],
1326
- }, // IQuorumSnapShot
1327
- );
1309
+ this.initializeProtocolState(
1310
+ attributes,
1311
+ {
1312
+ members: [],
1313
+ proposals: [],
1314
+ values: codeDetails !== undefined ? initQuorumValuesFromCodeDetails(codeDetails) : [],
1315
+ }, // IQuorumSnapShot
1316
+ );
1328
1317
 
1329
1318
  await this.instantiateContextDetached(
1330
1319
  true, // existing
@@ -1334,28 +1323,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1334
1323
  this.setLoaded();
1335
1324
  }
1336
1325
 
1337
- private async connectStorageService(): Promise<void> {
1338
- if (this._storageService !== undefined) {
1339
- return;
1340
- }
1341
-
1342
- assert(this.service !== undefined, 0x1ef /* "services must be defined" */);
1343
- const storageService = await this.service.connectToStorage();
1344
-
1345
- this._storageService =
1346
- new RetriableDocumentStorageService(storageService, this.mc.logger);
1347
-
1348
- if (this.options.summarizeProtocolTree === true) {
1349
- this.mc.logger.sendTelemetryEvent({ eventName: "summarizeProtocolTreeEnabled" });
1350
- this._storageService =
1351
- new ProtocolTreeStorageService(this._storageService, () => this.captureProtocolSummary());
1352
- }
1353
-
1354
- // ensure we did not lose that policy in the process of wrapping
1355
- assert(storageService.policies?.minBlobSize === this.storageService.policies?.minBlobSize,
1356
- 0x0e0 /* "lost minBlobSize policy" */);
1357
- }
1358
-
1359
1326
  private async getDocumentAttributes(
1360
1327
  storage: IDocumentStorageService,
1361
1328
  tree: ISnapshotTree | undefined,
@@ -1387,7 +1354,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1387
1354
  attributes: IDocumentAttributes,
1388
1355
  storage: IDocumentStorageService,
1389
1356
  snapshot: ISnapshotTree | undefined,
1390
- ): Promise<IProtocolHandler> {
1357
+ ): Promise<void> {
1391
1358
  const quorumSnapshot: IQuorumSnapshot = {
1392
1359
  members: [],
1393
1360
  proposals: [],
@@ -1403,20 +1370,19 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1403
1370
  ]);
1404
1371
  }
1405
1372
 
1406
- const protocolHandler = await this.initializeProtocolState(attributes, quorumSnapshot);
1407
- return protocolHandler;
1373
+ this.initializeProtocolState(attributes, quorumSnapshot);
1408
1374
  }
1409
1375
 
1410
- private async initializeProtocolState(
1376
+ private initializeProtocolState(
1411
1377
  attributes: IDocumentAttributes,
1412
1378
  quorumSnapshot: IQuorumSnapshot,
1413
- ): Promise<IProtocolHandler> {
1379
+ ): void {
1414
1380
  const protocolHandlerBuilder =
1415
1381
  this.protocolHandlerBuilder ?? ((...args) => new ProtocolHandler(...args, new Audience()));
1416
1382
  const protocol = protocolHandlerBuilder(
1417
1383
  attributes,
1418
1384
  quorumSnapshot,
1419
- (key, value) => this.submitMessage(MessageType.Propose, { key, value }),
1385
+ (key, value) => this.submitMessage(MessageType.Propose, JSON.stringify({ key, value })),
1420
1386
  this._initialClients ?? [],
1421
1387
  );
1422
1388
 
@@ -1452,8 +1418,11 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1452
1418
  });
1453
1419
  }
1454
1420
  });
1455
-
1456
- return protocol;
1421
+ // we need to make sure this member get set in a synchronous context,
1422
+ // or other things can happen after the object that will be set is created, but not yet set
1423
+ // this was breaking this._initialClients handling
1424
+ //
1425
+ this._protocolHandler = protocol;
1457
1426
  }
1458
1427
 
1459
1428
  private captureProtocolSummary(): ISummaryTree {
@@ -1557,15 +1526,9 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1557
1526
  }
1558
1527
  }
1559
1528
 
1560
- const deltaManagerForCatchingUp =
1561
- this.mc.config.getBoolean("Fluid.Container.CatchUpBeforeDeclaringConnected") === true ?
1562
- this.deltaManager
1563
- : undefined;
1564
-
1565
1529
  this.connectionStateHandler.receivedConnectEvent(
1566
1530
  this.connectionMode,
1567
1531
  details,
1568
- deltaManagerForCatchingUp,
1569
1532
  );
1570
1533
  });
1571
1534
 
@@ -1585,6 +1548,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1585
1548
  });
1586
1549
 
1587
1550
  deltaManager.on("readonly", (readonly) => {
1551
+ this.setContextConnectedState(this.connectionState === ConnectionState.Connected, readonly);
1588
1552
  this.emit("readonly", readonly);
1589
1553
  });
1590
1554
 
@@ -1639,11 +1603,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1639
1603
  opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
1640
1604
  }
1641
1605
  }
1642
- if (this.firstConnection) {
1643
- connectionInitiationReason = "InitialConnect";
1644
- } else {
1645
- connectionInitiationReason = "AutoReconnect";
1646
- }
1606
+ connectionInitiationReason = this.firstConnection ? "InitialConnect" : "AutoReconnect";
1647
1607
  }
1648
1608
 
1649
1609
  this.mc.logger.sendPerformanceEvent({
@@ -1669,7 +1629,17 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1669
1629
  }
1670
1630
  }
1671
1631
 
1672
- private propagateConnectionState() {
1632
+ private propagateConnectionState(initialTransition: boolean) {
1633
+ // When container loaded, we want to propagate initial connection state.
1634
+ // After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
1635
+ // This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
1636
+ if (!initialTransition &&
1637
+ this.connectionState !== ConnectionState.Connected &&
1638
+ this.connectionState !== ConnectionState.Disconnected) {
1639
+ return;
1640
+ }
1641
+ const state = this.connectionState === ConnectionState.Connected;
1642
+
1673
1643
  const logOpsOnReconnect: boolean =
1674
1644
  this.connectionState === ConnectionState.Connected &&
1675
1645
  !this.firstConnection &&
@@ -1678,13 +1648,9 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1678
1648
  this.messageCountAfterDisconnection = 0;
1679
1649
  }
1680
1650
 
1681
- const state = this.connectionState === ConnectionState.Connected;
1682
-
1683
1651
  // Both protocol and context should not be undefined if we got so far.
1684
1652
 
1685
- if (this._context?.disposed === false) {
1686
- this.context.setConnectionState(state, this.clientId);
1687
- }
1653
+ this.setContextConnectedState(state, this._deltaManager.connectionManager.readOnlyInfo.readonly ?? false);
1688
1654
  this.protocolHandler.setConnectionState(state, this.clientId);
1689
1655
  raiseConnectedEvent(this.mc.logger, this, state, this.clientId);
1690
1656
 
@@ -1694,35 +1660,53 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1694
1660
  }
1695
1661
  }
1696
1662
 
1663
+ // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
1697
1664
  private submitContainerMessage(type: MessageType, contents: any, batch?: boolean, metadata?: any): number {
1698
- const outboundMessageType: string = type;
1699
- switch (outboundMessageType) {
1665
+ switch (type) {
1700
1666
  case MessageType.Operation:
1701
- case MessageType.RemoteHelp:
1702
- break;
1703
- case MessageType.Summarize: {
1704
- // github #6451: this is only needed for staging so the server
1705
- // know when the protocol tree is included
1706
- // this can be removed once all clients send
1707
- // protocol tree by default
1708
- const summary = contents as ISummaryContent;
1709
- if (summary.details === undefined) {
1710
- summary.details = {};
1711
- }
1712
- summary.details.includesProtocolTree =
1713
- this.options.summarizeProtocolTree === true;
1714
- break;
1715
- }
1667
+ return this.submitMessage(
1668
+ type,
1669
+ JSON.stringify(contents),
1670
+ batch,
1671
+ metadata);
1672
+ case MessageType.Summarize:
1673
+ return this.submitSummaryMessage(contents as unknown as ISummaryContent);
1716
1674
  default:
1717
1675
  this.close(new GenericError("invalidContainerSubmitOpType",
1718
1676
  undefined /* error */,
1719
1677
  { messageType: type }));
1720
1678
  return -1;
1721
1679
  }
1722
- return this.submitMessage(type, contents, batch, metadata);
1723
1680
  }
1724
1681
 
1725
- private submitMessage(type: MessageType, contents: any, batch?: boolean, metadata?: any): number {
1682
+ /** @returns clientSequenceNumber of last message in a batch */
1683
+ private submitBatch(batch: IBatchMessage[]): number {
1684
+ let clientSequenceNumber = -1;
1685
+ for (const message of batch) {
1686
+ clientSequenceNumber = this.submitMessage(
1687
+ MessageType.Operation,
1688
+ message.contents,
1689
+ true, // batch
1690
+ message.metadata);
1691
+ }
1692
+ this._deltaManager.flush();
1693
+ return clientSequenceNumber;
1694
+ }
1695
+
1696
+ private submitSummaryMessage(summary: ISummaryContent) {
1697
+ // github #6451: this is only needed for staging so the server
1698
+ // know when the protocol tree is included
1699
+ // this can be removed once all clients send
1700
+ // protocol tree by default
1701
+ if (summary.details === undefined) {
1702
+ summary.details = {};
1703
+ }
1704
+ summary.details.includesProtocolTree =
1705
+ this.options.summarizeProtocolTree === true;
1706
+ return this.submitMessage(MessageType.Summarize, JSON.stringify(summary), false /* batch */);
1707
+ }
1708
+
1709
+ private submitMessage(type: MessageType, contents?: string, batch?: boolean, metadata?: any): number {
1726
1710
  if (this.connectionState !== ConnectionState.Connected) {
1727
1711
  this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
1728
1712
  return -1;
@@ -1733,28 +1717,14 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1733
1717
  return this._deltaManager.submit(type, contents, batch, metadata);
1734
1718
  }
1735
1719
 
1736
- private processRemoteMessage(message: ISequencedDocumentMessage): IProcessMessageResult {
1720
+ private processRemoteMessage(message: ISequencedDocumentMessage) {
1737
1721
  const local = this.clientId === message.clientId;
1738
1722
 
1739
1723
  // Allow the protocol handler to process the message
1740
- let result: IProcessMessageResult = { immediateNoOp: false };
1741
- try {
1742
- result = this.protocolHandler.processMessage(message, local);
1743
- } catch (error) {
1744
- this.close(wrapError(error, (errorMessage) =>
1745
- new DataCorruptionError(errorMessage, extractSafePropertiesFromMessage(message))));
1746
- }
1724
+ const result = this.protocolHandler.processMessage(message, local);
1747
1725
 
1748
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1749
- if (isUnpackedRuntimeMessage(message) && !isRuntimeMessage(message)) {
1750
- this.mc.logger.sendTelemetryEvent(
1751
- { eventName: "UnpackedRuntimeMessage", type: message.type });
1752
- }
1753
- // Forward non system messages to the loaded runtime for processing
1754
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1755
- if (isRuntimeMessage(message) || isUnpackedRuntimeMessage(message)) {
1756
- this.context.process(message, local, undefined);
1757
- }
1726
+ // Forward messages to the loaded runtime for processing
1727
+ this.context.process(message, local, undefined);
1758
1728
 
1759
1729
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
1760
1730
  if (this.activeConnection()) {
@@ -1767,10 +1737,10 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1767
1737
  this.serviceConfiguration !== undefined,
1768
1738
  0x2e4 /* "there should be service config for active connection" */);
1769
1739
  this.collabWindowTracker = new CollabWindowTracker(
1770
- (type, contents) => {
1740
+ (type) => {
1771
1741
  assert(this.activeConnection(),
1772
1742
  0x241 /* "disconnect should result in stopSequenceNumberUpdate() call" */);
1773
- this.submitMessage(type, contents);
1743
+ this.submitMessage(type);
1774
1744
  },
1775
1745
  this.serviceConfiguration.noopTimeFrequency,
1776
1746
  this.serviceConfiguration.noopCountFrequency,
@@ -1780,8 +1750,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1780
1750
  }
1781
1751
 
1782
1752
  this.emit("op", message);
1783
-
1784
- return result;
1785
1753
  }
1786
1754
 
1787
1755
  private submitSignal(message: any) {
@@ -1857,6 +1825,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1857
1825
  new QuorumProxy(this.protocolHandler.quorum),
1858
1826
  loader,
1859
1827
  (type, contents, batch, metadata) => this.submitContainerMessage(type, contents, batch, metadata),
1828
+ (summaryOp: ISummaryContent) => this.submitSummaryMessage(summaryOp),
1829
+ (batch: IBatchMessage[]) => this.submitBatch(batch),
1860
1830
  (message) => this.submitSignal(message),
1861
1831
  (error?: ICriticalContainerError) => this.close(error),
1862
1832
  Container.version,
@@ -1879,4 +1849,22 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1879
1849
  private logContainerError(warning: ContainerWarning) {
1880
1850
  this.mc.logger.sendErrorEvent({ eventName: "ContainerWarning" }, warning);
1881
1851
  }
1852
+
1853
+ /**
1854
+ * Set the connected state of the ContainerContext
1855
+ * This controls the "connected" state of the ContainerRuntime as well
1856
+ * @param state - Is the container currently connected?
1857
+ * @param readonly - Is the container in readonly mode?
1858
+ */
1859
+ private setContextConnectedState(state: boolean, readonly: boolean): void {
1860
+ if (this._context?.disposed === false) {
1861
+ /**
1862
+ * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
1863
+ * ops getting through to the DeltaManager.
1864
+ * The ContainerRuntime's "connected" state simply means it is ok to send ops
1865
+ * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
1866
+ */
1867
+ this.context.setConnectionState(state && !readonly, this.clientId);
1868
+ }
1869
+ }
1882
1870
  }