@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.
- package/CHANGELOG.md +4 -0
- package/container-runtime.test-files.tar +0 -0
- package/dist/containerRuntime.d.ts +35 -17
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +174 -127
- package/dist/containerRuntime.js.map +1 -1
- package/dist/opLifecycle/batchManager.d.ts +4 -0
- package/dist/opLifecycle/batchManager.d.ts.map +1 -1
- package/dist/opLifecycle/batchManager.js +7 -0
- package/dist/opLifecycle/batchManager.js.map +1 -1
- package/dist/opLifecycle/outbox.d.ts +1 -0
- package/dist/opLifecycle/outbox.d.ts.map +1 -1
- package/dist/opLifecycle/outbox.js +6 -1
- package/dist/opLifecycle/outbox.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/pendingStateManager.d.ts +4 -0
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +16 -0
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/runCounter.d.ts.map +1 -1
- package/dist/runCounter.js +1 -1
- package/dist/runCounter.js.map +1 -1
- package/lib/containerRuntime.d.ts +35 -17
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +174 -128
- package/lib/containerRuntime.js.map +1 -1
- package/lib/opLifecycle/batchManager.d.ts +4 -0
- package/lib/opLifecycle/batchManager.d.ts.map +1 -1
- package/lib/opLifecycle/batchManager.js +7 -0
- package/lib/opLifecycle/batchManager.js.map +1 -1
- package/lib/opLifecycle/outbox.d.ts +1 -0
- package/lib/opLifecycle/outbox.d.ts.map +1 -1
- package/lib/opLifecycle/outbox.js +6 -1
- package/lib/opLifecycle/outbox.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pendingStateManager.d.ts +4 -0
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +16 -0
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/runCounter.d.ts.map +1 -1
- package/lib/runCounter.js +1 -1
- package/lib/runCounter.js.map +1 -1
- package/package.json +18 -18
- package/src/containerRuntime.ts +263 -152
- package/src/opLifecycle/batchManager.ts +8 -0
- package/src/opLifecycle/outbox.ts +8 -1
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +17 -0
- package/src/runCounter.ts +4 -1
package/lib/containerRuntime.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
698
|
-
|
|
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
|
|
962
|
-
|
|
963
|
-
context.updateDirtyContainerState(this.
|
|
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
|
-
//
|
|
1432
|
-
// dirty state change events
|
|
1433
|
-
//
|
|
1434
|
-
|
|
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
|
-
//
|
|
1451
|
-
|
|
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
|
-
//
|
|
1456
|
-
this.updateDocumentDirtyState(
|
|
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
|
-
|
|
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
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2838
|
-
this.updateDocumentDirtyState(true);
|
|
2839
|
-
}
|
|
2833
|
+
this.updateDocumentDirtyState();
|
|
2840
2834
|
}
|
|
2841
2835
|
scheduleFlush() {
|
|
2842
|
-
if (this.
|
|
2836
|
+
if (this.flushScheduled) {
|
|
2843
2837
|
return;
|
|
2844
2838
|
}
|
|
2845
|
-
this.
|
|
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
|
-
|
|
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
|