@fluidframework/container-runtime 2.41.0-338401 → 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 (161) hide show
  1. package/CHANGELOG.md +8 -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 +67 -28
  12. package/dist/containerRuntime.d.ts.map +1 -1
  13. package/dist/containerRuntime.js +332 -186
  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/batchManager.d.ts +4 -0
  34. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  35. package/dist/opLifecycle/batchManager.js +7 -0
  36. package/dist/opLifecycle/batchManager.js.map +1 -1
  37. package/dist/opLifecycle/definitions.d.ts +6 -5
  38. package/dist/opLifecycle/definitions.d.ts.map +1 -1
  39. package/dist/opLifecycle/definitions.js.map +1 -1
  40. package/dist/opLifecycle/index.d.ts +1 -1
  41. package/dist/opLifecycle/index.d.ts.map +1 -1
  42. package/dist/opLifecycle/index.js.map +1 -1
  43. package/dist/opLifecycle/opGroupingManager.d.ts +9 -0
  44. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  45. package/dist/opLifecycle/opGroupingManager.js +6 -4
  46. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  47. package/dist/opLifecycle/opSerialization.d.ts +2 -1
  48. package/dist/opLifecycle/opSerialization.d.ts.map +1 -1
  49. package/dist/opLifecycle/opSerialization.js.map +1 -1
  50. package/dist/opLifecycle/outbox.d.ts +1 -0
  51. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  52. package/dist/opLifecycle/outbox.js +6 -1
  53. package/dist/opLifecycle/outbox.js.map +1 -1
  54. package/dist/packageVersion.d.ts +1 -1
  55. package/dist/packageVersion.d.ts.map +1 -1
  56. package/dist/packageVersion.js +1 -1
  57. package/dist/packageVersion.js.map +1 -1
  58. package/dist/pendingStateManager.d.ts +22 -5
  59. package/dist/pendingStateManager.d.ts.map +1 -1
  60. package/dist/pendingStateManager.js +34 -11
  61. package/dist/pendingStateManager.js.map +1 -1
  62. package/dist/runCounter.d.ts.map +1 -1
  63. package/dist/runCounter.js +1 -1
  64. package/dist/runCounter.js.map +1 -1
  65. package/dist/summary/documentSchema.d.ts +42 -18
  66. package/dist/summary/documentSchema.d.ts.map +1 -1
  67. package/dist/summary/documentSchema.js +62 -52
  68. package/dist/summary/documentSchema.js.map +1 -1
  69. package/dist/summary/index.d.ts +1 -1
  70. package/dist/summary/index.d.ts.map +1 -1
  71. package/dist/summary/index.js.map +1 -1
  72. package/lib/channelCollection.d.ts +1 -1
  73. package/lib/channelCollection.d.ts.map +1 -1
  74. package/lib/channelCollection.js +4 -4
  75. package/lib/channelCollection.js.map +1 -1
  76. package/lib/compatUtils.d.ts +22 -1
  77. package/lib/compatUtils.d.ts.map +1 -1
  78. package/lib/compatUtils.js +102 -3
  79. package/lib/compatUtils.js.map +1 -1
  80. package/lib/containerRuntime.d.ts +67 -28
  81. package/lib/containerRuntime.d.ts.map +1 -1
  82. package/lib/containerRuntime.js +333 -188
  83. package/lib/containerRuntime.js.map +1 -1
  84. package/lib/dataStore.d.ts.map +1 -1
  85. package/lib/dataStore.js +5 -0
  86. package/lib/dataStore.js.map +1 -1
  87. package/lib/gc/garbageCollection.d.ts.map +1 -1
  88. package/lib/gc/garbageCollection.js +2 -0
  89. package/lib/gc/garbageCollection.js.map +1 -1
  90. package/lib/gc/gcDefinitions.d.ts +1 -1
  91. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  92. package/lib/gc/gcDefinitions.js.map +1 -1
  93. package/lib/index.d.ts +1 -1
  94. package/lib/index.d.ts.map +1 -1
  95. package/lib/index.js.map +1 -1
  96. package/lib/messageTypes.d.ts +5 -4
  97. package/lib/messageTypes.d.ts.map +1 -1
  98. package/lib/messageTypes.js.map +1 -1
  99. package/lib/metadata.d.ts +1 -1
  100. package/lib/metadata.d.ts.map +1 -1
  101. package/lib/metadata.js.map +1 -1
  102. package/lib/opLifecycle/batchManager.d.ts +4 -0
  103. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  104. package/lib/opLifecycle/batchManager.js +7 -0
  105. package/lib/opLifecycle/batchManager.js.map +1 -1
  106. package/lib/opLifecycle/definitions.d.ts +6 -5
  107. package/lib/opLifecycle/definitions.d.ts.map +1 -1
  108. package/lib/opLifecycle/definitions.js.map +1 -1
  109. package/lib/opLifecycle/index.d.ts +1 -1
  110. package/lib/opLifecycle/index.d.ts.map +1 -1
  111. package/lib/opLifecycle/index.js.map +1 -1
  112. package/lib/opLifecycle/opGroupingManager.d.ts +9 -0
  113. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  114. package/lib/opLifecycle/opGroupingManager.js +6 -4
  115. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  116. package/lib/opLifecycle/opSerialization.d.ts +2 -1
  117. package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
  118. package/lib/opLifecycle/opSerialization.js.map +1 -1
  119. package/lib/opLifecycle/outbox.d.ts +1 -0
  120. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  121. package/lib/opLifecycle/outbox.js +6 -1
  122. package/lib/opLifecycle/outbox.js.map +1 -1
  123. package/lib/packageVersion.d.ts +1 -1
  124. package/lib/packageVersion.d.ts.map +1 -1
  125. package/lib/packageVersion.js +1 -1
  126. package/lib/packageVersion.js.map +1 -1
  127. package/lib/pendingStateManager.d.ts +22 -5
  128. package/lib/pendingStateManager.d.ts.map +1 -1
  129. package/lib/pendingStateManager.js +34 -11
  130. package/lib/pendingStateManager.js.map +1 -1
  131. package/lib/runCounter.d.ts.map +1 -1
  132. package/lib/runCounter.js +1 -1
  133. package/lib/runCounter.js.map +1 -1
  134. package/lib/summary/documentSchema.d.ts +42 -18
  135. package/lib/summary/documentSchema.d.ts.map +1 -1
  136. package/lib/summary/documentSchema.js +62 -52
  137. package/lib/summary/documentSchema.js.map +1 -1
  138. package/lib/summary/index.d.ts +1 -1
  139. package/lib/summary/index.d.ts.map +1 -1
  140. package/lib/summary/index.js.map +1 -1
  141. package/package.json +19 -19
  142. package/src/channelCollection.ts +4 -4
  143. package/src/compatUtils.ts +145 -10
  144. package/src/containerRuntime.ts +472 -225
  145. package/src/dataStore.ts +7 -0
  146. package/src/gc/garbageCollection.ts +2 -0
  147. package/src/gc/gcDefinitions.ts +1 -1
  148. package/src/index.ts +2 -1
  149. package/src/messageTypes.ts +12 -5
  150. package/src/metadata.ts +1 -1
  151. package/src/opLifecycle/batchManager.ts +8 -0
  152. package/src/opLifecycle/definitions.ts +7 -3
  153. package/src/opLifecycle/index.ts +1 -0
  154. package/src/opLifecycle/opGroupingManager.ts +17 -4
  155. package/src/opLifecycle/opSerialization.ts +6 -1
  156. package/src/opLifecycle/outbox.ts +8 -1
  157. package/src/packageVersion.ts +1 -1
  158. package/src/pendingStateManager.ts +64 -20
  159. package/src/runCounter.ts +4 -1
  160. package/src/summary/documentSchema.ts +111 -86
  161. package/src/summary/index.ts +2 -1
@@ -4,7 +4,7 @@
4
4
  * Licensed under the MIT License.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.createNewSignalEnvelope = exports.ContainerRuntime = exports.loadContainerRuntime = exports.getSingleUseLegacyLogCallback = exports.makeLegacySendBatchFn = exports.getDeviceSpec = exports.agentSchedulerId = exports.isUnpackedRuntimeMessage = exports.defaultPendingOpsRetryDelayMs = exports.defaultPendingOpsWaitTimeoutMs = exports.defaultRuntimeHeaderData = exports.InactiveResponseHeaderKey = exports.TombstoneResponseHeaderKey = exports.DeletedResponseHeaderKey = void 0;
7
+ exports.isContainerMessageDirtyable = exports.createNewSignalEnvelope = exports.ContainerRuntime = exports.loadContainerRuntime = exports.getSingleUseLegacyLogCallback = exports.makeLegacySendBatchFn = exports.getDeviceSpec = exports.agentSchedulerId = exports.isUnpackedRuntimeMessage = exports.defaultPendingOpsRetryDelayMs = exports.defaultPendingOpsWaitTimeoutMs = exports.defaultRuntimeHeaderData = exports.InactiveResponseHeaderKey = exports.TombstoneResponseHeaderKey = exports.DeletedResponseHeaderKey = void 0;
8
8
  const client_utils_1 = require("@fluid-internal/client-utils");
9
9
  const container_definitions_1 = require("@fluidframework/container-definitions");
10
10
  const internal_1 = require("@fluidframework/container-definitions/internal");
@@ -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 = {
@@ -503,15 +520,19 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
503
520
  }
504
521
  /**
505
522
  * Invokes the given callback and expects that no ops are submitted
506
- * until execution finishes. If an op is submitted, an error will be raised.
523
+ * until execution finishes. If an op is submitted, it will be marked as reentrant.
507
524
  *
508
525
  * @param callback - the callback to be invoked
509
526
  */
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
@@ -575,7 +596,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
575
596
  // Once it loads, it will process all such ops and we will stop accumulating further ops - ops will be processes as they come in.
576
597
  this.pendingIdCompressorOps = [];
577
598
  this.batchRunner = new runCounter_js_1.BatchRunCounter();
578
- this.flushTaskExists = false;
599
+ this.flushScheduled = false;
579
600
  this.consecutiveReconnects = 0;
580
601
  this.dataModelChangeRunner = new runCounter_js_1.RunCounter();
581
602
  this._disposed = false;
@@ -588,6 +609,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
588
609
  this.snapshotCacheForLoadingGroupIds = new internal_2.PromiseCache({
589
610
  expiry: { policy: "absolute", durationMs: 60000 },
590
611
  });
612
+ this.extensions = new Map();
591
613
  this.notifyReadOnlyState = (readonly) => this.channelCollection.notifyReadOnlyState(readonly);
592
614
  /**
593
615
  * Enter Staging Mode, such that ops submitted to the ContainerRuntime will not be sent to the ordering service.
@@ -598,7 +620,10 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
598
620
  // eslint-disable-next-line import/no-deprecated
599
621
  this.enterStagingMode = () => {
600
622
  if (this.stageControls !== undefined) {
601
- 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");
602
627
  }
603
628
  // Make sure all BatchManagers are empty before entering staging mode,
604
629
  // since we mark whole batches as "staged" or not to indicate whether to submit them.
@@ -619,11 +644,9 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
619
644
  // Pop all staged batches from the PSM and roll them back in LIFO order
620
645
  this.pendingStateManager.popStagedBatches(({ runtimeOp, localOpMetadata }) => {
621
646
  (0, internal_2.assert)(runtimeOp !== undefined, 0xb82 /* Staged batches expected to have runtimeOp defined */);
622
- this.rollback(runtimeOp, localOpMetadata);
647
+ this.rollbackStagedChanges(runtimeOp, localOpMetadata);
623
648
  });
624
- if (this.attachState === container_definitions_1.AttachState.Attached) {
625
- this.updateDocumentDirtyState(this.pendingMessagesCount !== 0);
626
- }
649
+ this.updateDocumentDirtyState();
627
650
  }),
628
651
  commitChanges: (optionsParam) => {
629
652
  const options = { ...defaultStagingCommitOptions, ...optionsParam };
@@ -640,6 +663,15 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
640
663
  return this.stageControls;
641
664
  };
642
665
  this.readAndParseBlob = async (id) => (0, internal_4.readAndParse)(this.storage, id);
666
+ // While internal, ContainerRuntime has not been converted to use the new events support.
667
+ // Recreate the required events (new pattern) with injected, wrapper new emitter.
668
+ // It is lazily create to avoid listeners (old events) that ultimately go nowhere.
669
+ this.lazyEventsForExtensions = new internal_2.Lazy(() => {
670
+ const eventEmitter = (0, client_utils_1.createEmitter)();
671
+ this.on("connected", (clientId) => eventEmitter.emit("connected", clientId));
672
+ this.on("disconnected", () => eventEmitter.emit("disconnected"));
673
+ return eventEmitter;
674
+ });
643
675
  const { options, clientDetails, connected, baseSnapshot, submitFn, submitBatchFn, submitSummaryFn, submitSignalFn, disposeFn, closeFn, deltaManager, quorum, audience, pendingLocalState, supportedFeatures, snapshotWithContents, } = context;
644
676
  // In old loaders without dispose functionality, closeFn is equivalent but will also switch container to readonly mode
645
677
  this.disposeFn = disposeFn ?? closeFn;
@@ -649,6 +681,11 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
649
681
  this.mc = (0, internal_8.createChildMonitoringContext)({
650
682
  logger: this.baseLogger,
651
683
  namespace: "ContainerRuntime",
684
+ properties: {
685
+ all: {
686
+ inStagingMode: this.inStagingMode,
687
+ },
688
+ },
652
689
  });
653
690
  // If we support multiple algorithms in the future, then we would need to manage it here carefully.
654
691
  // We can use runtimeOptions.compressionOptions.compressionAlgorithm, but only if it's in the schema list!
@@ -668,12 +705,28 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
668
705
  this.submitSummaryFn =
669
706
  submitSummaryFn ??
670
707
  ((summaryOp, refseq) => submitFn(internal_3.MessageType.Summarize, summaryOp, false));
671
- this.submitSignalFn = submitSignalFn;
708
+ const sequenceAndSubmitSignal = (envelope, targetClientId) => {
709
+ if (targetClientId === undefined) {
710
+ this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope);
711
+ }
712
+ submitSignalFn(envelope, targetClientId);
713
+ };
714
+ this.submitSignalFn = (envelope, targetClientId) => {
715
+ if (envelope.address?.startsWith("/")) {
716
+ throw new Error("General path based addressing is not implemented");
717
+ }
718
+ sequenceAndSubmitSignal(envelope, targetClientId);
719
+ };
720
+ this.submitExtensionSignal = (id, addressChain, message) => {
721
+ this.verifyNotClosed();
722
+ const envelope = createNewSignalEnvelope(`/ext/${id}/${addressChain.join("/")}`, message.type, message.content);
723
+ sequenceAndSubmitSignal(envelope, message.targetClientId);
724
+ };
672
725
  // TODO: After IContainerContext.options is removed, we'll just create a new blank object {} here.
673
726
  // Values are generally expected to be set from the runtime side.
674
727
  this.options = options ?? {};
675
728
  this.clientDetails = clientDetails;
676
- const isSummarizerClient = this.clientDetails.type === index_js_4.summarizerClientType;
729
+ this.isSummarizerClient = this.clientDetails.type === index_js_4.summarizerClientType;
677
730
  this.loadedFromVersionId = context.getLoadedFromVersion()?.id;
678
731
  // eslint-disable-next-line unicorn/consistent-destructuring
679
732
  this._getClientId = () => context.clientId;
@@ -700,8 +753,8 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
700
753
  this.mc.logger.sendTelemetryEvent({
701
754
  eventName: "Attached",
702
755
  details: {
703
- dirtyContainer: this.dirtyContainer,
704
- hasPendingMessages: this.hasPendingMessages(),
756
+ lastEmittedDirty: this.lastEmittedDirty,
757
+ currentDirtyState: this.computeCurrentDirtyState(),
705
758
  },
706
759
  });
707
760
  });
@@ -710,7 +763,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
710
763
  details: { attachState: this.attachState },
711
764
  }));
712
765
  // In cases of summarizer, we want to dispose instead since consumer doesn't interact with this container
713
- this.closeFn = isSummarizerClient ? this.disposeFn : closeFn;
766
+ this.closeFn = this.isSummarizerClient ? this.disposeFn : closeFn;
714
767
  let loadSummaryNumber;
715
768
  // Get the container creation metadata. For new container, we initialize these. For existing containers,
716
769
  // get the values from the metadata blob.
@@ -734,7 +787,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
734
787
  this.messageAtLastSummary = lastMessageFromMetadata(metadata);
735
788
  // Note that we only need to pull the *initial* connected state from the context.
736
789
  // Later updates come through calls to setConnectionState.
737
- this._connected = connected;
790
+ this.canSendOps = connected;
738
791
  this.mc.logger.sendTelemetryEvent({
739
792
  eventName: "GCFeatureMatrix",
740
793
  metadataValue: JSON.stringify(metadata?.gcFeatureMatrix),
@@ -827,7 +880,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
827
880
  existing,
828
881
  metadata,
829
882
  createContainerMetadata: this.createContainerMetadata,
830
- isSummarizerClient,
883
+ isSummarizerClient: this.isSummarizerClient,
831
884
  getNodePackagePath: async (nodePath) => this.getGCNodePackagePath(nodePath),
832
885
  getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
833
886
  readAndParseBlob: async (id) => (0, internal_4.readAndParse)(this.storage, id),
@@ -868,9 +921,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
868
921
  // verifyNotClosed is called in FluidDataStoreContext, which is *the* expected caller.
869
922
  const envelope1 = content;
870
923
  const envelope2 = createNewSignalEnvelope(envelope1.address, type, envelope1.contents);
871
- if (targetClientId === undefined) {
872
- this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope2);
873
- }
874
924
  this.submitSignalFn(envelope2, targetClientId);
875
925
  };
876
926
  let snapshot = (0, channelCollection_js_1.getSummaryForDatastores)(baseSnapshot, metadata);
@@ -914,7 +964,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
914
964
  // Keep the old flag name even though we renamed the class member (it shipped in 2.31.0)
915
965
  this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableFlushBeforeProcess") === true;
916
966
  this.outbox = new index_js_3.Outbox({
917
- shouldSend: () => this.canSendOps(),
967
+ shouldSend: () => this.shouldSendOps(),
918
968
  pendingStateManager: this.pendingStateManager,
919
969
  submitBatchFn,
920
970
  legacySendBatchFn,
@@ -964,9 +1014,9 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
964
1014
  const closeSummarizerDelayOverride = this.mc.config.getNumber("Fluid.ContainerRuntime.Test.CloseSummarizerDelayOverrideMs");
965
1015
  this.closeSummarizerDelayMs =
966
1016
  closeSummarizerDelayOverride ?? defaultCloseSummarizerDelayMs;
967
- this.dirtyContainer =
968
- this.attachState !== container_definitions_1.AttachState.Attached || this.hasPendingMessages();
969
- context.updateDirtyContainerState(this.dirtyContainer);
1017
+ // We haven't emitted dirty/saved yet, but this is the baseline so we know to emit when it changes
1018
+ this.lastEmittedDirty = this.computeCurrentDirtyState();
1019
+ context.updateDirtyContainerState(this.lastEmittedDirty);
970
1020
  if (!this.skipSafetyFlushDuringProcessStack) {
971
1021
  // Reference Sequence Number may have just changed, and it must be consistent across a batch,
972
1022
  // so we should flush now to clear the way for the next ops.
@@ -1053,7 +1103,14 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1053
1103
  await this.initializeSummarizer(loader);
1054
1104
  if (this.sessionSchema.idCompressorMode === "on" ||
1055
1105
  (this.sessionSchema.idCompressorMode === "delayed" && this.connected)) {
1056
- 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
+ });
1057
1114
  // This is called from loadRuntime(), long before we process any ops, so there should be no ops accumulated yet.
1058
1115
  (0, internal_2.assert)(this.pendingIdCompressorOps.length === 0, 0x8ec /* no pending ops */);
1059
1116
  }
@@ -1082,8 +1139,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1082
1139
  const orderedClientCollection = new index_js_4.OrderedClientCollection(orderedClientLogger, this.innerDeltaManager, this._quorum);
1083
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"));
1084
1141
  this.summarizerClientElection = new index_js_4.SummarizerClientElection(orderedClientLogger, summaryCollection, orderedClientElectionForSummarizer, maxOpsSinceLastSummary);
1085
- const isSummarizerClient = this.clientDetails.type === index_js_4.summarizerClientType;
1086
- if (isSummarizerClient) {
1142
+ if (this.isSummarizerClient) {
1087
1143
  // We want to dynamically import any thing inside summaryDelayLoadedModule module only when we are the summarizer client,
1088
1144
  // so that all non summarizer clients don't have to load the code inside this module.
1089
1145
  const module = await import(
@@ -1431,35 +1487,32 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1431
1487
  }
1432
1488
  replayPendingStates() {
1433
1489
  // We need to be able to send ops to replay states
1434
- if (!this.canSendOps()) {
1490
+ if (!this.shouldSendOps()) {
1435
1491
  return;
1436
1492
  }
1437
- // We need to temporary clear the dirty flags and disable
1438
- // dirty state change events to detect whether replaying ops
1439
- // has any effect.
1440
- // Save the old state, reset to false, disable event emit
1441
- const oldState = this.dirtyContainer;
1442
- this.dirtyContainer = false;
1493
+ // Replaying is an internal operation and we don't want to generate noise while doing it.
1494
+ // So temporarily disable dirty state change events, and save the old state.
1495
+ // When we're done, we'll emit the event if the state changed.
1496
+ const oldState = this.lastEmittedDirty;
1443
1497
  (0, internal_2.assert)(this.emitDirtyDocumentEvent, 0x127 /* "dirty document event not set on replay" */);
1444
1498
  this.emitDirtyDocumentEvent = false;
1445
- let newState;
1446
1499
  try {
1447
1500
  // Any ID Allocation ops that failed to submit after the pending state was queued need to have
1448
1501
  // the corresponding ranges resubmitted (note this call replaces the typical resubmit flow).
1449
1502
  // Since we don't submit ID Allocation ops when staged, any outstanding ranges would be from
1450
1503
  // before staging mode so we can simply say staged: false.
1451
1504
  this.submitIdAllocationOpIfNeeded({ resubmitOutstandingRanges: true, staged: false });
1505
+ this.scheduleFlush();
1452
1506
  // replay the ops
1453
1507
  this.pendingStateManager.replayPendingStates();
1454
1508
  }
1455
1509
  finally {
1456
- // Save the new start and restore the old state, re-enable event emit
1457
- newState = this.dirtyContainer;
1458
- this.dirtyContainer = oldState;
1510
+ // Restore the old state, re-enable event emit
1511
+ this.lastEmittedDirty = oldState;
1459
1512
  this.emitDirtyDocumentEvent = true;
1460
1513
  }
1461
- // Officially transition from the old state to the new state.
1462
- this.updateDocumentDirtyState(newState);
1514
+ // This will emit an event if the state changed relative to before replay
1515
+ this.updateDocumentDirtyState();
1463
1516
  }
1464
1517
  /**
1465
1518
  * Parse an op's type and actual content from given serialized content
@@ -1516,25 +1569,35 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1516
1569
  loadIdCompressor() {
1517
1570
  if (this._idCompressor === undefined &&
1518
1571
  this.sessionSchema.idCompressorMode !== undefined) {
1519
- this._idCompressor = this.createIdCompressorFn();
1520
- // Finalize any ranges we received while the compressor was turned off.
1521
- const ops = this.pendingIdCompressorOps;
1522
- this.pendingIdCompressorOps = [];
1523
- for (const range of ops) {
1524
- this._idCompressor.finalizeCreationRange(range);
1525
- }
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
+ });
1526
1589
  (0, internal_2.assert)(this.pendingIdCompressorOps.length === 0, 0x976 /* No new ops added */);
1527
1590
  }
1528
1591
  }
1529
- setConnectionState(connected, clientId) {
1592
+ setConnectionState(canSendOps, clientId) {
1530
1593
  // Validate we have consistent state
1531
1594
  const currentClientId = this._audience.getSelf()?.clientId;
1532
1595
  (0, internal_2.assert)(clientId === currentClientId, 0x977 /* input clientId does not match Audience */);
1533
1596
  (0, internal_2.assert)(this.clientId === currentClientId, 0x978 /* this.clientId does not match Audience */);
1534
- if (connected && this.sessionSchema.idCompressorMode === "delayed") {
1597
+ if (canSendOps && this.sessionSchema.idCompressorMode === "delayed") {
1535
1598
  this.loadIdCompressor();
1536
1599
  }
1537
- if (connected === false && this.delayConnectClientId !== undefined) {
1600
+ if (canSendOps === false && this.delayConnectClientId !== undefined) {
1538
1601
  this.delayConnectClientId = undefined;
1539
1602
  this.mc.logger.sendTelemetryEvent({
1540
1603
  eventName: "UnsuccessfulConnectedTransition",
@@ -1542,37 +1605,39 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1542
1605
  // Don't propagate "disconnected" event because we didn't propagate the previous "connected" event
1543
1606
  return;
1544
1607
  }
1545
- if (!connected) {
1546
- this.documentsSchemaController.onDisconnect();
1547
- }
1548
1608
  // If there are stashed blobs in the pending state, we need to delay
1549
1609
  // propagation of the "connected" event until we have uploaded them to
1550
1610
  // ensure we don't submit ops referencing a blob that has not been uploaded
1551
- const connecting = connected && !this._connected;
1611
+ const connecting = canSendOps && !this.canSendOps;
1552
1612
  if (connecting && this.blobManager.hasPendingStashedUploads()) {
1553
1613
  (0, internal_2.assert)(!this.delayConnectClientId, 0x791 /* Connect event delay must be canceled before subsequent connect event */);
1554
1614
  (0, internal_2.assert)(!!clientId, 0x792 /* Must have clientId when connecting */);
1555
1615
  this.delayConnectClientId = clientId;
1556
1616
  return;
1557
1617
  }
1558
- this.setConnectionStateCore(connected, clientId);
1618
+ this.setConnectionStateCore(canSendOps, clientId);
1559
1619
  }
1560
- 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) {
1561
1626
  (0, internal_2.assert)(!this.delayConnectClientId, 0x394 /* connect event delay must be cleared before propagating connect event */);
1562
1627
  this.verifyNotClosed();
1563
1628
  // There might be no change of state due to Container calling this API after loading runtime.
1564
- const changeOfState = this._connected !== connected;
1565
- const reconnection = changeOfState && !connected;
1629
+ const canSendOpsChanged = this.canSendOps !== canSendOps;
1630
+ const reconnection = canSendOpsChanged && !canSendOps;
1566
1631
  // We need to flush the ops currently collected by Outbox to preserve original order.
1567
1632
  // This flush NEEDS to happen before we set the ContainerRuntime to "connected".
1568
1633
  // We want these ops to get to the PendingStateManager without sending to service and have them return to the Outbox upon calling "replayPendingStates".
1569
- if (changeOfState && connected) {
1634
+ if (canSendOpsChanged && canSendOps) {
1570
1635
  this.flush();
1571
1636
  }
1572
- this._connected = connected;
1573
- if (connected) {
1637
+ this.canSendOps = canSendOps;
1638
+ if (canSendOps) {
1574
1639
  (0, internal_2.assert)(this.attachState === container_definitions_1.AttachState.Attached, 0x3cd /* Connection is possible only if container exists in storage */);
1575
- if (changeOfState) {
1640
+ if (canSendOpsChanged) {
1576
1641
  this.signalTelemetryManager.resetTracking();
1577
1642
  }
1578
1643
  }
@@ -1588,12 +1653,12 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1588
1653
  return;
1589
1654
  }
1590
1655
  }
1591
- if (changeOfState) {
1656
+ if (canSendOpsChanged) {
1592
1657
  this.replayPendingStates();
1593
1658
  }
1594
- this.channelCollection.setConnectionState(connected, clientId);
1595
- this.garbageCollector.setConnectionState(connected, clientId);
1596
- (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);
1597
1662
  }
1598
1663
  async notifyOpReplay(message) {
1599
1664
  await this.pendingStateManager.applyStashedOpsAt(message.sequenceNumber);
@@ -1721,6 +1786,8 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1721
1786
  * @param groupedBatch - true if these messages are part of a grouped op batch.
1722
1787
  */
1723
1788
  processInboundMessages(messagesWithMetadata, locationInBatch, local, savedOp, runtimeBatch, groupedBatch) {
1789
+ // This message could have been the last pending local (dirtyable) message, in which case we need to update dirty state to "saved"
1790
+ this.updateDocumentDirtyState();
1724
1791
  if (locationInBatch.batchStart) {
1725
1792
  const firstMessage = messagesWithMetadata[0]?.message;
1726
1793
  (0, internal_2.assert)(firstMessage !== undefined, 0xa31 /* Batch must have at least one message */);
@@ -1815,11 +1882,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1815
1882
  message.minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
1816
1883
  }
1817
1884
  this._processedClientSequenceNumber = message.clientSequenceNumber;
1818
- // If there are no more pending messages after processing a local message,
1819
- // the document is no longer dirty.
1820
- if (!this.hasPendingMessages()) {
1821
- this.updateDocumentDirtyState(false);
1822
- }
1823
1885
  // The DeltaManager used to do this, but doesn't anymore as of Loader v2.4
1824
1886
  // Anyone listening to our "op" event would expect the contents to be parsed per this same logic
1825
1887
  if (typeof message.contents === "string" &&
@@ -1841,11 +1903,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1841
1903
  *
1842
1904
  */
1843
1905
  validateAndProcessRuntimeMessages(message, messagesContent, local, savedOp) {
1844
- // If there are no more pending messages after processing a local message,
1845
- // the document is no longer dirty.
1846
- if (!this.hasPendingMessages()) {
1847
- this.updateDocumentDirtyState(false);
1848
- }
1849
1906
  // Get the contents without the localOpMetadata because not all message types know about localOpMetadata.
1850
1907
  const contents = messagesContent.map((c) => c.contents);
1851
1908
  switch (message.type) {
@@ -1921,20 +1978,44 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1921
1978
  if (message.clientId === this.clientId) {
1922
1979
  this.signalTelemetryManager.trackReceivedSignal(envelope, this.mc.logger, this.consecutiveReconnects);
1923
1980
  }
1924
- if (envelope.address === undefined) {
1981
+ const fullAddress = envelope.address;
1982
+ if (fullAddress === undefined) {
1925
1983
  // No address indicates a container signal message.
1926
1984
  this.emit("signal", transformed, local);
1927
1985
  return;
1928
1986
  }
1929
- // Due to a mismatch between different layers in terms of
1930
- // what is the interface of passing signals, we need to adjust
1931
- // the signal envelope before sending it to the datastores to be processed
1932
- const envelope2 = {
1933
- address: envelope.address,
1934
- contents: transformed.content,
1935
- };
1936
- transformed.content = envelope2;
1937
- this.channelCollection.processSignal(transformed, local);
1987
+ this.routeNonContainerSignal(fullAddress, transformed, local);
1988
+ }
1989
+ routeNonContainerSignal(address, signalMessage, local) {
1990
+ // channelCollection signals are identified by no starting `/` in address.
1991
+ if (!address.startsWith("/")) {
1992
+ // Due to a mismatch between different layers in terms of
1993
+ // what is the interface of passing signals, we need to adjust
1994
+ // the signal envelope before sending it to the datastores to be processed
1995
+ const envelope = {
1996
+ address,
1997
+ contents: signalMessage.content,
1998
+ };
1999
+ signalMessage.content = envelope;
2000
+ this.channelCollection.processSignal(signalMessage, local);
2001
+ return;
2002
+ }
2003
+ const addresses = address.split("/");
2004
+ if (addresses.length > 2 && addresses[1] === "ext") {
2005
+ const id = addresses[2];
2006
+ const entry = this.extensions.get(id);
2007
+ if (entry !== undefined) {
2008
+ entry.extension.processSignal?.(addresses.slice(3), signalMessage, local);
2009
+ return;
2010
+ }
2011
+ }
2012
+ (0, internal_2.assert)(!local, 0xba0 /* No recipient found for local signal */);
2013
+ this.mc.logger.sendTelemetryEvent({
2014
+ eventName: "SignalAddressNotFound",
2015
+ ...(0, internal_8.tagCodeArtifacts)({
2016
+ address,
2017
+ }),
2018
+ });
1938
2019
  }
1939
2020
  /**
1940
2021
  * Flush the current batch of ops to the ordering service for sequencing
@@ -1944,6 +2025,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1944
2025
  * @param resubmitInfo - If defined, indicates this is a resubmission of a batch with the given Batch info needed for resubmit.
1945
2026
  */
1946
2027
  flush(resubmitInfo) {
2028
+ this.flushScheduled = false;
1947
2029
  try {
1948
2030
  (0, internal_2.assert)(!this.batchRunner.running, 0x24c /* "Cannot call `flush()` while manually accumulating a batch (e.g. under orderSequentially) */);
1949
2031
  this.outbox.flush(resubmitInfo);
@@ -1964,7 +2046,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1964
2046
  */
1965
2047
  orderSequentially(callback) {
1966
2048
  let checkpoint;
1967
- const checkpointDirtyState = this.dirtyContainer;
1968
2049
  // eslint-disable-next-line import/no-deprecated
1969
2050
  let stageControls;
1970
2051
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
@@ -1984,11 +2065,10 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1984
2065
  if (checkpoint) {
1985
2066
  // This will throw and close the container if rollback fails
1986
2067
  try {
1987
- checkpoint.rollback((message) => this.rollback(message.runtimeOp, message.localOpMetadata));
1988
- // reset the dirty state after rollback to what it was before to keep it consistent
1989
- if (this.dirtyContainer !== checkpointDirtyState) {
1990
- this.updateDocumentDirtyState(checkpointDirtyState);
1991
- }
2068
+ checkpoint.rollback((message) =>
2069
+ // These changes are staged since we entered staging mode above
2070
+ this.rollbackStagedChanges(message.runtimeOp, message.localOpMetadata));
2071
+ this.updateDocumentDirtyState();
1992
2072
  stageControls?.discardChanges();
1993
2073
  stageControls = undefined;
1994
2074
  }
@@ -2064,17 +2144,11 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2064
2144
  const context = this.channelCollection.createDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], loadingGroupId);
2065
2145
  return (0, dataStore_js_1.channelToDataStore)(await context.realize(), context.id, this.channelCollection, this.mc.logger);
2066
2146
  }
2067
- canSendOps() {
2147
+ shouldSendOps() {
2068
2148
  // Note that the real (non-proxy) delta manager is needed here to get the readonly info. This is because
2069
2149
  // container runtime's ability to send ops depend on the actual readonly state of the delta manager.
2070
2150
  return (this.connected && !this.innerDeltaManager.readOnlyInfo.readonly && !this.imminentClosure);
2071
2151
  }
2072
- /**
2073
- * Typically ops are batched and later flushed together, but in some cases we want to flush immediately.
2074
- */
2075
- currentlyBatching() {
2076
- return this.flushMode !== internal_6.FlushMode.Immediate || this.batchRunner.running;
2077
- }
2078
2152
  getQuorum() {
2079
2153
  return this._quorum;
2080
2154
  }
@@ -2086,36 +2160,17 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2086
2160
  * either were not sent out to delta stream or were not yet acknowledged.
2087
2161
  */
2088
2162
  get isDirty() {
2089
- return this.dirtyContainer;
2163
+ // Rather than recomputing the dirty state in this moment,
2164
+ // just regurgitate the last emitted dirty state.
2165
+ return this.lastEmittedDirty;
2090
2166
  }
2091
- isContainerMessageDirtyable({ type, contents, }) {
2092
- // Certain container runtime messages should not mark the container dirty such as the old built-in
2093
- // AgentScheduler and Garbage collector messages.
2094
- switch (type) {
2095
- case messageTypes_js_1.ContainerMessageType.Attach: {
2096
- const attachMessage = contents;
2097
- if (attachMessage.id === exports.agentSchedulerId) {
2098
- return false;
2099
- }
2100
- break;
2101
- }
2102
- case messageTypes_js_1.ContainerMessageType.FluidDataStoreOp: {
2103
- const envelope = contents;
2104
- if (envelope.address === exports.agentSchedulerId) {
2105
- return false;
2106
- }
2107
- break;
2108
- }
2109
- case messageTypes_js_1.ContainerMessageType.IdAllocation:
2110
- case messageTypes_js_1.ContainerMessageType.DocumentSchemaChange:
2111
- case messageTypes_js_1.ContainerMessageType.GC: {
2112
- return false;
2113
- }
2114
- default: {
2115
- break;
2116
- }
2117
- }
2118
- return true;
2167
+ /**
2168
+ * Returns true if the container is dirty: not attached, or no pending user messages (could be some "non-dirtyable" ones though)
2169
+ */
2170
+ computeCurrentDirtyState() {
2171
+ return (this.attachState !== container_definitions_1.AttachState.Attached ||
2172
+ this.pendingStateManager.hasPendingUserChanges() ||
2173
+ this.outbox.containsUserChanges());
2119
2174
  }
2120
2175
  /**
2121
2176
  * Submits the signal to be sent to other clients.
@@ -2132,9 +2187,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2132
2187
  submitSignal(type, content, targetClientId) {
2133
2188
  this.verifyNotClosed();
2134
2189
  const envelope = createNewSignalEnvelope(undefined /* address */, type, content);
2135
- if (targetClientId === undefined) {
2136
- this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope);
2137
- }
2138
2190
  this.submitSignalFn(envelope, targetClientId);
2139
2191
  }
2140
2192
  setAttachState(attachState) {
@@ -2145,9 +2197,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2145
2197
  (0, internal_2.assert)(this.attachState === container_definitions_1.AttachState.Attached, 0x12e /* "Container Context should already be in attached state" */);
2146
2198
  this.emit("attached");
2147
2199
  }
2148
- if (attachState === container_definitions_1.AttachState.Attached && !this.hasPendingMessages()) {
2149
- this.updateDocumentDirtyState(false);
2150
- }
2200
+ this.updateDocumentDirtyState();
2151
2201
  this.channelCollection.setAttachState(attachState);
2152
2202
  }
2153
2203
  /**
@@ -2715,18 +2765,20 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2715
2765
  hasPendingMessages() {
2716
2766
  return this.pendingMessagesCount !== 0;
2717
2767
  }
2718
- updateDocumentDirtyState(dirty) {
2719
- if (this.attachState === container_definitions_1.AttachState.Attached) {
2720
- // Other way is not true = see this.isContainerMessageDirtyable()
2721
- (0, internal_2.assert)(!dirty || this.hasPendingMessages(), 0x3d3 /* if doc is dirty, there has to be pending ops */);
2722
- }
2723
- else {
2724
- (0, internal_2.assert)(dirty, 0x3d2 /* Non-attached container is dirty */);
2725
- }
2726
- if (this.dirtyContainer === dirty) {
2768
+ /**
2769
+ * Emit "dirty" or "saved" event based on the current dirty state of the document.
2770
+ * This must be called every time the states underlying the dirty state change.
2771
+ *
2772
+ * @privateRemarks - It's helpful to think of this as an event handler registered
2773
+ * for hypothetical "changed" events for PendingStateManager, Outbox, and Container Attach machinery.
2774
+ * But those events don't exist so we manually call this wherever we know those changes happen.
2775
+ */
2776
+ updateDocumentDirtyState() {
2777
+ const dirty = this.computeCurrentDirtyState();
2778
+ if (this.lastEmittedDirty === dirty) {
2727
2779
  return;
2728
2780
  }
2729
- this.dirtyContainer = dirty;
2781
+ this.lastEmittedDirty = dirty;
2730
2782
  if (this.emitDirtyDocumentEvent) {
2731
2783
  this.emit(dirty ? "dirty" : "saved");
2732
2784
  }
@@ -2781,6 +2833,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2781
2833
  try {
2782
2834
  // If we're resubmitting a batch, keep the same "staged" value as before. Otherwise, use the current "global" state.
2783
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 */);
2784
2837
  // Before submitting any non-staged change, submit the ID Allocation op to cover any compressed IDs included in the op.
2785
2838
  if (!staged) {
2786
2839
  this.submitIdAllocationOpIfNeeded({ staged: false });
@@ -2788,7 +2841,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2788
2841
  // Allow document schema controller to send a message if it needs to propose change in document schema.
2789
2842
  // If it needs to send a message, it will call provided callback with payload of such message and rely
2790
2843
  // on this callback to do actual sending.
2791
- const schemaChangeMessage = this.documentsSchemaController.maybeSendSchemaMessage();
2844
+ const schemaChangeMessage = this.documentsSchemaController.maybeGenerateSchemaMessage();
2792
2845
  if (schemaChangeMessage) {
2793
2846
  this.mc.logger.sendTelemetryEvent({
2794
2847
  eventName: "SchemaChangeProposal",
@@ -2824,14 +2877,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2824
2877
  else {
2825
2878
  this.outbox.submit(message);
2826
2879
  }
2827
- // Note: Technically, the system "always" batches - if this case is true we'll just have a single-message batch.
2828
- const flushImmediatelyOnSubmit = !this.currentlyBatching();
2829
- if (flushImmediatelyOnSubmit) {
2830
- this.flush();
2831
- }
2832
- else {
2833
- this.scheduleFlush();
2834
- }
2880
+ this.scheduleFlush();
2835
2881
  }
2836
2882
  catch (error) {
2837
2883
  const dpe = internal_8.DataProcessingError.wrapIfUnrecognized(error, "ContainerRuntime.submit", {
@@ -2840,27 +2886,26 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2840
2886
  this.closeFn(dpe);
2841
2887
  throw dpe;
2842
2888
  }
2843
- if (this.isContainerMessageDirtyable(containerRuntimeMessage)) {
2844
- this.updateDocumentDirtyState(true);
2845
- }
2889
+ this.updateDocumentDirtyState();
2846
2890
  }
2847
2891
  scheduleFlush() {
2848
- if (this.flushTaskExists) {
2892
+ if (this.flushScheduled) {
2849
2893
  return;
2850
2894
  }
2851
- this.flushTaskExists = true;
2852
- // TODO: hoist this out of the function scope to save unnecessary allocations
2853
- // eslint-disable-next-line unicorn/consistent-function-scoping -- Separate `flush` method already exists in outer scope
2854
- const flush = () => {
2855
- this.flushTaskExists = false;
2856
- this.flush();
2857
- };
2895
+ this.flushScheduled = true;
2858
2896
  switch (this.flushMode) {
2897
+ case internal_6.FlushMode.Immediate: {
2898
+ // When in Immediate flush mode, flush immediately unless we are intentionally batching multiple ops (e.g. via orderSequentially)
2899
+ if (!this.batchRunner.running) {
2900
+ this.flush();
2901
+ }
2902
+ break;
2903
+ }
2859
2904
  case internal_6.FlushMode.TurnBased: {
2860
2905
  // When in TurnBased flush mode the runtime will buffer operations in the current turn and send them as a single
2861
2906
  // batch at the end of the turn
2862
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
2863
- Promise.resolve().then(flush);
2907
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- Container will close if flush throws
2908
+ Promise.resolve().then(() => this.flush());
2864
2909
  break;
2865
2910
  }
2866
2911
  // FlushModeExperimental is experimental and not exposed directly in the runtime APIs
@@ -2868,12 +2913,11 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2868
2913
  // When in Async flush mode, the runtime will accumulate all operations across JS turns and send them as a single
2869
2914
  // batch when all micro-tasks are complete.
2870
2915
  // Compared to TurnBased, this flush mode will capture more ops into the same batch.
2871
- setTimeout(flush, 0);
2916
+ setTimeout(() => this.flush(), 0);
2872
2917
  break;
2873
2918
  }
2874
2919
  default: {
2875
- (0, internal_2.assert)(this.batchRunner.running, 0x587 /* Unreachable unless manually accumulating a batch */);
2876
- break;
2920
+ (0, internal_2.fail)(0x587 /* Unreachable unless manually accumulating a batch */);
2877
2921
  }
2878
2922
  }
2879
2923
  }
@@ -2895,43 +2939,72 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2895
2939
  }
2896
2940
  /**
2897
2941
  * Resubmits each message in the batch, and then flushes the outbox.
2942
+ * This typically happens when we reconnect and there are pending messages.
2943
+ *
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)
2898
2947
  *
2899
- * @remarks - If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
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,
2900
2950
  * for correlation to detect container forking.
2901
2951
  */
2902
2952
  reSubmitBatch(batch, { batchId, staged, squash }) {
2953
+ (0, internal_2.assert)(this._summarizer === undefined, 0x8f2 /* Summarizer never reconnects so should never resubmit */);
2903
2954
  const resubmitInfo = {
2904
2955
  // Only include Batch ID if "Offline Load" feature is enabled
2905
2956
  // It's only needed to identify batches across container forks arising from misuse of offline load.
2906
2957
  batchId: this.offlineEnabled ? batchId : undefined,
2907
2958
  staged,
2908
2959
  };
2960
+ const resubmitFn = squash
2961
+ ? this.reSubmitWithSquashing.bind(this)
2962
+ : this.reSubmit.bind(this);
2909
2963
  this.batchRunner.run(() => {
2910
2964
  for (const message of batch) {
2911
- this.reSubmit(message, squash);
2965
+ resubmitFn(message);
2912
2966
  }
2913
2967
  }, resubmitInfo);
2914
2968
  this.flush(resubmitInfo);
2915
2969
  }
2916
- reSubmit(message, squash) {
2917
- 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
+ }
2918
2993
  }
2919
2994
  /**
2920
- * Finds the right store and asks it to resubmit the message. This typically happens when we
2921
- * reconnect and there are pending messages.
2922
- * ! Note: successfully resubmitting an op that has been successfully sequenced is not possible due to checks in the ConnectionStateHandler (Loader layer)
2923
- * @param message - The original LocalContainerRuntimeMessage.
2924
- * @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
2925
2998
  */
2926
- reSubmitCore(message, localOpMetadata, opMetadata, squash) {
2927
- (0, internal_2.assert)(this._summarizer === undefined, 0x8f2 /* Summarizer never reconnects so should never resubmit */);
2999
+ reSubmit({ runtimeOp: message, localOpMetadata, opMetadata, }) {
2928
3000
  switch (message.type) {
2929
3001
  case messageTypes_js_1.ContainerMessageType.FluidDataStoreOp:
2930
3002
  case messageTypes_js_1.ContainerMessageType.Attach:
2931
3003
  case messageTypes_js_1.ContainerMessageType.Alias: {
2932
3004
  // For Operations, call resubmitDataStoreOp which will find the right store
2933
3005
  // and trigger resubmission on it.
2934
- this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata, squash);
3006
+ this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata,
3007
+ /* squash: */ false);
2935
3008
  break;
2936
3009
  }
2937
3010
  case messageTypes_js_1.ContainerMessageType.IdAllocation: {
@@ -2957,9 +3030,9 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2957
3030
  break;
2958
3031
  }
2959
3032
  case messageTypes_js_1.ContainerMessageType.DocumentSchemaChange: {
2960
- // There is no need to resend this message. Document schema controller will properly resend it again (if needed)
2961
- // on a first occasion (any ops sent after reconnect). There is a good chance, though, that it will not want to
2962
- // 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();
2963
3036
  break;
2964
3037
  }
2965
3038
  default: {
@@ -2969,8 +3042,11 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2969
3042
  }
2970
3043
  }
2971
3044
  }
2972
- rollback(runtimeOp, localOpMetadata) {
2973
- 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 */);
2974
3050
  switch (type) {
2975
3051
  case messageTypes_js_1.ContainerMessageType.FluidDataStoreOp: {
2976
3052
  // For operations, call rollbackDataStoreOp which will find the right store
@@ -2978,8 +3054,24 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2978
3054
  this.channelCollection.rollback(type, contents, localOpMetadata);
2979
3055
  break;
2980
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
+ }
2981
3073
  default: {
2982
- throw new Error(`Can't rollback ${type}`);
3074
+ (0, internal_2.unreachableCase)(type);
2983
3075
  }
2984
3076
  }
2985
3077
  }
@@ -3144,6 +3236,30 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
3144
3236
  return this.summaryManager.enqueueSummarize(options);
3145
3237
  }
3146
3238
  }
3239
+ acquireExtension(id, factory, ...useContext) {
3240
+ let entry = this.extensions.get(id);
3241
+ if (entry === undefined) {
3242
+ const runtime = {
3243
+ isConnected: () => this.connected,
3244
+ getClientId: () => this.clientId,
3245
+ events: this.lazyEventsForExtensions.value,
3246
+ logger: this.baseLogger,
3247
+ submitAddressedSignal: (addressChain, message) => {
3248
+ this.submitExtensionSignal(id, addressChain, message);
3249
+ },
3250
+ getQuorum: this.getQuorum.bind(this),
3251
+ getAudience: this.getAudience.bind(this),
3252
+ supportedFeatures: this.ILayerCompatDetails.supportedFeatures,
3253
+ };
3254
+ entry = new factory(runtime, ...useContext);
3255
+ this.extensions.set(id, entry);
3256
+ }
3257
+ else {
3258
+ (0, internal_2.assert)(entry instanceof factory, 0xba1 /* Extension entry is not of the expected type */);
3259
+ entry.extension.onNewUse(...useContext);
3260
+ }
3261
+ return entry.interface;
3262
+ }
3147
3263
  get groupedBatchingEnabled() {
3148
3264
  return this.sessionSchema.opGroupingEnabled === true;
3149
3265
  }
@@ -3157,4 +3273,34 @@ function createNewSignalEnvelope(address, type, content) {
3157
3273
  return newEnvelope;
3158
3274
  }
3159
3275
  exports.createNewSignalEnvelope = createNewSignalEnvelope;
3276
+ function isContainerMessageDirtyable({ type, contents, }) {
3277
+ // Certain container runtime messages should not mark the container dirty such as the old built-in
3278
+ // AgentScheduler and Garbage collector messages.
3279
+ switch (type) {
3280
+ case messageTypes_js_1.ContainerMessageType.Attach: {
3281
+ const attachMessage = contents;
3282
+ if (attachMessage.id === exports.agentSchedulerId) {
3283
+ return false;
3284
+ }
3285
+ break;
3286
+ }
3287
+ case messageTypes_js_1.ContainerMessageType.FluidDataStoreOp: {
3288
+ const envelope = contents;
3289
+ if (envelope.address === exports.agentSchedulerId) {
3290
+ return false;
3291
+ }
3292
+ break;
3293
+ }
3294
+ case messageTypes_js_1.ContainerMessageType.IdAllocation:
3295
+ case messageTypes_js_1.ContainerMessageType.DocumentSchemaChange:
3296
+ case messageTypes_js_1.ContainerMessageType.GC: {
3297
+ return false;
3298
+ }
3299
+ default: {
3300
+ break;
3301
+ }
3302
+ }
3303
+ return true;
3304
+ }
3305
+ exports.isContainerMessageDirtyable = isContainerMessageDirtyable;
3160
3306
  //# sourceMappingURL=containerRuntime.js.map