@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/src/containerRuntime.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
ILayerCompatDetails,
|
|
8
8
|
IProvideLayerCompatDetails,
|
|
9
9
|
} from "@fluid-internal/client-utils";
|
|
10
|
-
import { Trace, TypedEventEmitter } from "@fluid-internal/client-utils";
|
|
10
|
+
import { createEmitter, Trace, TypedEventEmitter } from "@fluid-internal/client-utils";
|
|
11
11
|
import type {
|
|
12
12
|
IAudience,
|
|
13
13
|
ISelf,
|
|
@@ -25,8 +25,15 @@ import type {
|
|
|
25
25
|
} from "@fluidframework/container-definitions/internal";
|
|
26
26
|
import { isIDeltaManagerFull } from "@fluidframework/container-definitions/internal";
|
|
27
27
|
import type {
|
|
28
|
+
ContainerExtensionFactory,
|
|
29
|
+
ContainerExtensionId,
|
|
30
|
+
ExtensionHost,
|
|
31
|
+
ExtensionHostEvents,
|
|
32
|
+
ExtensionRuntimeProperties,
|
|
28
33
|
IContainerRuntime,
|
|
29
34
|
IContainerRuntimeEvents,
|
|
35
|
+
IContainerRuntimeInternal,
|
|
36
|
+
OutboundExtensionMessage,
|
|
30
37
|
} from "@fluidframework/container-runtime-definitions/internal";
|
|
31
38
|
import type {
|
|
32
39
|
FluidObject,
|
|
@@ -34,6 +41,7 @@ import type {
|
|
|
34
41
|
IRequest,
|
|
35
42
|
IResponse,
|
|
36
43
|
ITelemetryBaseLogger,
|
|
44
|
+
Listenable,
|
|
37
45
|
} from "@fluidframework/core-interfaces";
|
|
38
46
|
import type {
|
|
39
47
|
IErrorBase,
|
|
@@ -41,13 +49,17 @@ import type {
|
|
|
41
49
|
IFluidHandleInternal,
|
|
42
50
|
IProvideFluidHandleContext,
|
|
43
51
|
ISignalEnvelope,
|
|
52
|
+
JsonDeserialized,
|
|
53
|
+
TypedMessage,
|
|
44
54
|
} from "@fluidframework/core-interfaces/internal";
|
|
45
55
|
import {
|
|
46
56
|
assert,
|
|
47
57
|
Deferred,
|
|
58
|
+
Lazy,
|
|
48
59
|
LazyPromise,
|
|
49
60
|
PromiseCache,
|
|
50
61
|
delay,
|
|
62
|
+
fail,
|
|
51
63
|
} from "@fluidframework/core-utils/internal";
|
|
52
64
|
import type {
|
|
53
65
|
IClientDetails,
|
|
@@ -284,6 +296,16 @@ import {
|
|
|
284
296
|
} from "./summary/index.js";
|
|
285
297
|
import { Throttler, formExponentialFn } from "./throttler.js";
|
|
286
298
|
|
|
299
|
+
/**
|
|
300
|
+
* A {@link ContainerExtension}'s factory function as stored in extension map.
|
|
301
|
+
*/
|
|
302
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- `any` required to allow typed factory to be assignable per ContainerExtension.processSignal
|
|
303
|
+
type ExtensionEntry = ContainerExtensionFactory<unknown, any, unknown[]> extends new (
|
|
304
|
+
...args: any[]
|
|
305
|
+
) => infer T
|
|
306
|
+
? T
|
|
307
|
+
: never;
|
|
308
|
+
|
|
287
309
|
/**
|
|
288
310
|
* Creates an error object to be thrown / passed to Container's close fn in case of an unknown message type.
|
|
289
311
|
* The parameters are typed to support compile-time enforcement of handling all known types/behaviors
|
|
@@ -674,6 +696,8 @@ export let getSingleUseLegacyLogCallback = (logger: ITelemetryLoggerExt, type: s
|
|
|
674
696
|
};
|
|
675
697
|
};
|
|
676
698
|
|
|
699
|
+
type UnsequencedSignalEnvelope = Omit<ISignalEnvelope, "clientBroadcastSignalSequenceNumber">;
|
|
700
|
+
|
|
677
701
|
/**
|
|
678
702
|
* This object holds the parameters necessary for the {@link loadContainerRuntime} function.
|
|
679
703
|
* @legacy
|
|
@@ -757,7 +781,7 @@ const defaultMaxConsecutiveReconnects = 7;
|
|
|
757
781
|
export class ContainerRuntime
|
|
758
782
|
extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
759
783
|
implements
|
|
760
|
-
|
|
784
|
+
IContainerRuntimeInternal,
|
|
761
785
|
// eslint-disable-next-line import/no-deprecated
|
|
762
786
|
IContainerRuntimeBaseExperimental,
|
|
763
787
|
IRuntime,
|
|
@@ -1148,10 +1172,10 @@ export class ContainerRuntime
|
|
|
1148
1172
|
summaryOp: ISummaryContent,
|
|
1149
1173
|
referenceSequenceNumber?: number,
|
|
1150
1174
|
) => number;
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1175
|
+
private readonly submitSignalFn: (
|
|
1176
|
+
content: UnsequencedSignalEnvelope,
|
|
1177
|
+
targetClientId?: string,
|
|
1178
|
+
) => void;
|
|
1155
1179
|
public readonly disposeFn: (error?: ICriticalContainerError) => void;
|
|
1156
1180
|
public readonly closeFn: (error?: ICriticalContainerError) => void;
|
|
1157
1181
|
|
|
@@ -1266,7 +1290,7 @@ export class ContainerRuntime
|
|
|
1266
1290
|
private readonly batchRunner = new BatchRunCounter();
|
|
1267
1291
|
private readonly _flushMode: FlushMode;
|
|
1268
1292
|
private readonly offlineEnabled: boolean;
|
|
1269
|
-
private
|
|
1293
|
+
private flushScheduled = false;
|
|
1270
1294
|
|
|
1271
1295
|
private _connected: boolean;
|
|
1272
1296
|
|
|
@@ -1282,7 +1306,7 @@ export class ContainerRuntime
|
|
|
1282
1306
|
|
|
1283
1307
|
/**
|
|
1284
1308
|
* Invokes the given callback and expects that no ops are submitted
|
|
1285
|
-
* until execution finishes. If an op is submitted,
|
|
1309
|
+
* until execution finishes. If an op is submitted, it will be marked as reentrant.
|
|
1286
1310
|
*
|
|
1287
1311
|
* @param callback - the callback to be invoked
|
|
1288
1312
|
*/
|
|
@@ -1306,7 +1330,7 @@ export class ContainerRuntime
|
|
|
1306
1330
|
return this._disposed;
|
|
1307
1331
|
}
|
|
1308
1332
|
|
|
1309
|
-
private
|
|
1333
|
+
private lastEmittedDirty: boolean;
|
|
1310
1334
|
private emitDirtyDocumentEvent = true;
|
|
1311
1335
|
private readonly useDeltaManagerOpsProxy: boolean;
|
|
1312
1336
|
private readonly closeSummarizerDelayMs: number;
|
|
@@ -1405,6 +1429,8 @@ export class ContainerRuntime
|
|
|
1405
1429
|
*/
|
|
1406
1430
|
private readonly skipSafetyFlushDuringProcessStack: boolean;
|
|
1407
1431
|
|
|
1432
|
+
private readonly extensions = new Map<ContainerExtensionId, ExtensionEntry>();
|
|
1433
|
+
|
|
1408
1434
|
/***/
|
|
1409
1435
|
protected constructor(
|
|
1410
1436
|
context: IContainerContext,
|
|
@@ -1498,7 +1524,35 @@ export class ContainerRuntime
|
|
|
1498
1524
|
this.submitSummaryFn =
|
|
1499
1525
|
submitSummaryFn ??
|
|
1500
1526
|
((summaryOp, refseq) => submitFn(MessageType.Summarize, summaryOp, false));
|
|
1501
|
-
|
|
1527
|
+
|
|
1528
|
+
const sequenceAndSubmitSignal = (
|
|
1529
|
+
envelope: UnsequencedSignalEnvelope,
|
|
1530
|
+
targetClientId?: string,
|
|
1531
|
+
): void => {
|
|
1532
|
+
if (targetClientId === undefined) {
|
|
1533
|
+
this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope);
|
|
1534
|
+
}
|
|
1535
|
+
submitSignalFn(envelope, targetClientId);
|
|
1536
|
+
};
|
|
1537
|
+
this.submitSignalFn = (envelope: UnsequencedSignalEnvelope, targetClientId?: string) => {
|
|
1538
|
+
if (envelope.address?.startsWith("/")) {
|
|
1539
|
+
throw new Error("General path based addressing is not implemented");
|
|
1540
|
+
}
|
|
1541
|
+
sequenceAndSubmitSignal(envelope, targetClientId);
|
|
1542
|
+
};
|
|
1543
|
+
this.submitExtensionSignal = <TMessage extends TypedMessage>(
|
|
1544
|
+
id: string,
|
|
1545
|
+
addressChain: string[],
|
|
1546
|
+
message: OutboundExtensionMessage<TMessage>,
|
|
1547
|
+
): void => {
|
|
1548
|
+
this.verifyNotClosed();
|
|
1549
|
+
const envelope = createNewSignalEnvelope(
|
|
1550
|
+
`/ext/${id}/${addressChain.join("/")}`,
|
|
1551
|
+
message.type,
|
|
1552
|
+
message.content,
|
|
1553
|
+
);
|
|
1554
|
+
sequenceAndSubmitSignal(envelope, message.targetClientId);
|
|
1555
|
+
};
|
|
1502
1556
|
|
|
1503
1557
|
// TODO: After IContainerContext.options is removed, we'll just create a new blank object {} here.
|
|
1504
1558
|
// Values are generally expected to be set from the runtime side.
|
|
@@ -1532,8 +1586,8 @@ export class ContainerRuntime
|
|
|
1532
1586
|
this.mc.logger.sendTelemetryEvent({
|
|
1533
1587
|
eventName: "Attached",
|
|
1534
1588
|
details: {
|
|
1535
|
-
|
|
1536
|
-
|
|
1589
|
+
lastEmittedDirty: this.lastEmittedDirty,
|
|
1590
|
+
currentDirtyState: this.computeCurrentDirtyState(),
|
|
1537
1591
|
},
|
|
1538
1592
|
});
|
|
1539
1593
|
});
|
|
@@ -1757,9 +1811,6 @@ export class ContainerRuntime
|
|
|
1757
1811
|
// verifyNotClosed is called in FluidDataStoreContext, which is *the* expected caller.
|
|
1758
1812
|
const envelope1 = content as IEnvelope;
|
|
1759
1813
|
const envelope2 = createNewSignalEnvelope(envelope1.address, type, envelope1.contents);
|
|
1760
|
-
if (targetClientId === undefined) {
|
|
1761
|
-
this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope2);
|
|
1762
|
-
}
|
|
1763
1814
|
this.submitSignalFn(envelope2, targetClientId);
|
|
1764
1815
|
};
|
|
1765
1816
|
|
|
@@ -1897,9 +1948,9 @@ export class ContainerRuntime
|
|
|
1897
1948
|
this.closeSummarizerDelayMs =
|
|
1898
1949
|
closeSummarizerDelayOverride ?? defaultCloseSummarizerDelayMs;
|
|
1899
1950
|
|
|
1900
|
-
this
|
|
1901
|
-
|
|
1902
|
-
context.updateDirtyContainerState(this.
|
|
1951
|
+
// We haven't emitted dirty/saved yet, but this is the baseline so we know to emit when it changes
|
|
1952
|
+
this.lastEmittedDirty = this.computeCurrentDirtyState();
|
|
1953
|
+
context.updateDirtyContainerState(this.lastEmittedDirty);
|
|
1903
1954
|
|
|
1904
1955
|
if (!this.skipSafetyFlushDuringProcessStack) {
|
|
1905
1956
|
// Reference Sequence Number may have just changed, and it must be consistent across a batch,
|
|
@@ -2528,17 +2579,12 @@ export class ContainerRuntime
|
|
|
2528
2579
|
return;
|
|
2529
2580
|
}
|
|
2530
2581
|
|
|
2531
|
-
//
|
|
2532
|
-
// dirty state change events
|
|
2533
|
-
//
|
|
2534
|
-
|
|
2535
|
-
// Save the old state, reset to false, disable event emit
|
|
2536
|
-
const oldState = this.dirtyContainer;
|
|
2537
|
-
this.dirtyContainer = false;
|
|
2538
|
-
|
|
2582
|
+
// Replaying is an internal operation and we don't want to generate noise while doing it.
|
|
2583
|
+
// So temporarily disable dirty state change events, and save the old state.
|
|
2584
|
+
// When we're done, we'll emit the event if the state changed.
|
|
2585
|
+
const oldState = this.lastEmittedDirty;
|
|
2539
2586
|
assert(this.emitDirtyDocumentEvent, 0x127 /* "dirty document event not set on replay" */);
|
|
2540
2587
|
this.emitDirtyDocumentEvent = false;
|
|
2541
|
-
let newState: boolean;
|
|
2542
2588
|
|
|
2543
2589
|
try {
|
|
2544
2590
|
// Any ID Allocation ops that failed to submit after the pending state was queued need to have
|
|
@@ -2546,18 +2592,18 @@ export class ContainerRuntime
|
|
|
2546
2592
|
// Since we don't submit ID Allocation ops when staged, any outstanding ranges would be from
|
|
2547
2593
|
// before staging mode so we can simply say staged: false.
|
|
2548
2594
|
this.submitIdAllocationOpIfNeeded({ resubmitOutstandingRanges: true, staged: false });
|
|
2595
|
+
this.scheduleFlush();
|
|
2549
2596
|
|
|
2550
2597
|
// replay the ops
|
|
2551
2598
|
this.pendingStateManager.replayPendingStates();
|
|
2552
2599
|
} finally {
|
|
2553
|
-
//
|
|
2554
|
-
|
|
2555
|
-
this.dirtyContainer = oldState;
|
|
2600
|
+
// Restore the old state, re-enable event emit
|
|
2601
|
+
this.lastEmittedDirty = oldState;
|
|
2556
2602
|
this.emitDirtyDocumentEvent = true;
|
|
2557
2603
|
}
|
|
2558
2604
|
|
|
2559
|
-
//
|
|
2560
|
-
this.updateDocumentDirtyState(
|
|
2605
|
+
// This will emit an event if the state changed relative to before replay
|
|
2606
|
+
this.updateDocumentDirtyState();
|
|
2561
2607
|
}
|
|
2562
2608
|
|
|
2563
2609
|
/**
|
|
@@ -2928,6 +2974,9 @@ export class ContainerRuntime
|
|
|
2928
2974
|
runtimeBatch: boolean,
|
|
2929
2975
|
groupedBatch: boolean,
|
|
2930
2976
|
): void {
|
|
2977
|
+
// This message could have been the last pending local (dirtyable) message, in which case we need to update dirty state to "saved"
|
|
2978
|
+
this.updateDocumentDirtyState();
|
|
2979
|
+
|
|
2931
2980
|
if (locationInBatch.batchStart) {
|
|
2932
2981
|
const firstMessage = messagesWithMetadata[0]?.message;
|
|
2933
2982
|
assert(firstMessage !== undefined, 0xa31 /* Batch must have at least one message */);
|
|
@@ -3043,12 +3092,6 @@ export class ContainerRuntime
|
|
|
3043
3092
|
|
|
3044
3093
|
this._processedClientSequenceNumber = message.clientSequenceNumber;
|
|
3045
3094
|
|
|
3046
|
-
// If there are no more pending messages after processing a local message,
|
|
3047
|
-
// the document is no longer dirty.
|
|
3048
|
-
if (!this.hasPendingMessages()) {
|
|
3049
|
-
this.updateDocumentDirtyState(false);
|
|
3050
|
-
}
|
|
3051
|
-
|
|
3052
3095
|
// The DeltaManager used to do this, but doesn't anymore as of Loader v2.4
|
|
3053
3096
|
// Anyone listening to our "op" event would expect the contents to be parsed per this same logic
|
|
3054
3097
|
if (
|
|
@@ -3079,12 +3122,6 @@ export class ContainerRuntime
|
|
|
3079
3122
|
local: boolean,
|
|
3080
3123
|
savedOp?: boolean,
|
|
3081
3124
|
): void {
|
|
3082
|
-
// If there are no more pending messages after processing a local message,
|
|
3083
|
-
// the document is no longer dirty.
|
|
3084
|
-
if (!this.hasPendingMessages()) {
|
|
3085
|
-
this.updateDocumentDirtyState(false);
|
|
3086
|
-
}
|
|
3087
|
-
|
|
3088
3125
|
// Get the contents without the localOpMetadata because not all message types know about localOpMetadata.
|
|
3089
3126
|
const contents = messagesContent.map((c) => c.contents);
|
|
3090
3127
|
|
|
@@ -3171,9 +3208,15 @@ export class ContainerRuntime
|
|
|
3171
3208
|
}
|
|
3172
3209
|
}
|
|
3173
3210
|
|
|
3174
|
-
public processSignal(
|
|
3175
|
-
|
|
3176
|
-
|
|
3211
|
+
public processSignal(
|
|
3212
|
+
message: ISignalMessage<{
|
|
3213
|
+
type: string;
|
|
3214
|
+
content: ISignalEnvelope<{ type: string; content: JsonDeserialized<unknown> }>;
|
|
3215
|
+
}>,
|
|
3216
|
+
local: boolean,
|
|
3217
|
+
): void {
|
|
3218
|
+
const envelope = message.content;
|
|
3219
|
+
const transformed = {
|
|
3177
3220
|
clientId: message.clientId,
|
|
3178
3221
|
content: envelope.contents.content,
|
|
3179
3222
|
type: envelope.contents.type,
|
|
@@ -3189,22 +3232,53 @@ export class ContainerRuntime
|
|
|
3189
3232
|
);
|
|
3190
3233
|
}
|
|
3191
3234
|
|
|
3192
|
-
|
|
3235
|
+
const fullAddress = envelope.address;
|
|
3236
|
+
if (fullAddress === undefined) {
|
|
3193
3237
|
// No address indicates a container signal message.
|
|
3194
3238
|
this.emit("signal", transformed, local);
|
|
3195
3239
|
return;
|
|
3196
3240
|
}
|
|
3197
3241
|
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3242
|
+
this.routeNonContainerSignal(fullAddress, transformed, local);
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
private routeNonContainerSignal(
|
|
3246
|
+
address: string,
|
|
3247
|
+
signalMessage: IInboundSignalMessage<{ type: string; content: JsonDeserialized<unknown> }>,
|
|
3248
|
+
local: boolean,
|
|
3249
|
+
): void {
|
|
3250
|
+
// channelCollection signals are identified by no starting `/` in address.
|
|
3251
|
+
if (!address.startsWith("/")) {
|
|
3252
|
+
// Due to a mismatch between different layers in terms of
|
|
3253
|
+
// what is the interface of passing signals, we need to adjust
|
|
3254
|
+
// the signal envelope before sending it to the datastores to be processed
|
|
3255
|
+
const envelope = {
|
|
3256
|
+
address,
|
|
3257
|
+
contents: signalMessage.content,
|
|
3258
|
+
};
|
|
3259
|
+
signalMessage.content = envelope;
|
|
3260
|
+
|
|
3261
|
+
this.channelCollection.processSignal(signalMessage, local);
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3206
3264
|
|
|
3207
|
-
|
|
3265
|
+
const addresses = address.split("/");
|
|
3266
|
+
if (addresses.length > 2 && addresses[1] === "ext") {
|
|
3267
|
+
const id = addresses[2] as ContainerExtensionId;
|
|
3268
|
+
const entry = this.extensions.get(id);
|
|
3269
|
+
if (entry !== undefined) {
|
|
3270
|
+
entry.extension.processSignal?.(addresses.slice(3), signalMessage, local);
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
assert(!local, 0xba0 /* No recipient found for local signal */);
|
|
3276
|
+
this.mc.logger.sendTelemetryEvent({
|
|
3277
|
+
eventName: "SignalAddressNotFound",
|
|
3278
|
+
...tagCodeArtifacts({
|
|
3279
|
+
address,
|
|
3280
|
+
}),
|
|
3281
|
+
});
|
|
3208
3282
|
}
|
|
3209
3283
|
|
|
3210
3284
|
/**
|
|
@@ -3215,6 +3289,8 @@ export class ContainerRuntime
|
|
|
3215
3289
|
* @param resubmitInfo - If defined, indicates this is a resubmission of a batch with the given Batch info needed for resubmit.
|
|
3216
3290
|
*/
|
|
3217
3291
|
private flush(resubmitInfo?: BatchResubmitInfo): void {
|
|
3292
|
+
this.flushScheduled = false;
|
|
3293
|
+
|
|
3218
3294
|
try {
|
|
3219
3295
|
assert(
|
|
3220
3296
|
!this.batchRunner.running,
|
|
@@ -3239,7 +3315,6 @@ export class ContainerRuntime
|
|
|
3239
3315
|
*/
|
|
3240
3316
|
public orderSequentially<T>(callback: () => T): T {
|
|
3241
3317
|
let checkpoint: IBatchCheckpoint | undefined;
|
|
3242
|
-
const checkpointDirtyState = this.dirtyContainer;
|
|
3243
3318
|
// eslint-disable-next-line import/no-deprecated
|
|
3244
3319
|
let stageControls: StageControlsExperimental | undefined;
|
|
3245
3320
|
if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
|
|
@@ -3261,10 +3336,7 @@ export class ContainerRuntime
|
|
|
3261
3336
|
checkpoint.rollback((message: LocalBatchMessage) =>
|
|
3262
3337
|
this.rollback(message.runtimeOp, message.localOpMetadata),
|
|
3263
3338
|
);
|
|
3264
|
-
|
|
3265
|
-
if (this.dirtyContainer !== checkpointDirtyState) {
|
|
3266
|
-
this.updateDocumentDirtyState(checkpointDirtyState);
|
|
3267
|
-
}
|
|
3339
|
+
this.updateDocumentDirtyState();
|
|
3268
3340
|
stageControls?.discardChanges();
|
|
3269
3341
|
stageControls = undefined;
|
|
3270
3342
|
} catch (error_) {
|
|
@@ -3360,9 +3432,7 @@ export class ContainerRuntime
|
|
|
3360
3432
|
);
|
|
3361
3433
|
this.rollback(runtimeOp, localOpMetadata);
|
|
3362
3434
|
});
|
|
3363
|
-
|
|
3364
|
-
this.updateDocumentDirtyState(this.pendingMessagesCount !== 0);
|
|
3365
|
-
}
|
|
3435
|
+
this.updateDocumentDirtyState();
|
|
3366
3436
|
}),
|
|
3367
3437
|
commitChanges: (optionsParam) => {
|
|
3368
3438
|
const options = { ...defaultStagingCommitOptions, ...optionsParam };
|
|
@@ -3452,13 +3522,6 @@ export class ContainerRuntime
|
|
|
3452
3522
|
);
|
|
3453
3523
|
}
|
|
3454
3524
|
|
|
3455
|
-
/**
|
|
3456
|
-
* Typically ops are batched and later flushed together, but in some cases we want to flush immediately.
|
|
3457
|
-
*/
|
|
3458
|
-
private currentlyBatching(): boolean {
|
|
3459
|
-
return this.flushMode !== FlushMode.Immediate || this.batchRunner.running;
|
|
3460
|
-
}
|
|
3461
|
-
|
|
3462
3525
|
private readonly _quorum: IQuorumClients;
|
|
3463
3526
|
public getQuorum(): IQuorumClients {
|
|
3464
3527
|
return this._quorum;
|
|
@@ -3474,40 +3537,20 @@ export class ContainerRuntime
|
|
|
3474
3537
|
* either were not sent out to delta stream or were not yet acknowledged.
|
|
3475
3538
|
*/
|
|
3476
3539
|
public get isDirty(): boolean {
|
|
3477
|
-
|
|
3540
|
+
// Rather than recomputing the dirty state in this moment,
|
|
3541
|
+
// just regurgitate the last emitted dirty state.
|
|
3542
|
+
return this.lastEmittedDirty;
|
|
3478
3543
|
}
|
|
3479
3544
|
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
if (attachMessage.id === agentSchedulerId) {
|
|
3490
|
-
return false;
|
|
3491
|
-
}
|
|
3492
|
-
break;
|
|
3493
|
-
}
|
|
3494
|
-
case ContainerMessageType.FluidDataStoreOp: {
|
|
3495
|
-
const envelope = contents;
|
|
3496
|
-
if (envelope.address === agentSchedulerId) {
|
|
3497
|
-
return false;
|
|
3498
|
-
}
|
|
3499
|
-
break;
|
|
3500
|
-
}
|
|
3501
|
-
case ContainerMessageType.IdAllocation:
|
|
3502
|
-
case ContainerMessageType.DocumentSchemaChange:
|
|
3503
|
-
case ContainerMessageType.GC: {
|
|
3504
|
-
return false;
|
|
3505
|
-
}
|
|
3506
|
-
default: {
|
|
3507
|
-
break;
|
|
3508
|
-
}
|
|
3509
|
-
}
|
|
3510
|
-
return true;
|
|
3545
|
+
/**
|
|
3546
|
+
* Returns true if the container is dirty: not attached, or no pending user messages (could be some "non-dirtyable" ones though)
|
|
3547
|
+
*/
|
|
3548
|
+
private computeCurrentDirtyState(): boolean {
|
|
3549
|
+
return (
|
|
3550
|
+
this.attachState !== AttachState.Attached ||
|
|
3551
|
+
this.pendingStateManager.hasPendingUserChanges() ||
|
|
3552
|
+
this.outbox.containsUserChanges()
|
|
3553
|
+
);
|
|
3511
3554
|
}
|
|
3512
3555
|
|
|
3513
3556
|
/**
|
|
@@ -3525,9 +3568,6 @@ export class ContainerRuntime
|
|
|
3525
3568
|
public submitSignal(type: string, content: unknown, targetClientId?: string): void {
|
|
3526
3569
|
this.verifyNotClosed();
|
|
3527
3570
|
const envelope = createNewSignalEnvelope(undefined /* address */, type, content);
|
|
3528
|
-
if (targetClientId === undefined) {
|
|
3529
|
-
this.signalTelemetryManager.applyTrackingToBroadcastSignalEnvelope(envelope);
|
|
3530
|
-
}
|
|
3531
3571
|
this.submitSignalFn(envelope, targetClientId);
|
|
3532
3572
|
}
|
|
3533
3573
|
|
|
@@ -3545,9 +3585,7 @@ export class ContainerRuntime
|
|
|
3545
3585
|
this.emit("attached");
|
|
3546
3586
|
}
|
|
3547
3587
|
|
|
3548
|
-
|
|
3549
|
-
this.updateDocumentDirtyState(false);
|
|
3550
|
-
}
|
|
3588
|
+
this.updateDocumentDirtyState();
|
|
3551
3589
|
this.channelCollection.setAttachState(attachState);
|
|
3552
3590
|
}
|
|
3553
3591
|
|
|
@@ -4333,22 +4371,22 @@ export class ContainerRuntime
|
|
|
4333
4371
|
return this.pendingMessagesCount !== 0;
|
|
4334
4372
|
}
|
|
4335
4373
|
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4374
|
+
/**
|
|
4375
|
+
* Emit "dirty" or "saved" event based on the current dirty state of the document.
|
|
4376
|
+
* This must be called every time the states underlying the dirty state change.
|
|
4377
|
+
*
|
|
4378
|
+
* @privateRemarks - It's helpful to think of this as an event handler registered
|
|
4379
|
+
* for hypothetical "changed" events for PendingStateManager, Outbox, and Container Attach machinery.
|
|
4380
|
+
* But those events don't exist so we manually call this wherever we know those changes happen.
|
|
4381
|
+
*/
|
|
4382
|
+
private updateDocumentDirtyState(): void {
|
|
4383
|
+
const dirty: boolean = this.computeCurrentDirtyState();
|
|
4346
4384
|
|
|
4347
|
-
if (this.
|
|
4385
|
+
if (this.lastEmittedDirty === dirty) {
|
|
4348
4386
|
return;
|
|
4349
4387
|
}
|
|
4350
4388
|
|
|
4351
|
-
this.
|
|
4389
|
+
this.lastEmittedDirty = dirty;
|
|
4352
4390
|
if (this.emitDirtyDocumentEvent) {
|
|
4353
4391
|
this.emit(dirty ? "dirty" : "saved");
|
|
4354
4392
|
}
|
|
@@ -4486,13 +4524,7 @@ export class ContainerRuntime
|
|
|
4486
4524
|
this.outbox.submit(message);
|
|
4487
4525
|
}
|
|
4488
4526
|
|
|
4489
|
-
|
|
4490
|
-
const flushImmediatelyOnSubmit = !this.currentlyBatching();
|
|
4491
|
-
if (flushImmediatelyOnSubmit) {
|
|
4492
|
-
this.flush();
|
|
4493
|
-
} else {
|
|
4494
|
-
this.scheduleFlush();
|
|
4495
|
-
}
|
|
4527
|
+
this.scheduleFlush();
|
|
4496
4528
|
} catch (error) {
|
|
4497
4529
|
const dpe = DataProcessingError.wrapIfUnrecognized(error, "ContainerRuntime.submit", {
|
|
4498
4530
|
referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
|
|
@@ -4501,31 +4533,28 @@ export class ContainerRuntime
|
|
|
4501
4533
|
throw dpe;
|
|
4502
4534
|
}
|
|
4503
4535
|
|
|
4504
|
-
|
|
4505
|
-
this.updateDocumentDirtyState(true);
|
|
4506
|
-
}
|
|
4536
|
+
this.updateDocumentDirtyState();
|
|
4507
4537
|
}
|
|
4508
4538
|
|
|
4509
4539
|
private scheduleFlush(): void {
|
|
4510
|
-
if (this.
|
|
4540
|
+
if (this.flushScheduled) {
|
|
4511
4541
|
return;
|
|
4512
4542
|
}
|
|
4513
|
-
|
|
4514
|
-
this.flushTaskExists = true;
|
|
4515
|
-
|
|
4516
|
-
// TODO: hoist this out of the function scope to save unnecessary allocations
|
|
4517
|
-
// eslint-disable-next-line unicorn/consistent-function-scoping -- Separate `flush` method already exists in outer scope
|
|
4518
|
-
const flush = (): void => {
|
|
4519
|
-
this.flushTaskExists = false;
|
|
4520
|
-
this.flush();
|
|
4521
|
-
};
|
|
4543
|
+
this.flushScheduled = true;
|
|
4522
4544
|
|
|
4523
4545
|
switch (this.flushMode) {
|
|
4546
|
+
case FlushMode.Immediate: {
|
|
4547
|
+
// When in Immediate flush mode, flush immediately unless we are intentionally batching multiple ops (e.g. via orderSequentially)
|
|
4548
|
+
if (!this.batchRunner.running) {
|
|
4549
|
+
this.flush();
|
|
4550
|
+
}
|
|
4551
|
+
break;
|
|
4552
|
+
}
|
|
4524
4553
|
case FlushMode.TurnBased: {
|
|
4525
4554
|
// When in TurnBased flush mode the runtime will buffer operations in the current turn and send them as a single
|
|
4526
4555
|
// batch at the end of the turn
|
|
4527
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
4528
|
-
Promise.resolve().then(flush);
|
|
4556
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- Container will close if flush throws
|
|
4557
|
+
Promise.resolve().then(() => this.flush());
|
|
4529
4558
|
break;
|
|
4530
4559
|
}
|
|
4531
4560
|
|
|
@@ -4534,16 +4563,12 @@ export class ContainerRuntime
|
|
|
4534
4563
|
// When in Async flush mode, the runtime will accumulate all operations across JS turns and send them as a single
|
|
4535
4564
|
// batch when all micro-tasks are complete.
|
|
4536
4565
|
// Compared to TurnBased, this flush mode will capture more ops into the same batch.
|
|
4537
|
-
setTimeout(flush, 0);
|
|
4566
|
+
setTimeout(() => this.flush(), 0);
|
|
4538
4567
|
break;
|
|
4539
4568
|
}
|
|
4540
4569
|
|
|
4541
4570
|
default: {
|
|
4542
|
-
|
|
4543
|
-
this.batchRunner.running,
|
|
4544
|
-
0x587 /* Unreachable unless manually accumulating a batch */,
|
|
4545
|
-
);
|
|
4546
|
-
break;
|
|
4571
|
+
fail(0x587 /* Unreachable unless manually accumulating a batch */);
|
|
4547
4572
|
}
|
|
4548
4573
|
}
|
|
4549
4574
|
}
|
|
@@ -4921,6 +4946,59 @@ export class ContainerRuntime
|
|
|
4921
4946
|
}
|
|
4922
4947
|
}
|
|
4923
4948
|
|
|
4949
|
+
// While internal, ContainerRuntime has not been converted to use the new events support.
|
|
4950
|
+
// Recreate the required events (new pattern) with injected, wrapper new emitter.
|
|
4951
|
+
// It is lazily create to avoid listeners (old events) that ultimately go nowhere.
|
|
4952
|
+
private readonly lazyEventsForExtensions = new Lazy<Listenable<ExtensionHostEvents>>(() => {
|
|
4953
|
+
const eventEmitter = createEmitter<ExtensionHostEvents>();
|
|
4954
|
+
this.on("connected", (clientId) => eventEmitter.emit("connected", clientId));
|
|
4955
|
+
this.on("disconnected", () => eventEmitter.emit("disconnected"));
|
|
4956
|
+
return eventEmitter;
|
|
4957
|
+
});
|
|
4958
|
+
|
|
4959
|
+
private readonly submitExtensionSignal: <TMessage extends TypedMessage>(
|
|
4960
|
+
id: string,
|
|
4961
|
+
addressChain: string[],
|
|
4962
|
+
message: OutboundExtensionMessage<TMessage>,
|
|
4963
|
+
) => void;
|
|
4964
|
+
|
|
4965
|
+
public acquireExtension<
|
|
4966
|
+
T,
|
|
4967
|
+
TRuntimeProperties extends ExtensionRuntimeProperties,
|
|
4968
|
+
TUseContext extends unknown[],
|
|
4969
|
+
>(
|
|
4970
|
+
id: ContainerExtensionId,
|
|
4971
|
+
factory: ContainerExtensionFactory<T, TRuntimeProperties, TUseContext>,
|
|
4972
|
+
...useContext: TUseContext
|
|
4973
|
+
): T {
|
|
4974
|
+
let entry = this.extensions.get(id);
|
|
4975
|
+
if (entry === undefined) {
|
|
4976
|
+
const runtime = {
|
|
4977
|
+
isConnected: () => this.connected,
|
|
4978
|
+
getClientId: () => this.clientId,
|
|
4979
|
+
events: this.lazyEventsForExtensions.value,
|
|
4980
|
+
logger: this.baseLogger,
|
|
4981
|
+
submitAddressedSignal: (
|
|
4982
|
+
addressChain: string[],
|
|
4983
|
+
message: OutboundExtensionMessage<TRuntimeProperties["SignalMessages"]>,
|
|
4984
|
+
) => {
|
|
4985
|
+
this.submitExtensionSignal(id, addressChain, message);
|
|
4986
|
+
},
|
|
4987
|
+
getQuorum: this.getQuorum.bind(this),
|
|
4988
|
+
getAudience: this.getAudience.bind(this),
|
|
4989
|
+
} satisfies ExtensionHost<TRuntimeProperties>;
|
|
4990
|
+
entry = new factory(runtime, ...useContext);
|
|
4991
|
+
this.extensions.set(id, entry);
|
|
4992
|
+
} else {
|
|
4993
|
+
assert(
|
|
4994
|
+
entry instanceof factory,
|
|
4995
|
+
0xba1 /* Extension entry is not of the expected type */,
|
|
4996
|
+
);
|
|
4997
|
+
entry.extension.onNewUse(...useContext);
|
|
4998
|
+
}
|
|
4999
|
+
return entry.interface as T;
|
|
5000
|
+
}
|
|
5001
|
+
|
|
4924
5002
|
private get groupedBatchingEnabled(): boolean {
|
|
4925
5003
|
return this.sessionSchema.opGroupingEnabled === true;
|
|
4926
5004
|
}
|
|
@@ -4930,11 +5008,44 @@ export function createNewSignalEnvelope(
|
|
|
4930
5008
|
address: string | undefined,
|
|
4931
5009
|
type: string,
|
|
4932
5010
|
content: unknown,
|
|
4933
|
-
):
|
|
4934
|
-
const newEnvelope:
|
|
5011
|
+
): UnsequencedSignalEnvelope {
|
|
5012
|
+
const newEnvelope: UnsequencedSignalEnvelope = {
|
|
4935
5013
|
address,
|
|
4936
5014
|
contents: { type, content },
|
|
4937
5015
|
};
|
|
4938
5016
|
|
|
4939
5017
|
return newEnvelope;
|
|
4940
5018
|
}
|
|
5019
|
+
|
|
5020
|
+
export function isContainerMessageDirtyable({
|
|
5021
|
+
type,
|
|
5022
|
+
contents,
|
|
5023
|
+
}: LocalContainerRuntimeMessage): boolean {
|
|
5024
|
+
// Certain container runtime messages should not mark the container dirty such as the old built-in
|
|
5025
|
+
// AgentScheduler and Garbage collector messages.
|
|
5026
|
+
switch (type) {
|
|
5027
|
+
case ContainerMessageType.Attach: {
|
|
5028
|
+
const attachMessage = contents as InboundAttachMessage;
|
|
5029
|
+
if (attachMessage.id === agentSchedulerId) {
|
|
5030
|
+
return false;
|
|
5031
|
+
}
|
|
5032
|
+
break;
|
|
5033
|
+
}
|
|
5034
|
+
case ContainerMessageType.FluidDataStoreOp: {
|
|
5035
|
+
const envelope = contents;
|
|
5036
|
+
if (envelope.address === agentSchedulerId) {
|
|
5037
|
+
return false;
|
|
5038
|
+
}
|
|
5039
|
+
break;
|
|
5040
|
+
}
|
|
5041
|
+
case ContainerMessageType.IdAllocation:
|
|
5042
|
+
case ContainerMessageType.DocumentSchemaChange:
|
|
5043
|
+
case ContainerMessageType.GC: {
|
|
5044
|
+
return false;
|
|
5045
|
+
}
|
|
5046
|
+
default: {
|
|
5047
|
+
break;
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
5050
|
+
return true;
|
|
5051
|
+
}
|