@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
@@ -2,10 +2,10 @@
2
2
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
- import { Trace, TypedEventEmitter } from "@fluid-internal/client-utils";
5
+ import { createEmitter, Trace, TypedEventEmitter } from "@fluid-internal/client-utils";
6
6
  import { AttachState } from "@fluidframework/container-definitions";
7
7
  import { isIDeltaManagerFull } from "@fluidframework/container-definitions/internal";
8
- import { assert, Deferred, LazyPromise, PromiseCache, delay, } from "@fluidframework/core-utils/internal";
8
+ import { assert, Deferred, Lazy, LazyPromise, PromiseCache, delay, fail, unreachableCase, } from "@fluidframework/core-utils/internal";
9
9
  import { SummaryType } from "@fluidframework/driver-definitions";
10
10
  import { FetchSource, MessageType } from "@fluidframework/driver-definitions/internal";
11
11
  import { readAndParse } from "@fluidframework/driver-utils/internal";
@@ -19,7 +19,7 @@ import { v4 as uuid } from "uuid";
19
19
  import { BindBatchTracker } from "./batchTracker.js";
20
20
  import { BlobManager, blobManagerBasePath, blobsTreeName, isBlobPath, loadBlobManagerLoadInfo, } from "./blobManager/index.js";
21
21
  import { ChannelCollection, getSummaryForDatastores, wrapContext, } from "./channelCollection.js";
22
- import { defaultMinVersionForCollab, getMinVersionForCollabDefaults, isValidMinVersionForCollab, } from "./compatUtils.js";
22
+ import { defaultMinVersionForCollab, getMinVersionForCollabDefaults, isValidMinVersionForCollab, validateRuntimeOptions, } from "./compatUtils.js";
23
23
  import { CompressionAlgorithms, disabledCompressionConfig } from "./compressionDefinitions.js";
24
24
  import { ReportOpPerfTelemetry } from "./connectionTelemetry.js";
25
25
  import { ContainerFluidHandleContext } from "./containerHandleContext.js";
@@ -191,6 +191,20 @@ export async function loadContainerRuntime(params) {
191
191
  return ContainerRuntime.loadRuntime(params);
192
192
  }
193
193
  const defaultMaxConsecutiveReconnects = 7;
194
+ /**
195
+ * These are the ONLY message types that are allowed to be submitted while in staging mode
196
+ * (Does not apply to pre-StagingMode batches that are resubmitted, those are not considered to be staged)
197
+ */
198
+ function canStageMessageOfType(type) {
199
+ return (
200
+ // These are user changes coming up from the runtime's DataStores
201
+ type === ContainerMessageType.FluidDataStoreOp ||
202
+ // GC ops are used to detect issues in the reference graph so all clients can repair their GC state.
203
+ // These can be submitted at any time, including while in Staging Mode.
204
+ type === ContainerMessageType.GC ||
205
+ // These are typically sent shortly after boot and will not be common in Staging Mode, but it's possible.
206
+ type === ContainerMessageType.DocumentSchemaChange);
207
+ }
194
208
  /**
195
209
  * Represents the runtime of the container. Contains helper functions/state of the container.
196
210
  * It will define the store level mappings.
@@ -239,6 +253,9 @@ export class ContainerRuntime extends TypedEventEmitter {
239
253
  if (!isValidMinVersionForCollab(minVersionForCollab)) {
240
254
  throw new UsageError(`Invalid minVersionForCollab: ${minVersionForCollab}. It must be an existing FF version (i.e. 2.22.1).`);
241
255
  }
256
+ // We also validate that there is not a mismatch between `minVersionForCollab` and runtime options that
257
+ // were manually set.
258
+ validateRuntimeOptions(minVersionForCollab, runtimeOptions);
242
259
  const defaultsAffectingDocSchema = getMinVersionForCollabDefaults(minVersionForCollab);
243
260
  // The following are the default values for the options that do not affect the DocumentSchema.
244
261
  const defaultsNotAffectingDocSchema = {
@@ -497,15 +514,19 @@ export class ContainerRuntime extends TypedEventEmitter {
497
514
  }
498
515
  /**
499
516
  * Invokes the given callback and expects that no ops are submitted
500
- * until execution finishes. If an op is submitted, an error will be raised.
517
+ * until execution finishes. If an op is submitted, it will be marked as reentrant.
501
518
  *
502
519
  * @param callback - the callback to be invoked
503
520
  */
504
521
  ensureNoDataModelChanges(callback) {
505
522
  return this.dataModelChangeRunner.run(callback);
506
523
  }
524
+ /**
525
+ * Indicates whether the container is in a state where it is able to send
526
+ * ops (connected to op stream and not in readonly mode).
527
+ */
507
528
  get connected() {
508
- return this._connected;
529
+ return this.canSendOps;
509
530
  }
510
531
  /**
511
532
  * clientId of parent (non-summarizing) container that owns summarizer container
@@ -569,7 +590,7 @@ export class ContainerRuntime extends TypedEventEmitter {
569
590
  // Once it loads, it will process all such ops and we will stop accumulating further ops - ops will be processes as they come in.
570
591
  this.pendingIdCompressorOps = [];
571
592
  this.batchRunner = new BatchRunCounter();
572
- this.flushTaskExists = false;
593
+ this.flushScheduled = false;
573
594
  this.consecutiveReconnects = 0;
574
595
  this.dataModelChangeRunner = new RunCounter();
575
596
  this._disposed = false;
@@ -582,6 +603,7 @@ export class ContainerRuntime extends TypedEventEmitter {
582
603
  this.snapshotCacheForLoadingGroupIds = new PromiseCache({
583
604
  expiry: { policy: "absolute", durationMs: 60000 },
584
605
  });
606
+ this.extensions = new Map();
585
607
  this.notifyReadOnlyState = (readonly) => this.channelCollection.notifyReadOnlyState(readonly);
586
608
  /**
587
609
  * Enter Staging Mode, such that ops submitted to the ContainerRuntime will not be sent to the ordering service.
@@ -592,7 +614,10 @@ export class ContainerRuntime extends TypedEventEmitter {
592
614
  // eslint-disable-next-line import/no-deprecated
593
615
  this.enterStagingMode = () => {
594
616
  if (this.stageControls !== undefined) {
595
- throw new Error("already in staging mode");
617
+ throw new UsageError("already in staging mode");
618
+ }
619
+ if (this.attachState === AttachState.Detached) {
620
+ throw new UsageError("cannot enter staging mode while detached");
596
621
  }
597
622
  // Make sure all BatchManagers are empty before entering staging mode,
598
623
  // since we mark whole batches as "staged" or not to indicate whether to submit them.
@@ -613,11 +638,9 @@ export class ContainerRuntime extends TypedEventEmitter {
613
638
  // Pop all staged batches from the PSM and roll them back in LIFO order
614
639
  this.pendingStateManager.popStagedBatches(({ runtimeOp, localOpMetadata }) => {
615
640
  assert(runtimeOp !== undefined, 0xb82 /* Staged batches expected to have runtimeOp defined */);
616
- this.rollback(runtimeOp, localOpMetadata);
641
+ this.rollbackStagedChanges(runtimeOp, localOpMetadata);
617
642
  });
618
- if (this.attachState === AttachState.Attached) {
619
- this.updateDocumentDirtyState(this.pendingMessagesCount !== 0);
620
- }
643
+ this.updateDocumentDirtyState();
621
644
  }),
622
645
  commitChanges: (optionsParam) => {
623
646
  const options = { ...defaultStagingCommitOptions, ...optionsParam };
@@ -634,6 +657,15 @@ export class ContainerRuntime extends TypedEventEmitter {
634
657
  return this.stageControls;
635
658
  };
636
659
  this.readAndParseBlob = async (id) => readAndParse(this.storage, id);
660
+ // While internal, ContainerRuntime has not been converted to use the new events support.
661
+ // Recreate the required events (new pattern) with injected, wrapper new emitter.
662
+ // It is lazily create to avoid listeners (old events) that ultimately go nowhere.
663
+ this.lazyEventsForExtensions = new Lazy(() => {
664
+ const eventEmitter = createEmitter();
665
+ this.on("connected", (clientId) => eventEmitter.emit("connected", clientId));
666
+ this.on("disconnected", () => eventEmitter.emit("disconnected"));
667
+ return eventEmitter;
668
+ });
637
669
  const { options, clientDetails, connected, baseSnapshot, submitFn, submitBatchFn, submitSummaryFn, submitSignalFn, disposeFn, closeFn, deltaManager, quorum, audience, pendingLocalState, supportedFeatures, snapshotWithContents, } = context;
638
670
  // In old loaders without dispose functionality, closeFn is equivalent but will also switch container to readonly mode
639
671
  this.disposeFn = disposeFn ?? closeFn;
@@ -643,6 +675,11 @@ export class ContainerRuntime extends TypedEventEmitter {
643
675
  this.mc = createChildMonitoringContext({
644
676
  logger: this.baseLogger,
645
677
  namespace: "ContainerRuntime",
678
+ properties: {
679
+ all: {
680
+ inStagingMode: this.inStagingMode,
681
+ },
682
+ },
646
683
  });
647
684
  // If we support multiple algorithms in the future, then we would need to manage it here carefully.
648
685
  // We can use runtimeOptions.compressionOptions.compressionAlgorithm, but only if it's in the schema list!
@@ -662,12 +699,28 @@ export class ContainerRuntime extends TypedEventEmitter {
662
699
  this.submitSummaryFn =
663
700
  submitSummaryFn ??
664
701
  ((summaryOp, refseq) => submitFn(MessageType.Summarize, summaryOp, false));
665
- this.submitSignalFn = submitSignalFn;
702
+ const sequenceAndSubmitSignal = (envelope, targetClientId) => {
703
+ if (targetClientId === undefined) {
704
+ this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope);
705
+ }
706
+ submitSignalFn(envelope, targetClientId);
707
+ };
708
+ this.submitSignalFn = (envelope, targetClientId) => {
709
+ if (envelope.address?.startsWith("/")) {
710
+ throw new Error("General path based addressing is not implemented");
711
+ }
712
+ sequenceAndSubmitSignal(envelope, targetClientId);
713
+ };
714
+ this.submitExtensionSignal = (id, addressChain, message) => {
715
+ this.verifyNotClosed();
716
+ const envelope = createNewSignalEnvelope(`/ext/${id}/${addressChain.join("/")}`, message.type, message.content);
717
+ sequenceAndSubmitSignal(envelope, message.targetClientId);
718
+ };
666
719
  // TODO: After IContainerContext.options is removed, we'll just create a new blank object {} here.
667
720
  // Values are generally expected to be set from the runtime side.
668
721
  this.options = options ?? {};
669
722
  this.clientDetails = clientDetails;
670
- const isSummarizerClient = this.clientDetails.type === summarizerClientType;
723
+ this.isSummarizerClient = this.clientDetails.type === summarizerClientType;
671
724
  this.loadedFromVersionId = context.getLoadedFromVersion()?.id;
672
725
  // eslint-disable-next-line unicorn/consistent-destructuring
673
726
  this._getClientId = () => context.clientId;
@@ -694,8 +747,8 @@ export class ContainerRuntime extends TypedEventEmitter {
694
747
  this.mc.logger.sendTelemetryEvent({
695
748
  eventName: "Attached",
696
749
  details: {
697
- dirtyContainer: this.dirtyContainer,
698
- hasPendingMessages: this.hasPendingMessages(),
750
+ lastEmittedDirty: this.lastEmittedDirty,
751
+ currentDirtyState: this.computeCurrentDirtyState(),
699
752
  },
700
753
  });
701
754
  });
@@ -704,7 +757,7 @@ export class ContainerRuntime extends TypedEventEmitter {
704
757
  details: { attachState: this.attachState },
705
758
  }));
706
759
  // In cases of summarizer, we want to dispose instead since consumer doesn't interact with this container
707
- this.closeFn = isSummarizerClient ? this.disposeFn : closeFn;
760
+ this.closeFn = this.isSummarizerClient ? this.disposeFn : closeFn;
708
761
  let loadSummaryNumber;
709
762
  // Get the container creation metadata. For new container, we initialize these. For existing containers,
710
763
  // get the values from the metadata blob.
@@ -728,7 +781,7 @@ export class ContainerRuntime extends TypedEventEmitter {
728
781
  this.messageAtLastSummary = lastMessageFromMetadata(metadata);
729
782
  // Note that we only need to pull the *initial* connected state from the context.
730
783
  // Later updates come through calls to setConnectionState.
731
- this._connected = connected;
784
+ this.canSendOps = connected;
732
785
  this.mc.logger.sendTelemetryEvent({
733
786
  eventName: "GCFeatureMatrix",
734
787
  metadataValue: JSON.stringify(metadata?.gcFeatureMatrix),
@@ -821,7 +874,7 @@ export class ContainerRuntime extends TypedEventEmitter {
821
874
  existing,
822
875
  metadata,
823
876
  createContainerMetadata: this.createContainerMetadata,
824
- isSummarizerClient,
877
+ isSummarizerClient: this.isSummarizerClient,
825
878
  getNodePackagePath: async (nodePath) => this.getGCNodePackagePath(nodePath),
826
879
  getLastSummaryTimestampMs: () => this.messageAtLastSummary?.timestamp,
827
880
  readAndParseBlob: async (id) => readAndParse(this.storage, id),
@@ -862,9 +915,6 @@ export class ContainerRuntime extends TypedEventEmitter {
862
915
  // verifyNotClosed is called in FluidDataStoreContext, which is *the* expected caller.
863
916
  const envelope1 = content;
864
917
  const envelope2 = createNewSignalEnvelope(envelope1.address, type, envelope1.contents);
865
- if (targetClientId === undefined) {
866
- this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope2);
867
- }
868
918
  this.submitSignalFn(envelope2, targetClientId);
869
919
  };
870
920
  let snapshot = getSummaryForDatastores(baseSnapshot, metadata);
@@ -908,7 +958,7 @@ export class ContainerRuntime extends TypedEventEmitter {
908
958
  // Keep the old flag name even though we renamed the class member (it shipped in 2.31.0)
909
959
  this.mc.config.getBoolean("Fluid.ContainerRuntime.DisableFlushBeforeProcess") === true;
910
960
  this.outbox = new Outbox({
911
- shouldSend: () => this.canSendOps(),
961
+ shouldSend: () => this.shouldSendOps(),
912
962
  pendingStateManager: this.pendingStateManager,
913
963
  submitBatchFn,
914
964
  legacySendBatchFn,
@@ -958,9 +1008,9 @@ export class ContainerRuntime extends TypedEventEmitter {
958
1008
  const closeSummarizerDelayOverride = this.mc.config.getNumber("Fluid.ContainerRuntime.Test.CloseSummarizerDelayOverrideMs");
959
1009
  this.closeSummarizerDelayMs =
960
1010
  closeSummarizerDelayOverride ?? defaultCloseSummarizerDelayMs;
961
- this.dirtyContainer =
962
- this.attachState !== AttachState.Attached || this.hasPendingMessages();
963
- context.updateDirtyContainerState(this.dirtyContainer);
1011
+ // We haven't emitted dirty/saved yet, but this is the baseline so we know to emit when it changes
1012
+ this.lastEmittedDirty = this.computeCurrentDirtyState();
1013
+ context.updateDirtyContainerState(this.lastEmittedDirty);
964
1014
  if (!this.skipSafetyFlushDuringProcessStack) {
965
1015
  // Reference Sequence Number may have just changed, and it must be consistent across a batch,
966
1016
  // so we should flush now to clear the way for the next ops.
@@ -1047,7 +1097,14 @@ export class ContainerRuntime extends TypedEventEmitter {
1047
1097
  await this.initializeSummarizer(loader);
1048
1098
  if (this.sessionSchema.idCompressorMode === "on" ||
1049
1099
  (this.sessionSchema.idCompressorMode === "delayed" && this.connected)) {
1050
- this._idCompressor = this.createIdCompressorFn();
1100
+ PerformanceEvent.timedExec(this.mc.logger, { eventName: "CreateIdCompressorOnBoot" }, (event) => {
1101
+ this._idCompressor = this.createIdCompressorFn();
1102
+ event.end({
1103
+ details: {
1104
+ idCompressorMode: this.sessionSchema.idCompressorMode,
1105
+ },
1106
+ });
1107
+ });
1051
1108
  // This is called from loadRuntime(), long before we process any ops, so there should be no ops accumulated yet.
1052
1109
  assert(this.pendingIdCompressorOps.length === 0, 0x8ec /* no pending ops */);
1053
1110
  }
@@ -1076,8 +1133,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1076
1133
  const orderedClientCollection = new OrderedClientCollection(orderedClientLogger, this.innerDeltaManager, this._quorum);
1077
1134
  const orderedClientElectionForSummarizer = new OrderedClientElection(orderedClientLogger, orderedClientCollection, this.electedSummarizerData ?? this.innerDeltaManager.lastSequenceNumber, SummarizerClientElection.isClientEligible, this.mc.config.getBoolean("Fluid.ContainerRuntime.OrderedClientElection.EnablePerformanceEvents"));
1078
1135
  this.summarizerClientElection = new SummarizerClientElection(orderedClientLogger, summaryCollection, orderedClientElectionForSummarizer, maxOpsSinceLastSummary);
1079
- const isSummarizerClient = this.clientDetails.type === summarizerClientType;
1080
- if (isSummarizerClient) {
1136
+ if (this.isSummarizerClient) {
1081
1137
  // We want to dynamically import any thing inside summaryDelayLoadedModule module only when we are the summarizer client,
1082
1138
  // so that all non summarizer clients don't have to load the code inside this module.
1083
1139
  const module = await import(
@@ -1425,35 +1481,32 @@ export class ContainerRuntime extends TypedEventEmitter {
1425
1481
  }
1426
1482
  replayPendingStates() {
1427
1483
  // We need to be able to send ops to replay states
1428
- if (!this.canSendOps()) {
1484
+ if (!this.shouldSendOps()) {
1429
1485
  return;
1430
1486
  }
1431
- // We need to temporary clear the dirty flags and disable
1432
- // dirty state change events to detect whether replaying ops
1433
- // has any effect.
1434
- // Save the old state, reset to false, disable event emit
1435
- const oldState = this.dirtyContainer;
1436
- this.dirtyContainer = false;
1487
+ // Replaying is an internal operation and we don't want to generate noise while doing it.
1488
+ // So temporarily disable dirty state change events, and save the old state.
1489
+ // When we're done, we'll emit the event if the state changed.
1490
+ const oldState = this.lastEmittedDirty;
1437
1491
  assert(this.emitDirtyDocumentEvent, 0x127 /* "dirty document event not set on replay" */);
1438
1492
  this.emitDirtyDocumentEvent = false;
1439
- let newState;
1440
1493
  try {
1441
1494
  // Any ID Allocation ops that failed to submit after the pending state was queued need to have
1442
1495
  // the corresponding ranges resubmitted (note this call replaces the typical resubmit flow).
1443
1496
  // Since we don't submit ID Allocation ops when staged, any outstanding ranges would be from
1444
1497
  // before staging mode so we can simply say staged: false.
1445
1498
  this.submitIdAllocationOpIfNeeded({ resubmitOutstandingRanges: true, staged: false });
1499
+ this.scheduleFlush();
1446
1500
  // replay the ops
1447
1501
  this.pendingStateManager.replayPendingStates();
1448
1502
  }
1449
1503
  finally {
1450
- // Save the new start and restore the old state, re-enable event emit
1451
- newState = this.dirtyContainer;
1452
- this.dirtyContainer = oldState;
1504
+ // Restore the old state, re-enable event emit
1505
+ this.lastEmittedDirty = oldState;
1453
1506
  this.emitDirtyDocumentEvent = true;
1454
1507
  }
1455
- // Officially transition from the old state to the new state.
1456
- this.updateDocumentDirtyState(newState);
1508
+ // This will emit an event if the state changed relative to before replay
1509
+ this.updateDocumentDirtyState();
1457
1510
  }
1458
1511
  /**
1459
1512
  * Parse an op's type and actual content from given serialized content
@@ -1510,25 +1563,35 @@ export class ContainerRuntime extends TypedEventEmitter {
1510
1563
  loadIdCompressor() {
1511
1564
  if (this._idCompressor === undefined &&
1512
1565
  this.sessionSchema.idCompressorMode !== undefined) {
1513
- this._idCompressor = this.createIdCompressorFn();
1514
- // Finalize any ranges we received while the compressor was turned off.
1515
- const ops = this.pendingIdCompressorOps;
1516
- this.pendingIdCompressorOps = [];
1517
- for (const range of ops) {
1518
- this._idCompressor.finalizeCreationRange(range);
1519
- }
1566
+ PerformanceEvent.timedExec(this.mc.logger, { eventName: "CreateIdCompressorOnDelayedLoad" }, (event) => {
1567
+ this._idCompressor = this.createIdCompressorFn();
1568
+ // Finalize any ranges we received while the compressor was turned off.
1569
+ const ops = this.pendingIdCompressorOps;
1570
+ this.pendingIdCompressorOps = [];
1571
+ const trace = Trace.start();
1572
+ for (const range of ops) {
1573
+ this._idCompressor.finalizeCreationRange(range);
1574
+ }
1575
+ event.end({
1576
+ details: {
1577
+ finalizeCreationRangeDuration: trace.trace().duration,
1578
+ idCompressorMode: this.sessionSchema.idCompressorMode,
1579
+ pendingIdCompressorOps: ops.length,
1580
+ },
1581
+ });
1582
+ });
1520
1583
  assert(this.pendingIdCompressorOps.length === 0, 0x976 /* No new ops added */);
1521
1584
  }
1522
1585
  }
1523
- setConnectionState(connected, clientId) {
1586
+ setConnectionState(canSendOps, clientId) {
1524
1587
  // Validate we have consistent state
1525
1588
  const currentClientId = this._audience.getSelf()?.clientId;
1526
1589
  assert(clientId === currentClientId, 0x977 /* input clientId does not match Audience */);
1527
1590
  assert(this.clientId === currentClientId, 0x978 /* this.clientId does not match Audience */);
1528
- if (connected && this.sessionSchema.idCompressorMode === "delayed") {
1591
+ if (canSendOps && this.sessionSchema.idCompressorMode === "delayed") {
1529
1592
  this.loadIdCompressor();
1530
1593
  }
1531
- if (connected === false && this.delayConnectClientId !== undefined) {
1594
+ if (canSendOps === false && this.delayConnectClientId !== undefined) {
1532
1595
  this.delayConnectClientId = undefined;
1533
1596
  this.mc.logger.sendTelemetryEvent({
1534
1597
  eventName: "UnsuccessfulConnectedTransition",
@@ -1536,37 +1599,39 @@ export class ContainerRuntime extends TypedEventEmitter {
1536
1599
  // Don't propagate "disconnected" event because we didn't propagate the previous "connected" event
1537
1600
  return;
1538
1601
  }
1539
- if (!connected) {
1540
- this.documentsSchemaController.onDisconnect();
1541
- }
1542
1602
  // If there are stashed blobs in the pending state, we need to delay
1543
1603
  // propagation of the "connected" event until we have uploaded them to
1544
1604
  // ensure we don't submit ops referencing a blob that has not been uploaded
1545
- const connecting = connected && !this._connected;
1605
+ const connecting = canSendOps && !this.canSendOps;
1546
1606
  if (connecting && this.blobManager.hasPendingStashedUploads()) {
1547
1607
  assert(!this.delayConnectClientId, 0x791 /* Connect event delay must be canceled before subsequent connect event */);
1548
1608
  assert(!!clientId, 0x792 /* Must have clientId when connecting */);
1549
1609
  this.delayConnectClientId = clientId;
1550
1610
  return;
1551
1611
  }
1552
- this.setConnectionStateCore(connected, clientId);
1612
+ this.setConnectionStateCore(canSendOps, clientId);
1553
1613
  }
1554
- setConnectionStateCore(connected, clientId) {
1614
+ /**
1615
+ * Raises and propagates connected events.
1616
+ * @param canSendOps - Indicates whether the container can send ops or not (connected and not readonly).
1617
+ * @remarks The connection state from container context used here when raising connected events.
1618
+ */
1619
+ setConnectionStateCore(canSendOps, clientId) {
1555
1620
  assert(!this.delayConnectClientId, 0x394 /* connect event delay must be cleared before propagating connect event */);
1556
1621
  this.verifyNotClosed();
1557
1622
  // There might be no change of state due to Container calling this API after loading runtime.
1558
- const changeOfState = this._connected !== connected;
1559
- const reconnection = changeOfState && !connected;
1623
+ const canSendOpsChanged = this.canSendOps !== canSendOps;
1624
+ const reconnection = canSendOpsChanged && !canSendOps;
1560
1625
  // We need to flush the ops currently collected by Outbox to preserve original order.
1561
1626
  // This flush NEEDS to happen before we set the ContainerRuntime to "connected".
1562
1627
  // We want these ops to get to the PendingStateManager without sending to service and have them return to the Outbox upon calling "replayPendingStates".
1563
- if (changeOfState && connected) {
1628
+ if (canSendOpsChanged && canSendOps) {
1564
1629
  this.flush();
1565
1630
  }
1566
- this._connected = connected;
1567
- if (connected) {
1631
+ this.canSendOps = canSendOps;
1632
+ if (canSendOps) {
1568
1633
  assert(this.attachState === AttachState.Attached, 0x3cd /* Connection is possible only if container exists in storage */);
1569
- if (changeOfState) {
1634
+ if (canSendOpsChanged) {
1570
1635
  this.signalTelemetryManager.resetTracking();
1571
1636
  }
1572
1637
  }
@@ -1582,12 +1647,12 @@ export class ContainerRuntime extends TypedEventEmitter {
1582
1647
  return;
1583
1648
  }
1584
1649
  }
1585
- if (changeOfState) {
1650
+ if (canSendOpsChanged) {
1586
1651
  this.replayPendingStates();
1587
1652
  }
1588
- this.channelCollection.setConnectionState(connected, clientId);
1589
- this.garbageCollector.setConnectionState(connected, clientId);
1590
- raiseConnectedEvent(this.mc.logger, this, connected, clientId);
1653
+ this.channelCollection.setConnectionState(canSendOps, clientId);
1654
+ this.garbageCollector.setConnectionState(canSendOps, clientId);
1655
+ raiseConnectedEvent(this.mc.logger, this, this.connected /* canSendOps */, clientId);
1591
1656
  }
1592
1657
  async notifyOpReplay(message) {
1593
1658
  await this.pendingStateManager.applyStashedOpsAt(message.sequenceNumber);
@@ -1715,6 +1780,8 @@ export class ContainerRuntime extends TypedEventEmitter {
1715
1780
  * @param groupedBatch - true if these messages are part of a grouped op batch.
1716
1781
  */
1717
1782
  processInboundMessages(messagesWithMetadata, locationInBatch, local, savedOp, runtimeBatch, groupedBatch) {
1783
+ // This message could have been the last pending local (dirtyable) message, in which case we need to update dirty state to "saved"
1784
+ this.updateDocumentDirtyState();
1718
1785
  if (locationInBatch.batchStart) {
1719
1786
  const firstMessage = messagesWithMetadata[0]?.message;
1720
1787
  assert(firstMessage !== undefined, 0xa31 /* Batch must have at least one message */);
@@ -1809,11 +1876,6 @@ export class ContainerRuntime extends TypedEventEmitter {
1809
1876
  message.minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
1810
1877
  }
1811
1878
  this._processedClientSequenceNumber = message.clientSequenceNumber;
1812
- // If there are no more pending messages after processing a local message,
1813
- // the document is no longer dirty.
1814
- if (!this.hasPendingMessages()) {
1815
- this.updateDocumentDirtyState(false);
1816
- }
1817
1879
  // The DeltaManager used to do this, but doesn't anymore as of Loader v2.4
1818
1880
  // Anyone listening to our "op" event would expect the contents to be parsed per this same logic
1819
1881
  if (typeof message.contents === "string" &&
@@ -1835,11 +1897,6 @@ export class ContainerRuntime extends TypedEventEmitter {
1835
1897
  *
1836
1898
  */
1837
1899
  validateAndProcessRuntimeMessages(message, messagesContent, local, savedOp) {
1838
- // If there are no more pending messages after processing a local message,
1839
- // the document is no longer dirty.
1840
- if (!this.hasPendingMessages()) {
1841
- this.updateDocumentDirtyState(false);
1842
- }
1843
1900
  // Get the contents without the localOpMetadata because not all message types know about localOpMetadata.
1844
1901
  const contents = messagesContent.map((c) => c.contents);
1845
1902
  switch (message.type) {
@@ -1915,20 +1972,44 @@ export class ContainerRuntime extends TypedEventEmitter {
1915
1972
  if (message.clientId === this.clientId) {
1916
1973
  this.signalTelemetryManager.trackReceivedSignal(envelope, this.mc.logger, this.consecutiveReconnects);
1917
1974
  }
1918
- if (envelope.address === undefined) {
1975
+ const fullAddress = envelope.address;
1976
+ if (fullAddress === undefined) {
1919
1977
  // No address indicates a container signal message.
1920
1978
  this.emit("signal", transformed, local);
1921
1979
  return;
1922
1980
  }
1923
- // Due to a mismatch between different layers in terms of
1924
- // what is the interface of passing signals, we need to adjust
1925
- // the signal envelope before sending it to the datastores to be processed
1926
- const envelope2 = {
1927
- address: envelope.address,
1928
- contents: transformed.content,
1929
- };
1930
- transformed.content = envelope2;
1931
- this.channelCollection.processSignal(transformed, local);
1981
+ this.routeNonContainerSignal(fullAddress, transformed, local);
1982
+ }
1983
+ routeNonContainerSignal(address, signalMessage, local) {
1984
+ // channelCollection signals are identified by no starting `/` in address.
1985
+ if (!address.startsWith("/")) {
1986
+ // Due to a mismatch between different layers in terms of
1987
+ // what is the interface of passing signals, we need to adjust
1988
+ // the signal envelope before sending it to the datastores to be processed
1989
+ const envelope = {
1990
+ address,
1991
+ contents: signalMessage.content,
1992
+ };
1993
+ signalMessage.content = envelope;
1994
+ this.channelCollection.processSignal(signalMessage, local);
1995
+ return;
1996
+ }
1997
+ const addresses = address.split("/");
1998
+ if (addresses.length > 2 && addresses[1] === "ext") {
1999
+ const id = addresses[2];
2000
+ const entry = this.extensions.get(id);
2001
+ if (entry !== undefined) {
2002
+ entry.extension.processSignal?.(addresses.slice(3), signalMessage, local);
2003
+ return;
2004
+ }
2005
+ }
2006
+ assert(!local, 0xba0 /* No recipient found for local signal */);
2007
+ this.mc.logger.sendTelemetryEvent({
2008
+ eventName: "SignalAddressNotFound",
2009
+ ...tagCodeArtifacts({
2010
+ address,
2011
+ }),
2012
+ });
1932
2013
  }
1933
2014
  /**
1934
2015
  * Flush the current batch of ops to the ordering service for sequencing
@@ -1938,6 +2019,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1938
2019
  * @param resubmitInfo - If defined, indicates this is a resubmission of a batch with the given Batch info needed for resubmit.
1939
2020
  */
1940
2021
  flush(resubmitInfo) {
2022
+ this.flushScheduled = false;
1941
2023
  try {
1942
2024
  assert(!this.batchRunner.running, 0x24c /* "Cannot call `flush()` while manually accumulating a batch (e.g. under orderSequentially) */);
1943
2025
  this.outbox.flush(resubmitInfo);
@@ -1958,7 +2040,6 @@ export class ContainerRuntime extends TypedEventEmitter {
1958
2040
  */
1959
2041
  orderSequentially(callback) {
1960
2042
  let checkpoint;
1961
- const checkpointDirtyState = this.dirtyContainer;
1962
2043
  // eslint-disable-next-line import/no-deprecated
1963
2044
  let stageControls;
1964
2045
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
@@ -1978,11 +2059,10 @@ export class ContainerRuntime extends TypedEventEmitter {
1978
2059
  if (checkpoint) {
1979
2060
  // This will throw and close the container if rollback fails
1980
2061
  try {
1981
- checkpoint.rollback((message) => this.rollback(message.runtimeOp, message.localOpMetadata));
1982
- // reset the dirty state after rollback to what it was before to keep it consistent
1983
- if (this.dirtyContainer !== checkpointDirtyState) {
1984
- this.updateDocumentDirtyState(checkpointDirtyState);
1985
- }
2062
+ checkpoint.rollback((message) =>
2063
+ // These changes are staged since we entered staging mode above
2064
+ this.rollbackStagedChanges(message.runtimeOp, message.localOpMetadata));
2065
+ this.updateDocumentDirtyState();
1986
2066
  stageControls?.discardChanges();
1987
2067
  stageControls = undefined;
1988
2068
  }
@@ -2058,17 +2138,11 @@ export class ContainerRuntime extends TypedEventEmitter {
2058
2138
  const context = this.channelCollection.createDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], loadingGroupId);
2059
2139
  return channelToDataStore(await context.realize(), context.id, this.channelCollection, this.mc.logger);
2060
2140
  }
2061
- canSendOps() {
2141
+ shouldSendOps() {
2062
2142
  // Note that the real (non-proxy) delta manager is needed here to get the readonly info. This is because
2063
2143
  // container runtime's ability to send ops depend on the actual readonly state of the delta manager.
2064
2144
  return (this.connected && !this.innerDeltaManager.readOnlyInfo.readonly && !this.imminentClosure);
2065
2145
  }
2066
- /**
2067
- * Typically ops are batched and later flushed together, but in some cases we want to flush immediately.
2068
- */
2069
- currentlyBatching() {
2070
- return this.flushMode !== FlushMode.Immediate || this.batchRunner.running;
2071
- }
2072
2146
  getQuorum() {
2073
2147
  return this._quorum;
2074
2148
  }
@@ -2080,36 +2154,17 @@ export class ContainerRuntime extends TypedEventEmitter {
2080
2154
  * either were not sent out to delta stream or were not yet acknowledged.
2081
2155
  */
2082
2156
  get isDirty() {
2083
- return this.dirtyContainer;
2157
+ // Rather than recomputing the dirty state in this moment,
2158
+ // just regurgitate the last emitted dirty state.
2159
+ return this.lastEmittedDirty;
2084
2160
  }
2085
- isContainerMessageDirtyable({ type, contents, }) {
2086
- // Certain container runtime messages should not mark the container dirty such as the old built-in
2087
- // AgentScheduler and Garbage collector messages.
2088
- switch (type) {
2089
- case ContainerMessageType.Attach: {
2090
- const attachMessage = contents;
2091
- if (attachMessage.id === agentSchedulerId) {
2092
- return false;
2093
- }
2094
- break;
2095
- }
2096
- case ContainerMessageType.FluidDataStoreOp: {
2097
- const envelope = contents;
2098
- if (envelope.address === agentSchedulerId) {
2099
- return false;
2100
- }
2101
- break;
2102
- }
2103
- case ContainerMessageType.IdAllocation:
2104
- case ContainerMessageType.DocumentSchemaChange:
2105
- case ContainerMessageType.GC: {
2106
- return false;
2107
- }
2108
- default: {
2109
- break;
2110
- }
2111
- }
2112
- return true;
2161
+ /**
2162
+ * Returns true if the container is dirty: not attached, or no pending user messages (could be some "non-dirtyable" ones though)
2163
+ */
2164
+ computeCurrentDirtyState() {
2165
+ return (this.attachState !== AttachState.Attached ||
2166
+ this.pendingStateManager.hasPendingUserChanges() ||
2167
+ this.outbox.containsUserChanges());
2113
2168
  }
2114
2169
  /**
2115
2170
  * Submits the signal to be sent to other clients.
@@ -2126,9 +2181,6 @@ export class ContainerRuntime extends TypedEventEmitter {
2126
2181
  submitSignal(type, content, targetClientId) {
2127
2182
  this.verifyNotClosed();
2128
2183
  const envelope = createNewSignalEnvelope(undefined /* address */, type, content);
2129
- if (targetClientId === undefined) {
2130
- this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope);
2131
- }
2132
2184
  this.submitSignalFn(envelope, targetClientId);
2133
2185
  }
2134
2186
  setAttachState(attachState) {
@@ -2139,9 +2191,7 @@ export class ContainerRuntime extends TypedEventEmitter {
2139
2191
  assert(this.attachState === AttachState.Attached, 0x12e /* "Container Context should already be in attached state" */);
2140
2192
  this.emit("attached");
2141
2193
  }
2142
- if (attachState === AttachState.Attached && !this.hasPendingMessages()) {
2143
- this.updateDocumentDirtyState(false);
2144
- }
2194
+ this.updateDocumentDirtyState();
2145
2195
  this.channelCollection.setAttachState(attachState);
2146
2196
  }
2147
2197
  /**
@@ -2709,18 +2759,20 @@ export class ContainerRuntime extends TypedEventEmitter {
2709
2759
  hasPendingMessages() {
2710
2760
  return this.pendingMessagesCount !== 0;
2711
2761
  }
2712
- updateDocumentDirtyState(dirty) {
2713
- if (this.attachState === AttachState.Attached) {
2714
- // Other way is not true = see this.isContainerMessageDirtyable()
2715
- assert(!dirty || this.hasPendingMessages(), 0x3d3 /* if doc is dirty, there has to be pending ops */);
2716
- }
2717
- else {
2718
- assert(dirty, 0x3d2 /* Non-attached container is dirty */);
2719
- }
2720
- if (this.dirtyContainer === dirty) {
2762
+ /**
2763
+ * Emit "dirty" or "saved" event based on the current dirty state of the document.
2764
+ * This must be called every time the states underlying the dirty state change.
2765
+ *
2766
+ * @privateRemarks - It's helpful to think of this as an event handler registered
2767
+ * for hypothetical "changed" events for PendingStateManager, Outbox, and Container Attach machinery.
2768
+ * But those events don't exist so we manually call this wherever we know those changes happen.
2769
+ */
2770
+ updateDocumentDirtyState() {
2771
+ const dirty = this.computeCurrentDirtyState();
2772
+ if (this.lastEmittedDirty === dirty) {
2721
2773
  return;
2722
2774
  }
2723
- this.dirtyContainer = dirty;
2775
+ this.lastEmittedDirty = dirty;
2724
2776
  if (this.emitDirtyDocumentEvent) {
2725
2777
  this.emit(dirty ? "dirty" : "saved");
2726
2778
  }
@@ -2775,6 +2827,7 @@ export class ContainerRuntime extends TypedEventEmitter {
2775
2827
  try {
2776
2828
  // If we're resubmitting a batch, keep the same "staged" value as before. Otherwise, use the current "global" state.
2777
2829
  const staged = this.batchRunner.resubmitInfo?.staged ?? this.inStagingMode;
2830
+ assert(!staged || canStageMessageOfType(type), 0xbba /* Unexpected message type submitted in Staging Mode */);
2778
2831
  // Before submitting any non-staged change, submit the ID Allocation op to cover any compressed IDs included in the op.
2779
2832
  if (!staged) {
2780
2833
  this.submitIdAllocationOpIfNeeded({ staged: false });
@@ -2782,7 +2835,7 @@ export class ContainerRuntime extends TypedEventEmitter {
2782
2835
  // Allow document schema controller to send a message if it needs to propose change in document schema.
2783
2836
  // If it needs to send a message, it will call provided callback with payload of such message and rely
2784
2837
  // on this callback to do actual sending.
2785
- const schemaChangeMessage = this.documentsSchemaController.maybeSendSchemaMessage();
2838
+ const schemaChangeMessage = this.documentsSchemaController.maybeGenerateSchemaMessage();
2786
2839
  if (schemaChangeMessage) {
2787
2840
  this.mc.logger.sendTelemetryEvent({
2788
2841
  eventName: "SchemaChangeProposal",
@@ -2818,14 +2871,7 @@ export class ContainerRuntime extends TypedEventEmitter {
2818
2871
  else {
2819
2872
  this.outbox.submit(message);
2820
2873
  }
2821
- // Note: Technically, the system "always" batches - if this case is true we'll just have a single-message batch.
2822
- const flushImmediatelyOnSubmit = !this.currentlyBatching();
2823
- if (flushImmediatelyOnSubmit) {
2824
- this.flush();
2825
- }
2826
- else {
2827
- this.scheduleFlush();
2828
- }
2874
+ this.scheduleFlush();
2829
2875
  }
2830
2876
  catch (error) {
2831
2877
  const dpe = DataProcessingError.wrapIfUnrecognized(error, "ContainerRuntime.submit", {
@@ -2834,27 +2880,26 @@ export class ContainerRuntime extends TypedEventEmitter {
2834
2880
  this.closeFn(dpe);
2835
2881
  throw dpe;
2836
2882
  }
2837
- if (this.isContainerMessageDirtyable(containerRuntimeMessage)) {
2838
- this.updateDocumentDirtyState(true);
2839
- }
2883
+ this.updateDocumentDirtyState();
2840
2884
  }
2841
2885
  scheduleFlush() {
2842
- if (this.flushTaskExists) {
2886
+ if (this.flushScheduled) {
2843
2887
  return;
2844
2888
  }
2845
- this.flushTaskExists = true;
2846
- // TODO: hoist this out of the function scope to save unnecessary allocations
2847
- // eslint-disable-next-line unicorn/consistent-function-scoping -- Separate `flush` method already exists in outer scope
2848
- const flush = () => {
2849
- this.flushTaskExists = false;
2850
- this.flush();
2851
- };
2889
+ this.flushScheduled = true;
2852
2890
  switch (this.flushMode) {
2891
+ case FlushMode.Immediate: {
2892
+ // When in Immediate flush mode, flush immediately unless we are intentionally batching multiple ops (e.g. via orderSequentially)
2893
+ if (!this.batchRunner.running) {
2894
+ this.flush();
2895
+ }
2896
+ break;
2897
+ }
2853
2898
  case FlushMode.TurnBased: {
2854
2899
  // When in TurnBased flush mode the runtime will buffer operations in the current turn and send them as a single
2855
2900
  // batch at the end of the turn
2856
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
2857
- Promise.resolve().then(flush);
2901
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- Container will close if flush throws
2902
+ Promise.resolve().then(() => this.flush());
2858
2903
  break;
2859
2904
  }
2860
2905
  // FlushModeExperimental is experimental and not exposed directly in the runtime APIs
@@ -2862,12 +2907,11 @@ export class ContainerRuntime extends TypedEventEmitter {
2862
2907
  // When in Async flush mode, the runtime will accumulate all operations across JS turns and send them as a single
2863
2908
  // batch when all micro-tasks are complete.
2864
2909
  // Compared to TurnBased, this flush mode will capture more ops into the same batch.
2865
- setTimeout(flush, 0);
2910
+ setTimeout(() => this.flush(), 0);
2866
2911
  break;
2867
2912
  }
2868
2913
  default: {
2869
- assert(this.batchRunner.running, 0x587 /* Unreachable unless manually accumulating a batch */);
2870
- break;
2914
+ fail(0x587 /* Unreachable unless manually accumulating a batch */);
2871
2915
  }
2872
2916
  }
2873
2917
  }
@@ -2889,43 +2933,72 @@ export class ContainerRuntime extends TypedEventEmitter {
2889
2933
  }
2890
2934
  /**
2891
2935
  * Resubmits each message in the batch, and then flushes the outbox.
2936
+ * This typically happens when we reconnect and there are pending messages.
2937
+ *
2938
+ * @remarks
2939
+ * Attempting to resubmit a batch that has been successfully sequenced will not happen due to
2940
+ * checks in the ConnectionStateHandler (Loader layer)
2892
2941
  *
2893
- * @remarks - If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
2942
+ * The only exception to this would be if the Container "forks" due to misuse of the "Offline Load" feature.
2943
+ * If the "Offline Load" feature is enabled, the batchId is included in the resubmitted messages,
2894
2944
  * for correlation to detect container forking.
2895
2945
  */
2896
2946
  reSubmitBatch(batch, { batchId, staged, squash }) {
2947
+ assert(this._summarizer === undefined, 0x8f2 /* Summarizer never reconnects so should never resubmit */);
2897
2948
  const resubmitInfo = {
2898
2949
  // Only include Batch ID if "Offline Load" feature is enabled
2899
2950
  // It's only needed to identify batches across container forks arising from misuse of offline load.
2900
2951
  batchId: this.offlineEnabled ? batchId : undefined,
2901
2952
  staged,
2902
2953
  };
2954
+ const resubmitFn = squash
2955
+ ? this.reSubmitWithSquashing.bind(this)
2956
+ : this.reSubmit.bind(this);
2903
2957
  this.batchRunner.run(() => {
2904
2958
  for (const message of batch) {
2905
- this.reSubmit(message, squash);
2959
+ resubmitFn(message);
2906
2960
  }
2907
2961
  }, resubmitInfo);
2908
2962
  this.flush(resubmitInfo);
2909
2963
  }
2910
- reSubmit(message, squash) {
2911
- this.reSubmitCore(message.runtimeOp, message.localOpMetadata, message.opMetadata, squash);
2964
+ /**
2965
+ * Resubmit the given message as part of a squash rebase upon exiting Staging Mode.
2966
+ * How exactly to resubmit the message is up to the subsystem that submitted the op to begin with.
2967
+ */
2968
+ reSubmitWithSquashing(resubmitData) {
2969
+ const message = resubmitData.runtimeOp;
2970
+ assert(canStageMessageOfType(message.type), 0xbbb /* Expected message type to be compatible with staging */);
2971
+ switch (message.type) {
2972
+ case ContainerMessageType.FluidDataStoreOp: {
2973
+ this.channelCollection.reSubmit(message.type, message.contents, resubmitData.localOpMetadata,
2974
+ /* squash: */ true);
2975
+ break;
2976
+ }
2977
+ // NOTE: Squash doesn't apply to GC or DocumentSchemaChange ops, fallback to typical resubmit logic.
2978
+ case ContainerMessageType.GC:
2979
+ case ContainerMessageType.DocumentSchemaChange: {
2980
+ this.reSubmit(resubmitData);
2981
+ break;
2982
+ }
2983
+ default: {
2984
+ unreachableCase(message.type);
2985
+ }
2986
+ }
2912
2987
  }
2913
2988
  /**
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.
2989
+ * Resubmit the given message which was previously submitted to the ContainerRuntime but not successfully
2990
+ * transmitted to the ordering service (e.g. due to a disconnect, or being in Staging Mode)
2991
+ * How to resubmit is up to the subsystem that submitted the op to begin with
2919
2992
  */
2920
- reSubmitCore(message, localOpMetadata, opMetadata, squash) {
2921
- assert(this._summarizer === undefined, 0x8f2 /* Summarizer never reconnects so should never resubmit */);
2993
+ reSubmit({ runtimeOp: message, localOpMetadata, opMetadata, }) {
2922
2994
  switch (message.type) {
2923
2995
  case ContainerMessageType.FluidDataStoreOp:
2924
2996
  case ContainerMessageType.Attach:
2925
2997
  case ContainerMessageType.Alias: {
2926
2998
  // For Operations, call resubmitDataStoreOp which will find the right store
2927
2999
  // and trigger resubmission on it.
2928
- this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata, squash);
3000
+ this.channelCollection.reSubmit(message.type, message.contents, localOpMetadata,
3001
+ /* squash: */ false);
2929
3002
  break;
2930
3003
  }
2931
3004
  case ContainerMessageType.IdAllocation: {
@@ -2951,9 +3024,9 @@ export class ContainerRuntime extends TypedEventEmitter {
2951
3024
  break;
2952
3025
  }
2953
3026
  case 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.
3027
+ // We shouldn't directly resubmit due to Compare-And-Swap semantics.
3028
+ // If needed it will be generated from scratch before other ops are submitted.
3029
+ this.documentsSchemaController.pendingOpNotAcked();
2957
3030
  break;
2958
3031
  }
2959
3032
  default: {
@@ -2963,8 +3036,11 @@ export class ContainerRuntime extends TypedEventEmitter {
2963
3036
  }
2964
3037
  }
2965
3038
  }
2966
- rollback(runtimeOp, localOpMetadata) {
2967
- const { type, contents } = runtimeOp;
3039
+ /**
3040
+ * Rollback the given op which was only staged but not yet submitted.
3041
+ */
3042
+ rollbackStagedChanges({ type, contents }, localOpMetadata) {
3043
+ assert(canStageMessageOfType(type), 0xbbc /* Unexpected message type to be rolled back */);
2968
3044
  switch (type) {
2969
3045
  case ContainerMessageType.FluidDataStoreOp: {
2970
3046
  // For operations, call rollbackDataStoreOp which will find the right store
@@ -2972,8 +3048,24 @@ export class ContainerRuntime extends TypedEventEmitter {
2972
3048
  this.channelCollection.rollback(type, contents, localOpMetadata);
2973
3049
  break;
2974
3050
  }
3051
+ case ContainerMessageType.GC: {
3052
+ // Just drop it, but log an error, this is not expected and not ideal, but not critical failure either.
3053
+ // Currently the only expected type here is TombstoneLoaded, which will have been preceded by one of these events as well:
3054
+ // GC_Tombstone_DataStore_Requested, GC_Tombstone_SubDataStore_Requested, GC_Tombstone_Blob_Requested
3055
+ this.mc.logger.sendErrorEvent({
3056
+ eventName: "GC_OpDiscarded",
3057
+ details: { subType: contents.type },
3058
+ });
3059
+ break;
3060
+ }
3061
+ case ContainerMessageType.DocumentSchemaChange: {
3062
+ // Notify the document schema controller that the pending op was not acked.
3063
+ // This will allow it to propose the schema change again if needed.
3064
+ this.documentsSchemaController.pendingOpNotAcked();
3065
+ break;
3066
+ }
2975
3067
  default: {
2976
- throw new Error(`Can't rollback ${type}`);
3068
+ unreachableCase(type);
2977
3069
  }
2978
3070
  }
2979
3071
  }
@@ -3138,6 +3230,30 @@ export class ContainerRuntime extends TypedEventEmitter {
3138
3230
  return this.summaryManager.enqueueSummarize(options);
3139
3231
  }
3140
3232
  }
3233
+ acquireExtension(id, factory, ...useContext) {
3234
+ let entry = this.extensions.get(id);
3235
+ if (entry === undefined) {
3236
+ const runtime = {
3237
+ isConnected: () => this.connected,
3238
+ getClientId: () => this.clientId,
3239
+ events: this.lazyEventsForExtensions.value,
3240
+ logger: this.baseLogger,
3241
+ submitAddressedSignal: (addressChain, message) => {
3242
+ this.submitExtensionSignal(id, addressChain, message);
3243
+ },
3244
+ getQuorum: this.getQuorum.bind(this),
3245
+ getAudience: this.getAudience.bind(this),
3246
+ supportedFeatures: this.ILayerCompatDetails.supportedFeatures,
3247
+ };
3248
+ entry = new factory(runtime, ...useContext);
3249
+ this.extensions.set(id, entry);
3250
+ }
3251
+ else {
3252
+ assert(entry instanceof factory, 0xba1 /* Extension entry is not of the expected type */);
3253
+ entry.extension.onNewUse(...useContext);
3254
+ }
3255
+ return entry.interface;
3256
+ }
3141
3257
  get groupedBatchingEnabled() {
3142
3258
  return this.sessionSchema.opGroupingEnabled === true;
3143
3259
  }
@@ -3149,4 +3265,33 @@ export function createNewSignalEnvelope(address, type, content) {
3149
3265
  };
3150
3266
  return newEnvelope;
3151
3267
  }
3268
+ export function isContainerMessageDirtyable({ type, contents, }) {
3269
+ // Certain container runtime messages should not mark the container dirty such as the old built-in
3270
+ // AgentScheduler and Garbage collector messages.
3271
+ switch (type) {
3272
+ case ContainerMessageType.Attach: {
3273
+ const attachMessage = contents;
3274
+ if (attachMessage.id === agentSchedulerId) {
3275
+ return false;
3276
+ }
3277
+ break;
3278
+ }
3279
+ case ContainerMessageType.FluidDataStoreOp: {
3280
+ const envelope = contents;
3281
+ if (envelope.address === agentSchedulerId) {
3282
+ return false;
3283
+ }
3284
+ break;
3285
+ }
3286
+ case ContainerMessageType.IdAllocation:
3287
+ case ContainerMessageType.DocumentSchemaChange:
3288
+ case ContainerMessageType.GC: {
3289
+ return false;
3290
+ }
3291
+ default: {
3292
+ break;
3293
+ }
3294
+ }
3295
+ return true;
3296
+ }
3152
3297
  //# sourceMappingURL=containerRuntime.js.map