@fluidframework/container-loader 2.0.0-internal.1.1.3 → 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 +107 -125
  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 +110 -128
  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 +132 -147
  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;
@@ -1306,15 +1294,15 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1306
1294
  }
1307
1295
 
1308
1296
  const snapshotTree = getSnapshotTreeFromSerializedContainer(detachedContainerSnapshot);
1309
- this._storage.loadSnapshotForRehydratingContainer(snapshotTree);
1310
- const attributes = await this.getDocumentAttributes(this._storage, snapshotTree);
1297
+ this.storageService.loadSnapshotForRehydratingContainer(snapshotTree);
1298
+ const attributes = await this.getDocumentAttributes(this.storageService, snapshotTree);
1311
1299
 
1312
1300
  await this.attachDeltaManagerOpHandler(attributes);
1313
1301
 
1314
1302
  // Initialize the protocol handler
1315
1303
  const baseTree = getProtocolSnapshotTree(snapshotTree);
1316
1304
  const qValues = await readAndParse<[string, ICommittedProposal][]>(
1317
- this._storage,
1305
+ this.storageService,
1318
1306
  baseTree.blobs.quorumValues,
1319
1307
  );
1320
1308
  const codeDetails = getCodeDetailsFromQuorumValues(qValues);
@@ -1335,28 +1323,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1335
1323
  this.setLoaded();
1336
1324
  }
1337
1325
 
1338
- private async connectStorageService(): Promise<void> {
1339
- if (this._storageService !== undefined) {
1340
- return;
1341
- }
1342
-
1343
- assert(this.service !== undefined, 0x1ef /* "services must be defined" */);
1344
- const storageService = await this.service.connectToStorage();
1345
-
1346
- this._storageService =
1347
- new RetriableDocumentStorageService(storageService, this.mc.logger);
1348
-
1349
- if (this.options.summarizeProtocolTree === true) {
1350
- this.mc.logger.sendTelemetryEvent({ eventName: "summarizeProtocolTreeEnabled" });
1351
- this._storageService =
1352
- new ProtocolTreeStorageService(this._storageService, () => this.captureProtocolSummary());
1353
- }
1354
-
1355
- // ensure we did not lose that policy in the process of wrapping
1356
- assert(storageService.policies?.minBlobSize === this.storageService.policies?.minBlobSize,
1357
- 0x0e0 /* "lost minBlobSize policy" */);
1358
- }
1359
-
1360
1326
  private async getDocumentAttributes(
1361
1327
  storage: IDocumentStorageService,
1362
1328
  tree: ISnapshotTree | undefined,
@@ -1416,7 +1382,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
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
 
@@ -1560,15 +1526,9 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1560
1526
  }
1561
1527
  }
1562
1528
 
1563
- const deltaManagerForCatchingUp =
1564
- this.mc.config.getBoolean("Fluid.Container.CatchUpBeforeDeclaringConnected") === true ?
1565
- this.deltaManager
1566
- : undefined;
1567
-
1568
1529
  this.connectionStateHandler.receivedConnectEvent(
1569
1530
  this.connectionMode,
1570
1531
  details,
1571
- deltaManagerForCatchingUp,
1572
1532
  );
1573
1533
  });
1574
1534
 
@@ -1588,6 +1548,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1588
1548
  });
1589
1549
 
1590
1550
  deltaManager.on("readonly", (readonly) => {
1551
+ this.setContextConnectedState(this.connectionState === ConnectionState.Connected, readonly);
1591
1552
  this.emit("readonly", readonly);
1592
1553
  });
1593
1554
 
@@ -1642,11 +1603,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1642
1603
  opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
1643
1604
  }
1644
1605
  }
1645
- if (this.firstConnection) {
1646
- connectionInitiationReason = "InitialConnect";
1647
- } else {
1648
- connectionInitiationReason = "AutoReconnect";
1649
- }
1606
+ connectionInitiationReason = this.firstConnection ? "InitialConnect" : "AutoReconnect";
1650
1607
  }
1651
1608
 
1652
1609
  this.mc.logger.sendPerformanceEvent({
@@ -1672,7 +1629,17 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1672
1629
  }
1673
1630
  }
1674
1631
 
1675
- 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
+
1676
1643
  const logOpsOnReconnect: boolean =
1677
1644
  this.connectionState === ConnectionState.Connected &&
1678
1645
  !this.firstConnection &&
@@ -1681,13 +1648,9 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1681
1648
  this.messageCountAfterDisconnection = 0;
1682
1649
  }
1683
1650
 
1684
- const state = this.connectionState === ConnectionState.Connected;
1685
-
1686
1651
  // Both protocol and context should not be undefined if we got so far.
1687
1652
 
1688
- if (this._context?.disposed === false) {
1689
- this.context.setConnectionState(state, this.clientId);
1690
- }
1653
+ this.setContextConnectedState(state, this._deltaManager.connectionManager.readOnlyInfo.readonly ?? false);
1691
1654
  this.protocolHandler.setConnectionState(state, this.clientId);
1692
1655
  raiseConnectedEvent(this.mc.logger, this, state, this.clientId);
1693
1656
 
@@ -1697,35 +1660,53 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1697
1660
  }
1698
1661
  }
1699
1662
 
1663
+ // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
1700
1664
  private submitContainerMessage(type: MessageType, contents: any, batch?: boolean, metadata?: any): number {
1701
- const outboundMessageType: string = type;
1702
- switch (outboundMessageType) {
1665
+ switch (type) {
1703
1666
  case MessageType.Operation:
1704
- case MessageType.RemoteHelp:
1705
- break;
1706
- case MessageType.Summarize: {
1707
- // github #6451: this is only needed for staging so the server
1708
- // know when the protocol tree is included
1709
- // this can be removed once all clients send
1710
- // protocol tree by default
1711
- const summary = contents as ISummaryContent;
1712
- if (summary.details === undefined) {
1713
- summary.details = {};
1714
- }
1715
- summary.details.includesProtocolTree =
1716
- this.options.summarizeProtocolTree === true;
1717
- break;
1718
- }
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);
1719
1674
  default:
1720
1675
  this.close(new GenericError("invalidContainerSubmitOpType",
1721
1676
  undefined /* error */,
1722
1677
  { messageType: type }));
1723
1678
  return -1;
1724
1679
  }
1725
- return this.submitMessage(type, contents, batch, metadata);
1726
1680
  }
1727
1681
 
1728
- 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 {
1729
1710
  if (this.connectionState !== ConnectionState.Connected) {
1730
1711
  this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
1731
1712
  return -1;
@@ -1736,28 +1717,14 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1736
1717
  return this._deltaManager.submit(type, contents, batch, metadata);
1737
1718
  }
1738
1719
 
1739
- private processRemoteMessage(message: ISequencedDocumentMessage): IProcessMessageResult {
1720
+ private processRemoteMessage(message: ISequencedDocumentMessage) {
1740
1721
  const local = this.clientId === message.clientId;
1741
1722
 
1742
1723
  // Allow the protocol handler to process the message
1743
- let result: IProcessMessageResult = { immediateNoOp: false };
1744
- try {
1745
- result = this.protocolHandler.processMessage(message, local);
1746
- } catch (error) {
1747
- this.close(wrapError(error, (errorMessage) =>
1748
- new DataCorruptionError(errorMessage, extractSafePropertiesFromMessage(message))));
1749
- }
1724
+ const result = this.protocolHandler.processMessage(message, local);
1750
1725
 
1751
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1752
- if (isUnpackedRuntimeMessage(message) && !isRuntimeMessage(message)) {
1753
- this.mc.logger.sendTelemetryEvent(
1754
- { eventName: "UnpackedRuntimeMessage", type: message.type });
1755
- }
1756
- // Forward non system messages to the loaded runtime for processing
1757
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1758
- if (isRuntimeMessage(message) || isUnpackedRuntimeMessage(message)) {
1759
- this.context.process(message, local, undefined);
1760
- }
1726
+ // Forward messages to the loaded runtime for processing
1727
+ this.context.process(message, local, undefined);
1761
1728
 
1762
1729
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
1763
1730
  if (this.activeConnection()) {
@@ -1770,10 +1737,10 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1770
1737
  this.serviceConfiguration !== undefined,
1771
1738
  0x2e4 /* "there should be service config for active connection" */);
1772
1739
  this.collabWindowTracker = new CollabWindowTracker(
1773
- (type, contents) => {
1740
+ (type) => {
1774
1741
  assert(this.activeConnection(),
1775
1742
  0x241 /* "disconnect should result in stopSequenceNumberUpdate() call" */);
1776
- this.submitMessage(type, contents);
1743
+ this.submitMessage(type);
1777
1744
  },
1778
1745
  this.serviceConfiguration.noopTimeFrequency,
1779
1746
  this.serviceConfiguration.noopCountFrequency,
@@ -1783,8 +1750,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1783
1750
  }
1784
1751
 
1785
1752
  this.emit("op", message);
1786
-
1787
- return result;
1788
1753
  }
1789
1754
 
1790
1755
  private submitSignal(message: any) {
@@ -1860,6 +1825,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1860
1825
  new QuorumProxy(this.protocolHandler.quorum),
1861
1826
  loader,
1862
1827
  (type, contents, batch, metadata) => this.submitContainerMessage(type, contents, batch, metadata),
1828
+ (summaryOp: ISummaryContent) => this.submitSummaryMessage(summaryOp),
1829
+ (batch: IBatchMessage[]) => this.submitBatch(batch),
1863
1830
  (message) => this.submitSignal(message),
1864
1831
  (error?: ICriticalContainerError) => this.close(error),
1865
1832
  Container.version,
@@ -1882,4 +1849,22 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1882
1849
  private logContainerError(warning: ContainerWarning) {
1883
1850
  this.mc.logger.sendErrorEvent({ eventName: "ContainerWarning" }, warning);
1884
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
+ }
1885
1870
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
- import { assert, LazyPromise } from "@fluidframework/common-utils";
7
+ import { LazyPromise } from "@fluidframework/common-utils";
8
8
  import {
9
9
  IAudience,
10
10
  IContainerContext,
@@ -22,6 +22,7 @@ import {
22
22
  ICodeDetailsLoader,
23
23
  IFluidModuleWithDetails,
24
24
  ISnapshotTreeWithBlobContents,
25
+ IBatchMessage,
25
26
  } from "@fluidframework/container-definitions";
26
27
  import {
27
28
  IRequest,
@@ -42,6 +43,7 @@ import {
42
43
  ISummaryTree,
43
44
  IVersion,
44
45
  MessageType,
46
+ ISummaryContent,
45
47
  } from "@fluidframework/protocol-definitions";
46
48
  import { PerformanceEvent } from "@fluidframework/telemetry-utils";
47
49
  import { Container } from "./container";
@@ -59,6 +61,8 @@ export class ContainerContext implements IContainerContext {
59
61
  quorum: IQuorum,
60
62
  loader: ILoader,
61
63
  submitFn: (type: MessageType, contents: any, batch: boolean, appData: any) => number,
64
+ submitSummaryFn: (summaryOp: ISummaryContent) => number,
65
+ submitBatchFn: (batch: IBatchMessage[]) => number,
62
66
  submitSignalFn: (contents: any) => void,
63
67
  closeFn: (error?: ICriticalContainerError) => void,
64
68
  version: string,
@@ -76,6 +80,8 @@ export class ContainerContext implements IContainerContext {
76
80
  quorum,
77
81
  loader,
78
82
  submitFn,
83
+ submitSummaryFn,
84
+ submitBatchFn,
79
85
  submitSignalFn,
80
86
  closeFn,
81
87
  version,
@@ -107,8 +113,13 @@ export class ContainerContext implements IContainerContext {
107
113
  return this.container.clientDetails;
108
114
  }
109
115
 
116
+ private _connected: boolean;
117
+ /**
118
+ * When true, ops are free to flow
119
+ * When false, ops should be kept as pending or rejected
120
+ */
110
121
  public get connected(): boolean {
111
- return this.container.connected;
122
+ return this._connected;
112
123
  }
113
124
 
114
125
  public get canSummarize(): boolean {
@@ -166,6 +177,9 @@ export class ContainerContext implements IContainerContext {
166
177
  quorum: IQuorum,
167
178
  public readonly loader: ILoader,
168
179
  public readonly submitFn: (type: MessageType, contents: any, batch: boolean, appData: any) => number,
180
+ public readonly submitSummaryFn: (summaryOp: ISummaryContent) => number,
181
+ /** @returns clientSequenceNumber of last message in a batch */
182
+ public readonly submitBatchFn: (batch: IBatchMessage[]) => number,
169
183
  public readonly submitSignalFn: (contents: any) => void,
170
184
  public readonly closeFn: (error?: ICriticalContainerError) => void,
171
185
  public readonly version: string,
@@ -174,6 +188,7 @@ export class ContainerContext implements IContainerContext {
174
188
  public readonly pendingLocalState?: unknown,
175
189
 
176
190
  ) {
191
+ this._connected = this.container.connected;
177
192
  this._quorum = quorum;
178
193
  this.taggedLogger = container.subLogger;
179
194
  this._fluidModuleP = new LazyPromise<IFluidModuleWithDetails>(
@@ -183,9 +198,10 @@ export class ContainerContext implements IContainerContext {
183
198
  }
184
199
 
185
200
  /**
186
- * @deprecated - Temporary migratory API, to be removed when customers no longer need it. When removed,
187
- * ContainerContext should only take an IQuorumClients rather than an IQuorum. See IContainerContext for more
188
- * details.
201
+ * @deprecated Temporary migratory API, to be removed when customers no longer need it.
202
+ * When removed, `ContainerContext` should only take an {@link @fluidframework/container-definitions#IQuorumClients}
203
+ * rather than an {@link @fluidframework/protocol-definitions#IQuorum}.
204
+ * See {@link @fluidframework/container-definitions#IContainerContext} for more details.
189
205
  */
190
206
  public getSpecifiedCodeDetails(): IFluidCodeDetails | undefined {
191
207
  return (this._quorum.get("code") ?? this._quorum.get("code2")) as IFluidCodeDetails | undefined;
@@ -223,9 +239,7 @@ export class ContainerContext implements IContainerContext {
223
239
 
224
240
  public setConnectionState(connected: boolean, clientId?: string) {
225
241
  const runtime = this.runtime;
226
-
227
- assert(connected === this.connected, 0x0de /* "Mismatch in connection state while setting" */);
228
-
242
+ this._connected = connected;
229
243
  runtime.setConnectionState(connected, clientId);
230
244
  }
231
245