@fluidframework/container-runtime 2.41.0 → 2.42.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 (134) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/container-runtime.test-files.tar +0 -0
  3. package/dist/channelCollection.d.ts +1 -1
  4. package/dist/channelCollection.d.ts.map +1 -1
  5. package/dist/channelCollection.js +4 -4
  6. package/dist/channelCollection.js.map +1 -1
  7. package/dist/compatUtils.d.ts +22 -1
  8. package/dist/compatUtils.d.ts.map +1 -1
  9. package/dist/compatUtils.js +109 -7
  10. package/dist/compatUtils.js.map +1 -1
  11. package/dist/containerRuntime.d.ts +34 -13
  12. package/dist/containerRuntime.d.ts.map +1 -1
  13. package/dist/containerRuntime.js +158 -59
  14. package/dist/containerRuntime.js.map +1 -1
  15. package/dist/dataStore.d.ts.map +1 -1
  16. package/dist/dataStore.js +5 -0
  17. package/dist/dataStore.js.map +1 -1
  18. package/dist/gc/garbageCollection.d.ts.map +1 -1
  19. package/dist/gc/garbageCollection.js +2 -0
  20. package/dist/gc/garbageCollection.js.map +1 -1
  21. package/dist/gc/gcDefinitions.d.ts +1 -1
  22. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  23. package/dist/gc/gcDefinitions.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/messageTypes.d.ts +5 -4
  28. package/dist/messageTypes.d.ts.map +1 -1
  29. package/dist/messageTypes.js.map +1 -1
  30. package/dist/metadata.d.ts +1 -1
  31. package/dist/metadata.d.ts.map +1 -1
  32. package/dist/metadata.js.map +1 -1
  33. package/dist/opLifecycle/definitions.d.ts +6 -5
  34. package/dist/opLifecycle/definitions.d.ts.map +1 -1
  35. package/dist/opLifecycle/definitions.js.map +1 -1
  36. package/dist/opLifecycle/index.d.ts +1 -1
  37. package/dist/opLifecycle/index.d.ts.map +1 -1
  38. package/dist/opLifecycle/index.js.map +1 -1
  39. package/dist/opLifecycle/opGroupingManager.d.ts +9 -0
  40. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  41. package/dist/opLifecycle/opGroupingManager.js +6 -4
  42. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  43. package/dist/opLifecycle/opSerialization.d.ts +2 -1
  44. package/dist/opLifecycle/opSerialization.d.ts.map +1 -1
  45. package/dist/opLifecycle/opSerialization.js.map +1 -1
  46. package/dist/packageVersion.d.ts +1 -1
  47. package/dist/packageVersion.js +1 -1
  48. package/dist/packageVersion.js.map +1 -1
  49. package/dist/pendingStateManager.d.ts +18 -5
  50. package/dist/pendingStateManager.d.ts.map +1 -1
  51. package/dist/pendingStateManager.js +20 -13
  52. package/dist/pendingStateManager.js.map +1 -1
  53. package/dist/summary/documentSchema.d.ts +42 -18
  54. package/dist/summary/documentSchema.d.ts.map +1 -1
  55. package/dist/summary/documentSchema.js +62 -52
  56. package/dist/summary/documentSchema.js.map +1 -1
  57. package/dist/summary/index.d.ts +1 -1
  58. package/dist/summary/index.d.ts.map +1 -1
  59. package/dist/summary/index.js.map +1 -1
  60. package/lib/channelCollection.d.ts +1 -1
  61. package/lib/channelCollection.d.ts.map +1 -1
  62. package/lib/channelCollection.js +4 -4
  63. package/lib/channelCollection.js.map +1 -1
  64. package/lib/compatUtils.d.ts +22 -1
  65. package/lib/compatUtils.d.ts.map +1 -1
  66. package/lib/compatUtils.js +102 -3
  67. package/lib/compatUtils.js.map +1 -1
  68. package/lib/containerRuntime.d.ts +34 -13
  69. package/lib/containerRuntime.d.ts.map +1 -1
  70. package/lib/containerRuntime.js +160 -61
  71. package/lib/containerRuntime.js.map +1 -1
  72. package/lib/dataStore.d.ts.map +1 -1
  73. package/lib/dataStore.js +5 -0
  74. package/lib/dataStore.js.map +1 -1
  75. package/lib/gc/garbageCollection.d.ts.map +1 -1
  76. package/lib/gc/garbageCollection.js +2 -0
  77. package/lib/gc/garbageCollection.js.map +1 -1
  78. package/lib/gc/gcDefinitions.d.ts +1 -1
  79. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  80. package/lib/gc/gcDefinitions.js.map +1 -1
  81. package/lib/index.d.ts +1 -1
  82. package/lib/index.d.ts.map +1 -1
  83. package/lib/index.js.map +1 -1
  84. package/lib/messageTypes.d.ts +5 -4
  85. package/lib/messageTypes.d.ts.map +1 -1
  86. package/lib/messageTypes.js.map +1 -1
  87. package/lib/metadata.d.ts +1 -1
  88. package/lib/metadata.d.ts.map +1 -1
  89. package/lib/metadata.js.map +1 -1
  90. package/lib/opLifecycle/definitions.d.ts +6 -5
  91. package/lib/opLifecycle/definitions.d.ts.map +1 -1
  92. package/lib/opLifecycle/definitions.js.map +1 -1
  93. package/lib/opLifecycle/index.d.ts +1 -1
  94. package/lib/opLifecycle/index.d.ts.map +1 -1
  95. package/lib/opLifecycle/index.js.map +1 -1
  96. package/lib/opLifecycle/opGroupingManager.d.ts +9 -0
  97. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  98. package/lib/opLifecycle/opGroupingManager.js +6 -4
  99. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  100. package/lib/opLifecycle/opSerialization.d.ts +2 -1
  101. package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
  102. package/lib/opLifecycle/opSerialization.js.map +1 -1
  103. package/lib/packageVersion.d.ts +1 -1
  104. package/lib/packageVersion.js +1 -1
  105. package/lib/packageVersion.js.map +1 -1
  106. package/lib/pendingStateManager.d.ts +18 -5
  107. package/lib/pendingStateManager.d.ts.map +1 -1
  108. package/lib/pendingStateManager.js +20 -13
  109. package/lib/pendingStateManager.js.map +1 -1
  110. package/lib/summary/documentSchema.d.ts +42 -18
  111. package/lib/summary/documentSchema.d.ts.map +1 -1
  112. package/lib/summary/documentSchema.js +62 -52
  113. package/lib/summary/documentSchema.js.map +1 -1
  114. package/lib/summary/index.d.ts +1 -1
  115. package/lib/summary/index.d.ts.map +1 -1
  116. package/lib/summary/index.js.map +1 -1
  117. package/package.json +18 -18
  118. package/src/channelCollection.ts +4 -4
  119. package/src/compatUtils.ts +145 -10
  120. package/src/containerRuntime.ts +209 -73
  121. package/src/dataStore.ts +7 -0
  122. package/src/gc/garbageCollection.ts +2 -0
  123. package/src/gc/gcDefinitions.ts +1 -1
  124. package/src/index.ts +2 -1
  125. package/src/messageTypes.ts +12 -5
  126. package/src/metadata.ts +1 -1
  127. package/src/opLifecycle/definitions.ts +7 -3
  128. package/src/opLifecycle/index.ts +1 -0
  129. package/src/opLifecycle/opGroupingManager.ts +17 -4
  130. package/src/opLifecycle/opSerialization.ts +6 -1
  131. package/src/packageVersion.ts +1 -1
  132. package/src/pendingStateManager.ts +49 -22
  133. package/src/summary/documentSchema.ts +111 -86
  134. package/src/summary/index.ts +2 -1
@@ -197,6 +197,20 @@ async function loadContainerRuntime(params) {
197
197
  }
198
198
  exports.loadContainerRuntime = loadContainerRuntime;
199
199
  const defaultMaxConsecutiveReconnects = 7;
200
+ /**
201
+ * These are the ONLY message types that are allowed to be submitted while in staging mode
202
+ * (Does not apply to pre-StagingMode batches that are resubmitted, those are not considered to be staged)
203
+ */
204
+ function canStageMessageOfType(type) {
205
+ return (
206
+ // These are user changes coming up from the runtime's DataStores
207
+ type === messageTypes_js_1.ContainerMessageType.FluidDataStoreOp ||
208
+ // GC ops are used to detect issues in the reference graph so all clients can repair their GC state.
209
+ // These can be submitted at any time, including while in Staging Mode.
210
+ type === messageTypes_js_1.ContainerMessageType.GC ||
211
+ // These are typically sent shortly after boot and will not be common in Staging Mode, but it's possible.
212
+ type === messageTypes_js_1.ContainerMessageType.DocumentSchemaChange);
213
+ }
200
214
  /**
201
215
  * Represents the runtime of the container. Contains helper functions/state of the container.
202
216
  * It will define the store level mappings.
@@ -245,6 +259,9 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
245
259
  if (!(0, compatUtils_js_1.isValidMinVersionForCollab)(minVersionForCollab)) {
246
260
  throw new internal_8.UsageError(`Invalid minVersionForCollab: ${minVersionForCollab}. It must be an existing FF version (i.e. 2.22.1).`);
247
261
  }
262
+ // We also validate that there is not a mismatch between `minVersionForCollab` and runtime options that
263
+ // were manually set.
264
+ (0, compatUtils_js_1.validateRuntimeOptions)(minVersionForCollab, runtimeOptions);
248
265
  const defaultsAffectingDocSchema = (0, compatUtils_js_1.getMinVersionForCollabDefaults)(minVersionForCollab);
249
266
  // The following are the default values for the options that do not affect the DocumentSchema.
250
267
  const defaultsNotAffectingDocSchema = {
@@ -510,8 +527,12 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
510
527
  ensureNoDataModelChanges(callback) {
511
528
  return this.dataModelChangeRunner.run(callback);
512
529
  }
530
+ /**
531
+ * Indicates whether the container is in a state where it is able to send
532
+ * ops (connected to op stream and not in readonly mode).
533
+ */
513
534
  get connected() {
514
- return this._connected;
535
+ return this.canSendOps;
515
536
  }
516
537
  /**
517
538
  * clientId of parent (non-summarizing) container that owns summarizer container
@@ -599,7 +620,10 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
599
620
  // eslint-disable-next-line import/no-deprecated
600
621
  this.enterStagingMode = () => {
601
622
  if (this.stageControls !== undefined) {
602
- throw new Error("already in staging mode");
623
+ throw new internal_8.UsageError("already in staging mode");
624
+ }
625
+ if (this.attachState === container_definitions_1.AttachState.Detached) {
626
+ throw new internal_8.UsageError("cannot enter staging mode while detached");
603
627
  }
604
628
  // Make sure all BatchManagers are empty before entering staging mode,
605
629
  // since we mark whole batches as "staged" or not to indicate whether to submit them.
@@ -620,7 +644,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
620
644
  // Pop all staged batches from the PSM and roll them back in LIFO order
621
645
  this.pendingStateManager.popStagedBatches(({ runtimeOp, localOpMetadata }) => {
622
646
  (0, internal_2.assert)(runtimeOp !== undefined, 0xb82 /* Staged batches expected to have runtimeOp defined */);
623
- this.rollback(runtimeOp, localOpMetadata);
647
+ this.rollbackStagedChanges(runtimeOp, localOpMetadata);
624
648
  });
625
649
  this.updateDocumentDirtyState();
626
650
  }),
@@ -657,6 +681,11 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
657
681
  this.mc = (0, internal_8.createChildMonitoringContext)({
658
682
  logger: this.baseLogger,
659
683
  namespace: "ContainerRuntime",
684
+ properties: {
685
+ all: {
686
+ inStagingMode: this.inStagingMode,
687
+ },
688
+ },
660
689
  });
661
690
  // If we support multiple algorithms in the future, then we would need to manage it here carefully.
662
691
  // We can use runtimeOptions.compressionOptions.compressionAlgorithm, but only if it's in the schema list!
@@ -697,7 +726,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
697
726
  // Values are generally expected to be set from the runtime side.
698
727
  this.options = options ?? {};
699
728
  this.clientDetails = clientDetails;
700
- const isSummarizerClient = this.clientDetails.type === index_js_4.summarizerClientType;
729
+ this.isSummarizerClient = this.clientDetails.type === index_js_4.summarizerClientType;
701
730
  this.loadedFromVersionId = context.getLoadedFromVersion()?.id;
702
731
  // eslint-disable-next-line unicorn/consistent-destructuring
703
732
  this._getClientId = () => context.clientId;
@@ -734,7 +763,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
734
763
  details: { attachState: this.attachState },
735
764
  }));
736
765
  // In cases of summarizer, we want to dispose instead since consumer doesn't interact with this container
737
- this.closeFn = isSummarizerClient ? this.disposeFn : closeFn;
766
+ this.closeFn = this.isSummarizerClient ? this.disposeFn : closeFn;
738
767
  let loadSummaryNumber;
739
768
  // Get the container creation metadata. For new container, we initialize these. For existing containers,
740
769
  // get the values from the metadata blob.
@@ -758,7 +787,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
758
787
  this.messageAtLastSummary = lastMessageFromMetadata(metadata);
759
788
  // Note that we only need to pull the *initial* connected state from the context.
760
789
  // Later updates come through calls to setConnectionState.
761
- this._connected = connected;
790
+ this.canSendOps = connected;
762
791
  this.mc.logger.sendTelemetryEvent({
763
792
  eventName: "GCFeatureMatrix",
764
793
  metadataValue: JSON.stringify(metadata?.gcFeatureMatrix),
@@ -851,7 +880,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
851
880
  existing,
852
881
  metadata,
853
882
  createContainerMetadata: this.createContainerMetadata,
854
- isSummarizerClient,
883
+ isSummarizerClient: this.isSummarizerClient,
855
884
  getNodePackagePath: async (nodePath) => this.getGCNodePackagePath(nodePath),
856
885
  getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
857
886
  readAndParseBlob: async (id) => (0, internal_4.readAndParse)(this.storage, id),
@@ -935,7 +964,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
935
964
  // Keep the old flag name even though we renamed the class member (it shipped in 2.31.0)
936
965
  this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableFlushBeforeProcess") === true;
937
966
  this.outbox = new index_js_3.Outbox({
938
- shouldSend: () => this.canSendOps(),
967
+ shouldSend: () => this.shouldSendOps(),
939
968
  pendingStateManager: this.pendingStateManager,
940
969
  submitBatchFn,
941
970
  legacySendBatchFn,
@@ -1074,7 +1103,14 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1074
1103
  await this.initializeSummarizer(loader);
1075
1104
  if (this.sessionSchema.idCompressorMode === "on" ||
1076
1105
  (this.sessionSchema.idCompressorMode === "delayed" && this.connected)) {
1077
- this._idCompressor = this.createIdCompressorFn();
1106
+ internal_8.PerformanceEvent.timedExec(this.mc.logger, { eventName: "CreateIdCompressorOnBoot" }, (event) => {
1107
+ this._idCompressor = this.createIdCompressorFn();
1108
+ event.end({
1109
+ details: {
1110
+ idCompressorMode: this.sessionSchema.idCompressorMode,
1111
+ },
1112
+ });
1113
+ });
1078
1114
  // This is called from loadRuntime(), long before we process any ops, so there should be no ops accumulated yet.
1079
1115
  (0, internal_2.assert)(this.pendingIdCompressorOps.length === 0, 0x8ec /* no pending ops */);
1080
1116
  }
@@ -1103,8 +1139,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1103
1139
  const orderedClientCollection = new index_js_4.OrderedClientCollection(orderedClientLogger, this.innerDeltaManager, this._quorum);
1104
1140
  const orderedClientElectionForSummarizer = new index_js_4.OrderedClientElection(orderedClientLogger, orderedClientCollection, this.electedSummarizerData ?? this.innerDeltaManager.lastSequenceNumber, index_js_4.SummarizerClientElection.isClientEligible, this.mc.config.getBoolean("Fluid.ContainerRuntime.OrderedClientElection.EnablePerformanceEvents"));
1105
1141
  this.summarizerClientElection = new index_js_4.SummarizerClientElection(orderedClientLogger, summaryCollection, orderedClientElectionForSummarizer, maxOpsSinceLastSummary);
1106
- const isSummarizerClient = this.clientDetails.type === index_js_4.summarizerClientType;
1107
- if (isSummarizerClient) {
1142
+ if (this.isSummarizerClient) {
1108
1143
  // We want to dynamically import any thing inside summaryDelayLoadedModule module only when we are the summarizer client,
1109
1144
  // so that all non summarizer clients don't have to load the code inside this module.
1110
1145
  const module = await import(
@@ -1452,7 +1487,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1452
1487
  }
1453
1488
  replayPendingStates() {
1454
1489
  // We need to be able to send ops to replay states
1455
- if (!this.canSendOps()) {
1490
+ if (!this.shouldSendOps()) {
1456
1491
  return;
1457
1492
  }
1458
1493
  // Replaying is an internal operation and we don't want to generate noise while doing it.
@@ -1534,25 +1569,35 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1534
1569
  loadIdCompressor() {
1535
1570
  if (this._idCompressor === undefined &&
1536
1571
  this.sessionSchema.idCompressorMode !== undefined) {
1537
- this._idCompressor = this.createIdCompressorFn();
1538
- // Finalize any ranges we received while the compressor was turned off.
1539
- const ops = this.pendingIdCompressorOps;
1540
- this.pendingIdCompressorOps = [];
1541
- for (const range of ops) {
1542
- this._idCompressor.finalizeCreationRange(range);
1543
- }
1572
+ internal_8.PerformanceEvent.timedExec(this.mc.logger, { eventName: "CreateIdCompressorOnDelayedLoad" }, (event) => {
1573
+ this._idCompressor = this.createIdCompressorFn();
1574
+ // Finalize any ranges we received while the compressor was turned off.
1575
+ const ops = this.pendingIdCompressorOps;
1576
+ this.pendingIdCompressorOps = [];
1577
+ const trace = client_utils_1.Trace.start();
1578
+ for (const range of ops) {
1579
+ this._idCompressor.finalizeCreationRange(range);
1580
+ }
1581
+ event.end({
1582
+ details: {
1583
+ finalizeCreationRangeDuration: trace.trace().duration,
1584
+ idCompressorMode: this.sessionSchema.idCompressorMode,
1585
+ pendingIdCompressorOps: ops.length,
1586
+ },
1587
+ });
1588
+ });
1544
1589
  (0, internal_2.assert)(this.pendingIdCompressorOps.length === 0, 0x976 /* No new ops added */);
1545
1590
  }
1546
1591
  }
1547
- setConnectionState(connected, clientId) {
1592
+ setConnectionState(canSendOps, clientId) {
1548
1593
  // Validate we have consistent state
1549
1594
  const currentClientId = this._audience.getSelf()?.clientId;
1550
1595
  (0, internal_2.assert)(clientId === currentClientId, 0x977 /* input clientId does not match Audience */);
1551
1596
  (0, internal_2.assert)(this.clientId === currentClientId, 0x978 /* this.clientId does not match Audience */);
1552
- if (connected && this.sessionSchema.idCompressorMode === "delayed") {
1597
+ if (canSendOps && this.sessionSchema.idCompressorMode === "delayed") {
1553
1598
  this.loadIdCompressor();
1554
1599
  }
1555
- if (connected === false && this.delayConnectClientId !== undefined) {
1600
+ if (canSendOps === false && this.delayConnectClientId !== undefined) {
1556
1601
  this.delayConnectClientId = undefined;
1557
1602
  this.mc.logger.sendTelemetryEvent({
1558
1603
  eventName: "UnsuccessfulConnectedTransition",
@@ -1560,37 +1605,39 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1560
1605
  // Don't propagate "disconnected" event because we didn't propagate the previous "connected" event
1561
1606
  return;
1562
1607
  }
1563
- if (!connected) {
1564
- this.documentsSchemaController.onDisconnect();
1565
- }
1566
1608
  // If there are stashed blobs in the pending state, we need to delay
1567
1609
  // propagation of the "connected" event until we have uploaded them to
1568
1610
  // ensure we don't submit ops referencing a blob that has not been uploaded
1569
- const connecting = connected && !this._connected;
1611
+ const connecting = canSendOps && !this.canSendOps;
1570
1612
  if (connecting && this.blobManager.hasPendingStashedUploads()) {
1571
1613
  (0, internal_2.assert)(!this.delayConnectClientId, 0x791 /* Connect event delay must be canceled before subsequent connect event */);
1572
1614
  (0, internal_2.assert)(!!clientId, 0x792 /* Must have clientId when connecting */);
1573
1615
  this.delayConnectClientId = clientId;
1574
1616
  return;
1575
1617
  }
1576
- this.setConnectionStateCore(connected, clientId);
1618
+ this.setConnectionStateCore(canSendOps, clientId);
1577
1619
  }
1578
- setConnectionStateCore(connected, clientId) {
1620
+ /**
1621
+ * Raises and propagates connected events.
1622
+ * @param canSendOps - Indicates whether the container can send ops or not (connected and not readonly).
1623
+ * @remarks The connection state from container context used here when raising connected events.
1624
+ */
1625
+ setConnectionStateCore(canSendOps, clientId) {
1579
1626
  (0, internal_2.assert)(!this.delayConnectClientId, 0x394 /* connect event delay must be cleared before propagating connect event */);
1580
1627
  this.verifyNotClosed();
1581
1628
  // There might be no change of state due to Container calling this API after loading runtime.
1582
- const changeOfState = this._connected !== connected;
1583
- const reconnection = changeOfState && !connected;
1629
+ const canSendOpsChanged = this.canSendOps !== canSendOps;
1630
+ const reconnection = canSendOpsChanged && !canSendOps;
1584
1631
  // We need to flush the ops currently collected by Outbox to preserve original order.
1585
1632
  // This flush NEEDS to happen before we set the ContainerRuntime to "connected".
1586
1633
  // We want these ops to get to the PendingStateManager without sending to service and have them return to the Outbox upon calling "replayPendingStates".
1587
- if (changeOfState && connected) {
1634
+ if (canSendOpsChanged && canSendOps) {
1588
1635
  this.flush();
1589
1636
  }
1590
- this._connected = connected;
1591
- if (connected) {
1637
+ this.canSendOps = canSendOps;
1638
+ if (canSendOps) {
1592
1639
  (0, internal_2.assert)(this.attachState === container_definitions_1.AttachState.Attached, 0x3cd /* Connection is possible only if container exists in storage */);
1593
- if (changeOfState) {
1640
+ if (canSendOpsChanged) {
1594
1641
  this.signalTelemetryManager.resetTracking();
1595
1642
  }
1596
1643
  }
@@ -1606,12 +1653,12 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1606
1653
  return;
1607
1654
  }
1608
1655
  }
1609
- if (changeOfState) {
1656
+ if (canSendOpsChanged) {
1610
1657
  this.replayPendingStates();
1611
1658
  }
1612
- this.channelCollection.setConnectionState(connected, clientId);
1613
- this.garbageCollector.setConnectionState(connected, clientId);
1614
- (0, internal_8.raiseConnectedEvent)(this.mc.logger, this, connected, clientId);
1659
+ this.channelCollection.setConnectionState(canSendOps, clientId);
1660
+ this.garbageCollector.setConnectionState(canSendOps, clientId);
1661
+ (0, internal_8.raiseConnectedEvent)(this.mc.logger, this, this.connected /* canSendOps */, clientId);
1615
1662
  }
1616
1663
  async notifyOpReplay(message) {
1617
1664
  await this.pendingStateManager.applyStashedOpsAt(message.sequenceNumber);
@@ -2018,7 +2065,9 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2018
2065
  if (checkpoint) {
2019
2066
  // This will throw and close the container if rollback fails
2020
2067
  try {
2021
- checkpoint.rollback((message) => this.rollback(message.runtimeOp, message.localOpMetadata));
2068
+ checkpoint.rollback((message) =>
2069
+ // These changes are staged since we entered staging mode above
2070
+ this.rollbackStagedChanges(message.runtimeOp, message.localOpMetadata));
2022
2071
  this.updateDocumentDirtyState();
2023
2072
  stageControls?.discardChanges();
2024
2073
  stageControls = undefined;
@@ -2095,7 +2144,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2095
2144
  const context = this.channelCollection.createDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], loadingGroupId);
2096
2145
  return (0, dataStore_js_1.channelToDataStore)(await context.realize(), context.id, this.channelCollection, this.mc.logger);
2097
2146
  }
2098
- canSendOps() {
2147
+ shouldSendOps() {
2099
2148
  // Note that the real (non-proxy) delta manager is needed here to get the readonly info. This is because
2100
2149
  // container runtime's ability to send ops depend on the actual readonly state of the delta manager.
2101
2150
  return (this.connected && !this.innerDeltaManager.readOnlyInfo.readonly && !this.imminentClosure);
@@ -2784,6 +2833,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2784
2833
  try {
2785
2834
  // If we're resubmitting a batch, keep the same "staged" value as before. Otherwise, use the current "global" state.
2786
2835
  const staged = this.batchRunner.resubmitInfo?.staged ?? this.inStagingMode;
2836
+ (0, internal_2.assert)(!staged || canStageMessageOfType(type), 0xbba /* Unexpected message type submitted in Staging Mode */);
2787
2837
  // Before submitting any non-staged change, submit the ID Allocation op to cover any compressed IDs included in the op.
2788
2838
  if (!staged) {
2789
2839
  this.submitIdAllocationOpIfNeeded({ staged: false });
@@ -2791,7 +2841,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2791
2841
  // Allow document schema controller to send a message if it needs to propose change in document schema.
2792
2842
  // If it needs to send a message, it will call provided callback with payload of such message and rely
2793
2843
  // on this callback to do actual sending.
2794
- const schemaChangeMessage = this.documentsSchemaController.maybeSendSchemaMessage();
2844
+ const schemaChangeMessage = this.documentsSchemaController.maybeGenerateSchemaMessage();
2795
2845
  if (schemaChangeMessage) {
2796
2846
  this.mc.logger.sendTelemetryEvent({
2797
2847
  eventName: "SchemaChangeProposal",
@@ -2889,43 +2939,72 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2889
2939
  }
2890
2940
  /**
2891
2941
  * Resubmits each message in the batch, and then flushes the outbox.
2942
+ * This typically happens when we reconnect and there are pending messages.
2892
2943
  *
2893
- * @remarks - If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
2944
+ * @remarks
2945
+ * Attempting to resubmit a batch that has been successfully sequenced will not happen due to
2946
+ * checks in the ConnectionStateHandler (Loader layer)
2947
+ *
2948
+ * The only exception to this would be if the Container "forks" due to misuse of the "Offline Load" feature.
2949
+ * If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
2894
2950
  * for correlation to detect container forking.
2895
2951
  */
2896
2952
  reSubmitBatch(batch, { batchId, staged, squash }) {
2953
+ (0, internal_2.assert)(this._summarizer === undefined, 0x8f2 /* Summarizer never reconnects so should never resubmit */);
2897
2954
  const resubmitInfo = {
2898
2955
  // Only include Batch ID if "Offline Load" feature is enabled
2899
2956
  // It's only needed to identify batches across container forks arising from misuse of offline load.
2900
2957
  batchId: this.offlineEnabled ? batchId : undefined,
2901
2958
  staged,
2902
2959
  };
2960
+ const resubmitFn = squash
2961
+ ? this.reSubmitWithSquashing.bind(this)
2962
+ : this.reSubmit.bind(this);
2903
2963
  this.batchRunner.run(() => {
2904
2964
  for (const message of batch) {
2905
- this.reSubmit(message, squash);
2965
+ resubmitFn(message);
2906
2966
  }
2907
2967
  }, resubmitInfo);
2908
2968
  this.flush(resubmitInfo);
2909
2969
  }
2910
- reSubmit(message, squash) {
2911
- this.reSubmitCore(message.runtimeOp, message.localOpMetadata, message.opMetadata, squash);
2970
+ /**
2971
+ * Resubmit the given message as part of a squash rebase upon exiting Staging Mode.
2972
+ * How exactly to resubmit the message is up to the subsystem that submitted the op to begin with.
2973
+ */
2974
+ reSubmitWithSquashing(resubmitData) {
2975
+ const message = resubmitData.runtimeOp;
2976
+ (0, internal_2.assert)(canStageMessageOfType(message.type), 0xbbb /* Expected message type to be compatible with staging */);
2977
+ switch (message.type) {
2978
+ case messageTypes_js_1.ContainerMessageType.FluidDataStoreOp: {
2979
+ this.channelCollection.reSubmit(message.type, message.contents, resubmitData.localOpMetadata,
2980
+ /* squash: */ true);
2981
+ break;
2982
+ }
2983
+ // NOTE: Squash doesn't apply to GC or DocumentSchemaChange ops, fallback to typical resubmit logic.
2984
+ case messageTypes_js_1.ContainerMessageType.GC:
2985
+ case messageTypes_js_1.ContainerMessageType.DocumentSchemaChange: {
2986
+ this.reSubmit(resubmitData);
2987
+ break;
2988
+ }
2989
+ default: {
2990
+ (0, internal_2.unreachableCase)(message.type);
2991
+ }
2992
+ }
2912
2993
  }
2913
2994
  /**
2914
- * Finds the right store and asks it to resubmit the message. This typically happens when we
2915
- * reconnect and there are pending messages.
2916
- * ! Note: successfully resubmitting an op that has been successfully sequenced is not possible due to checks in the ConnectionStateHandler (Loader layer)
2917
- * @param message - The original LocalContainerRuntimeMessage.
2918
- * @param localOpMetadata - The local metadata associated with the original message.
2995
+ * Resubmit the given message which was previously submitted to the ContainerRuntime but not successfully
2996
+ * transmitted to the ordering service (e.g. due to a disconnect, or being in Staging Mode)
2997
+ * How to resubmit is up to the subsystem that submitted the op to begin with
2919
2998
  */
2920
- reSubmitCore(message, localOpMetadata, opMetadata, squash) {
2921
- (0, internal_2.assert)(this._summarizer === undefined, 0x8f2 /* Summarizer never reconnects so should never resubmit */);
2999
+ reSubmit({ runtimeOp: message, localOpMetadata, opMetadata, }) {
2922
3000
  switch (message.type) {
2923
3001
  case messageTypes_js_1.ContainerMessageType.FluidDataStoreOp:
2924
3002
  case messageTypes_js_1.ContainerMessageType.Attach:
2925
3003
  case messageTypes_js_1.ContainerMessageType.Alias: {
2926
3004
  // For Operations, call resubmitDataStoreOp which will find the right store
2927
3005
  // and trigger resubmission on it.
2928
- this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata, squash);
3006
+ this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata,
3007
+ /* squash: */ false);
2929
3008
  break;
2930
3009
  }
2931
3010
  case messageTypes_js_1.ContainerMessageType.IdAllocation: {
@@ -2951,9 +3030,9 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2951
3030
  break;
2952
3031
  }
2953
3032
  case messageTypes_js_1.ContainerMessageType.DocumentSchemaChange: {
2954
- // There is no need to resend this message. Document schema controller will properly resend it again (if needed)
2955
- // on a first occasion (any ops sent after reconnect). There is a good chance, though, that it will not want to
2956
- // send any ops, as some other client already changed schema.
3033
+ // We shouldn't directly resubmit due to Compare-And-Swap semantics.
3034
+ // If needed it will be generated from scratch before other ops are submitted.
3035
+ this.documentsSchemaController.pendingOpNotAcked();
2957
3036
  break;
2958
3037
  }
2959
3038
  default: {
@@ -2963,8 +3042,11 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2963
3042
  }
2964
3043
  }
2965
3044
  }
2966
- rollback(runtimeOp, localOpMetadata) {
2967
- const { type, contents } = runtimeOp;
3045
+ /**
3046
+ * Rollback the given op which was only staged but not yet submitted.
3047
+ */
3048
+ rollbackStagedChanges({ type, contents }, localOpMetadata) {
3049
+ (0, internal_2.assert)(canStageMessageOfType(type), 0xbbc /* Unexpected message type to be rolled back */);
2968
3050
  switch (type) {
2969
3051
  case messageTypes_js_1.ContainerMessageType.FluidDataStoreOp: {
2970
3052
  // For operations, call rollbackDataStoreOp which will find the right store
@@ -2972,8 +3054,24 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2972
3054
  this.channelCollection.rollback(type, contents, localOpMetadata);
2973
3055
  break;
2974
3056
  }
3057
+ case messageTypes_js_1.ContainerMessageType.GC: {
3058
+ // Just drop it, but log an error, this is not expected and not ideal, but not critical failure either.
3059
+ // Currently the only expected type here is TombstoneLoaded, which will have been preceded by one of these events as well:
3060
+ // GC_Tombstone_DataStore_Requested, GC_Tombstone_SubDataStore_Requested, GC_Tombstone_Blob_Requested
3061
+ this.mc.logger.sendErrorEvent({
3062
+ eventName: "GC_OpDiscarded",
3063
+ details: { subType: contents.type },
3064
+ });
3065
+ break;
3066
+ }
3067
+ case messageTypes_js_1.ContainerMessageType.DocumentSchemaChange: {
3068
+ // Notify the document schema controller that the pending op was not acked.
3069
+ // This will allow it to propose the schema change again if needed.
3070
+ this.documentsSchemaController.pendingOpNotAcked();
3071
+ break;
3072
+ }
2975
3073
  default: {
2976
- throw new Error(`Can't rollback ${type}`);
3074
+ (0, internal_2.unreachableCase)(type);
2977
3075
  }
2978
3076
  }
2979
3077
  }
@@ -3151,6 +3249,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
3151
3249
  },
3152
3250
  getQuorum: this.getQuorum.bind(this),
3153
3251
  getAudience: this.getAudience.bind(this),
3252
+ supportedFeatures: this.ILayerCompatDetails.supportedFeatures,
3154
3253
  };
3155
3254
  entry = new factory(runtime, ...useContext);
3156
3255
  this.extensions.set(id, entry);