@fluidframework/container-runtime 2.41.0-338186 → 2.41.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 (55) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/container-runtime.test-files.tar +0 -0
  3. package/dist/containerRuntime.d.ts +35 -17
  4. package/dist/containerRuntime.d.ts.map +1 -1
  5. package/dist/containerRuntime.js +174 -127
  6. package/dist/containerRuntime.js.map +1 -1
  7. package/dist/opLifecycle/batchManager.d.ts +4 -0
  8. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  9. package/dist/opLifecycle/batchManager.js +7 -0
  10. package/dist/opLifecycle/batchManager.js.map +1 -1
  11. package/dist/opLifecycle/outbox.d.ts +1 -0
  12. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  13. package/dist/opLifecycle/outbox.js +6 -1
  14. package/dist/opLifecycle/outbox.js.map +1 -1
  15. package/dist/packageVersion.d.ts +1 -1
  16. package/dist/packageVersion.d.ts.map +1 -1
  17. package/dist/packageVersion.js +1 -1
  18. package/dist/packageVersion.js.map +1 -1
  19. package/dist/pendingStateManager.d.ts +4 -0
  20. package/dist/pendingStateManager.d.ts.map +1 -1
  21. package/dist/pendingStateManager.js +16 -0
  22. package/dist/pendingStateManager.js.map +1 -1
  23. package/dist/runCounter.d.ts.map +1 -1
  24. package/dist/runCounter.js +1 -1
  25. package/dist/runCounter.js.map +1 -1
  26. package/lib/containerRuntime.d.ts +35 -17
  27. package/lib/containerRuntime.d.ts.map +1 -1
  28. package/lib/containerRuntime.js +174 -128
  29. package/lib/containerRuntime.js.map +1 -1
  30. package/lib/opLifecycle/batchManager.d.ts +4 -0
  31. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  32. package/lib/opLifecycle/batchManager.js +7 -0
  33. package/lib/opLifecycle/batchManager.js.map +1 -1
  34. package/lib/opLifecycle/outbox.d.ts +1 -0
  35. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  36. package/lib/opLifecycle/outbox.js +6 -1
  37. package/lib/opLifecycle/outbox.js.map +1 -1
  38. package/lib/packageVersion.d.ts +1 -1
  39. package/lib/packageVersion.d.ts.map +1 -1
  40. package/lib/packageVersion.js +1 -1
  41. package/lib/packageVersion.js.map +1 -1
  42. package/lib/pendingStateManager.d.ts +4 -0
  43. package/lib/pendingStateManager.d.ts.map +1 -1
  44. package/lib/pendingStateManager.js +16 -0
  45. package/lib/pendingStateManager.js.map +1 -1
  46. package/lib/runCounter.d.ts.map +1 -1
  47. package/lib/runCounter.js +1 -1
  48. package/lib/runCounter.js.map +1 -1
  49. package/package.json +18 -18
  50. package/src/containerRuntime.ts +263 -152
  51. package/src/opLifecycle/batchManager.ts +8 -0
  52. package/src/opLifecycle/outbox.ts +8 -1
  53. package/src/packageVersion.ts +1 -1
  54. package/src/pendingStateManager.ts +17 -0
  55. package/src/runCounter.ts +4 -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");
@@ -503,7 +503,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
503
503
  }
504
504
  /**
505
505
  * 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.
506
+ * until execution finishes. If an op is submitted, it will be marked as reentrant.
507
507
  *
508
508
  * @param callback - the callback to be invoked
509
509
  */
@@ -575,7 +575,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
575
575
  // 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
576
  this.pendingIdCompressorOps = [];
577
577
  this.batchRunner = new runCounter_js_1.BatchRunCounter();
578
- this.flushTaskExists = false;
578
+ this.flushScheduled = false;
579
579
  this.consecutiveReconnects = 0;
580
580
  this.dataModelChangeRunner = new runCounter_js_1.RunCounter();
581
581
  this._disposed = false;
@@ -588,6 +588,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
588
588
  this.snapshotCacheForLoadingGroupIds = new internal_2.PromiseCache({
589
589
  expiry: { policy: "absolute", durationMs: 60000 },
590
590
  });
591
+ this.extensions = new Map();
591
592
  this.notifyReadOnlyState = (readonly) => this.channelCollection.notifyReadOnlyState(readonly);
592
593
  /**
593
594
  * Enter Staging Mode, such that ops submitted to the ContainerRuntime will not be sent to the ordering service.
@@ -621,9 +622,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
621
622
  (0, internal_2.assert)(runtimeOp !== undefined, 0xb82 /* Staged batches expected to have runtimeOp defined */);
622
623
  this.rollback(runtimeOp, localOpMetadata);
623
624
  });
624
- if (this.attachState === container_definitions_1.AttachState.Attached) {
625
- this.updateDocumentDirtyState(this.pendingMessagesCount !== 0);
626
- }
625
+ this.updateDocumentDirtyState();
627
626
  }),
628
627
  commitChanges: (optionsParam) => {
629
628
  const options = { ...defaultStagingCommitOptions, ...optionsParam };
@@ -640,6 +639,15 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
640
639
  return this.stageControls;
641
640
  };
642
641
  this.readAndParseBlob = async (id) => (0, internal_4.readAndParse)(this.storage, id);
642
+ // While internal, ContainerRuntime has not been converted to use the new events support.
643
+ // Recreate the required events (new pattern) with injected, wrapper new emitter.
644
+ // It is lazily create to avoid listeners (old events) that ultimately go nowhere.
645
+ this.lazyEventsForExtensions = new internal_2.Lazy(() => {
646
+ const eventEmitter = (0, client_utils_1.createEmitter)();
647
+ this.on("connected", (clientId) => eventEmitter.emit("connected", clientId));
648
+ this.on("disconnected", () => eventEmitter.emit("disconnected"));
649
+ return eventEmitter;
650
+ });
643
651
  const { options, clientDetails, connected, baseSnapshot, submitFn, submitBatchFn, submitSummaryFn, submitSignalFn, disposeFn, closeFn, deltaManager, quorum, audience, pendingLocalState, supportedFeatures, snapshotWithContents, } = context;
644
652
  // In old loaders without dispose functionality, closeFn is equivalent but will also switch container to readonly mode
645
653
  this.disposeFn = disposeFn ?? closeFn;
@@ -668,7 +676,23 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
668
676
  this.submitSummaryFn =
669
677
  submitSummaryFn ??
670
678
  ((summaryOp, refseq) => submitFn(internal_3.MessageType.Summarize, summaryOp, false));
671
- this.submitSignalFn = submitSignalFn;
679
+ const sequenceAndSubmitSignal = (envelope, targetClientId) => {
680
+ if (targetClientId === undefined) {
681
+ this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope);
682
+ }
683
+ submitSignalFn(envelope, targetClientId);
684
+ };
685
+ this.submitSignalFn = (envelope, targetClientId) => {
686
+ if (envelope.address?.startsWith("/")) {
687
+ throw new Error("General path based addressing is not implemented");
688
+ }
689
+ sequenceAndSubmitSignal(envelope, targetClientId);
690
+ };
691
+ this.submitExtensionSignal = (id, addressChain, message) => {
692
+ this.verifyNotClosed();
693
+ const envelope = createNewSignalEnvelope(`/ext/${id}/${addressChain.join("/")}`, message.type, message.content);
694
+ sequenceAndSubmitSignal(envelope, message.targetClientId);
695
+ };
672
696
  // TODO: After IContainerContext.options is removed, we'll just create a new blank object {} here.
673
697
  // Values are generally expected to be set from the runtime side.
674
698
  this.options = options ?? {};
@@ -700,8 +724,8 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
700
724
  this.mc.logger.sendTelemetryEvent({
701
725
  eventName: "Attached",
702
726
  details: {
703
- dirtyContainer: this.dirtyContainer,
704
- hasPendingMessages: this.hasPendingMessages(),
727
+ lastEmittedDirty: this.lastEmittedDirty,
728
+ currentDirtyState: this.computeCurrentDirtyState(),
705
729
  },
706
730
  });
707
731
  });
@@ -868,9 +892,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
868
892
  // verifyNotClosed is called in FluidDataStoreContext, which is *the* expected caller.
869
893
  const envelope1 = content;
870
894
  const envelope2 = createNewSignalEnvelope(envelope1.address, type, envelope1.contents);
871
- if (targetClientId === undefined) {
872
- this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope2);
873
- }
874
895
  this.submitSignalFn(envelope2, targetClientId);
875
896
  };
876
897
  let snapshot = (0, channelCollection_js_1.getSummaryForDatastores)(baseSnapshot, metadata);
@@ -964,9 +985,9 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
964
985
  const closeSummarizerDelayOverride = this.mc.config.getNumber("Fluid.ContainerRuntime.Test.CloseSummarizerDelayOverrideMs");
965
986
  this.closeSummarizerDelayMs =
966
987
  closeSummarizerDelayOverride ?? defaultCloseSummarizerDelayMs;
967
- this.dirtyContainer =
968
- this.attachState !== container_definitions_1.AttachState.Attached || this.hasPendingMessages();
969
- context.updateDirtyContainerState(this.dirtyContainer);
988
+ // We haven't emitted dirty/saved yet, but this is the baseline so we know to emit when it changes
989
+ this.lastEmittedDirty = this.computeCurrentDirtyState();
990
+ context.updateDirtyContainerState(this.lastEmittedDirty);
970
991
  if (!this.skipSafetyFlushDuringProcessStack) {
971
992
  // Reference Sequence Number may have just changed, and it must be consistent across a batch,
972
993
  // so we should flush now to clear the way for the next ops.
@@ -1434,32 +1455,29 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1434
1455
  if (!this.canSendOps()) {
1435
1456
  return;
1436
1457
  }
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;
1458
+ // Replaying is an internal operation and we don't want to generate noise while doing it.
1459
+ // So temporarily disable dirty state change events, and save the old state.
1460
+ // When we're done, we'll emit the event if the state changed.
1461
+ const oldState = this.lastEmittedDirty;
1443
1462
  (0, internal_2.assert)(this.emitDirtyDocumentEvent, 0x127 /* "dirty document event not set on replay" */);
1444
1463
  this.emitDirtyDocumentEvent = false;
1445
- let newState;
1446
1464
  try {
1447
1465
  // Any ID Allocation ops that failed to submit after the pending state was queued need to have
1448
1466
  // the corresponding ranges resubmitted (note this call replaces the typical resubmit flow).
1449
1467
  // Since we don't submit ID Allocation ops when staged, any outstanding ranges would be from
1450
1468
  // before staging mode so we can simply say staged: false.
1451
1469
  this.submitIdAllocationOpIfNeeded({ resubmitOutstandingRanges: true, staged: false });
1470
+ this.scheduleFlush();
1452
1471
  // replay the ops
1453
1472
  this.pendingStateManager.replayPendingStates();
1454
1473
  }
1455
1474
  finally {
1456
- // Save the new start and restore the old state, re-enable event emit
1457
- newState = this.dirtyContainer;
1458
- this.dirtyContainer = oldState;
1475
+ // Restore the old state, re-enable event emit
1476
+ this.lastEmittedDirty = oldState;
1459
1477
  this.emitDirtyDocumentEvent = true;
1460
1478
  }
1461
- // Officially transition from the old state to the new state.
1462
- this.updateDocumentDirtyState(newState);
1479
+ // This will emit an event if the state changed relative to before replay
1480
+ this.updateDocumentDirtyState();
1463
1481
  }
1464
1482
  /**
1465
1483
  * Parse an op's type and actual content from given serialized content
@@ -1721,6 +1739,8 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1721
1739
  * @param groupedBatch - true if these messages are part of a grouped op batch.
1722
1740
  */
1723
1741
  processInboundMessages(messagesWithMetadata, locationInBatch, local, savedOp, runtimeBatch, groupedBatch) {
1742
+ // This message could have been the last pending local (dirtyable) message, in which case we need to update dirty state to "saved"
1743
+ this.updateDocumentDirtyState();
1724
1744
  if (locationInBatch.batchStart) {
1725
1745
  const firstMessage = messagesWithMetadata[0]?.message;
1726
1746
  (0, internal_2.assert)(firstMessage !== undefined, 0xa31 /* Batch must have at least one message */);
@@ -1815,11 +1835,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1815
1835
  message.minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
1816
1836
  }
1817
1837
  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
1838
  // The DeltaManager used to do this, but doesn't anymore as of Loader v2.4
1824
1839
  // Anyone listening to our "op" event would expect the contents to be parsed per this same logic
1825
1840
  if (typeof message.contents === "string" &&
@@ -1841,11 +1856,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1841
1856
  *
1842
1857
  */
1843
1858
  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
1859
  // Get the contents without the localOpMetadata because not all message types know about localOpMetadata.
1850
1860
  const contents = messagesContent.map((c) => c.contents);
1851
1861
  switch (message.type) {
@@ -1921,20 +1931,44 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1921
1931
  if (message.clientId === this.clientId) {
1922
1932
  this.signalTelemetryManager.trackReceivedSignal(envelope, this.mc.logger, this.consecutiveReconnects);
1923
1933
  }
1924
- if (envelope.address === undefined) {
1934
+ const fullAddress = envelope.address;
1935
+ if (fullAddress === undefined) {
1925
1936
  // No address indicates a container signal message.
1926
1937
  this.emit("signal", transformed, local);
1927
1938
  return;
1928
1939
  }
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);
1940
+ this.routeNonContainerSignal(fullAddress, transformed, local);
1941
+ }
1942
+ routeNonContainerSignal(address, signalMessage, local) {
1943
+ // channelCollection signals are identified by no starting `/` in address.
1944
+ if (!address.startsWith("/")) {
1945
+ // Due to a mismatch between different layers in terms of
1946
+ // what is the interface of passing signals, we need to adjust
1947
+ // the signal envelope before sending it to the datastores to be processed
1948
+ const envelope = {
1949
+ address,
1950
+ contents: signalMessage.content,
1951
+ };
1952
+ signalMessage.content = envelope;
1953
+ this.channelCollection.processSignal(signalMessage, local);
1954
+ return;
1955
+ }
1956
+ const addresses = address.split("/");
1957
+ if (addresses.length > 2 && addresses[1] === "ext") {
1958
+ const id = addresses[2];
1959
+ const entry = this.extensions.get(id);
1960
+ if (entry !== undefined) {
1961
+ entry.extension.processSignal?.(addresses.slice(3), signalMessage, local);
1962
+ return;
1963
+ }
1964
+ }
1965
+ (0, internal_2.assert)(!local, 0xba0 /* No recipient found for local signal */);
1966
+ this.mc.logger.sendTelemetryEvent({
1967
+ eventName: "SignalAddressNotFound",
1968
+ ...(0, internal_8.tagCodeArtifacts)({
1969
+ address,
1970
+ }),
1971
+ });
1938
1972
  }
1939
1973
  /**
1940
1974
  * Flush the current batch of ops to the ordering service for sequencing
@@ -1944,6 +1978,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1944
1978
  * @param resubmitInfo - If defined, indicates this is a resubmission of a batch with the given Batch info needed for resubmit.
1945
1979
  */
1946
1980
  flush(resubmitInfo) {
1981
+ this.flushScheduled = false;
1947
1982
  try {
1948
1983
  (0, internal_2.assert)(!this.batchRunner.running, 0x24c /* "Cannot call `flush()` while manually accumulating a batch (e.g. under orderSequentially) */);
1949
1984
  this.outbox.flush(resubmitInfo);
@@ -1964,7 +1999,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1964
1999
  */
1965
2000
  orderSequentially(callback) {
1966
2001
  let checkpoint;
1967
- const checkpointDirtyState = this.dirtyContainer;
1968
2002
  // eslint-disable-next-line import/no-deprecated
1969
2003
  let stageControls;
1970
2004
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
@@ -1985,10 +2019,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1985
2019
  // This will throw and close the container if rollback fails
1986
2020
  try {
1987
2021
  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
- }
2022
+ this.updateDocumentDirtyState();
1992
2023
  stageControls?.discardChanges();
1993
2024
  stageControls = undefined;
1994
2025
  }
@@ -2069,12 +2100,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2069
2100
  // container runtime's ability to send ops depend on the actual readonly state of the delta manager.
2070
2101
  return (this.connected && !this.innerDeltaManager.readOnlyInfo.readonly && !this.imminentClosure);
2071
2102
  }
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
2103
  getQuorum() {
2079
2104
  return this._quorum;
2080
2105
  }
@@ -2086,36 +2111,17 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2086
2111
  * either were not sent out to delta stream or were not yet acknowledged.
2087
2112
  */
2088
2113
  get isDirty() {
2089
- return this.dirtyContainer;
2114
+ // Rather than recomputing the dirty state in this moment,
2115
+ // just regurgitate the last emitted dirty state.
2116
+ return this.lastEmittedDirty;
2090
2117
  }
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;
2118
+ /**
2119
+ * Returns true if the container is dirty: not attached, or no pending user messages (could be some "non-dirtyable" ones though)
2120
+ */
2121
+ computeCurrentDirtyState() {
2122
+ return (this.attachState !== container_definitions_1.AttachState.Attached ||
2123
+ this.pendingStateManager.hasPendingUserChanges() ||
2124
+ this.outbox.containsUserChanges());
2119
2125
  }
2120
2126
  /**
2121
2127
  * Submits the signal to be sent to other clients.
@@ -2132,9 +2138,6 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2132
2138
  submitSignal(type, content, targetClientId) {
2133
2139
  this.verifyNotClosed();
2134
2140
  const envelope = createNewSignalEnvelope(undefined /* address */, type, content);
2135
- if (targetClientId === undefined) {
2136
- this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope);
2137
- }
2138
2141
  this.submitSignalFn(envelope, targetClientId);
2139
2142
  }
2140
2143
  setAttachState(attachState) {
@@ -2145,9 +2148,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2145
2148
  (0, internal_2.assert)(this.attachState === container_definitions_1.AttachState.Attached, 0x12e /* "Container Context should already be in attached state" */);
2146
2149
  this.emit("attached");
2147
2150
  }
2148
- if (attachState === container_definitions_1.AttachState.Attached && !this.hasPendingMessages()) {
2149
- this.updateDocumentDirtyState(false);
2150
- }
2151
+ this.updateDocumentDirtyState();
2151
2152
  this.channelCollection.setAttachState(attachState);
2152
2153
  }
2153
2154
  /**
@@ -2715,18 +2716,20 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2715
2716
  hasPendingMessages() {
2716
2717
  return this.pendingMessagesCount !== 0;
2717
2718
  }
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) {
2719
+ /**
2720
+ * Emit "dirty" or "saved" event based on the current dirty state of the document.
2721
+ * This must be called every time the states underlying the dirty state change.
2722
+ *
2723
+ * @privateRemarks - It's helpful to think of this as an event handler registered
2724
+ * for hypothetical "changed" events for PendingStateManager, Outbox, and Container Attach machinery.
2725
+ * But those events don't exist so we manually call this wherever we know those changes happen.
2726
+ */
2727
+ updateDocumentDirtyState() {
2728
+ const dirty = this.computeCurrentDirtyState();
2729
+ if (this.lastEmittedDirty === dirty) {
2727
2730
  return;
2728
2731
  }
2729
- this.dirtyContainer = dirty;
2732
+ this.lastEmittedDirty = dirty;
2730
2733
  if (this.emitDirtyDocumentEvent) {
2731
2734
  this.emit(dirty ? "dirty" : "saved");
2732
2735
  }
@@ -2824,14 +2827,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2824
2827
  else {
2825
2828
  this.outbox.submit(message);
2826
2829
  }
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
- }
2830
+ this.scheduleFlush();
2835
2831
  }
2836
2832
  catch (error) {
2837
2833
  const dpe = internal_8.DataProcessingError.wrapIfUnrecognized(error, "ContainerRuntime.submit", {
@@ -2840,27 +2836,26 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2840
2836
  this.closeFn(dpe);
2841
2837
  throw dpe;
2842
2838
  }
2843
- if (this.isContainerMessageDirtyable(containerRuntimeMessage)) {
2844
- this.updateDocumentDirtyState(true);
2845
- }
2839
+ this.updateDocumentDirtyState();
2846
2840
  }
2847
2841
  scheduleFlush() {
2848
- if (this.flushTaskExists) {
2842
+ if (this.flushScheduled) {
2849
2843
  return;
2850
2844
  }
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
- };
2845
+ this.flushScheduled = true;
2858
2846
  switch (this.flushMode) {
2847
+ case internal_6.FlushMode.Immediate: {
2848
+ // When in Immediate flush mode, flush immediately unless we are intentionally batching multiple ops (e.g. via orderSequentially)
2849
+ if (!this.batchRunner.running) {
2850
+ this.flush();
2851
+ }
2852
+ break;
2853
+ }
2859
2854
  case internal_6.FlushMode.TurnBased: {
2860
2855
  // When in TurnBased flush mode the runtime will buffer operations in the current turn and send them as a single
2861
2856
  // batch at the end of the turn
2862
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
2863
- Promise.resolve().then(flush);
2857
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- Container will close if flush throws
2858
+ Promise.resolve().then(() => this.flush());
2864
2859
  break;
2865
2860
  }
2866
2861
  // FlushModeExperimental is experimental and not exposed directly in the runtime APIs
@@ -2868,12 +2863,11 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2868
2863
  // When in Async flush mode, the runtime will accumulate all operations across JS turns and send them as a single
2869
2864
  // batch when all micro-tasks are complete.
2870
2865
  // Compared to TurnBased, this flush mode will capture more ops into the same batch.
2871
- setTimeout(flush, 0);
2866
+ setTimeout(() => this.flush(), 0);
2872
2867
  break;
2873
2868
  }
2874
2869
  default: {
2875
- (0, internal_2.assert)(this.batchRunner.running, 0x587 /* Unreachable unless manually accumulating a batch */);
2876
- break;
2870
+ (0, internal_2.fail)(0x587 /* Unreachable unless manually accumulating a batch */);
2877
2871
  }
2878
2872
  }
2879
2873
  }
@@ -3144,6 +3138,29 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
3144
3138
  return this.summaryManager.enqueueSummarize(options);
3145
3139
  }
3146
3140
  }
3141
+ acquireExtension(id, factory, ...useContext) {
3142
+ let entry = this.extensions.get(id);
3143
+ if (entry === undefined) {
3144
+ const runtime = {
3145
+ isConnected: () => this.connected,
3146
+ getClientId: () => this.clientId,
3147
+ events: this.lazyEventsForExtensions.value,
3148
+ logger: this.baseLogger,
3149
+ submitAddressedSignal: (addressChain, message) => {
3150
+ this.submitExtensionSignal(id, addressChain, message);
3151
+ },
3152
+ getQuorum: this.getQuorum.bind(this),
3153
+ getAudience: this.getAudience.bind(this),
3154
+ };
3155
+ entry = new factory(runtime, ...useContext);
3156
+ this.extensions.set(id, entry);
3157
+ }
3158
+ else {
3159
+ (0, internal_2.assert)(entry instanceof factory, 0xba1 /* Extension entry is not of the expected type */);
3160
+ entry.extension.onNewUse(...useContext);
3161
+ }
3162
+ return entry.interface;
3163
+ }
3147
3164
  get groupedBatchingEnabled() {
3148
3165
  return this.sessionSchema.opGroupingEnabled === true;
3149
3166
  }
@@ -3157,4 +3174,34 @@ function createNewSignalEnvelope(address, type, content) {
3157
3174
  return newEnvelope;
3158
3175
  }
3159
3176
  exports.createNewSignalEnvelope = createNewSignalEnvelope;
3177
+ function isContainerMessageDirtyable({ type, contents, }) {
3178
+ // Certain container runtime messages should not mark the container dirty such as the old built-in
3179
+ // AgentScheduler and Garbage collector messages.
3180
+ switch (type) {
3181
+ case messageTypes_js_1.ContainerMessageType.Attach: {
3182
+ const attachMessage = contents;
3183
+ if (attachMessage.id === exports.agentSchedulerId) {
3184
+ return false;
3185
+ }
3186
+ break;
3187
+ }
3188
+ case messageTypes_js_1.ContainerMessageType.FluidDataStoreOp: {
3189
+ const envelope = contents;
3190
+ if (envelope.address === exports.agentSchedulerId) {
3191
+ return false;
3192
+ }
3193
+ break;
3194
+ }
3195
+ case messageTypes_js_1.ContainerMessageType.IdAllocation:
3196
+ case messageTypes_js_1.ContainerMessageType.DocumentSchemaChange:
3197
+ case messageTypes_js_1.ContainerMessageType.GC: {
3198
+ return false;
3199
+ }
3200
+ default: {
3201
+ break;
3202
+ }
3203
+ }
3204
+ return true;
3205
+ }
3206
+ exports.isContainerMessageDirtyable = isContainerMessageDirtyable;
3160
3207
  //# sourceMappingURL=containerRuntime.js.map