@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/api-report/container-loader.legacy.alpha.api.md +13 -0
  3. package/dist/container.d.ts +7 -4
  4. package/dist/container.d.ts.map +1 -1
  5. package/dist/container.js +92 -16
  6. package/dist/container.js.map +1 -1
  7. package/dist/containerContext.d.ts +1 -0
  8. package/dist/containerContext.d.ts.map +1 -1
  9. package/dist/containerContext.js +1 -0
  10. package/dist/containerContext.js.map +1 -1
  11. package/dist/containerStorageAdapter.d.ts +1 -8
  12. package/dist/containerStorageAdapter.d.ts.map +1 -1
  13. package/dist/containerStorageAdapter.js +0 -9
  14. package/dist/containerStorageAdapter.js.map +1 -1
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/legacyAlpha.d.ts +1 -0
  20. package/dist/loaderLayerCompatState.d.ts +4 -3
  21. package/dist/loaderLayerCompatState.d.ts.map +1 -1
  22. package/dist/loaderLayerCompatState.js +4 -34
  23. package/dist/loaderLayerCompatState.js.map +1 -1
  24. package/dist/packageVersion.d.ts +1 -1
  25. package/dist/packageVersion.d.ts.map +1 -1
  26. package/dist/packageVersion.js +1 -1
  27. package/dist/packageVersion.js.map +1 -1
  28. package/dist/pendingLocalStateStore.d.ts +84 -0
  29. package/dist/pendingLocalStateStore.d.ts.map +1 -0
  30. package/dist/pendingLocalStateStore.js +157 -0
  31. package/dist/pendingLocalStateStore.js.map +1 -0
  32. package/dist/protocol.d.ts +1 -0
  33. package/dist/protocol.d.ts.map +1 -1
  34. package/dist/protocol.js +43 -1
  35. package/dist/protocol.js.map +1 -1
  36. package/dist/utils.d.ts +1 -0
  37. package/dist/utils.d.ts.map +1 -1
  38. package/dist/utils.js +0 -4
  39. package/dist/utils.js.map +1 -1
  40. package/lib/container.d.ts +7 -4
  41. package/lib/container.d.ts.map +1 -1
  42. package/lib/container.js +93 -17
  43. package/lib/container.js.map +1 -1
  44. package/lib/containerContext.d.ts +1 -0
  45. package/lib/containerContext.d.ts.map +1 -1
  46. package/lib/containerContext.js +1 -0
  47. package/lib/containerContext.js.map +1 -1
  48. package/lib/containerStorageAdapter.d.ts +1 -8
  49. package/lib/containerStorageAdapter.d.ts.map +1 -1
  50. package/lib/containerStorageAdapter.js +0 -9
  51. package/lib/containerStorageAdapter.js.map +1 -1
  52. package/lib/index.d.ts +1 -0
  53. package/lib/index.d.ts.map +1 -1
  54. package/lib/index.js +1 -0
  55. package/lib/index.js.map +1 -1
  56. package/lib/legacyAlpha.d.ts +1 -0
  57. package/lib/loaderLayerCompatState.d.ts +4 -3
  58. package/lib/loaderLayerCompatState.d.ts.map +1 -1
  59. package/lib/loaderLayerCompatState.js +5 -35
  60. package/lib/loaderLayerCompatState.js.map +1 -1
  61. package/lib/packageVersion.d.ts +1 -1
  62. package/lib/packageVersion.d.ts.map +1 -1
  63. package/lib/packageVersion.js +1 -1
  64. package/lib/packageVersion.js.map +1 -1
  65. package/lib/pendingLocalStateStore.d.ts +84 -0
  66. package/lib/pendingLocalStateStore.d.ts.map +1 -0
  67. package/lib/pendingLocalStateStore.js +153 -0
  68. package/lib/pendingLocalStateStore.js.map +1 -0
  69. package/lib/protocol.d.ts +1 -0
  70. package/lib/protocol.d.ts.map +1 -1
  71. package/lib/protocol.js +41 -0
  72. package/lib/protocol.js.map +1 -1
  73. package/lib/utils.d.ts +1 -0
  74. package/lib/utils.d.ts.map +1 -1
  75. package/lib/utils.js +0 -4
  76. package/lib/utils.js.map +1 -1
  77. package/package.json +11 -11
  78. package/src/container.ts +129 -31
  79. package/src/containerContext.ts +2 -0
  80. package/src/containerStorageAdapter.ts +1 -11
  81. package/src/index.ts +1 -0
  82. package/src/loaderLayerCompatState.ts +21 -36
  83. package/src/packageVersion.ts +1 -1
  84. package/src/pendingLocalStateStore.ts +160 -0
  85. package/src/protocol.ts +49 -0
  86. 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
- this.setContextConnectedState(false /* connected */, readonly);
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
- attributes: IDocumentAttributes,
833
- quorumSnapshot: IQuorumSnapshot,
834
- sendProposal: (key: string, value: unknown) => number,
835
- ): ProtocolHandlerInternal =>
836
- new ProtocolHandler(
837
- attributes,
838
- quorumSnapshot,
839
- sendProposal,
840
- new Audience(),
841
- (clientId: string) => this.clientsWhoShouldHaveLeft.has(clientId),
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.setContextConnectedState(
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(maybeRuntimeCompatDetails.ILayerCompatDetails);
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
- * Set the connected state of the ContainerContext
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 setContextConnectedState(connected: boolean, readonly: boolean): void {
2535
+ private setConnectionStatus(
2536
+ readonly: boolean,
2537
+ onlyCallSetConnectionStateIfConnectedOrDisconnected: boolean = false,
2538
+ ): void {
2510
2539
  if (this._runtime?.disposed === false && this.loaded) {
2511
- this.runtime.setConnectionState(
2512
- connected &&
2513
- !readonly /* container can send ops if connected to service and not in readonly mode */,
2514
- this.clientId,
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
 
@@ -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 { ISummaryHandle, ISummaryTree } from "@fluidframework/driver-definitions";
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
@@ -54,3 +54,4 @@ export type {
54
54
  QuorumClientsSnapshot,
55
55
  QuorumProposalsSnapshot,
56
56
  } from "./protocol/index.js";
57
+ export { PendingLocalStateStore } from "./pendingLocalStateStore.js";
@@ -3,13 +3,15 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import {
7
- checkLayerCompatibility,
8
- type ILayerCompatDetails,
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 { UsageError } from "@fluidframework/telemetry-utils/internal";
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
- const layerCheckResult = checkLayerCompatibility(
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
- const layerCheckResult = checkLayerCompatibility(
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
  }
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-loader";
9
- export const pkgVersion = "2.70.0-361248";
9
+ export const pkgVersion = "2.70.0";
@@ -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 {