@fluidframework/container-loader 2.63.0-358419 → 2.63.0-359286

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 (82) hide show
  1. package/api-report/container-loader.legacy.alpha.api.md +1 -9
  2. package/dist/connectionManager.d.ts.map +1 -1
  3. package/dist/connectionManager.js +64 -49
  4. package/dist/connectionManager.js.map +1 -1
  5. package/dist/container.d.ts.map +1 -1
  6. package/dist/container.js +3 -5
  7. package/dist/container.js.map +1 -1
  8. package/dist/containerContext.d.ts +10 -22
  9. package/dist/containerContext.d.ts.map +1 -1
  10. package/dist/containerContext.js +3 -1
  11. package/dist/containerContext.js.map +1 -1
  12. package/dist/contracts.d.ts +5 -1
  13. package/dist/contracts.d.ts.map +1 -1
  14. package/dist/contracts.js.map +1 -1
  15. package/dist/createAndLoadContainerUtils.d.ts +1 -33
  16. package/dist/createAndLoadContainerUtils.d.ts.map +1 -1
  17. package/dist/createAndLoadContainerUtils.js +1 -1
  18. package/dist/createAndLoadContainerUtils.js.map +1 -1
  19. package/dist/deltaManager.d.ts.map +1 -1
  20. package/dist/deltaManager.js +56 -55
  21. package/dist/deltaManager.js.map +1 -1
  22. package/dist/frozenServices.d.ts +2 -0
  23. package/dist/frozenServices.d.ts.map +1 -1
  24. package/dist/frozenServices.js +20 -12
  25. package/dist/frozenServices.js.map +1 -1
  26. package/dist/packageVersion.d.ts +1 -1
  27. package/dist/packageVersion.js +1 -1
  28. package/dist/packageVersion.js.map +1 -1
  29. package/dist/protocol.d.ts +51 -8
  30. package/dist/protocol.d.ts.map +1 -1
  31. package/dist/protocol.js +11 -12
  32. package/dist/protocol.js.map +1 -1
  33. package/dist/serializedStateManager.d.ts +4 -3
  34. package/dist/serializedStateManager.d.ts.map +1 -1
  35. package/dist/serializedStateManager.js +63 -23
  36. package/dist/serializedStateManager.js.map +1 -1
  37. package/lib/connectionManager.d.ts.map +1 -1
  38. package/lib/connectionManager.js +18 -3
  39. package/lib/connectionManager.js.map +1 -1
  40. package/lib/container.d.ts.map +1 -1
  41. package/lib/container.js +3 -5
  42. package/lib/container.js.map +1 -1
  43. package/lib/containerContext.d.ts +10 -22
  44. package/lib/containerContext.d.ts.map +1 -1
  45. package/lib/containerContext.js +3 -1
  46. package/lib/containerContext.js.map +1 -1
  47. package/lib/contracts.d.ts +5 -1
  48. package/lib/contracts.d.ts.map +1 -1
  49. package/lib/contracts.js.map +1 -1
  50. package/lib/createAndLoadContainerUtils.d.ts +1 -33
  51. package/lib/createAndLoadContainerUtils.d.ts.map +1 -1
  52. package/lib/createAndLoadContainerUtils.js +1 -1
  53. package/lib/createAndLoadContainerUtils.js.map +1 -1
  54. package/lib/deltaManager.d.ts.map +1 -1
  55. package/lib/deltaManager.js +2 -1
  56. package/lib/deltaManager.js.map +1 -1
  57. package/lib/frozenServices.d.ts +2 -0
  58. package/lib/frozenServices.d.ts.map +1 -1
  59. package/lib/frozenServices.js +20 -12
  60. package/lib/frozenServices.js.map +1 -1
  61. package/lib/packageVersion.d.ts +1 -1
  62. package/lib/packageVersion.js +1 -1
  63. package/lib/packageVersion.js.map +1 -1
  64. package/lib/protocol.d.ts +51 -8
  65. package/lib/protocol.d.ts.map +1 -1
  66. package/lib/protocol.js +5 -6
  67. package/lib/protocol.js.map +1 -1
  68. package/lib/serializedStateManager.d.ts +4 -3
  69. package/lib/serializedStateManager.d.ts.map +1 -1
  70. package/lib/serializedStateManager.js +63 -23
  71. package/lib/serializedStateManager.js.map +1 -1
  72. package/package.json +12 -12
  73. package/src/connectionManager.ts +31 -14
  74. package/src/container.ts +9 -13
  75. package/src/containerContext.ts +34 -34
  76. package/src/contracts.ts +4 -1
  77. package/src/createAndLoadContainerUtils.ts +3 -42
  78. package/src/deltaManager.ts +12 -5
  79. package/src/frozenServices.ts +24 -12
  80. package/src/packageVersion.ts +1 -1
  81. package/src/protocol.ts +67 -10
  82. package/src/serializedStateManager.ts +60 -29
package/src/container.ts CHANGED
@@ -140,9 +140,10 @@ import { NoopHeuristic } from "./noopHeuristic.js";
140
140
  import { pkgVersion } from "./packageVersion.js";
141
141
  import type { IQuorumSnapshot } from "./protocol/index.js";
142
142
  import {
143
- type IProtocolHandler,
143
+ type InternalProtocolHandlerBuilder,
144
144
  ProtocolHandler,
145
145
  type ProtocolHandlerBuilder,
146
+ type ProtocolHandlerInternal,
146
147
  protocolHandlerShouldProcessSignal,
147
148
  } from "./protocol.js";
148
149
  import { initQuorumValuesFromCodeDetails } from "./quorum.js";
@@ -495,7 +496,7 @@ export class Container
495
496
  private readonly scope: FluidObject;
496
497
  private readonly subLogger: ITelemetryLoggerExt;
497
498
  private readonly detachedBlobStorage: MemoryDetachedBlobStorage | undefined;
498
- private readonly protocolHandlerBuilder: ProtocolHandlerBuilder;
499
+ private readonly protocolHandlerBuilder: InternalProtocolHandlerBuilder;
499
500
  private readonly client: IClient;
500
501
 
501
502
  private readonly mc: MonitoringContext;
@@ -597,8 +598,8 @@ export class Container
597
598
  }
598
599
  return this._runtime;
599
600
  }
600
- private _protocolHandler: IProtocolHandler | undefined;
601
- private get protocolHandler(): IProtocolHandler {
601
+ private _protocolHandler: ProtocolHandlerInternal | undefined;
602
+ private get protocolHandler(): ProtocolHandlerInternal {
602
603
  if (this._protocolHandler === undefined) {
603
604
  throw new Error("Attempted to access protocolHandler before it was defined");
604
605
  }
@@ -830,7 +831,7 @@ export class Container
830
831
  attributes: IDocumentAttributes,
831
832
  quorumSnapshot: IQuorumSnapshot,
832
833
  sendProposal: (key: string, value: unknown) => number,
833
- ): ProtocolHandler =>
834
+ ): ProtocolHandlerInternal =>
834
835
  new ProtocolHandler(
835
836
  attributes,
836
837
  quorumSnapshot,
@@ -1641,10 +1642,9 @@ export class Container
1641
1642
  const timings: Record<string, number> = { phase1: performanceNow() };
1642
1643
  this.service = await this.createDocumentService(resolvedUrl, { mode: "load" });
1643
1644
 
1644
- // Except in cases where it has stashed ops or requested by feature gate, the container will connect in "read" mode
1645
+ // Except in cases where its requested by feature gate, the container will connect in "read" mode
1645
1646
  const mode =
1646
- this.mc.config.getBoolean("Fluid.Container.ForceWriteConnection") === true ||
1647
- (pendingLocalState?.savedOps.length ?? 0) > 0
1647
+ this.mc.config.getBoolean("Fluid.Container.ForceWriteConnection") === true
1648
1648
  ? "write"
1649
1649
  : "read";
1650
1650
  const connectionArgs: IConnectionArgs = {
@@ -1668,14 +1668,10 @@ export class Container
1668
1668
  timings.phase2 = performanceNow();
1669
1669
 
1670
1670
  // Fetch specified snapshot.
1671
- const { baseSnapshot, version } =
1671
+ const { baseSnapshot, version, attributes } =
1672
1672
  await this.serializedStateManager.fetchSnapshot(specifiedVersion);
1673
1673
  const baseSnapshotTree: ISnapshotTree | undefined = getSnapshotTree(baseSnapshot);
1674
1674
  this._loadedFromVersion = version;
1675
- const attributes: IDocumentAttributes = await getDocumentAttributes(
1676
- this.storageAdapter,
1677
- baseSnapshotTree,
1678
- );
1679
1675
 
1680
1676
  // If we saved ops, we will replay them and don't need DeltaManager to fetch them
1681
1677
  const lastProcessedSequenceNumber =
@@ -39,52 +39,50 @@ import { loaderCompatDetailsForRuntime } from "./loaderLayerCompatState.js";
39
39
 
40
40
  /**
41
41
  * Configuration object for ContainerContext constructor.
42
+ *
43
+ * @remarks
44
+ * A large subset of properties are from {@link IContainerContext}. Select
45
+ * properties are explicitly omitted so that, as {@link ContainerContext} is
46
+ * extended, newly added properties are not overlooked (but may also be
47
+ * explicitly omitted here by adding to the list).
42
48
  */
43
- export interface IContainerContextConfig {
49
+ export interface IContainerContextConfig
50
+ extends Readonly<
51
+ Required<
52
+ Omit<
53
+ IContainerContext,
54
+ | "clientId"
55
+ | "connected"
56
+ | "attachState"
57
+ | "getLoadedFromVersion"
58
+ | "supportedFeatures"
59
+ | "id"
60
+ | "snapshotWithContents"
61
+ >
62
+ >
63
+ > {
64
+ // This overrides IContainerContext.options with specific type.
44
65
  readonly options: ILoaderOptions;
45
- readonly scope: FluidObject;
46
- readonly baseSnapshot: ISnapshotTree | undefined;
47
66
  readonly version: IVersion | undefined;
48
- readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>;
49
- readonly storage: IContainerStorageService;
50
- readonly quorum: IQuorumClients;
51
- readonly audience: IAudience;
52
- readonly loader: ILoader;
53
- readonly submitFn: (
54
- type: MessageType,
55
- contents: unknown,
56
- batch: boolean,
57
- appData: unknown,
58
- ) => number;
59
- readonly submitSummaryFn: (
60
- summaryOp: ISummaryContent,
61
- referenceSequenceNumber?: number,
62
- ) => number;
63
- readonly submitBatchFn: (batch: IBatchMessage[], referenceSequenceNumber?: number) => number;
64
- readonly submitSignalFn: (
65
- content: unknown | ISignalEnvelope,
66
- targetClientId?: string,
67
- ) => void;
68
- readonly disposeFn: (error?: ICriticalContainerError) => void;
69
- readonly closeFn: (error?: ICriticalContainerError) => void;
70
- readonly updateDirtyContainerState: (dirty: boolean) => void;
71
- readonly getAbsoluteUrl: (relativeUrl: string) => Promise<string | undefined>;
72
67
  readonly getContainerDiagnosticId: () => string | undefined;
73
68
  readonly getClientId: () => string | undefined;
74
69
  readonly getAttachState: () => AttachState;
75
70
  readonly getConnected: () => boolean;
76
- readonly getConnectionState: () => ConnectionState;
77
- readonly clientDetails: IClientDetails;
78
71
  readonly existing: boolean;
79
72
  readonly taggedLogger: ITelemetryLoggerExt;
80
- readonly pendingLocalState?: unknown;
81
- readonly snapshotWithContents?: ISnapshot;
73
+ // This "overrides" IContainerContext.snapshotWithContents to be required but allow `undefined`.
74
+ readonly snapshotWithContents: IContainerContext["snapshotWithContents"] | undefined;
82
75
  }
83
76
 
84
77
  /**
85
78
  * {@inheritDoc @fluidframework/container-definitions#IContainerContext}
86
79
  */
87
- export class ContainerContext implements IContainerContext, IProvideLayerCompatDetails {
80
+ export class ContainerContext
81
+ implements
82
+ Required<Omit<IContainerContext, "snapshotWithContents">>,
83
+ Pick<IContainerContext, "snapshotWithContents">,
84
+ IProvideLayerCompatDetails
85
+ {
88
86
  /**
89
87
  * @deprecated - This has been replaced by ILayerCompatDetails.
90
88
  */
@@ -139,7 +137,7 @@ export class ContainerContext implements IContainerContext, IProvideLayerCompatD
139
137
  public readonly existing: boolean;
140
138
  public readonly taggedLogger: ITelemetryLoggerExt;
141
139
  public readonly pendingLocalState: unknown;
142
- public readonly snapshotWithContents: ISnapshot | undefined;
140
+ public readonly snapshotWithContents?: ISnapshot;
143
141
 
144
142
  public readonly getConnectionState: () => ConnectionState;
145
143
 
@@ -197,7 +195,9 @@ export class ContainerContext implements IContainerContext, IProvideLayerCompatD
197
195
  this.existing = config.existing;
198
196
  this.taggedLogger = config.taggedLogger;
199
197
  this.pendingLocalState = config.pendingLocalState;
200
- this.snapshotWithContents = config.snapshotWithContents;
198
+ if (config.snapshotWithContents !== undefined) {
199
+ this.snapshotWithContents = config.snapshotWithContents;
200
+ }
201
201
 
202
202
  this.getConnectionState = config.getConnectionState;
203
203
  this._getClientId = config.getClientId;
package/src/contracts.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  type IConnectionDetails,
13
13
  } from "@fluidframework/container-definitions/internal";
14
14
  import type { IErrorBase, ITelemetryBaseProperties } from "@fluidframework/core-interfaces";
15
+ import type { JsonString } from "@fluidframework/core-interfaces/internal";
15
16
  import type { ConnectionMode, IClientDetails } from "@fluidframework/driver-definitions";
16
17
  import type {
17
18
  IContainerPackageInfo,
@@ -145,7 +146,9 @@ export interface IConnectionManagerFactoryArgs {
145
146
  * Called by connection manager for each incoming signal.
146
147
  * May be called before connectHandler is called (due to initial signals on socket connection)
147
148
  */
148
- readonly signalHandler: (signals: ISignalMessage[]) => void;
149
+ readonly signalHandler: (
150
+ signals: ISignalMessage<{ type: never; content: JsonString<unknown> }>[],
151
+ ) => void;
149
152
 
150
153
  /**
151
154
  * Called when connection manager experiences delay in connecting to relay service.
@@ -179,51 +179,12 @@ export async function loadExistingContainer(
179
179
  * Properties required to load a frozen container from pending state.
180
180
  * @legacy @alpha
181
181
  */
182
- export interface ILoadFrozenContainerFromPendingStateProps {
183
- /**
184
- * The code loader handles loading the necessary code for running a container once it is loaded.
185
- */
186
- readonly codeLoader: ICodeDetailsLoader;
187
-
188
- /**
189
- * The url resolver used by the loader for resolving external urls into Fluid urls.
190
- */
191
- readonly urlResolver: IUrlResolver;
192
-
193
- /**
194
- * The request to resolve the container.
195
- */
196
- readonly request: IRequest;
197
-
182
+ export interface ILoadFrozenContainerFromPendingStateProps
183
+ extends ILoadExistingContainerProps {
198
184
  /**
199
185
  * Pending local state to be applied to the container.
200
186
  */
201
187
  readonly pendingLocalState: string;
202
-
203
- /**
204
- * A property bag of options/policies used by various layers to control features.
205
- */
206
- readonly options?: IContainerPolicies | undefined;
207
-
208
- /**
209
- * Scope is provided to all container and is a set of shared services for container's to integrate with their host environment.
210
- */
211
- readonly scope?: FluidObject | undefined;
212
-
213
- /**
214
- * The logger that all telemetry should be pushed to.
215
- */
216
- readonly logger?: ITelemetryBaseLogger | undefined;
217
-
218
- /**
219
- * The configuration provider which may be used to control features.
220
- */
221
- readonly configProvider?: IConfigProviderBase | undefined;
222
-
223
- /**
224
- * Client details provided in the override will be merged over the default client.
225
- */
226
- readonly clientDetailsOverride?: IClientDetails | undefined;
227
188
  }
228
189
 
229
190
  /**
@@ -236,6 +197,6 @@ export async function loadFrozenContainerFromPendingState(
236
197
  ): Promise<IContainer> {
237
198
  return loadExistingContainer({
238
199
  ...props,
239
- documentServiceFactory: new FrozenDocumentServiceFactory(),
200
+ documentServiceFactory: new FrozenDocumentServiceFactory(props.documentServiceFactory),
240
201
  });
241
202
  }
@@ -16,7 +16,8 @@ import type {
16
16
  ITelemetryBaseEvent,
17
17
  ITelemetryBaseProperties,
18
18
  } from "@fluidframework/core-interfaces";
19
- import type { IThrottlingWarning } from "@fluidframework/core-interfaces/internal";
19
+ import { JsonParse } from "@fluidframework/core-interfaces/internal";
20
+ import type { IThrottlingWarning, JsonString } from "@fluidframework/core-interfaces/internal";
20
21
  import { assert } from "@fluidframework/core-utils/internal";
21
22
  import type { ConnectionMode } from "@fluidframework/driver-definitions";
22
23
  import {
@@ -210,7 +211,9 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
210
211
  private initSequenceNumber: number = 0;
211
212
 
212
213
  private readonly _inbound: DeltaQueue<ISequencedDocumentMessage>;
213
- private readonly _inboundSignal: DeltaQueue<ISignalMessage>;
214
+ private readonly _inboundSignal: DeltaQueue<
215
+ ISignalMessage<{ type: never; content: JsonString<unknown> }>
216
+ >;
214
217
 
215
218
  private _closed = false;
216
219
  private _disposed = false;
@@ -433,7 +436,9 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
433
436
  this.close(normalizeError(error));
434
437
  }
435
438
  },
436
- signalHandler: (signals: ISignalMessage[]) => {
439
+ signalHandler: (
440
+ signals: ISignalMessage<{ type: never; content: JsonString<unknown> }>[],
441
+ ) => {
437
442
  for (const signal of signals) {
438
443
  this._inboundSignal.push(signal);
439
444
  }
@@ -474,14 +479,16 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
474
479
  });
475
480
 
476
481
  // Inbound signal queue
477
- this._inboundSignal = new DeltaQueue<ISignalMessage>((message) => {
482
+ this._inboundSignal = new DeltaQueue<
483
+ ISignalMessage<{ type: never; content: JsonString<unknown> }>
484
+ >((message) => {
478
485
  if (this.handler === undefined) {
479
486
  throw new Error("Attempted to process an inbound signal without a handler attached");
480
487
  }
481
488
 
482
489
  this.handler.processSignal({
483
490
  ...message,
484
- content: JSON.parse(message.content as string),
491
+ content: JsonParse(message.content),
485
492
  });
486
493
  });
487
494
 
@@ -29,8 +29,13 @@ import {
29
29
  import type { IConnectionStateChangeReason } from "./contracts.js";
30
30
 
31
31
  export class FrozenDocumentServiceFactory implements IDocumentServiceFactory {
32
+ constructor(private readonly documentServiceFactory?: IDocumentServiceFactory) {}
33
+
32
34
  async createDocumentService(resolvedUrl: IResolvedUrl): Promise<IDocumentService> {
33
- return new FrozenDocumentService(resolvedUrl);
35
+ return new FrozenDocumentService(
36
+ resolvedUrl,
37
+ await this.documentServiceFactory?.createDocumentService(resolvedUrl),
38
+ );
34
39
  }
35
40
  async createContainer(): Promise<IDocumentService> {
36
41
  throw new Error("The FrozenDocumentServiceFactory cannot be used to create containers.");
@@ -41,7 +46,10 @@ class FrozenDocumentService
41
46
  extends TypedEventEmitter<IDocumentServiceEvents>
42
47
  implements IDocumentService
43
48
  {
44
- constructor(public readonly resolvedUrl: IResolvedUrl) {
49
+ constructor(
50
+ public readonly resolvedUrl: IResolvedUrl,
51
+ private readonly documentService?: IDocumentService,
52
+ ) {
45
53
  super();
46
54
  }
47
55
 
@@ -49,7 +57,7 @@ class FrozenDocumentService
49
57
  storageOnly: true,
50
58
  };
51
59
  async connectToStorage(): Promise<IDocumentStorageService> {
52
- return frozenDocumentStorageService;
60
+ return new FrozenDocumentStorageService(await this.documentService?.connectToStorage());
53
61
  }
54
62
  async connectToDeltaStorage(): Promise<IDocumentDeltaStorageService> {
55
63
  return frozenDocumentDeltaStorageService;
@@ -63,15 +71,19 @@ class FrozenDocumentService
63
71
  const frozenDocumentStorageServiceHandler = (): never => {
64
72
  throw new Error("Operations are not supported on the FrozenDocumentStorageService.");
65
73
  };
66
- const frozenDocumentStorageService: IDocumentStorageService = {
67
- getSnapshotTree: frozenDocumentStorageServiceHandler,
68
- getSnapshot: frozenDocumentStorageServiceHandler,
69
- getVersions: frozenDocumentStorageServiceHandler,
70
- createBlob: frozenDocumentStorageServiceHandler,
71
- readBlob: frozenDocumentStorageServiceHandler,
72
- uploadSummaryWithContext: frozenDocumentStorageServiceHandler,
73
- downloadSummary: frozenDocumentStorageServiceHandler,
74
- };
74
+ class FrozenDocumentStorageService implements IDocumentStorageService {
75
+ constructor(private readonly documentStorageService?: IDocumentStorageService) {}
76
+
77
+ getSnapshotTree = frozenDocumentStorageServiceHandler;
78
+ getSnapshot = frozenDocumentStorageServiceHandler;
79
+ getVersions = frozenDocumentStorageServiceHandler;
80
+ createBlob = frozenDocumentStorageServiceHandler;
81
+ readBlob =
82
+ this.documentStorageService?.readBlob.bind(this.documentStorageService) ??
83
+ frozenDocumentStorageServiceHandler;
84
+ uploadSummaryWithContext = frozenDocumentStorageServiceHandler;
85
+ downloadSummary = frozenDocumentStorageServiceHandler;
86
+ }
75
87
 
76
88
  const frozenDocumentDeltaStorageService: IDocumentDeltaStorageService = {
77
89
  fetchMessages: () => ({
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-loader";
9
- export const pkgVersion = "2.63.0-358419";
9
+ export const pkgVersion = "2.63.0-359286";
package/src/protocol.ts CHANGED
@@ -21,12 +21,39 @@ import {
21
21
  } from "./protocol/index.js";
22
22
 
23
23
  // ADO: #1986: Start using enum from protocol-base.
24
- export enum SignalType {
25
- ClientJoin = "join", // same value as MessageType.ClientJoin,
26
- ClientLeave = "leave", // same value as MessageType.ClientLeave,
27
- Clear = "clear", // used only by client for synthetic signals
24
+ export const SignalType = {
25
+ ClientJoin: "join", // same value as MessageType.ClientJoin,
26
+ ClientLeave: "leave", // same value as MessageType.ClientLeave,
27
+ Clear: "clear", // used only by client for synthetic signals
28
+ } as const;
29
+
30
+ interface SystemSignalContent {
31
+ type: (typeof SignalType)[keyof typeof SignalType];
32
+ content?: unknown;
33
+ }
34
+
35
+ interface InboundSystemSignal<TSignalContent extends SystemSignalContent>
36
+ extends ISignalMessage<{ type: never; content: TSignalContent }> {
37
+ // eslint-disable-next-line @rushstack/no-new-null -- `null` is used in JSON protocol to indicate system message
38
+ readonly clientId: null;
28
39
  }
29
40
 
41
+ type ClientJoinSignal = InboundSystemSignal<{
42
+ type: typeof SignalType.ClientJoin;
43
+ content: ISignalClient;
44
+ }>;
45
+
46
+ type ClientLeaveSignal = InboundSystemSignal<{
47
+ type: typeof SignalType.ClientLeave;
48
+ content: string; // clientId of leaving client
49
+ }>;
50
+
51
+ type ClearClientsSignal = InboundSystemSignal<{
52
+ type: typeof SignalType.Clear;
53
+ }>;
54
+
55
+ type AudienceSignal = ClientJoinSignal | ClientLeaveSignal | ClearClientsSignal;
56
+
30
57
  /**
31
58
  * Function to be used for creating a protocol handler.
32
59
  * @legacy @beta
@@ -47,7 +74,35 @@ export interface IProtocolHandler extends IBaseProtocolHandler {
47
74
  processSignal(message: ISignalMessage);
48
75
  }
49
76
 
50
- export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandler {
77
+ /**
78
+ * More specific version of {@link IProtocolHandler} with narrower call
79
+ * constraints for {@link IProtocolHandler.processSignal}.
80
+ */
81
+ export interface ProtocolHandlerInternal extends IProtocolHandler {
82
+ /**
83
+ * Process the audience related signal.
84
+ * @privateRemarks
85
+ * Internally, only {@link AudienceSignal} messages need handling.
86
+ */
87
+ processSignal(message: AudienceSignal): void;
88
+ }
89
+
90
+ /**
91
+ * Function to be used for creating a protocol handler.
92
+ *
93
+ * @remarks This is the same are {@link ProtocolHandlerBuilder} but
94
+ * returns the {@link ProtocolHandlerInternal} which has narrower
95
+ * expectations for `processSignal`.
96
+ */
97
+ export type InternalProtocolHandlerBuilder = (
98
+ attributes: IDocumentAttributes,
99
+ snapshot: IQuorumSnapshot,
100
+ // TODO: use a real type (breaking change)
101
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
+ sendProposal: (key: string, value: any) => number,
103
+ ) => ProtocolHandlerInternal;
104
+
105
+ export class ProtocolHandler extends ProtocolOpHandler implements ProtocolHandlerInternal {
51
106
  constructor(
52
107
  attributes: IDocumentAttributes,
53
108
  quorumSnapshot: IQuorumSnapshot,
@@ -104,8 +159,8 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
104
159
  return super.processMessage(message, local);
105
160
  }
106
161
 
107
- public processSignal(message: ISignalMessage): void {
108
- const innerContent = message.content as { content: unknown; type: string };
162
+ public processSignal(message: AudienceSignal): void {
163
+ const innerContent = message.content;
109
164
  switch (innerContent.type) {
110
165
  case SignalType.Clear: {
111
166
  const members = this.audience.getMembers();
@@ -117,7 +172,7 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
117
172
  break;
118
173
  }
119
174
  case SignalType.ClientJoin: {
120
- const newClient = innerContent.content as ISignalClient;
175
+ const newClient = innerContent.content;
121
176
  // Ignore write clients - quorum will control such clients.
122
177
  if (newClient.client.mode === "read") {
123
178
  this.audience.addMember(newClient.clientId, newClient.client);
@@ -125,7 +180,7 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
125
180
  break;
126
181
  }
127
182
  case SignalType.ClientLeave: {
128
- const leftClientId = innerContent.content as string;
183
+ const leftClientId = innerContent.content;
129
184
  // Ignore write clients - quorum will control such clients.
130
185
  if (this.audience.getMember(leftClientId)?.mode === "read") {
131
186
  this.audience.removeMember(leftClientId);
@@ -144,7 +199,9 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
144
199
  * The protocol handler should strictly handle only ClientJoin, ClientLeave
145
200
  * and Clear signal types.
146
201
  */
147
- export function protocolHandlerShouldProcessSignal(message: ISignalMessage): boolean {
202
+ export function protocolHandlerShouldProcessSignal(
203
+ message: ISignalMessage,
204
+ ): message is AudienceSignal {
148
205
  // Signal originates from server
149
206
  if (message.clientId === null) {
150
207
  const innerContent = message.content as { content: unknown; type: string };
@@ -9,7 +9,6 @@ import type {
9
9
  IEventProvider,
10
10
  IEvent,
11
11
  ITelemetryBaseLogger,
12
- Tagged,
13
12
  } from "@fluidframework/core-interfaces";
14
13
  import { Timer, assert } from "@fluidframework/core-utils/internal";
15
14
  import {
@@ -28,7 +27,6 @@ import {
28
27
  PerformanceEvent,
29
28
  UsageError,
30
29
  createChildMonitoringContext,
31
- type TelemetryEventPropertyTypeExt,
32
30
  } from "@fluidframework/telemetry-utils/internal";
33
31
 
34
32
  import {
@@ -135,6 +133,27 @@ interface ISerializerEvent extends IEvent {
135
133
  (event: "saved", listener: (dirty: boolean) => void): void;
136
134
  }
137
135
 
136
+ class RefreshPromiseTracker {
137
+ public get hasPromise(): boolean {
138
+ return this.#promise !== undefined;
139
+ }
140
+ public get Promise(): Promise<number> | undefined {
141
+ return this.#promise;
142
+ }
143
+ constructor(private readonly catchHandler: (error: Error) => void) {}
144
+
145
+ #promise: Promise<number> | undefined;
146
+ setPromise(p: Promise<number>): void {
147
+ if (this.hasPromise) {
148
+ throw new Error("Cannot set promise while promise exists");
149
+ }
150
+ this.#promise = p.finally(() => {
151
+ this.#promise = undefined;
152
+ });
153
+ p.catch(this.catchHandler);
154
+ }
155
+ }
156
+
138
157
  /**
139
158
  * Helper class to manage the state of the container needed for proper serialization.
140
159
  *
@@ -147,7 +166,16 @@ export class SerializedStateManager {
147
166
  private readonly mc: MonitoringContext;
148
167
  private snapshot: ISnapshotInfo | undefined;
149
168
  private latestSnapshot: ISnapshotInfo | undefined;
150
- private _refreshSnapshotP: Promise<number> | undefined;
169
+ private readonly refreshTracker = new RefreshPromiseTracker(
170
+ // eslint-disable-next-line unicorn/consistent-function-scoping
171
+ (error) =>
172
+ this.mc.logger.sendErrorEvent(
173
+ {
174
+ eventName: "RefreshLatestSnapshotFailed",
175
+ },
176
+ error,
177
+ ),
178
+ );
151
179
  private readonly lastSavedOpSequenceNumber: number = 0;
152
180
  private readonly refreshTimer: Timer;
153
181
  private readonly snapshotRefreshTimeoutMs: number = 60 * 60 * 24 * 1000;
@@ -199,8 +227,8 @@ export class SerializedStateManager {
199
227
  * only intended to be used for testing purposes.
200
228
  * @returns The snapshot sequence number associated with the latest fetched snapshot
201
229
  */
202
- public get refreshSnapshotP(): Promise<number> | undefined {
203
- return this._refreshSnapshotP;
230
+ public get refreshSnapshotP(): Promise<number | undefined> | undefined {
231
+ return this.refreshTracker.Promise;
204
232
  }
205
233
 
206
234
  /**
@@ -226,6 +254,7 @@ export class SerializedStateManager {
226
254
  public async fetchSnapshot(specifiedVersion: string | undefined): Promise<{
227
255
  baseSnapshot: ISnapshot | ISnapshotTree;
228
256
  version: IVersion | undefined;
257
+ attributes: IDocumentAttributes;
229
258
  }> {
230
259
  if (this.pendingLocalState === undefined) {
231
260
  const { baseSnapshot, version } = await getSnapshot(
@@ -235,18 +264,24 @@ export class SerializedStateManager {
235
264
  specifiedVersion,
236
265
  );
237
266
  const baseSnapshotTree: ISnapshotTree | undefined = getSnapshotTree(baseSnapshot);
267
+ const attributes = await getDocumentAttributes(this.storageAdapter, baseSnapshotTree);
238
268
  // non-interactive clients will not have any pending state we want to save
239
269
  if (this.offlineLoadEnabled) {
240
- const snapshotBlobs = await getBlobContentsFromTree(baseSnapshot, this.storageAdapter);
241
- const attributes = await getDocumentAttributes(this.storageAdapter, baseSnapshotTree);
242
- this.snapshot = {
243
- baseSnapshot: baseSnapshotTree,
244
- snapshotBlobs,
245
- snapshotSequenceNumber: attributes.sequenceNumber,
246
- };
247
- this.refreshTimer.start();
270
+ // we defer getting the blobs to not impact the container load flow
271
+ // only getPendingState depends on the resolution of this promise
272
+ this.refreshTracker.setPromise(
273
+ getBlobContentsFromTree(baseSnapshot, this.storageAdapter).then((snapshotBlobs) => {
274
+ this.snapshot = {
275
+ baseSnapshot: baseSnapshotTree,
276
+ snapshotBlobs,
277
+ snapshotSequenceNumber: attributes.sequenceNumber,
278
+ };
279
+ this.refreshTimer.start();
280
+ return attributes.sequenceNumber;
281
+ }),
282
+ );
248
283
  }
249
- return { baseSnapshot, version };
284
+ return { baseSnapshot, version, attributes };
250
285
  } else {
251
286
  const { baseSnapshot, snapshotBlobs } = this.pendingLocalState;
252
287
  const attributes = await getDocumentAttributes(this.storageAdapter, baseSnapshot);
@@ -268,30 +303,18 @@ export class SerializedStateManager {
268
303
  ops: [],
269
304
  snapshotFormatV: 1,
270
305
  };
271
- return { baseSnapshot: iSnapshot, version: undefined };
306
+ return { baseSnapshot: iSnapshot, version: undefined, attributes };
272
307
  }
273
308
  }
274
309
 
275
310
  private tryRefreshSnapshot(): void {
276
311
  if (
277
312
  this.mc.config.getBoolean("Fluid.Container.enableOfflineSnapshotRefresh") === true &&
278
- this._refreshSnapshotP === undefined &&
313
+ !this.refreshTracker.hasPromise &&
279
314
  this.latestSnapshot === undefined
280
315
  ) {
281
316
  // Don't block on the refresh snapshot call - it is for the next time we serialize, not booting this incarnation
282
- this._refreshSnapshotP = this.refreshLatestSnapshot(this.supportGetSnapshotApi());
283
- this._refreshSnapshotP
284
- .catch(
285
- (error: TelemetryEventPropertyTypeExt | Tagged<TelemetryEventPropertyTypeExt>) => {
286
- this.mc.logger.sendTelemetryEvent({
287
- eventName: "RefreshLatestSnapshotFailed",
288
- error,
289
- });
290
- },
291
- )
292
- .finally(() => {
293
- this._refreshSnapshotP = undefined;
294
- });
317
+ this.refreshTracker.setPromise(this.refreshLatestSnapshot(this.supportGetSnapshotApi()));
295
318
  }
296
319
  }
297
320
 
@@ -439,6 +462,14 @@ export class SerializedStateManager {
439
462
  if (!this.offlineLoadEnabled) {
440
463
  throw new UsageError("Can't get pending local state unless offline load is enabled");
441
464
  }
465
+ if (this.snapshot === undefined && this.refreshTracker.hasPromise) {
466
+ // we deferred the initial download of the snapshot to not block
467
+ // the container load flow, so if it is not resolved
468
+ // and we don't have a snapshot, we will wait for the download
469
+ // to finish.
470
+ await this.refreshTracker.Promise;
471
+ }
472
+
442
473
  assert(this.snapshot !== undefined, 0x8e5 /* no base data */);
443
474
  const pendingRuntimeState = await runtime.getPendingLocalState({
444
475
  notifyImminentClosure: false,