@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
@@ -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, } 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";
@@ -497,7 +497,7 @@ export class ContainerRuntime extends TypedEventEmitter {
497
497
  }
498
498
  /**
499
499
  * 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.
500
+ * until execution finishes. If an op is submitted, it will be marked as reentrant.
501
501
  *
502
502
  * @param callback - the callback to be invoked
503
503
  */
@@ -569,7 +569,7 @@ export class ContainerRuntime extends TypedEventEmitter {
569
569
  // 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
570
  this.pendingIdCompressorOps = [];
571
571
  this.batchRunner = new BatchRunCounter();
572
- this.flushTaskExists = false;
572
+ this.flushScheduled = false;
573
573
  this.consecutiveReconnects = 0;
574
574
  this.dataModelChangeRunner = new RunCounter();
575
575
  this._disposed = false;
@@ -582,6 +582,7 @@ export class ContainerRuntime extends TypedEventEmitter {
582
582
  this.snapshotCacheForLoadingGroupIds = new PromiseCache({
583
583
  expiry: { policy: "absolute", durationMs: 60000 },
584
584
  });
585
+ this.extensions = new Map();
585
586
  this.notifyReadOnlyState = (readonly) => this.channelCollection.notifyReadOnlyState(readonly);
586
587
  /**
587
588
  * Enter Staging Mode, such that ops submitted to the ContainerRuntime will not be sent to the ordering service.
@@ -615,9 +616,7 @@ export class ContainerRuntime extends TypedEventEmitter {
615
616
  assert(runtimeOp !== undefined, 0xb82 /* Staged batches expected to have runtimeOp defined */);
616
617
  this.rollback(runtimeOp, localOpMetadata);
617
618
  });
618
- if (this.attachState === AttachState.Attached) {
619
- this.updateDocumentDirtyState(this.pendingMessagesCount !== 0);
620
- }
619
+ this.updateDocumentDirtyState();
621
620
  }),
622
621
  commitChanges: (optionsParam) => {
623
622
  const options = { ...defaultStagingCommitOptions, ...optionsParam };
@@ -634,6 +633,15 @@ export class ContainerRuntime extends TypedEventEmitter {
634
633
  return this.stageControls;
635
634
  };
636
635
  this.readAndParseBlob = async (id) => readAndParse(this.storage, id);
636
+ // While internal, ContainerRuntime has not been converted to use the new events support.
637
+ // Recreate the required events (new pattern) with injected, wrapper new emitter.
638
+ // It is lazily create to avoid listeners (old events) that ultimately go nowhere.
639
+ this.lazyEventsForExtensions = new Lazy(() => {
640
+ const eventEmitter = createEmitter();
641
+ this.on("connected", (clientId) => eventEmitter.emit("connected", clientId));
642
+ this.on("disconnected", () => eventEmitter.emit("disconnected"));
643
+ return eventEmitter;
644
+ });
637
645
  const { options, clientDetails, connected, baseSnapshot, submitFn, submitBatchFn, submitSummaryFn, submitSignalFn, disposeFn, closeFn, deltaManager, quorum, audience, pendingLocalState, supportedFeatures, snapshotWithContents, } = context;
638
646
  // In old loaders without dispose functionality, closeFn is equivalent but will also switch container to readonly mode
639
647
  this.disposeFn = disposeFn ?? closeFn;
@@ -662,7 +670,23 @@ export class ContainerRuntime extends TypedEventEmitter {
662
670
  this.submitSummaryFn =
663
671
  submitSummaryFn ??
664
672
  ((summaryOp, refseq) => submitFn(MessageType.Summarize, summaryOp, false));
665
- this.submitSignalFn = submitSignalFn;
673
+ const sequenceAndSubmitSignal = (envelope, targetClientId) => {
674
+ if (targetClientId === undefined) {
675
+ this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope);
676
+ }
677
+ submitSignalFn(envelope, targetClientId);
678
+ };
679
+ this.submitSignalFn = (envelope, targetClientId) => {
680
+ if (envelope.address?.startsWith("/")) {
681
+ throw new Error("General path based addressing is not implemented");
682
+ }
683
+ sequenceAndSubmitSignal(envelope, targetClientId);
684
+ };
685
+ this.submitExtensionSignal = (id, addressChain, message) => {
686
+ this.verifyNotClosed();
687
+ const envelope = createNewSignalEnvelope(`/ext/${id}/${addressChain.join("/")}`, message.type, message.content);
688
+ sequenceAndSubmitSignal(envelope, message.targetClientId);
689
+ };
666
690
  // TODO: After IContainerContext.options is removed, we'll just create a new blank object {} here.
667
691
  // Values are generally expected to be set from the runtime side.
668
692
  this.options = options ?? {};
@@ -694,8 +718,8 @@ export class ContainerRuntime extends TypedEventEmitter {
694
718
  this.mc.logger.sendTelemetryEvent({
695
719
  eventName: "Attached",
696
720
  details: {
697
- dirtyContainer: this.dirtyContainer,
698
- hasPendingMessages: this.hasPendingMessages(),
721
+ lastEmittedDirty: this.lastEmittedDirty,
722
+ currentDirtyState: this.computeCurrentDirtyState(),
699
723
  },
700
724
  });
701
725
  });
@@ -862,9 +886,6 @@ export class ContainerRuntime extends TypedEventEmitter {
862
886
  // verifyNotClosed is called in FluidDataStoreContext, which is *the* expected caller.
863
887
  const envelope1 = content;
864
888
  const envelope2 = createNewSignalEnvelope(envelope1.address, type, envelope1.contents);
865
- if (targetClientId === undefined) {
866
- this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope2);
867
- }
868
889
  this.submitSignalFn(envelope2, targetClientId);
869
890
  };
870
891
  let snapshot = getSummaryForDatastores(baseSnapshot, metadata);
@@ -958,9 +979,9 @@ export class ContainerRuntime extends TypedEventEmitter {
958
979
  const closeSummarizerDelayOverride = this.mc.config.getNumber("Fluid.ContainerRuntime.Test.CloseSummarizerDelayOverrideMs");
959
980
  this.closeSummarizerDelayMs =
960
981
  closeSummarizerDelayOverride ?? defaultCloseSummarizerDelayMs;
961
- this.dirtyContainer =
962
- this.attachState !== AttachState.Attached || this.hasPendingMessages();
963
- context.updateDirtyContainerState(this.dirtyContainer);
982
+ // We haven't emitted dirty/saved yet, but this is the baseline so we know to emit when it changes
983
+ this.lastEmittedDirty = this.computeCurrentDirtyState();
984
+ context.updateDirtyContainerState(this.lastEmittedDirty);
964
985
  if (!this.skipSafetyFlushDuringProcessStack) {
965
986
  // Reference Sequence Number may have just changed, and it must be consistent across a batch,
966
987
  // so we should flush now to clear the way for the next ops.
@@ -1428,32 +1449,29 @@ export class ContainerRuntime extends TypedEventEmitter {
1428
1449
  if (!this.canSendOps()) {
1429
1450
  return;
1430
1451
  }
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;
1452
+ // Replaying is an internal operation and we don't want to generate noise while doing it.
1453
+ // So temporarily disable dirty state change events, and save the old state.
1454
+ // When we're done, we'll emit the event if the state changed.
1455
+ const oldState = this.lastEmittedDirty;
1437
1456
  assert(this.emitDirtyDocumentEvent, 0x127 /* "dirty document event not set on replay" */);
1438
1457
  this.emitDirtyDocumentEvent = false;
1439
- let newState;
1440
1458
  try {
1441
1459
  // Any ID Allocation ops that failed to submit after the pending state was queued need to have
1442
1460
  // the corresponding ranges resubmitted (note this call replaces the typical resubmit flow).
1443
1461
  // Since we don't submit ID Allocation ops when staged, any outstanding ranges would be from
1444
1462
  // before staging mode so we can simply say staged: false.
1445
1463
  this.submitIdAllocationOpIfNeeded({ resubmitOutstandingRanges: true, staged: false });
1464
+ this.scheduleFlush();
1446
1465
  // replay the ops
1447
1466
  this.pendingStateManager.replayPendingStates();
1448
1467
  }
1449
1468
  finally {
1450
- // Save the new start and restore the old state, re-enable event emit
1451
- newState = this.dirtyContainer;
1452
- this.dirtyContainer = oldState;
1469
+ // Restore the old state, re-enable event emit
1470
+ this.lastEmittedDirty = oldState;
1453
1471
  this.emitDirtyDocumentEvent = true;
1454
1472
  }
1455
- // Officially transition from the old state to the new state.
1456
- this.updateDocumentDirtyState(newState);
1473
+ // This will emit an event if the state changed relative to before replay
1474
+ this.updateDocumentDirtyState();
1457
1475
  }
1458
1476
  /**
1459
1477
  * Parse an op's type and actual content from given serialized content
@@ -1715,6 +1733,8 @@ export class ContainerRuntime extends TypedEventEmitter {
1715
1733
  * @param groupedBatch - true if these messages are part of a grouped op batch.
1716
1734
  */
1717
1735
  processInboundMessages(messagesWithMetadata, locationInBatch, local, savedOp, runtimeBatch, groupedBatch) {
1736
+ // This message could have been the last pending local (dirtyable) message, in which case we need to update dirty state to "saved"
1737
+ this.updateDocumentDirtyState();
1718
1738
  if (locationInBatch.batchStart) {
1719
1739
  const firstMessage = messagesWithMetadata[0]?.message;
1720
1740
  assert(firstMessage !== undefined, 0xa31 /* Batch must have at least one message */);
@@ -1809,11 +1829,6 @@ export class ContainerRuntime extends TypedEventEmitter {
1809
1829
  message.minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
1810
1830
  }
1811
1831
  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
1832
  // The DeltaManager used to do this, but doesn't anymore as of Loader v2.4
1818
1833
  // Anyone listening to our "op" event would expect the contents to be parsed per this same logic
1819
1834
  if (typeof message.contents === "string" &&
@@ -1835,11 +1850,6 @@ export class ContainerRuntime extends TypedEventEmitter {
1835
1850
  *
1836
1851
  */
1837
1852
  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
1853
  // Get the contents without the localOpMetadata because not all message types know about localOpMetadata.
1844
1854
  const contents = messagesContent.map((c) => c.contents);
1845
1855
  switch (message.type) {
@@ -1915,20 +1925,44 @@ export class ContainerRuntime extends TypedEventEmitter {
1915
1925
  if (message.clientId === this.clientId) {
1916
1926
  this.signalTelemetryManager.trackReceivedSignal(envelope, this.mc.logger, this.consecutiveReconnects);
1917
1927
  }
1918
- if (envelope.address === undefined) {
1928
+ const fullAddress = envelope.address;
1929
+ if (fullAddress === undefined) {
1919
1930
  // No address indicates a container signal message.
1920
1931
  this.emit("signal", transformed, local);
1921
1932
  return;
1922
1933
  }
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);
1934
+ this.routeNonContainerSignal(fullAddress, transformed, local);
1935
+ }
1936
+ routeNonContainerSignal(address, signalMessage, local) {
1937
+ // channelCollection signals are identified by no starting `/` in address.
1938
+ if (!address.startsWith("/")) {
1939
+ // Due to a mismatch between different layers in terms of
1940
+ // what is the interface of passing signals, we need to adjust
1941
+ // the signal envelope before sending it to the datastores to be processed
1942
+ const envelope = {
1943
+ address,
1944
+ contents: signalMessage.content,
1945
+ };
1946
+ signalMessage.content = envelope;
1947
+ this.channelCollection.processSignal(signalMessage, local);
1948
+ return;
1949
+ }
1950
+ const addresses = address.split("/");
1951
+ if (addresses.length > 2 && addresses[1] === "ext") {
1952
+ const id = addresses[2];
1953
+ const entry = this.extensions.get(id);
1954
+ if (entry !== undefined) {
1955
+ entry.extension.processSignal?.(addresses.slice(3), signalMessage, local);
1956
+ return;
1957
+ }
1958
+ }
1959
+ assert(!local, 0xba0 /* No recipient found for local signal */);
1960
+ this.mc.logger.sendTelemetryEvent({
1961
+ eventName: "SignalAddressNotFound",
1962
+ ...tagCodeArtifacts({
1963
+ address,
1964
+ }),
1965
+ });
1932
1966
  }
1933
1967
  /**
1934
1968
  * Flush the current batch of ops to the ordering service for sequencing
@@ -1938,6 +1972,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1938
1972
  * @param resubmitInfo - If defined, indicates this is a resubmission of a batch with the given Batch info needed for resubmit.
1939
1973
  */
1940
1974
  flush(resubmitInfo) {
1975
+ this.flushScheduled = false;
1941
1976
  try {
1942
1977
  assert(!this.batchRunner.running, 0x24c /* "Cannot call `flush()` while manually accumulating a batch (e.g. under orderSequentially) */);
1943
1978
  this.outbox.flush(resubmitInfo);
@@ -1958,7 +1993,6 @@ export class ContainerRuntime extends TypedEventEmitter {
1958
1993
  */
1959
1994
  orderSequentially(callback) {
1960
1995
  let checkpoint;
1961
- const checkpointDirtyState = this.dirtyContainer;
1962
1996
  // eslint-disable-next-line import/no-deprecated
1963
1997
  let stageControls;
1964
1998
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
@@ -1979,10 +2013,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1979
2013
  // This will throw and close the container if rollback fails
1980
2014
  try {
1981
2015
  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
- }
2016
+ this.updateDocumentDirtyState();
1986
2017
  stageControls?.discardChanges();
1987
2018
  stageControls = undefined;
1988
2019
  }
@@ -2063,12 +2094,6 @@ export class ContainerRuntime extends TypedEventEmitter {
2063
2094
  // container runtime's ability to send ops depend on the actual readonly state of the delta manager.
2064
2095
  return (this.connected && !this.innerDeltaManager.readOnlyInfo.readonly && !this.imminentClosure);
2065
2096
  }
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
2097
  getQuorum() {
2073
2098
  return this._quorum;
2074
2099
  }
@@ -2080,36 +2105,17 @@ export class ContainerRuntime extends TypedEventEmitter {
2080
2105
  * either were not sent out to delta stream or were not yet acknowledged.
2081
2106
  */
2082
2107
  get isDirty() {
2083
- return this.dirtyContainer;
2108
+ // Rather than recomputing the dirty state in this moment,
2109
+ // just regurgitate the last emitted dirty state.
2110
+ return this.lastEmittedDirty;
2084
2111
  }
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;
2112
+ /**
2113
+ * Returns true if the container is dirty: not attached, or no pending user messages (could be some "non-dirtyable" ones though)
2114
+ */
2115
+ computeCurrentDirtyState() {
2116
+ return (this.attachState !== AttachState.Attached ||
2117
+ this.pendingStateManager.hasPendingUserChanges() ||
2118
+ this.outbox.containsUserChanges());
2113
2119
  }
2114
2120
  /**
2115
2121
  * Submits the signal to be sent to other clients.
@@ -2126,9 +2132,6 @@ export class ContainerRuntime extends TypedEventEmitter {
2126
2132
  submitSignal(type, content, targetClientId) {
2127
2133
  this.verifyNotClosed();
2128
2134
  const envelope = createNewSignalEnvelope(undefined /* address */, type, content);
2129
- if (targetClientId === undefined) {
2130
- this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope);
2131
- }
2132
2135
  this.submitSignalFn(envelope, targetClientId);
2133
2136
  }
2134
2137
  setAttachState(attachState) {
@@ -2139,9 +2142,7 @@ export class ContainerRuntime extends TypedEventEmitter {
2139
2142
  assert(this.attachState === AttachState.Attached, 0x12e /* "Container Context should already be in attached state" */);
2140
2143
  this.emit("attached");
2141
2144
  }
2142
- if (attachState === AttachState.Attached && !this.hasPendingMessages()) {
2143
- this.updateDocumentDirtyState(false);
2144
- }
2145
+ this.updateDocumentDirtyState();
2145
2146
  this.channelCollection.setAttachState(attachState);
2146
2147
  }
2147
2148
  /**
@@ -2709,18 +2710,20 @@ export class ContainerRuntime extends TypedEventEmitter {
2709
2710
  hasPendingMessages() {
2710
2711
  return this.pendingMessagesCount !== 0;
2711
2712
  }
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) {
2713
+ /**
2714
+ * Emit "dirty" or "saved" event based on the current dirty state of the document.
2715
+ * This must be called every time the states underlying the dirty state change.
2716
+ *
2717
+ * @privateRemarks - It's helpful to think of this as an event handler registered
2718
+ * for hypothetical "changed" events for PendingStateManager, Outbox, and Container Attach machinery.
2719
+ * But those events don't exist so we manually call this wherever we know those changes happen.
2720
+ */
2721
+ updateDocumentDirtyState() {
2722
+ const dirty = this.computeCurrentDirtyState();
2723
+ if (this.lastEmittedDirty === dirty) {
2721
2724
  return;
2722
2725
  }
2723
- this.dirtyContainer = dirty;
2726
+ this.lastEmittedDirty = dirty;
2724
2727
  if (this.emitDirtyDocumentEvent) {
2725
2728
  this.emit(dirty ? "dirty" : "saved");
2726
2729
  }
@@ -2818,14 +2821,7 @@ export class ContainerRuntime extends TypedEventEmitter {
2818
2821
  else {
2819
2822
  this.outbox.submit(message);
2820
2823
  }
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
- }
2824
+ this.scheduleFlush();
2829
2825
  }
2830
2826
  catch (error) {
2831
2827
  const dpe = DataProcessingError.wrapIfUnrecognized(error, "ContainerRuntime.submit", {
@@ -2834,27 +2830,26 @@ export class ContainerRuntime extends TypedEventEmitter {
2834
2830
  this.closeFn(dpe);
2835
2831
  throw dpe;
2836
2832
  }
2837
- if (this.isContainerMessageDirtyable(containerRuntimeMessage)) {
2838
- this.updateDocumentDirtyState(true);
2839
- }
2833
+ this.updateDocumentDirtyState();
2840
2834
  }
2841
2835
  scheduleFlush() {
2842
- if (this.flushTaskExists) {
2836
+ if (this.flushScheduled) {
2843
2837
  return;
2844
2838
  }
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
- };
2839
+ this.flushScheduled = true;
2852
2840
  switch (this.flushMode) {
2841
+ case FlushMode.Immediate: {
2842
+ // When in Immediate flush mode, flush immediately unless we are intentionally batching multiple ops (e.g. via orderSequentially)
2843
+ if (!this.batchRunner.running) {
2844
+ this.flush();
2845
+ }
2846
+ break;
2847
+ }
2853
2848
  case FlushMode.TurnBased: {
2854
2849
  // When in TurnBased flush mode the runtime will buffer operations in the current turn and send them as a single
2855
2850
  // batch at the end of the turn
2856
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
2857
- Promise.resolve().then(flush);
2851
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- Container will close if flush throws
2852
+ Promise.resolve().then(() => this.flush());
2858
2853
  break;
2859
2854
  }
2860
2855
  // FlushModeExperimental is experimental and not exposed directly in the runtime APIs
@@ -2862,12 +2857,11 @@ export class ContainerRuntime extends TypedEventEmitter {
2862
2857
  // When in Async flush mode, the runtime will accumulate all operations across JS turns and send them as a single
2863
2858
  // batch when all micro-tasks are complete.
2864
2859
  // Compared to TurnBased, this flush mode will capture more ops into the same batch.
2865
- setTimeout(flush, 0);
2860
+ setTimeout(() => this.flush(), 0);
2866
2861
  break;
2867
2862
  }
2868
2863
  default: {
2869
- assert(this.batchRunner.running, 0x587 /* Unreachable unless manually accumulating a batch */);
2870
- break;
2864
+ fail(0x587 /* Unreachable unless manually accumulating a batch */);
2871
2865
  }
2872
2866
  }
2873
2867
  }
@@ -3138,6 +3132,29 @@ export class ContainerRuntime extends TypedEventEmitter {
3138
3132
  return this.summaryManager.enqueueSummarize(options);
3139
3133
  }
3140
3134
  }
3135
+ acquireExtension(id, factory, ...useContext) {
3136
+ let entry = this.extensions.get(id);
3137
+ if (entry === undefined) {
3138
+ const runtime = {
3139
+ isConnected: () => this.connected,
3140
+ getClientId: () => this.clientId,
3141
+ events: this.lazyEventsForExtensions.value,
3142
+ logger: this.baseLogger,
3143
+ submitAddressedSignal: (addressChain, message) => {
3144
+ this.submitExtensionSignal(id, addressChain, message);
3145
+ },
3146
+ getQuorum: this.getQuorum.bind(this),
3147
+ getAudience: this.getAudience.bind(this),
3148
+ };
3149
+ entry = new factory(runtime, ...useContext);
3150
+ this.extensions.set(id, entry);
3151
+ }
3152
+ else {
3153
+ assert(entry instanceof factory, 0xba1 /* Extension entry is not of the expected type */);
3154
+ entry.extension.onNewUse(...useContext);
3155
+ }
3156
+ return entry.interface;
3157
+ }
3141
3158
  get groupedBatchingEnabled() {
3142
3159
  return this.sessionSchema.opGroupingEnabled === true;
3143
3160
  }
@@ -3149,4 +3166,33 @@ export function createNewSignalEnvelope(address, type, content) {
3149
3166
  };
3150
3167
  return newEnvelope;
3151
3168
  }
3169
+ export function isContainerMessageDirtyable({ type, contents, }) {
3170
+ // Certain container runtime messages should not mark the container dirty such as the old built-in
3171
+ // AgentScheduler and Garbage collector messages.
3172
+ switch (type) {
3173
+ case ContainerMessageType.Attach: {
3174
+ const attachMessage = contents;
3175
+ if (attachMessage.id === agentSchedulerId) {
3176
+ return false;
3177
+ }
3178
+ break;
3179
+ }
3180
+ case ContainerMessageType.FluidDataStoreOp: {
3181
+ const envelope = contents;
3182
+ if (envelope.address === agentSchedulerId) {
3183
+ return false;
3184
+ }
3185
+ break;
3186
+ }
3187
+ case ContainerMessageType.IdAllocation:
3188
+ case ContainerMessageType.DocumentSchemaChange:
3189
+ case ContainerMessageType.GC: {
3190
+ return false;
3191
+ }
3192
+ default: {
3193
+ break;
3194
+ }
3195
+ }
3196
+ return true;
3197
+ }
3152
3198
  //# sourceMappingURL=containerRuntime.js.map