@fluidframework/container-loader 2.80.0 → 2.81.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/dist/connectionManager.d.ts.map +1 -1
- package/dist/connectionManager.js +2 -0
- package/dist/connectionManager.js.map +1 -1
- package/dist/container.js +1 -1
- package/dist/container.js.map +1 -1
- package/dist/deltaManager.d.ts.map +1 -1
- package/dist/deltaManager.js +2 -0
- package/dist/deltaManager.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/serializedStateManager.d.ts +7 -11
- package/dist/serializedStateManager.d.ts.map +1 -1
- package/dist/serializedStateManager.js +17 -77
- package/dist/serializedStateManager.js.map +1 -1
- package/dist/snapshotRefresher.d.ts +68 -0
- package/dist/snapshotRefresher.d.ts.map +1 -0
- package/dist/snapshotRefresher.js +167 -0
- package/dist/snapshotRefresher.js.map +1 -0
- package/eslint.config.mts +4 -4
- package/lib/connectionManager.d.ts.map +1 -1
- package/lib/connectionManager.js +2 -0
- package/lib/connectionManager.js.map +1 -1
- package/lib/container.js +1 -1
- package/lib/container.js.map +1 -1
- package/lib/deltaManager.d.ts.map +1 -1
- package/lib/deltaManager.js +2 -0
- package/lib/deltaManager.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/serializedStateManager.d.ts +7 -11
- package/lib/serializedStateManager.d.ts.map +1 -1
- package/lib/serializedStateManager.js +18 -78
- package/lib/serializedStateManager.js.map +1 -1
- package/lib/snapshotRefresher.d.ts +68 -0
- package/lib/snapshotRefresher.d.ts.map +1 -0
- package/lib/snapshotRefresher.js +163 -0
- package/lib/snapshotRefresher.js.map +1 -0
- package/package.json +15 -15
- package/src/connectionManager.ts +2 -0
- package/src/container.ts +1 -1
- package/src/deltaManager.ts +2 -0
- package/src/packageVersion.ts +1 -1
- package/src/serializedStateManager.ts +27 -99
- package/src/snapshotRefresher.ts +201 -0
- package/.eslintrc.cjs +0 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluidframework/container-loader",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.81.0",
|
|
4
4
|
"description": "Fluid container loader",
|
|
5
5
|
"homepage": "https://fluidframework.com",
|
|
6
6
|
"repository": {
|
|
@@ -129,13 +129,13 @@
|
|
|
129
129
|
"temp-directory": "nyc/.nyc_output"
|
|
130
130
|
},
|
|
131
131
|
"dependencies": {
|
|
132
|
-
"@fluid-internal/client-utils": "~2.
|
|
133
|
-
"@fluidframework/container-definitions": "~2.
|
|
134
|
-
"@fluidframework/core-interfaces": "~2.
|
|
135
|
-
"@fluidframework/core-utils": "~2.
|
|
136
|
-
"@fluidframework/driver-definitions": "~2.
|
|
137
|
-
"@fluidframework/driver-utils": "~2.
|
|
138
|
-
"@fluidframework/telemetry-utils": "~2.
|
|
132
|
+
"@fluid-internal/client-utils": "~2.81.0",
|
|
133
|
+
"@fluidframework/container-definitions": "~2.81.0",
|
|
134
|
+
"@fluidframework/core-interfaces": "~2.81.0",
|
|
135
|
+
"@fluidframework/core-utils": "~2.81.0",
|
|
136
|
+
"@fluidframework/driver-definitions": "~2.81.0",
|
|
137
|
+
"@fluidframework/driver-utils": "~2.81.0",
|
|
138
|
+
"@fluidframework/telemetry-utils": "~2.81.0",
|
|
139
139
|
"@types/events_pkg": "npm:@types/events@^3.0.0",
|
|
140
140
|
"@ungap/structured-clone": "^1.2.0",
|
|
141
141
|
"debug": "^4.3.4",
|
|
@@ -146,14 +146,14 @@
|
|
|
146
146
|
"devDependencies": {
|
|
147
147
|
"@arethetypeswrong/cli": "^0.18.2",
|
|
148
148
|
"@biomejs/biome": "~1.9.3",
|
|
149
|
-
"@fluid-internal/client-utils": "~2.
|
|
150
|
-
"@fluid-internal/mocha-test-setup": "~2.
|
|
151
|
-
"@fluid-private/test-loader-utils": "~2.
|
|
152
|
-
"@fluid-tools/build-cli": "^0.
|
|
149
|
+
"@fluid-internal/client-utils": "~2.81.0",
|
|
150
|
+
"@fluid-internal/mocha-test-setup": "~2.81.0",
|
|
151
|
+
"@fluid-private/test-loader-utils": "~2.81.0",
|
|
152
|
+
"@fluid-tools/build-cli": "^0.63.0",
|
|
153
153
|
"@fluidframework/build-common": "^2.0.3",
|
|
154
|
-
"@fluidframework/build-tools": "^0.
|
|
155
|
-
"@fluidframework/container-loader-previous": "npm:@fluidframework/container-loader@2.
|
|
156
|
-
"@fluidframework/eslint-config-fluid": "~2.
|
|
154
|
+
"@fluidframework/build-tools": "^0.63.0",
|
|
155
|
+
"@fluidframework/container-loader-previous": "npm:@fluidframework/container-loader@2.80.0",
|
|
156
|
+
"@fluidframework/eslint-config-fluid": "~2.81.0",
|
|
157
157
|
"@microsoft/api-extractor": "7.52.11",
|
|
158
158
|
"@types/debug": "^4.1.5",
|
|
159
159
|
"@types/double-ended-queue": "^2.1.0",
|
package/src/connectionManager.ts
CHANGED
|
@@ -564,6 +564,7 @@ export class ConnectionManager implements IConnectionManager {
|
|
|
564
564
|
this.logger.sendTelemetryEvent(
|
|
565
565
|
{
|
|
566
566
|
eventName: "ConnectionReceived",
|
|
567
|
+
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- using ?. could change behavior
|
|
567
568
|
connected: connection !== undefined && connection.disposed === false,
|
|
568
569
|
},
|
|
569
570
|
undefined,
|
|
@@ -573,6 +574,7 @@ export class ConnectionManager implements IConnectionManager {
|
|
|
573
574
|
this.logger.sendTelemetryEvent(
|
|
574
575
|
{
|
|
575
576
|
eventName: "ConnectToDeltaStreamException",
|
|
577
|
+
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- using ?. could change behavior
|
|
576
578
|
connected: connection !== undefined && connection.disposed === false,
|
|
577
579
|
},
|
|
578
580
|
undefined,
|
package/src/container.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
/* eslint-disable unicorn/consistent-function-scoping */
|
|
6
|
+
/* eslint-disable unicorn/consistent-function-scoping, @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/prefer-optional-chain */
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
TypedEventEmitter,
|
package/src/deltaManager.ts
CHANGED
|
@@ -668,6 +668,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
668
668
|
throw new Error("Delta manager is not attached");
|
|
669
669
|
}
|
|
670
670
|
|
|
671
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- using ??= could change behavior if value is falsy
|
|
671
672
|
if (this.deltaStorage === undefined) {
|
|
672
673
|
this.deltaStorage = await docService.connectToDeltaStorage();
|
|
673
674
|
}
|
|
@@ -964,6 +965,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
964
965
|
duplicate++;
|
|
965
966
|
} else if (message.sequenceNumber !== prev + 1) {
|
|
966
967
|
gap++;
|
|
968
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- using ??= could change behavior if value is falsy
|
|
967
969
|
if (firstMissing === undefined) {
|
|
968
970
|
firstMissing = prev + 1;
|
|
969
971
|
}
|
package/src/packageVersion.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
ITelemetryBaseLogger,
|
|
12
12
|
} from "@fluidframework/core-interfaces";
|
|
13
13
|
import type { IDisposable } from "@fluidframework/core-interfaces/internal";
|
|
14
|
-
import {
|
|
14
|
+
import { assert } from "@fluidframework/core-utils/internal";
|
|
15
15
|
import {
|
|
16
16
|
FetchSource,
|
|
17
17
|
type IDocumentStorageService,
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type ContainerStorageAdapter,
|
|
36
36
|
type ISerializableBlobContents,
|
|
37
37
|
} from "./containerStorageAdapter.js";
|
|
38
|
+
import { SnapshotRefresher } from "./snapshotRefresher.js";
|
|
38
39
|
import {
|
|
39
40
|
convertISnapshotToSnapshotWithBlobs,
|
|
40
41
|
convertSnapshotToSnapshotInfo,
|
|
@@ -127,7 +128,7 @@ export interface SerializedSnapshotInfo extends SnapshotWithBlobs {
|
|
|
127
128
|
snapshotSequenceNumber: number;
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
interface ISnapshotInfo {
|
|
131
|
+
export interface ISnapshotInfo {
|
|
131
132
|
snapshotSequenceNumber: number;
|
|
132
133
|
snapshotFetchedTime?: number | undefined;
|
|
133
134
|
snapshot: ISnapshot | ISnapshotTree;
|
|
@@ -144,27 +145,6 @@ interface ISerializerEvent extends IEvent {
|
|
|
144
145
|
(event: "saved", listener: (dirty: boolean) => void): void;
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
class RefreshPromiseTracker {
|
|
148
|
-
public get hasPromise(): boolean {
|
|
149
|
-
return this.#promise !== undefined;
|
|
150
|
-
}
|
|
151
|
-
public get Promise(): Promise<number> | undefined {
|
|
152
|
-
return this.#promise;
|
|
153
|
-
}
|
|
154
|
-
constructor(private readonly catchHandler: (error: Error) => void) {}
|
|
155
|
-
|
|
156
|
-
#promise: Promise<number> | undefined;
|
|
157
|
-
setPromise(p: Promise<number>): void {
|
|
158
|
-
if (this.hasPromise) {
|
|
159
|
-
throw new Error("Cannot set promise while promise exists");
|
|
160
|
-
}
|
|
161
|
-
this.#promise = p.finally(() => {
|
|
162
|
-
this.#promise = undefined;
|
|
163
|
-
});
|
|
164
|
-
p.catch(this.catchHandler);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
148
|
/**
|
|
169
149
|
* Helper class to manage the state of the container needed for proper serialization.
|
|
170
150
|
*
|
|
@@ -177,20 +157,8 @@ export class SerializedStateManager implements IDisposable {
|
|
|
177
157
|
private readonly mc: MonitoringContext;
|
|
178
158
|
private snapshotInfo: ISnapshotInfo | undefined;
|
|
179
159
|
private latestSnapshot: ISnapshotInfo | undefined;
|
|
180
|
-
private readonly refreshTracker = new RefreshPromiseTracker(
|
|
181
|
-
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
182
|
-
(error) =>
|
|
183
|
-
this.mc.logger.sendErrorEvent(
|
|
184
|
-
{
|
|
185
|
-
eventName: "RefreshLatestSnapshotFailed",
|
|
186
|
-
},
|
|
187
|
-
error,
|
|
188
|
-
),
|
|
189
|
-
);
|
|
190
160
|
private lastSavedOpSequenceNumber: number = 0;
|
|
191
|
-
private readonly
|
|
192
|
-
private readonly snapshotRefreshTimeoutMs: number = 60 * 60 * 24 * 1000;
|
|
193
|
-
readonly #snapshotRefreshEnabled: boolean;
|
|
161
|
+
private readonly snapshotRefresher: SnapshotRefresher | undefined;
|
|
194
162
|
#disposed: boolean = false;
|
|
195
163
|
|
|
196
164
|
/**
|
|
@@ -214,16 +182,17 @@ export class SerializedStateManager implements IDisposable {
|
|
|
214
182
|
namespace: "serializedStateManager",
|
|
215
183
|
});
|
|
216
184
|
|
|
217
|
-
this.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
185
|
+
this.snapshotRefresher = this.offlineLoadEnabled
|
|
186
|
+
? new SnapshotRefresher(
|
|
187
|
+
subLogger,
|
|
188
|
+
this.storageAdapter,
|
|
189
|
+
this.offlineLoadEnabled,
|
|
190
|
+
this.supportGetSnapshotApi,
|
|
191
|
+
(snapshot: ISnapshotInfo) => this.handleSnapshotRefreshed(snapshot),
|
|
192
|
+
snapshotRefreshTimeoutMs,
|
|
193
|
+
)
|
|
226
194
|
: undefined;
|
|
195
|
+
|
|
227
196
|
containerEvent.on("saved", () => this.updateSnapshotAndProcessedOpsMaybe());
|
|
228
197
|
}
|
|
229
198
|
public get disposed(): boolean {
|
|
@@ -231,7 +200,7 @@ export class SerializedStateManager implements IDisposable {
|
|
|
231
200
|
}
|
|
232
201
|
dispose(): void {
|
|
233
202
|
this.#disposed = true;
|
|
234
|
-
this.
|
|
203
|
+
this.snapshotRefresher?.dispose();
|
|
235
204
|
}
|
|
236
205
|
|
|
237
206
|
private verifyNotDisposed(): void {
|
|
@@ -245,8 +214,8 @@ export class SerializedStateManager implements IDisposable {
|
|
|
245
214
|
* only intended to be used for testing purposes.
|
|
246
215
|
* @returns The snapshot sequence number associated with the latest fetched snapshot
|
|
247
216
|
*/
|
|
248
|
-
public get refreshSnapshotP(): Promise<number
|
|
249
|
-
return this.
|
|
217
|
+
public get refreshSnapshotP(): Promise<number> | undefined {
|
|
218
|
+
return this.snapshotRefresher?.refreshSnapshotP;
|
|
250
219
|
}
|
|
251
220
|
|
|
252
221
|
/**
|
|
@@ -288,7 +257,7 @@ export class SerializedStateManager implements IDisposable {
|
|
|
288
257
|
const baseSnapshotTree: ISnapshotTree | undefined = getSnapshotTree(snapshot);
|
|
289
258
|
const attributes = await getDocumentAttributes(this.storageAdapter, baseSnapshotTree);
|
|
290
259
|
if (this.offlineLoadEnabled) {
|
|
291
|
-
this.
|
|
260
|
+
this.snapshotRefresher?.startTimer();
|
|
292
261
|
this.snapshotInfo = {
|
|
293
262
|
snapshot,
|
|
294
263
|
snapshotSequenceNumber: attributes.sequenceNumber,
|
|
@@ -326,60 +295,19 @@ export class SerializedStateManager implements IDisposable {
|
|
|
326
295
|
snapshot,
|
|
327
296
|
snapshotSequenceNumber: attributes.sequenceNumber,
|
|
328
297
|
};
|
|
329
|
-
this.tryRefreshSnapshot();
|
|
298
|
+
this.snapshotRefresher?.tryRefreshSnapshot();
|
|
330
299
|
}
|
|
331
300
|
return { snapshot, version: undefined, attributes };
|
|
332
301
|
}
|
|
333
302
|
}
|
|
334
303
|
|
|
335
|
-
private tryRefreshSnapshot(): void {
|
|
336
|
-
if (
|
|
337
|
-
this.#snapshotRefreshEnabled &&
|
|
338
|
-
!this.#disposed &&
|
|
339
|
-
!this.refreshTracker.hasPromise &&
|
|
340
|
-
this.latestSnapshot === undefined
|
|
341
|
-
) {
|
|
342
|
-
// Don't block on the refresh snapshot call - it is for the next time we serialize, not booting this incarnation
|
|
343
|
-
this.refreshTracker.setPromise(this.refreshLatestSnapshot(this.supportGetSnapshotApi()));
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
304
|
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
* @param supportGetSnapshotApi - a boolean indicating whether to use the fetchISnapshot or fetchISnapshotTree (must be true to fetch by groupIds)
|
|
305
|
+
* Handles the snapshotRefreshed event from SnapshotRefresher.
|
|
306
|
+
* Decides whether to accept the new snapshot based on processed ops.
|
|
307
|
+
* @returns The snapshot sequence number if updated, -1 otherwise
|
|
352
308
|
*/
|
|
353
|
-
private
|
|
354
|
-
this.latestSnapshot =
|
|
355
|
-
this.mc,
|
|
356
|
-
this.storageAdapter,
|
|
357
|
-
supportGetSnapshotApi,
|
|
358
|
-
);
|
|
359
|
-
|
|
360
|
-
if (this.#disposed) {
|
|
361
|
-
return -1;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// These are loading groupIds that the containerRuntime has requested over its lifetime.
|
|
365
|
-
// We will fetch the latest snapshot for the groupIds, which will update storageAdapter.loadedGroupIdSnapshots's cache
|
|
366
|
-
const downloadedGroupIds = Object.keys(this.storageAdapter.loadedGroupIdSnapshots);
|
|
367
|
-
if (supportGetSnapshotApi && downloadedGroupIds.length > 0) {
|
|
368
|
-
assert(
|
|
369
|
-
this.storageAdapter.getSnapshot !== undefined,
|
|
370
|
-
0x972 /* getSnapshot should exist */,
|
|
371
|
-
);
|
|
372
|
-
// (This is a separate network call from above because it requires work for storage to add a special base groupId)
|
|
373
|
-
const snapshot = await this.storageAdapter.getSnapshot({
|
|
374
|
-
versionId: undefined,
|
|
375
|
-
scenarioName: "getLatestSnapshotInfo",
|
|
376
|
-
cacheSnapshot: false,
|
|
377
|
-
loadingGroupIds: downloadedGroupIds,
|
|
378
|
-
fetchSource: FetchSource.noCache,
|
|
379
|
-
});
|
|
380
|
-
assert(snapshot !== undefined, 0x973 /* Snapshot should exist */);
|
|
381
|
-
}
|
|
382
|
-
|
|
309
|
+
private handleSnapshotRefreshed(latestSnapshot: ISnapshotInfo): number {
|
|
310
|
+
this.latestSnapshot = latestSnapshot;
|
|
383
311
|
return this.updateSnapshotAndProcessedOpsMaybe();
|
|
384
312
|
}
|
|
385
313
|
|
|
@@ -415,14 +343,14 @@ export class SerializedStateManager implements IDisposable {
|
|
|
415
343
|
stashedSnapshotSequenceNumber: this.snapshotInfo?.snapshotSequenceNumber,
|
|
416
344
|
});
|
|
417
345
|
this.latestSnapshot = undefined;
|
|
418
|
-
this.
|
|
346
|
+
this.snapshotRefresher?.clearLatestSnapshot();
|
|
419
347
|
} else if (snapshotSequenceNumber <= lastProcessedOpSequenceNumber) {
|
|
420
348
|
// Snapshot seq num is between the first and last processed op.
|
|
421
349
|
// Remove the ops that are already part of the snapshot
|
|
422
350
|
this.processedOps.splice(0, snapshotSequenceNumber - firstProcessedOpSequenceNumber + 1);
|
|
423
351
|
this.snapshotInfo = this.latestSnapshot;
|
|
424
352
|
this.latestSnapshot = undefined;
|
|
425
|
-
this.
|
|
353
|
+
this.snapshotRefresher?.clearLatestSnapshot();
|
|
426
354
|
this.mc.logger.sendTelemetryEvent({
|
|
427
355
|
eventName: "SnapshotRefreshed",
|
|
428
356
|
snapshotSequenceNumber,
|
|
@@ -448,7 +376,7 @@ export class SerializedStateManager implements IDisposable {
|
|
|
448
376
|
snapshotSequenceNumber: snapshot.sequenceNumber ?? 0,
|
|
449
377
|
snapshotFetchedTime: Date.now(),
|
|
450
378
|
};
|
|
451
|
-
this.
|
|
379
|
+
this.snapshotRefresher?.startTimer();
|
|
452
380
|
}
|
|
453
381
|
}
|
|
454
382
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ITelemetryBaseLogger } from "@fluidframework/core-interfaces";
|
|
7
|
+
import type { IDisposable } from "@fluidframework/core-interfaces/internal";
|
|
8
|
+
import { assert, Timer } from "@fluidframework/core-utils/internal";
|
|
9
|
+
import { FetchSource } from "@fluidframework/driver-definitions/internal";
|
|
10
|
+
import {
|
|
11
|
+
createChildMonitoringContext,
|
|
12
|
+
type MonitoringContext,
|
|
13
|
+
} from "@fluidframework/telemetry-utils/internal";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
getLatestSnapshotInfo,
|
|
17
|
+
type ISerializedStateManagerDocumentStorageService,
|
|
18
|
+
type ISnapshotInfo,
|
|
19
|
+
} from "./serializedStateManager.js";
|
|
20
|
+
|
|
21
|
+
class RefreshPromiseTracker {
|
|
22
|
+
public get hasPromise(): boolean {
|
|
23
|
+
return this.#promise !== undefined;
|
|
24
|
+
}
|
|
25
|
+
public get promise(): Promise<number> | undefined {
|
|
26
|
+
return this.#promise;
|
|
27
|
+
}
|
|
28
|
+
constructor(private readonly catchHandler: (error: Error) => void) {}
|
|
29
|
+
|
|
30
|
+
#promise: Promise<number> | undefined;
|
|
31
|
+
setPromise(p: Promise<number>): void {
|
|
32
|
+
if (this.hasPromise) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"Cannot start new snapshot refresh while a refresh is already in progress",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
this.#promise = p.finally(() => {
|
|
38
|
+
this.#promise = undefined;
|
|
39
|
+
});
|
|
40
|
+
p.catch(this.catchHandler);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Manages periodic refresh of the latest snapshot for a document.
|
|
46
|
+
*
|
|
47
|
+
* `SnapshotRefresher` polls the storage service for the most recent snapshot and, when a newer
|
|
48
|
+
* snapshot is discovered, invokes the provided `onSnapshotRefreshed` callback with the updated
|
|
49
|
+
* snapshot metadata. It is responsible for:
|
|
50
|
+
*
|
|
51
|
+
* - Tracking the most recent snapshot that has been observed.
|
|
52
|
+
* - Scheduling and managing refresh attempts via an internal timer.
|
|
53
|
+
* - Emitting telemetry for successful and failed refresh attempts.
|
|
54
|
+
*
|
|
55
|
+
* The refresh behavior can be configured via constructor arguments, including whether offline
|
|
56
|
+
* loading and the `getSnapshot` API are supported, as well as the refresh timeout. Callers
|
|
57
|
+
* should dispose this instance when snapshot refresh is no longer needed to stop any pending
|
|
58
|
+
* timers and prevent further refresh attempts.
|
|
59
|
+
*/
|
|
60
|
+
export class SnapshotRefresher implements IDisposable {
|
|
61
|
+
private readonly mc: MonitoringContext;
|
|
62
|
+
private latestSnapshot: ISnapshotInfo | undefined;
|
|
63
|
+
#disposed: boolean = false;
|
|
64
|
+
|
|
65
|
+
public get disposed(): boolean {
|
|
66
|
+
return this.#disposed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private readonly refreshTracker = new RefreshPromiseTracker(
|
|
70
|
+
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
71
|
+
(error) =>
|
|
72
|
+
this.mc.logger.sendErrorEvent(
|
|
73
|
+
{
|
|
74
|
+
eventName: "RefreshLatestSnapshotFailed",
|
|
75
|
+
},
|
|
76
|
+
error,
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
private readonly refreshTimer: Timer | undefined;
|
|
80
|
+
private readonly snapshotRefreshTimeoutMs: number = 60 * 60 * 24 * 1000;
|
|
81
|
+
readonly #snapshotRefreshEnabled: boolean;
|
|
82
|
+
|
|
83
|
+
constructor(
|
|
84
|
+
subLogger: ITelemetryBaseLogger,
|
|
85
|
+
private readonly storageAdapter: ISerializedStateManagerDocumentStorageService,
|
|
86
|
+
private readonly offlineLoadEnabled: boolean,
|
|
87
|
+
private readonly supportGetSnapshotApi: () => boolean,
|
|
88
|
+
private readonly onSnapshotRefreshed: (snapshot: ISnapshotInfo) => number,
|
|
89
|
+
snapshotRefreshTimeoutMs?: number,
|
|
90
|
+
) {
|
|
91
|
+
this.mc = createChildMonitoringContext({
|
|
92
|
+
logger: subLogger,
|
|
93
|
+
namespace: "serializedStateManager",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.snapshotRefreshTimeoutMs = snapshotRefreshTimeoutMs ?? this.snapshotRefreshTimeoutMs;
|
|
97
|
+
|
|
98
|
+
this.#snapshotRefreshEnabled =
|
|
99
|
+
this.offlineLoadEnabled &&
|
|
100
|
+
(this.mc.config.getBoolean("Fluid.Container.enableOfflineSnapshotRefresh") ??
|
|
101
|
+
this.mc.config.getBoolean("Fluid.Container.enableOfflineFull")) === true;
|
|
102
|
+
|
|
103
|
+
this.refreshTimer = this.#snapshotRefreshEnabled
|
|
104
|
+
? new Timer(this.snapshotRefreshTimeoutMs, () => this.tryRefreshSnapshot())
|
|
105
|
+
: undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public tryRefreshSnapshot(): void {
|
|
109
|
+
if (
|
|
110
|
+
this.#snapshotRefreshEnabled &&
|
|
111
|
+
!this.#disposed &&
|
|
112
|
+
!this.refreshTracker.hasPromise &&
|
|
113
|
+
this.latestSnapshot === undefined
|
|
114
|
+
) {
|
|
115
|
+
// Don't block on the refresh snapshot call - it is for the next time we serialize, not booting this incarnation
|
|
116
|
+
this.refreshTracker.setPromise(this.refreshLatestSnapshot(this.supportGetSnapshotApi()));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Fetch the latest snapshot for the container, including delay-loaded groupIds if pendingLocalState was provided and contained any groupIds.
|
|
122
|
+
* Note that this will update the StorageAdapter's cached snapshots for the groupIds (if present)
|
|
123
|
+
*
|
|
124
|
+
* @param supportGetSnapshotApi - a boolean indicating whether to use the fetchISnapshot or fetchISnapshotTree (must be true to fetch by groupIds)
|
|
125
|
+
*/
|
|
126
|
+
private async refreshLatestSnapshot(supportGetSnapshotApi: boolean): Promise<number> {
|
|
127
|
+
this.latestSnapshot = await getLatestSnapshotInfo(
|
|
128
|
+
this.mc,
|
|
129
|
+
this.storageAdapter,
|
|
130
|
+
supportGetSnapshotApi,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (this.#disposed) {
|
|
134
|
+
return -1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// These are loading groupIds that the containerRuntime has requested over its lifetime.
|
|
138
|
+
// We will fetch the latest snapshot for the groupIds, which will update storageAdapter.loadedGroupIdSnapshots's cache
|
|
139
|
+
const downloadedGroupIds = Object.keys(this.storageAdapter.loadedGroupIdSnapshots);
|
|
140
|
+
if (supportGetSnapshotApi && downloadedGroupIds.length > 0) {
|
|
141
|
+
assert(
|
|
142
|
+
this.storageAdapter.getSnapshot !== undefined,
|
|
143
|
+
0x972 /* getSnapshot should exist */,
|
|
144
|
+
);
|
|
145
|
+
// (This is a separate network call from above because it requires work for storage to add a special base groupId)
|
|
146
|
+
const snapshot = await this.storageAdapter.getSnapshot({
|
|
147
|
+
versionId: undefined,
|
|
148
|
+
scenarioName: "getLatestSnapshotInfo",
|
|
149
|
+
cacheSnapshot: false,
|
|
150
|
+
loadingGroupIds: downloadedGroupIds,
|
|
151
|
+
fetchSource: FetchSource.noCache,
|
|
152
|
+
});
|
|
153
|
+
assert(snapshot !== undefined, 0x973 /* Snapshot should exist */);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Notify the manager about the fetched snapshot - let it decide what to do with it
|
|
157
|
+
const result =
|
|
158
|
+
this.latestSnapshot === undefined ? -1 : this.onSnapshotRefreshed(this.latestSnapshot);
|
|
159
|
+
|
|
160
|
+
this.refreshTimer?.restart();
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Clears the latest snapshot after it's been consumed by the manager.
|
|
166
|
+
* This allows the next refresh cycle to proceed.
|
|
167
|
+
*/
|
|
168
|
+
public clearLatestSnapshot(): void {
|
|
169
|
+
this.latestSnapshot = undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Starts the refresh timer.
|
|
174
|
+
*/
|
|
175
|
+
public startTimer(): void {
|
|
176
|
+
this.refreshTimer?.start();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Restarts the refresh timer.
|
|
181
|
+
*/
|
|
182
|
+
public restartTimer(): void {
|
|
183
|
+
this.refreshTimer?.restart();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Gets the current refresh promise for testing purposes.
|
|
188
|
+
* @returns The snapshot sequence number promise, or undefined if no refresh is in progress
|
|
189
|
+
*/
|
|
190
|
+
public get refreshSnapshotP(): Promise<number> | undefined {
|
|
191
|
+
return this.refreshTracker.promise;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Disposes the refresher and clears the timer.
|
|
196
|
+
*/
|
|
197
|
+
public dispose(): void {
|
|
198
|
+
this.#disposed = true;
|
|
199
|
+
this.refreshTimer?.clear();
|
|
200
|
+
}
|
|
201
|
+
}
|
package/.eslintrc.cjs
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
-
* Licensed under the MIT License.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
module.exports = {
|
|
7
|
-
extends: [require.resolve("@fluidframework/eslint-config-fluid"), "prettier"],
|
|
8
|
-
parserOptions: {
|
|
9
|
-
project: ["./tsconfig.json", "./src/test/tsconfig.json"],
|
|
10
|
-
},
|
|
11
|
-
rules: {
|
|
12
|
-
"@fluid-internal/fluid/no-unchecked-record-access": "warn",
|
|
13
|
-
},
|
|
14
|
-
overrides: [
|
|
15
|
-
{
|
|
16
|
-
// Rules only for test files
|
|
17
|
-
files: ["*.spec.ts", "src/test/**"],
|
|
18
|
-
rules: {
|
|
19
|
-
// Test files are run in node only so additional node libraries can be used.
|
|
20
|
-
"import-x/no-nodejs-modules": ["error", { allow: ["node:assert"] }],
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
],
|
|
24
|
-
};
|