@fluidframework/container-loader 2.70.0-361248 → 2.70.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/api-report/container-loader.legacy.alpha.api.md +13 -0
- package/dist/container.d.ts +7 -4
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +92 -16
- package/dist/container.js.map +1 -1
- package/dist/containerContext.d.ts +1 -0
- package/dist/containerContext.d.ts.map +1 -1
- package/dist/containerContext.js +1 -0
- package/dist/containerContext.js.map +1 -1
- package/dist/containerStorageAdapter.d.ts +1 -8
- package/dist/containerStorageAdapter.d.ts.map +1 -1
- package/dist/containerStorageAdapter.js +0 -9
- package/dist/containerStorageAdapter.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/legacyAlpha.d.ts +1 -0
- package/dist/loaderLayerCompatState.d.ts +4 -3
- package/dist/loaderLayerCompatState.d.ts.map +1 -1
- package/dist/loaderLayerCompatState.js +4 -34
- package/dist/loaderLayerCompatState.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/pendingLocalStateStore.d.ts +84 -0
- package/dist/pendingLocalStateStore.d.ts.map +1 -0
- package/dist/pendingLocalStateStore.js +157 -0
- package/dist/pendingLocalStateStore.js.map +1 -0
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +43 -1
- package/dist/protocol.js.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +0 -4
- package/dist/utils.js.map +1 -1
- package/lib/container.d.ts +7 -4
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +93 -17
- package/lib/container.js.map +1 -1
- package/lib/containerContext.d.ts +1 -0
- package/lib/containerContext.d.ts.map +1 -1
- package/lib/containerContext.js +1 -0
- package/lib/containerContext.js.map +1 -1
- package/lib/containerStorageAdapter.d.ts +1 -8
- package/lib/containerStorageAdapter.d.ts.map +1 -1
- package/lib/containerStorageAdapter.js +0 -9
- package/lib/containerStorageAdapter.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/legacyAlpha.d.ts +1 -0
- package/lib/loaderLayerCompatState.d.ts +4 -3
- package/lib/loaderLayerCompatState.d.ts.map +1 -1
- package/lib/loaderLayerCompatState.js +5 -35
- package/lib/loaderLayerCompatState.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/pendingLocalStateStore.d.ts +84 -0
- package/lib/pendingLocalStateStore.d.ts.map +1 -0
- package/lib/pendingLocalStateStore.js +153 -0
- package/lib/pendingLocalStateStore.js.map +1 -0
- package/lib/protocol.d.ts +1 -0
- package/lib/protocol.d.ts.map +1 -1
- package/lib/protocol.js +41 -0
- package/lib/protocol.js.map +1 -1
- package/lib/utils.d.ts +1 -0
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +0 -4
- package/lib/utils.js.map +1 -1
- package/package.json +11 -11
- package/src/container.ts +129 -31
- package/src/containerContext.ts +2 -0
- package/src/containerStorageAdapter.ts +1 -11
- package/src/index.ts +1 -0
- package/src/loaderLayerCompatState.ts +21 -36
- package/src/packageVersion.ts +1 -1
- package/src/pendingLocalStateStore.ts +160 -0
- package/src/protocol.ts +49 -0
- package/src/utils.ts +6 -0
package/src/container.ts
CHANGED
|
@@ -145,6 +145,7 @@ import {
|
|
|
145
145
|
type ProtocolHandlerBuilder,
|
|
146
146
|
type ProtocolHandlerInternal,
|
|
147
147
|
protocolHandlerShouldProcessSignal,
|
|
148
|
+
wrapProtocolHandlerBuilder,
|
|
148
149
|
} from "./protocol.js";
|
|
149
150
|
import { initQuorumValuesFromCodeDetails } from "./quorum.js";
|
|
150
151
|
import {
|
|
@@ -498,6 +499,7 @@ export class Container
|
|
|
498
499
|
private readonly subLogger: ITelemetryLoggerExt;
|
|
499
500
|
private readonly detachedBlobStorage: MemoryDetachedBlobStorage | undefined;
|
|
500
501
|
private readonly protocolHandlerBuilder: InternalProtocolHandlerBuilder;
|
|
502
|
+
private readonly signalAudience = new Audience();
|
|
501
503
|
private readonly client: IClient;
|
|
502
504
|
|
|
503
505
|
private readonly mc: MonitoringContext;
|
|
@@ -557,12 +559,19 @@ export class Container
|
|
|
557
559
|
// Ideally, we should supply pendingLocalState?.clientId here as well, not in constructor, but it does not matter (at least today)
|
|
558
560
|
this.connectionStateHandler.initProtocol(this.protocolHandler);
|
|
559
561
|
|
|
560
|
-
// Propagate current connection state through the system.
|
|
561
|
-
const readonly = this.readOnlyInfo.readonly ?? false;
|
|
562
562
|
// This call does not look like needed any more, with delaying all connection-related events past loaded phase.
|
|
563
563
|
// Yet, there could be some customer code that would break if we do not deliver it.
|
|
564
564
|
// Will be removed in further PRs with proper changeset.
|
|
565
|
-
|
|
565
|
+
const runtime = this._runtime;
|
|
566
|
+
if (
|
|
567
|
+
runtime !== undefined &&
|
|
568
|
+
// Check for older runtime that may need this call
|
|
569
|
+
!("setConnectionStatus" in runtime) &&
|
|
570
|
+
runtime.disposed === false
|
|
571
|
+
) {
|
|
572
|
+
runtime.setConnectionState(false /* canSendOps */, this.clientId);
|
|
573
|
+
}
|
|
574
|
+
|
|
566
575
|
// Deliver delayed calls to DeltaManager - we ignored "connect" events while loading.
|
|
567
576
|
const cm = this._deltaManager.connectionManager;
|
|
568
577
|
if (cm.connected) {
|
|
@@ -810,6 +819,7 @@ export class Container
|
|
|
810
819
|
validateDriverCompatibility(
|
|
811
820
|
maybeDriverCompatDetails.ILayerCompatDetails,
|
|
812
821
|
(error) => {} /* disposeFn */, // There is nothing to dispose here, so just ignore the error.
|
|
822
|
+
subLogger,
|
|
813
823
|
);
|
|
814
824
|
|
|
815
825
|
this.connectionTransitionTimes[ConnectionState.Disconnected] = performanceNow();
|
|
@@ -826,20 +836,22 @@ export class Container
|
|
|
826
836
|
// Tracking alternative ways to handle this in AB#4129.
|
|
827
837
|
this.options = { ...options };
|
|
828
838
|
this.scope = scope;
|
|
829
|
-
this.protocolHandlerBuilder =
|
|
839
|
+
this.protocolHandlerBuilder = wrapProtocolHandlerBuilder(
|
|
830
840
|
protocolHandlerBuilder ??
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
841
|
+
((
|
|
842
|
+
attributes: IDocumentAttributes,
|
|
843
|
+
quorumSnapshot: IQuorumSnapshot,
|
|
844
|
+
sendProposal: (key: string, value: unknown) => number,
|
|
845
|
+
): ProtocolHandlerInternal =>
|
|
846
|
+
new ProtocolHandler(
|
|
847
|
+
attributes,
|
|
848
|
+
quorumSnapshot,
|
|
849
|
+
sendProposal,
|
|
850
|
+
new Audience(),
|
|
851
|
+
(clientId: string) => this.clientsWhoShouldHaveLeft.has(clientId),
|
|
852
|
+
)),
|
|
853
|
+
this.signalAudience,
|
|
854
|
+
);
|
|
843
855
|
|
|
844
856
|
// Note that we capture the createProps here so we can replicate the creation call when we want to clone.
|
|
845
857
|
this.clone = async (
|
|
@@ -2117,10 +2129,7 @@ export class Container
|
|
|
2117
2129
|
|
|
2118
2130
|
deltaManager.on("readonly", (readonly) => {
|
|
2119
2131
|
if (this.loaded) {
|
|
2120
|
-
this.
|
|
2121
|
-
this.connectionState === ConnectionState.Connected,
|
|
2122
|
-
readonly,
|
|
2123
|
-
);
|
|
2132
|
+
this.setConnectionStatus(readonly);
|
|
2124
2133
|
}
|
|
2125
2134
|
this.emit("readonly", readonly);
|
|
2126
2135
|
});
|
|
@@ -2222,8 +2231,20 @@ export class Container
|
|
|
2222
2231
|
const clientId = this.connectionStateHandler.clientId;
|
|
2223
2232
|
assert(clientId !== undefined, 0x96e /* there has to be clientId */);
|
|
2224
2233
|
this.protocolHandler.audience.setCurrentClientId(clientId);
|
|
2234
|
+
this.signalAudience.setCurrentClientId(clientId);
|
|
2235
|
+
} else if (this.connectionState === ConnectionState.CatchingUp) {
|
|
2236
|
+
// Signal-based Audience does not wait for ops. So provide clientId
|
|
2237
|
+
// as soon as possible.
|
|
2238
|
+
const clientId = this.connectionStateHandler.pendingClientId;
|
|
2239
|
+
assert(clientId !== undefined, 0xc89 /* catching up without clientId */);
|
|
2240
|
+
this.signalAudience.setCurrentClientId(clientId);
|
|
2225
2241
|
}
|
|
2226
2242
|
|
|
2243
|
+
this.setConnectionStatus(
|
|
2244
|
+
/* readonly */ this.readOnlyInfo.readonly ?? false,
|
|
2245
|
+
/* onlyCallSetConnectionStateIfConnectedOrDisconnected */ true,
|
|
2246
|
+
);
|
|
2247
|
+
|
|
2227
2248
|
// We communicate only transitions to Connected & Disconnected states, skipping all other states.
|
|
2228
2249
|
// This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
|
|
2229
2250
|
if (
|
|
@@ -2235,7 +2256,6 @@ export class Container
|
|
|
2235
2256
|
|
|
2236
2257
|
// Both protocol and context should not be undefined if we got so far.
|
|
2237
2258
|
|
|
2238
|
-
this.setContextConnectedState(connected, this.readOnlyInfo.readonly ?? false);
|
|
2239
2259
|
this.protocolHandler.setConnectionState(connected, this.clientId);
|
|
2240
2260
|
raiseConnectedEvent(
|
|
2241
2261
|
this.mc.logger,
|
|
@@ -2448,6 +2468,7 @@ export class Container
|
|
|
2448
2468
|
storage: this.storageAdapter,
|
|
2449
2469
|
quorum: this.protocolHandler.quorum,
|
|
2450
2470
|
audience: this.protocolHandler.audience,
|
|
2471
|
+
signalAudience: this.signalAudience,
|
|
2451
2472
|
loader,
|
|
2452
2473
|
submitFn: (type, contents, batch, metadata) =>
|
|
2453
2474
|
this.submitContainerMessage(type, contents, batch, metadata),
|
|
@@ -2481,7 +2502,10 @@ export class Container
|
|
|
2481
2502
|
|
|
2482
2503
|
// Validate that the Runtime is compatible with this Loader.
|
|
2483
2504
|
const maybeRuntimeCompatDetails = runtime as FluidObject<ILayerCompatDetails>;
|
|
2484
|
-
validateRuntimeCompatibility(
|
|
2505
|
+
validateRuntimeCompatibility(
|
|
2506
|
+
maybeRuntimeCompatDetails.ILayerCompatDetails,
|
|
2507
|
+
this.mc.logger,
|
|
2508
|
+
);
|
|
2485
2509
|
|
|
2486
2510
|
this._runtime = runtime;
|
|
2487
2511
|
this._lifecycleEvents.emit("runtimeInstantiated");
|
|
@@ -2501,18 +2525,92 @@ export class Container
|
|
|
2501
2525
|
};
|
|
2502
2526
|
|
|
2503
2527
|
/**
|
|
2504
|
-
*
|
|
2505
|
-
* This controls the "connected" state of the ContainerRuntime as well
|
|
2506
|
-
* @param connected - Is the container currently connected?
|
|
2528
|
+
* Send the connected status to the runtime.
|
|
2507
2529
|
* @param readonly - Is the container in readonly mode?
|
|
2530
|
+
* @param onlyCallSetConnectionStateIfConnectedOrDisconnected - If true, only
|
|
2531
|
+
* call older `setConnectionState` on the runtime if the connection state is
|
|
2532
|
+
* either Connected or Disconnected. This exists to preserve older behavior
|
|
2533
|
+
* where the runtime was only notified of these two states.
|
|
2508
2534
|
*/
|
|
2509
|
-
private
|
|
2535
|
+
private setConnectionStatus(
|
|
2536
|
+
readonly: boolean,
|
|
2537
|
+
onlyCallSetConnectionStateIfConnectedOrDisconnected: boolean = false,
|
|
2538
|
+
): void {
|
|
2510
2539
|
if (this._runtime?.disposed === false && this.loaded) {
|
|
2511
|
-
this.runtime.
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2540
|
+
const setConnectionStatus = this.runtime.setConnectionStatus?.bind(this.runtime);
|
|
2541
|
+
if (setConnectionStatus === undefined) {
|
|
2542
|
+
if (
|
|
2543
|
+
!onlyCallSetConnectionStateIfConnectedOrDisconnected ||
|
|
2544
|
+
this.connectionState === ConnectionState.Connected ||
|
|
2545
|
+
this.connectionState === ConnectionState.Disconnected
|
|
2546
|
+
) {
|
|
2547
|
+
this.runtime.setConnectionState(
|
|
2548
|
+
this.connectionState === ConnectionState.Connected &&
|
|
2549
|
+
!readonly /* container can send ops if connected to service and not in readonly mode */,
|
|
2550
|
+
this.clientId,
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
} else {
|
|
2554
|
+
const pendingClientConnectionId = this.connectionStateHandler.pendingClientId;
|
|
2555
|
+
const connectionState = this.connectionState;
|
|
2556
|
+
switch (connectionState) {
|
|
2557
|
+
case ConnectionState.EstablishingConnection: {
|
|
2558
|
+
setConnectionStatus({
|
|
2559
|
+
connectionState,
|
|
2560
|
+
canSendOps: false,
|
|
2561
|
+
readonly,
|
|
2562
|
+
});
|
|
2563
|
+
|
|
2564
|
+
break;
|
|
2565
|
+
}
|
|
2566
|
+
case ConnectionState.CatchingUp: {
|
|
2567
|
+
// When catching up, we have a pending clientId, but it
|
|
2568
|
+
// is not usable for ops. Send clientId with canSendOps false.
|
|
2569
|
+
assert(
|
|
2570
|
+
pendingClientConnectionId !== undefined,
|
|
2571
|
+
0xc8a /* catching up without clientId */,
|
|
2572
|
+
);
|
|
2573
|
+
setConnectionStatus({
|
|
2574
|
+
connectionState,
|
|
2575
|
+
pendingClientConnectionId,
|
|
2576
|
+
canSendOps: false,
|
|
2577
|
+
readonly,
|
|
2578
|
+
});
|
|
2579
|
+
|
|
2580
|
+
break;
|
|
2581
|
+
}
|
|
2582
|
+
case ConnectionState.Connected: {
|
|
2583
|
+
// When connected, we have an active clientId. Pass it along
|
|
2584
|
+
// with canSendOps true/false based on readonly.
|
|
2585
|
+
const clientConnectionId = this.clientId;
|
|
2586
|
+
assert(clientConnectionId !== undefined, 0xc8b /* connected without clientId */);
|
|
2587
|
+
assert(
|
|
2588
|
+
clientConnectionId === pendingClientConnectionId,
|
|
2589
|
+
0xc8c /* connected with different clientId than pending */,
|
|
2590
|
+
);
|
|
2591
|
+
setConnectionStatus({
|
|
2592
|
+
connectionState,
|
|
2593
|
+
clientConnectionId,
|
|
2594
|
+
canSendOps: !readonly,
|
|
2595
|
+
readonly,
|
|
2596
|
+
});
|
|
2597
|
+
|
|
2598
|
+
break;
|
|
2599
|
+
}
|
|
2600
|
+
case ConnectionState.Disconnected: {
|
|
2601
|
+
setConnectionStatus({
|
|
2602
|
+
connectionState,
|
|
2603
|
+
priorPendingClientConnectionId: pendingClientConnectionId,
|
|
2604
|
+
priorConnectedClientConnectionId: this.clientId,
|
|
2605
|
+
canSendOps: false,
|
|
2606
|
+
readonly,
|
|
2607
|
+
});
|
|
2608
|
+
|
|
2609
|
+
break;
|
|
2610
|
+
}
|
|
2611
|
+
// No default
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2516
2614
|
}
|
|
2517
2615
|
}
|
|
2518
2616
|
|
package/src/containerContext.ts
CHANGED
|
@@ -102,6 +102,7 @@ export class ContainerContext
|
|
|
102
102
|
public readonly storage: IContainerStorageService;
|
|
103
103
|
public readonly quorum: IQuorumClients;
|
|
104
104
|
public readonly audience: IAudience;
|
|
105
|
+
public readonly signalAudience: IAudience;
|
|
105
106
|
public readonly loader: ILoader;
|
|
106
107
|
public readonly submitFn: (
|
|
107
108
|
type: MessageType,
|
|
@@ -182,6 +183,7 @@ export class ContainerContext
|
|
|
182
183
|
this.storage = config.storage;
|
|
183
184
|
this.quorum = config.quorum;
|
|
184
185
|
this.audience = config.audience;
|
|
186
|
+
this.signalAudience = config.signalAudience;
|
|
185
187
|
this.loader = config.loader;
|
|
186
188
|
this.submitFn = config.submitFn;
|
|
187
189
|
this.submitSummaryFn = config.submitSummaryFn;
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
} from "@fluidframework/container-definitions/internal";
|
|
11
11
|
import type { IDisposable } from "@fluidframework/core-interfaces";
|
|
12
12
|
import { assert } from "@fluidframework/core-utils/internal";
|
|
13
|
-
import type {
|
|
13
|
+
import type { ISummaryTree } from "@fluidframework/driver-definitions";
|
|
14
14
|
import type {
|
|
15
15
|
FetchSource,
|
|
16
16
|
IDocumentService,
|
|
@@ -247,16 +247,6 @@ export class ContainerStorageAdapter
|
|
|
247
247
|
public async createBlob(file: ArrayBufferLike): Promise<ICreateBlobResponse> {
|
|
248
248
|
return this._storageService.createBlob(file);
|
|
249
249
|
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* {@link IRuntimeStorageService.downloadSummary}.
|
|
253
|
-
*
|
|
254
|
-
* @deprecated This API is deprecated and will be removed in a future release. No replacement is planned as
|
|
255
|
-
* it is unused in the Runtime and below layers.
|
|
256
|
-
*/
|
|
257
|
-
public async downloadSummary(handle: ISummaryHandle): Promise<ISummaryTree> {
|
|
258
|
-
return this._storageService.downloadSummary(handle);
|
|
259
|
-
}
|
|
260
250
|
}
|
|
261
251
|
|
|
262
252
|
/**
|
package/src/index.ts
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type ILayerCompatSupportRequirements,
|
|
6
|
+
import type {
|
|
7
|
+
ILayerCompatDetails,
|
|
8
|
+
ILayerCompatSupportRequirements,
|
|
10
9
|
} from "@fluid-internal/client-utils";
|
|
11
10
|
import type { ICriticalContainerError } from "@fluidframework/container-definitions";
|
|
12
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
validateLayerCompatibility,
|
|
13
|
+
type ITelemetryLoggerExt,
|
|
14
|
+
} from "@fluidframework/telemetry-utils/internal";
|
|
13
15
|
|
|
14
16
|
import { pkgVersion } from "./packageVersion.js";
|
|
15
17
|
|
|
@@ -78,25 +80,17 @@ export const driverSupportRequirementsForLoader: ILayerCompatSupportRequirements
|
|
|
78
80
|
*/
|
|
79
81
|
export function validateRuntimeCompatibility(
|
|
80
82
|
maybeRuntimeCompatDetails: ILayerCompatDetails | undefined,
|
|
83
|
+
logger: ITelemetryLoggerExt,
|
|
81
84
|
): void {
|
|
82
|
-
|
|
85
|
+
validateLayerCompatibility(
|
|
86
|
+
"loader",
|
|
87
|
+
"runtime",
|
|
88
|
+
loaderCompatDetailsForRuntime,
|
|
83
89
|
runtimeSupportRequirementsForLoader,
|
|
84
90
|
maybeRuntimeCompatDetails,
|
|
91
|
+
() => {} /* disposeFn - no op. This will be handled by the caller */,
|
|
92
|
+
logger,
|
|
85
93
|
);
|
|
86
|
-
if (!layerCheckResult.isCompatible) {
|
|
87
|
-
const error = new UsageError("Loader is not compatible with Runtime", {
|
|
88
|
-
errorDetails: JSON.stringify({
|
|
89
|
-
loaderVersion: loaderCompatDetailsForRuntime.pkgVersion,
|
|
90
|
-
runtimeVersion: maybeRuntimeCompatDetails?.pkgVersion,
|
|
91
|
-
loaderGeneration: loaderCompatDetailsForRuntime.generation,
|
|
92
|
-
runtimeGeneration: maybeRuntimeCompatDetails?.generation,
|
|
93
|
-
minSupportedGeneration: runtimeSupportRequirementsForLoader.minSupportedGeneration,
|
|
94
|
-
isGenerationCompatible: layerCheckResult.isGenerationCompatible,
|
|
95
|
-
unsupportedFeatures: layerCheckResult.unsupportedFeatures,
|
|
96
|
-
}),
|
|
97
|
-
});
|
|
98
|
-
throw error;
|
|
99
|
-
}
|
|
100
94
|
}
|
|
101
95
|
|
|
102
96
|
/**
|
|
@@ -106,24 +100,15 @@ export function validateRuntimeCompatibility(
|
|
|
106
100
|
export function validateDriverCompatibility(
|
|
107
101
|
maybeDriverCompatDetails: ILayerCompatDetails | undefined,
|
|
108
102
|
disposeFn: (error?: ICriticalContainerError) => void,
|
|
103
|
+
logger: ITelemetryLoggerExt,
|
|
109
104
|
): void {
|
|
110
|
-
|
|
105
|
+
validateLayerCompatibility(
|
|
106
|
+
"loader",
|
|
107
|
+
"driver",
|
|
108
|
+
loaderCompatDetailsForRuntime,
|
|
111
109
|
driverSupportRequirementsForLoader,
|
|
112
110
|
maybeDriverCompatDetails,
|
|
111
|
+
disposeFn,
|
|
112
|
+
logger,
|
|
113
113
|
);
|
|
114
|
-
if (!layerCheckResult.isCompatible) {
|
|
115
|
-
const error = new UsageError("Loader is not compatible with Driver", {
|
|
116
|
-
errorDetails: JSON.stringify({
|
|
117
|
-
loaderVersion: loaderCoreCompatDetails.pkgVersion,
|
|
118
|
-
driverVersion: maybeDriverCompatDetails?.pkgVersion,
|
|
119
|
-
loaderGeneration: loaderCoreCompatDetails.generation,
|
|
120
|
-
driverGeneration: maybeDriverCompatDetails?.generation,
|
|
121
|
-
minSupportedGeneration: driverSupportRequirementsForLoader.minSupportedGeneration,
|
|
122
|
-
isGenerationCompatible: layerCheckResult.isGenerationCompatible,
|
|
123
|
-
unsupportedFeatures: layerCheckResult.unsupportedFeatures,
|
|
124
|
-
}),
|
|
125
|
-
});
|
|
126
|
-
disposeFn(error);
|
|
127
|
-
throw error;
|
|
128
|
-
}
|
|
129
114
|
}
|
package/src/packageVersion.ts
CHANGED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ISequencedDocumentMessage } from "@fluidframework/driver-definitions/internal";
|
|
7
|
+
import { UsageError } from "@fluidframework/telemetry-utils/internal";
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
IPendingContainerState,
|
|
11
|
+
SerializedSnapshotInfo,
|
|
12
|
+
} from "./serializedStateManager.js";
|
|
13
|
+
import { getAttachedContainerStateFromSerializedContainer } from "./utils.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A Map-like store for managing pending local container states from attached containers.
|
|
17
|
+
* Optimizes storage by deduplicating shared resources across stored states.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const store = new PendingLocalStateStore<string>();
|
|
22
|
+
*
|
|
23
|
+
* // Store pending state
|
|
24
|
+
* const pendingState = await attachedContainer.getPendingLocalState();
|
|
25
|
+
* store.set("session1", pendingState);
|
|
26
|
+
*
|
|
27
|
+
* // Load from stored state
|
|
28
|
+
* const restored = store.get("session1");
|
|
29
|
+
* const newContainer = await loadFrozenContainerFromPendingState({
|
|
30
|
+
* pendingLocalState: restored,
|
|
31
|
+
* // ... other loader options
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Only use with attached containers from the same URL. Only store strings
|
|
37
|
+
* returned by `container.getPendingLocalState()`.
|
|
38
|
+
*
|
|
39
|
+
* @typeParam TKey - The type of keys used to identify stored states
|
|
40
|
+
*
|
|
41
|
+
* @legacy @alpha
|
|
42
|
+
*/
|
|
43
|
+
export class PendingLocalStateStore<TKey> {
|
|
44
|
+
#firstUrl: string | undefined;
|
|
45
|
+
readonly #pendingStates = new Map<TKey, IPendingContainerState>();
|
|
46
|
+
readonly #savedOps: Record<number, ISequencedDocumentMessage> = {};
|
|
47
|
+
readonly #blobs: Record<string, string> = {};
|
|
48
|
+
readonly #loadingGroups: Record<string, SerializedSnapshotInfo> = {};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Removes all stored pending states.
|
|
52
|
+
*/
|
|
53
|
+
clear(): void {
|
|
54
|
+
return this.#pendingStates.clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Removes the pending state for the specified key.
|
|
59
|
+
*
|
|
60
|
+
* @param key - The key to remove
|
|
61
|
+
* @returns `true` if the state existed and was removed, `false` otherwise
|
|
62
|
+
*/
|
|
63
|
+
delete(key: TKey): boolean {
|
|
64
|
+
return this.#pendingStates.delete(key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Retrieves the serialized pending state for the specified key.
|
|
69
|
+
*
|
|
70
|
+
* @param key - The key to retrieve
|
|
71
|
+
* @returns The serialized state as a JSON string, or `undefined` if not found
|
|
72
|
+
*/
|
|
73
|
+
get(key: TKey): string | undefined {
|
|
74
|
+
return JSON.stringify(this.#pendingStates.get(key));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Checks whether a pending state exists for the specified key.
|
|
79
|
+
*/
|
|
80
|
+
has(key: TKey): boolean {
|
|
81
|
+
return this.#pendingStates.has(key);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Stores a pending state from `container.getPendingLocalState()`.
|
|
86
|
+
*
|
|
87
|
+
* @param key - The key to associate with the state
|
|
88
|
+
* @param pendingLocalState - String returned by `getPendingLocalState()` from an attached container
|
|
89
|
+
* @returns This store instance for method chaining
|
|
90
|
+
*
|
|
91
|
+
* @throws When storing states from different container URLs
|
|
92
|
+
*/
|
|
93
|
+
set(key: TKey, pendingLocalState: string): this {
|
|
94
|
+
const state = getAttachedContainerStateFromSerializedContainer(pendingLocalState);
|
|
95
|
+
const { savedOps, snapshotBlobs, loadedGroupIdSnapshots, url } = state;
|
|
96
|
+
|
|
97
|
+
this.#firstUrl ??= url;
|
|
98
|
+
if (this.#firstUrl !== url) {
|
|
99
|
+
throw new UsageError("PendingLocalStateStore can only be used with a single container.");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < savedOps.length; i++) {
|
|
103
|
+
savedOps[i] = this.#savedOps[savedOps[i].sequenceNumber] ??= savedOps[i];
|
|
104
|
+
}
|
|
105
|
+
for (const [id, blob] of Object.entries(snapshotBlobs)) {
|
|
106
|
+
snapshotBlobs[id] = this.#blobs[id] ??= blob;
|
|
107
|
+
}
|
|
108
|
+
if (loadedGroupIdSnapshots !== undefined) {
|
|
109
|
+
for (const [id, lg] of Object.entries(loadedGroupIdSnapshots)) {
|
|
110
|
+
if (
|
|
111
|
+
this.#loadingGroups[id] === undefined ||
|
|
112
|
+
lg.snapshotSequenceNumber < this.#loadingGroups[id].snapshotSequenceNumber
|
|
113
|
+
) {
|
|
114
|
+
loadedGroupIdSnapshots[id] = this.#loadingGroups[id] = lg;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.#pendingStates.set(key, state);
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Gets the number of stored pending states.
|
|
125
|
+
*/
|
|
126
|
+
get size(): number {
|
|
127
|
+
return this.#pendingStates.size;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Returns an iterator over [key, serializedState] pairs.
|
|
132
|
+
*/
|
|
133
|
+
entries(): Iterator<[TKey, string]> {
|
|
134
|
+
const iterator = this.#pendingStates.entries();
|
|
135
|
+
return {
|
|
136
|
+
next: (): IteratorResult<[TKey, string]> => {
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
138
|
+
const { done, value } = iterator.next();
|
|
139
|
+
if (done === true) {
|
|
140
|
+
return { done, value: undefined };
|
|
141
|
+
}
|
|
142
|
+
return { done, value: [value[0], JSON.stringify(value[1])] };
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Returns an iterator over the stored keys.
|
|
149
|
+
*/
|
|
150
|
+
keys(): IterableIterator<TKey> {
|
|
151
|
+
return this.#pendingStates.keys();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Makes the store iterable with `for...of` loops.
|
|
156
|
+
*/
|
|
157
|
+
[Symbol.iterator](): Iterator<[TKey, string]> {
|
|
158
|
+
return this.entries();
|
|
159
|
+
}
|
|
160
|
+
}
|
package/src/protocol.ts
CHANGED
|
@@ -213,3 +213,52 @@ export function protocolHandlerShouldProcessSignal(
|
|
|
213
213
|
}
|
|
214
214
|
return false;
|
|
215
215
|
}
|
|
216
|
+
|
|
217
|
+
export function wrapProtocolHandlerBuilder(
|
|
218
|
+
builder: ProtocolHandlerBuilder,
|
|
219
|
+
signalAudience: IAudienceOwner,
|
|
220
|
+
): InternalProtocolHandlerBuilder {
|
|
221
|
+
return (
|
|
222
|
+
attributes: IDocumentAttributes,
|
|
223
|
+
snapshot: IQuorumSnapshot,
|
|
224
|
+
sendProposal: (key: string, value: unknown) => number,
|
|
225
|
+
): ProtocolHandlerInternal => {
|
|
226
|
+
const baseHandler = builder(attributes, snapshot, sendProposal);
|
|
227
|
+
// Create proxy handler with an overridden processSignal method.
|
|
228
|
+
// Use a Proxy since base may use [dynamic] property getters.
|
|
229
|
+
return new Proxy(baseHandler, {
|
|
230
|
+
get(target, prop, receiver) {
|
|
231
|
+
if (prop === "processSignal") {
|
|
232
|
+
return (message: AudienceSignal) => {
|
|
233
|
+
const innerContent = message.content;
|
|
234
|
+
switch (innerContent.type) {
|
|
235
|
+
case SignalType.Clear: {
|
|
236
|
+
const members = signalAudience.getMembers();
|
|
237
|
+
for (const clientId of members.keys()) {
|
|
238
|
+
signalAudience.removeMember(clientId);
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case SignalType.ClientJoin: {
|
|
243
|
+
const newClient = innerContent.content;
|
|
244
|
+
signalAudience.addMember(newClient.clientId, newClient.client);
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case SignalType.ClientLeave: {
|
|
248
|
+
const leftClientId = innerContent.content;
|
|
249
|
+
signalAudience.removeMember(leftClientId);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
default: {
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
target.processSignal(message);
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
260
|
+
return Reflect.get(target, prop, receiver);
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -380,6 +380,12 @@ export function getDetachedContainerStateFromSerializedContainer(
|
|
|
380
380
|
* Blindly parses the given string into {@link IPendingContainerState} format.
|
|
381
381
|
* This is the inverse of the JSON.stringify call in {@link SerializedStateManager.getPendingLocalState}
|
|
382
382
|
*/
|
|
383
|
+
export function getAttachedContainerStateFromSerializedContainer(
|
|
384
|
+
serializedContainer: string,
|
|
385
|
+
): IPendingContainerState;
|
|
386
|
+
export function getAttachedContainerStateFromSerializedContainer(
|
|
387
|
+
serializedContainer: string | undefined,
|
|
388
|
+
): IPendingContainerState | undefined;
|
|
383
389
|
export function getAttachedContainerStateFromSerializedContainer(
|
|
384
390
|
serializedContainer: string | undefined,
|
|
385
391
|
): IPendingContainerState | undefined {
|