@fluidframework/container-loader 2.74.0 → 2.81.0-374083

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 (63) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/connectionManager.d.ts.map +1 -1
  3. package/dist/connectionManager.js +2 -0
  4. package/dist/connectionManager.js.map +1 -1
  5. package/dist/container.d.ts.map +1 -1
  6. package/dist/container.js +3 -3
  7. package/dist/container.js.map +1 -1
  8. package/dist/deltaManager.d.ts +10 -0
  9. package/dist/deltaManager.d.ts.map +1 -1
  10. package/dist/deltaManager.js +44 -1
  11. package/dist/deltaManager.js.map +1 -1
  12. package/dist/loaderLayerCompatState.d.ts +4 -4
  13. package/dist/loaderLayerCompatState.d.ts.map +1 -1
  14. package/dist/loaderLayerCompatState.js +6 -5
  15. package/dist/loaderLayerCompatState.js.map +1 -1
  16. package/dist/packageVersion.d.ts +1 -1
  17. package/dist/packageVersion.d.ts.map +1 -1
  18. package/dist/packageVersion.js +1 -1
  19. package/dist/packageVersion.js.map +1 -1
  20. package/dist/serializedStateManager.d.ts +10 -15
  21. package/dist/serializedStateManager.d.ts.map +1 -1
  22. package/dist/serializedStateManager.js +20 -83
  23. package/dist/serializedStateManager.js.map +1 -1
  24. package/dist/snapshotRefresher.d.ts +68 -0
  25. package/dist/snapshotRefresher.d.ts.map +1 -0
  26. package/dist/snapshotRefresher.js +167 -0
  27. package/dist/snapshotRefresher.js.map +1 -0
  28. package/eslint.config.mts +4 -4
  29. package/lib/connectionManager.d.ts.map +1 -1
  30. package/lib/connectionManager.js +2 -0
  31. package/lib/connectionManager.js.map +1 -1
  32. package/lib/container.d.ts.map +1 -1
  33. package/lib/container.js +3 -3
  34. package/lib/container.js.map +1 -1
  35. package/lib/deltaManager.d.ts +10 -0
  36. package/lib/deltaManager.d.ts.map +1 -1
  37. package/lib/deltaManager.js +44 -1
  38. package/lib/deltaManager.js.map +1 -1
  39. package/lib/loaderLayerCompatState.d.ts +4 -4
  40. package/lib/loaderLayerCompatState.d.ts.map +1 -1
  41. package/lib/loaderLayerCompatState.js +6 -5
  42. package/lib/loaderLayerCompatState.js.map +1 -1
  43. package/lib/packageVersion.d.ts +1 -1
  44. package/lib/packageVersion.d.ts.map +1 -1
  45. package/lib/packageVersion.js +1 -1
  46. package/lib/packageVersion.js.map +1 -1
  47. package/lib/serializedStateManager.d.ts +10 -15
  48. package/lib/serializedStateManager.d.ts.map +1 -1
  49. package/lib/serializedStateManager.js +21 -84
  50. package/lib/serializedStateManager.js.map +1 -1
  51. package/lib/snapshotRefresher.d.ts +68 -0
  52. package/lib/snapshotRefresher.d.ts.map +1 -0
  53. package/lib/snapshotRefresher.js +163 -0
  54. package/lib/snapshotRefresher.js.map +1 -0
  55. package/package.json +20 -20
  56. package/src/connectionManager.ts +2 -0
  57. package/src/container.ts +3 -6
  58. package/src/deltaManager.ts +54 -1
  59. package/src/loaderLayerCompatState.ts +10 -9
  60. package/src/packageVersion.ts +1 -1
  61. package/src/serializedStateManager.ts +29 -105
  62. package/src/snapshotRefresher.ts +201 -0
  63. package/.eslintrc.cjs +0 -24
@@ -0,0 +1,163 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
6
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
7
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
8
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
9
+ };
10
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
11
+ if (kind === "m") throw new TypeError("Private method is not writable");
12
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
13
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
14
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
15
+ };
16
+ var _RefreshPromiseTracker_promise, _SnapshotRefresher_disposed, _SnapshotRefresher_snapshotRefreshEnabled;
17
+ import { assert, Timer } from "@fluidframework/core-utils/internal";
18
+ import { FetchSource } from "@fluidframework/driver-definitions/internal";
19
+ import { createChildMonitoringContext, } from "@fluidframework/telemetry-utils/internal";
20
+ import { getLatestSnapshotInfo, } from "./serializedStateManager.js";
21
+ class RefreshPromiseTracker {
22
+ get hasPromise() {
23
+ return __classPrivateFieldGet(this, _RefreshPromiseTracker_promise, "f") !== undefined;
24
+ }
25
+ get promise() {
26
+ return __classPrivateFieldGet(this, _RefreshPromiseTracker_promise, "f");
27
+ }
28
+ constructor(catchHandler) {
29
+ this.catchHandler = catchHandler;
30
+ _RefreshPromiseTracker_promise.set(this, void 0);
31
+ }
32
+ setPromise(p) {
33
+ if (this.hasPromise) {
34
+ throw new Error("Cannot start new snapshot refresh while a refresh is already in progress");
35
+ }
36
+ __classPrivateFieldSet(this, _RefreshPromiseTracker_promise, p.finally(() => {
37
+ __classPrivateFieldSet(this, _RefreshPromiseTracker_promise, undefined, "f");
38
+ }), "f");
39
+ p.catch(this.catchHandler);
40
+ }
41
+ }
42
+ _RefreshPromiseTracker_promise = new WeakMap();
43
+ /**
44
+ * Manages periodic refresh of the latest snapshot for a document.
45
+ *
46
+ * `SnapshotRefresher` polls the storage service for the most recent snapshot and, when a newer
47
+ * snapshot is discovered, invokes the provided `onSnapshotRefreshed` callback with the updated
48
+ * snapshot metadata. It is responsible for:
49
+ *
50
+ * - Tracking the most recent snapshot that has been observed.
51
+ * - Scheduling and managing refresh attempts via an internal timer.
52
+ * - Emitting telemetry for successful and failed refresh attempts.
53
+ *
54
+ * The refresh behavior can be configured via constructor arguments, including whether offline
55
+ * loading and the `getSnapshot` API are supported, as well as the refresh timeout. Callers
56
+ * should dispose this instance when snapshot refresh is no longer needed to stop any pending
57
+ * timers and prevent further refresh attempts.
58
+ */
59
+ export class SnapshotRefresher {
60
+ get disposed() {
61
+ return __classPrivateFieldGet(this, _SnapshotRefresher_disposed, "f");
62
+ }
63
+ constructor(subLogger, storageAdapter, offlineLoadEnabled, supportGetSnapshotApi, onSnapshotRefreshed, snapshotRefreshTimeoutMs) {
64
+ this.storageAdapter = storageAdapter;
65
+ this.offlineLoadEnabled = offlineLoadEnabled;
66
+ this.supportGetSnapshotApi = supportGetSnapshotApi;
67
+ this.onSnapshotRefreshed = onSnapshotRefreshed;
68
+ _SnapshotRefresher_disposed.set(this, false);
69
+ this.refreshTracker = new RefreshPromiseTracker(
70
+ // eslint-disable-next-line unicorn/consistent-function-scoping
71
+ (error) => this.mc.logger.sendErrorEvent({
72
+ eventName: "RefreshLatestSnapshotFailed",
73
+ }, error));
74
+ this.snapshotRefreshTimeoutMs = 60 * 60 * 24 * 1000;
75
+ _SnapshotRefresher_snapshotRefreshEnabled.set(this, void 0);
76
+ this.mc = createChildMonitoringContext({
77
+ logger: subLogger,
78
+ namespace: "serializedStateManager",
79
+ });
80
+ this.snapshotRefreshTimeoutMs = snapshotRefreshTimeoutMs ?? this.snapshotRefreshTimeoutMs;
81
+ __classPrivateFieldSet(this, _SnapshotRefresher_snapshotRefreshEnabled, this.offlineLoadEnabled &&
82
+ (this.mc.config.getBoolean("Fluid.Container.enableOfflineSnapshotRefresh") ??
83
+ this.mc.config.getBoolean("Fluid.Container.enableOfflineFull")) === true, "f");
84
+ this.refreshTimer = __classPrivateFieldGet(this, _SnapshotRefresher_snapshotRefreshEnabled, "f")
85
+ ? new Timer(this.snapshotRefreshTimeoutMs, () => this.tryRefreshSnapshot())
86
+ : undefined;
87
+ }
88
+ tryRefreshSnapshot() {
89
+ if (__classPrivateFieldGet(this, _SnapshotRefresher_snapshotRefreshEnabled, "f") &&
90
+ !__classPrivateFieldGet(this, _SnapshotRefresher_disposed, "f") &&
91
+ !this.refreshTracker.hasPromise &&
92
+ this.latestSnapshot === undefined) {
93
+ // Don't block on the refresh snapshot call - it is for the next time we serialize, not booting this incarnation
94
+ this.refreshTracker.setPromise(this.refreshLatestSnapshot(this.supportGetSnapshotApi()));
95
+ }
96
+ }
97
+ /**
98
+ * Fetch the latest snapshot for the container, including delay-loaded groupIds if pendingLocalState was provided and contained any groupIds.
99
+ * Note that this will update the StorageAdapter's cached snapshots for the groupIds (if present)
100
+ *
101
+ * @param supportGetSnapshotApi - a boolean indicating whether to use the fetchISnapshot or fetchISnapshotTree (must be true to fetch by groupIds)
102
+ */
103
+ async refreshLatestSnapshot(supportGetSnapshotApi) {
104
+ this.latestSnapshot = await getLatestSnapshotInfo(this.mc, this.storageAdapter, supportGetSnapshotApi);
105
+ if (__classPrivateFieldGet(this, _SnapshotRefresher_disposed, "f")) {
106
+ return -1;
107
+ }
108
+ // These are loading groupIds that the containerRuntime has requested over its lifetime.
109
+ // We will fetch the latest snapshot for the groupIds, which will update storageAdapter.loadedGroupIdSnapshots's cache
110
+ const downloadedGroupIds = Object.keys(this.storageAdapter.loadedGroupIdSnapshots);
111
+ if (supportGetSnapshotApi && downloadedGroupIds.length > 0) {
112
+ assert(this.storageAdapter.getSnapshot !== undefined, 0x972 /* getSnapshot should exist */);
113
+ // (This is a separate network call from above because it requires work for storage to add a special base groupId)
114
+ const snapshot = await this.storageAdapter.getSnapshot({
115
+ versionId: undefined,
116
+ scenarioName: "getLatestSnapshotInfo",
117
+ cacheSnapshot: false,
118
+ loadingGroupIds: downloadedGroupIds,
119
+ fetchSource: FetchSource.noCache,
120
+ });
121
+ assert(snapshot !== undefined, 0x973 /* Snapshot should exist */);
122
+ }
123
+ // Notify the manager about the fetched snapshot - let it decide what to do with it
124
+ const result = this.latestSnapshot === undefined ? -1 : this.onSnapshotRefreshed(this.latestSnapshot);
125
+ this.refreshTimer?.restart();
126
+ return result;
127
+ }
128
+ /**
129
+ * Clears the latest snapshot after it's been consumed by the manager.
130
+ * This allows the next refresh cycle to proceed.
131
+ */
132
+ clearLatestSnapshot() {
133
+ this.latestSnapshot = undefined;
134
+ }
135
+ /**
136
+ * Starts the refresh timer.
137
+ */
138
+ startTimer() {
139
+ this.refreshTimer?.start();
140
+ }
141
+ /**
142
+ * Restarts the refresh timer.
143
+ */
144
+ restartTimer() {
145
+ this.refreshTimer?.restart();
146
+ }
147
+ /**
148
+ * Gets the current refresh promise for testing purposes.
149
+ * @returns The snapshot sequence number promise, or undefined if no refresh is in progress
150
+ */
151
+ get refreshSnapshotP() {
152
+ return this.refreshTracker.promise;
153
+ }
154
+ /**
155
+ * Disposes the refresher and clears the timer.
156
+ */
157
+ dispose() {
158
+ __classPrivateFieldSet(this, _SnapshotRefresher_disposed, true, "f");
159
+ this.refreshTimer?.clear();
160
+ }
161
+ }
162
+ _SnapshotRefresher_disposed = new WeakMap(), _SnapshotRefresher_snapshotRefreshEnabled = new WeakMap();
163
+ //# sourceMappingURL=snapshotRefresher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshotRefresher.js","sourceRoot":"","sources":["../src/snapshotRefresher.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport type { ITelemetryBaseLogger } from \"@fluidframework/core-interfaces\";\nimport type { IDisposable } from \"@fluidframework/core-interfaces/internal\";\nimport { assert, Timer } from \"@fluidframework/core-utils/internal\";\nimport { FetchSource } from \"@fluidframework/driver-definitions/internal\";\nimport {\n\tcreateChildMonitoringContext,\n\ttype MonitoringContext,\n} from \"@fluidframework/telemetry-utils/internal\";\n\nimport {\n\tgetLatestSnapshotInfo,\n\ttype ISerializedStateManagerDocumentStorageService,\n\ttype ISnapshotInfo,\n} from \"./serializedStateManager.js\";\n\nclass RefreshPromiseTracker {\n\tpublic get hasPromise(): boolean {\n\t\treturn this.#promise !== undefined;\n\t}\n\tpublic get promise(): Promise<number> | undefined {\n\t\treturn this.#promise;\n\t}\n\tconstructor(private readonly catchHandler: (error: Error) => void) {}\n\n\t#promise: Promise<number> | undefined;\n\tsetPromise(p: Promise<number>): void {\n\t\tif (this.hasPromise) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Cannot start new snapshot refresh while a refresh is already in progress\",\n\t\t\t);\n\t\t}\n\t\tthis.#promise = p.finally(() => {\n\t\t\tthis.#promise = undefined;\n\t\t});\n\t\tp.catch(this.catchHandler);\n\t}\n}\n\n/**\n * Manages periodic refresh of the latest snapshot for a document.\n *\n * `SnapshotRefresher` polls the storage service for the most recent snapshot and, when a newer\n * snapshot is discovered, invokes the provided `onSnapshotRefreshed` callback with the updated\n * snapshot metadata. It is responsible for:\n *\n * - Tracking the most recent snapshot that has been observed.\n * - Scheduling and managing refresh attempts via an internal timer.\n * - Emitting telemetry for successful and failed refresh attempts.\n *\n * The refresh behavior can be configured via constructor arguments, including whether offline\n * loading and the `getSnapshot` API are supported, as well as the refresh timeout. Callers\n * should dispose this instance when snapshot refresh is no longer needed to stop any pending\n * timers and prevent further refresh attempts.\n */\nexport class SnapshotRefresher implements IDisposable {\n\tprivate readonly mc: MonitoringContext;\n\tprivate latestSnapshot: ISnapshotInfo | undefined;\n\t#disposed: boolean = false;\n\n\tpublic get disposed(): boolean {\n\t\treturn this.#disposed;\n\t}\n\n\tprivate readonly refreshTracker = new RefreshPromiseTracker(\n\t\t// eslint-disable-next-line unicorn/consistent-function-scoping\n\t\t(error) =>\n\t\t\tthis.mc.logger.sendErrorEvent(\n\t\t\t\t{\n\t\t\t\t\teventName: \"RefreshLatestSnapshotFailed\",\n\t\t\t\t},\n\t\t\t\terror,\n\t\t\t),\n\t);\n\tprivate readonly refreshTimer: Timer | undefined;\n\tprivate readonly snapshotRefreshTimeoutMs: number = 60 * 60 * 24 * 1000;\n\treadonly #snapshotRefreshEnabled: boolean;\n\n\tconstructor(\n\t\tsubLogger: ITelemetryBaseLogger,\n\t\tprivate readonly storageAdapter: ISerializedStateManagerDocumentStorageService,\n\t\tprivate readonly offlineLoadEnabled: boolean,\n\t\tprivate readonly supportGetSnapshotApi: () => boolean,\n\t\tprivate readonly onSnapshotRefreshed: (snapshot: ISnapshotInfo) => number,\n\t\tsnapshotRefreshTimeoutMs?: number,\n\t) {\n\t\tthis.mc = createChildMonitoringContext({\n\t\t\tlogger: subLogger,\n\t\t\tnamespace: \"serializedStateManager\",\n\t\t});\n\n\t\tthis.snapshotRefreshTimeoutMs = snapshotRefreshTimeoutMs ?? this.snapshotRefreshTimeoutMs;\n\n\t\tthis.#snapshotRefreshEnabled =\n\t\t\tthis.offlineLoadEnabled &&\n\t\t\t(this.mc.config.getBoolean(\"Fluid.Container.enableOfflineSnapshotRefresh\") ??\n\t\t\t\tthis.mc.config.getBoolean(\"Fluid.Container.enableOfflineFull\")) === true;\n\n\t\tthis.refreshTimer = this.#snapshotRefreshEnabled\n\t\t\t? new Timer(this.snapshotRefreshTimeoutMs, () => this.tryRefreshSnapshot())\n\t\t\t: undefined;\n\t}\n\n\tpublic tryRefreshSnapshot(): void {\n\t\tif (\n\t\t\tthis.#snapshotRefreshEnabled &&\n\t\t\t!this.#disposed &&\n\t\t\t!this.refreshTracker.hasPromise &&\n\t\t\tthis.latestSnapshot === undefined\n\t\t) {\n\t\t\t// Don't block on the refresh snapshot call - it is for the next time we serialize, not booting this incarnation\n\t\t\tthis.refreshTracker.setPromise(this.refreshLatestSnapshot(this.supportGetSnapshotApi()));\n\t\t}\n\t}\n\n\t/**\n\t * Fetch the latest snapshot for the container, including delay-loaded groupIds if pendingLocalState was provided and contained any groupIds.\n\t * Note that this will update the StorageAdapter's cached snapshots for the groupIds (if present)\n\t *\n\t * @param supportGetSnapshotApi - a boolean indicating whether to use the fetchISnapshot or fetchISnapshotTree (must be true to fetch by groupIds)\n\t */\n\tprivate async refreshLatestSnapshot(supportGetSnapshotApi: boolean): Promise<number> {\n\t\tthis.latestSnapshot = await getLatestSnapshotInfo(\n\t\t\tthis.mc,\n\t\t\tthis.storageAdapter,\n\t\t\tsupportGetSnapshotApi,\n\t\t);\n\n\t\tif (this.#disposed) {\n\t\t\treturn -1;\n\t\t}\n\n\t\t// These are loading groupIds that the containerRuntime has requested over its lifetime.\n\t\t// We will fetch the latest snapshot for the groupIds, which will update storageAdapter.loadedGroupIdSnapshots's cache\n\t\tconst downloadedGroupIds = Object.keys(this.storageAdapter.loadedGroupIdSnapshots);\n\t\tif (supportGetSnapshotApi && downloadedGroupIds.length > 0) {\n\t\t\tassert(\n\t\t\t\tthis.storageAdapter.getSnapshot !== undefined,\n\t\t\t\t0x972 /* getSnapshot should exist */,\n\t\t\t);\n\t\t\t// (This is a separate network call from above because it requires work for storage to add a special base groupId)\n\t\t\tconst snapshot = await this.storageAdapter.getSnapshot({\n\t\t\t\tversionId: undefined,\n\t\t\t\tscenarioName: \"getLatestSnapshotInfo\",\n\t\t\t\tcacheSnapshot: false,\n\t\t\t\tloadingGroupIds: downloadedGroupIds,\n\t\t\t\tfetchSource: FetchSource.noCache,\n\t\t\t});\n\t\t\tassert(snapshot !== undefined, 0x973 /* Snapshot should exist */);\n\t\t}\n\n\t\t// Notify the manager about the fetched snapshot - let it decide what to do with it\n\t\tconst result =\n\t\t\tthis.latestSnapshot === undefined ? -1 : this.onSnapshotRefreshed(this.latestSnapshot);\n\n\t\tthis.refreshTimer?.restart();\n\t\treturn result;\n\t}\n\n\t/**\n\t * Clears the latest snapshot after it's been consumed by the manager.\n\t * This allows the next refresh cycle to proceed.\n\t */\n\tpublic clearLatestSnapshot(): void {\n\t\tthis.latestSnapshot = undefined;\n\t}\n\n\t/**\n\t * Starts the refresh timer.\n\t */\n\tpublic startTimer(): void {\n\t\tthis.refreshTimer?.start();\n\t}\n\n\t/**\n\t * Restarts the refresh timer.\n\t */\n\tpublic restartTimer(): void {\n\t\tthis.refreshTimer?.restart();\n\t}\n\n\t/**\n\t * Gets the current refresh promise for testing purposes.\n\t * @returns The snapshot sequence number promise, or undefined if no refresh is in progress\n\t */\n\tpublic get refreshSnapshotP(): Promise<number> | undefined {\n\t\treturn this.refreshTracker.promise;\n\t}\n\n\t/**\n\t * Disposes the refresher and clears the timer.\n\t */\n\tpublic dispose(): void {\n\t\tthis.#disposed = true;\n\t\tthis.refreshTimer?.clear();\n\t}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/container-loader",
3
- "version": "2.74.0",
3
+ "version": "2.81.0-374083",
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.74.0",
133
- "@fluidframework/container-definitions": "~2.74.0",
134
- "@fluidframework/core-interfaces": "~2.74.0",
135
- "@fluidframework/core-utils": "~2.74.0",
136
- "@fluidframework/driver-definitions": "~2.74.0",
137
- "@fluidframework/driver-utils": "~2.74.0",
138
- "@fluidframework/telemetry-utils": "~2.74.0",
132
+ "@fluid-internal/client-utils": "2.81.0-374083",
133
+ "@fluidframework/container-definitions": "2.81.0-374083",
134
+ "@fluidframework/core-interfaces": "2.81.0-374083",
135
+ "@fluidframework/core-utils": "2.81.0-374083",
136
+ "@fluidframework/driver-definitions": "2.81.0-374083",
137
+ "@fluidframework/driver-utils": "2.81.0-374083",
138
+ "@fluidframework/telemetry-utils": "2.81.0-374083",
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",
@@ -144,16 +144,16 @@
144
144
  "uuid": "^11.1.0"
145
145
  },
146
146
  "devDependencies": {
147
- "@arethetypeswrong/cli": "^0.17.1",
147
+ "@arethetypeswrong/cli": "^0.18.2",
148
148
  "@biomejs/biome": "~1.9.3",
149
- "@fluid-internal/client-utils": "~2.74.0",
150
- "@fluid-internal/mocha-test-setup": "~2.74.0",
151
- "@fluid-private/test-loader-utils": "~2.74.0",
152
- "@fluid-tools/build-cli": "^0.61.0",
149
+ "@fluid-internal/client-utils": "2.81.0-374083",
150
+ "@fluid-internal/mocha-test-setup": "2.81.0-374083",
151
+ "@fluid-private/test-loader-utils": "2.81.0-374083",
152
+ "@fluid-tools/build-cli": "^0.63.0",
153
153
  "@fluidframework/build-common": "^2.0.3",
154
- "@fluidframework/build-tools": "^0.61.0",
155
- "@fluidframework/container-loader-previous": "npm:@fluidframework/container-loader@2.73.0",
156
- "@fluidframework/eslint-config-fluid": "~2.74.0",
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-374083",
157
157
  "@microsoft/api-extractor": "7.52.11",
158
158
  "@types/debug": "^4.1.5",
159
159
  "@types/double-ended-queue": "^2.1.0",
@@ -162,14 +162,14 @@
162
162
  "@types/sinon": "^17.0.3",
163
163
  "@types/ungap__structured-clone": "^1.2.0",
164
164
  "c8": "^10.1.3",
165
- "concurrently": "^8.2.1",
165
+ "concurrently": "^9.2.1",
166
166
  "copyfiles": "^2.4.1",
167
- "cross-env": "^7.0.3",
168
- "eslint": "~8.57.1",
167
+ "cross-env": "^10.1.0",
168
+ "eslint": "~9.39.1",
169
169
  "jiti": "^2.6.1",
170
170
  "mocha": "^10.8.2",
171
171
  "mocha-multi-reporters": "^1.5.1",
172
- "rimraf": "^4.4.0",
172
+ "rimraf": "^6.1.2",
173
173
  "sinon": "^18.0.1",
174
174
  "typescript": "~5.4.5"
175
175
  },
@@ -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,
@@ -751,7 +751,7 @@ export class Container
751
751
  validateDriverCompatibility(
752
752
  maybeDriverCompatDetails.ILayerCompatDetails,
753
753
  (error) => {} /* disposeFn */, // There is nothing to dispose here, so just ignore the error.
754
- subLogger,
754
+ createChildMonitoringContext({ logger: subLogger, namespace: "Container" }),
755
755
  );
756
756
 
757
757
  this.connectionTransitionTimes[ConnectionState.Disconnected] = performanceNow();
@@ -2440,10 +2440,7 @@ export class Container
2440
2440
 
2441
2441
  // Validate that the Runtime is compatible with this Loader.
2442
2442
  const maybeRuntimeCompatDetails = runtime as FluidObject<ILayerCompatDetails>;
2443
- validateRuntimeCompatibility(
2444
- maybeRuntimeCompatDetails.ILayerCompatDetails,
2445
- this.mc.logger,
2446
- );
2443
+ validateRuntimeCompatibility(maybeRuntimeCompatDetails.ILayerCompatDetails, this.mc);
2447
2444
 
2448
2445
  this._runtime = runtime;
2449
2446
  this._lifecycleEvents.emit("runtimeInstantiated");
@@ -30,6 +30,7 @@ import {
30
30
  type ISignalMessage,
31
31
  type IClientDetails,
32
32
  type IClientConfiguration,
33
+ type ISequencedDocumentSystemMessage,
33
34
  } from "@fluidframework/driver-definitions/internal";
34
35
  import { NonRetryableError, isRuntimeMessage } from "@fluidframework/driver-utils/internal";
35
36
  import {
@@ -189,6 +190,12 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
189
190
  private lastProcessedSequenceNumber: number = 0;
190
191
  private lastProcessedMessage: ISequencedDocumentMessage | undefined;
191
192
 
193
+ /**
194
+ * Map of clientId to the last observed message from that client. This is used to validate
195
+ * that clientSequenceNumbers are always increasing for a given clientId.
196
+ */
197
+ private readonly lastObservedMessageByClient = new Map<string, ISequencedDocumentMessage>();
198
+
192
199
  /**
193
200
  * Count the number of noops sent by the client which may not be acked
194
201
  */
@@ -661,6 +668,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
661
668
  throw new Error("Delta manager is not attached");
662
669
  }
663
670
 
671
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- using ??= could change behavior if value is falsy
664
672
  if (this.deltaStorage === undefined) {
665
673
  this.deltaStorage = await docService.connectToDeltaStorage();
666
674
  }
@@ -861,7 +869,50 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
861
869
  // Also payload goes to telemetry, so no content or anything else that shouldn't be logged for privacy reasons
862
870
  // Note: It's possible for a duplicate op to be broadcasted and have everything the same except the timestamp.
863
871
  private comparableMessagePayload(m: ISequencedDocumentMessage): string {
864
- return `${m.clientId}-${m.type}-${m.minimumSequenceNumber}-${m.referenceSequenceNumber}-${m.timestamp}`;
872
+ return `${m.clientId}-${m.type}-${m.sequenceNumber}-${m.minimumSequenceNumber}-${m.referenceSequenceNumber}-${m.timestamp}`;
873
+ }
874
+
875
+ /**
876
+ * Validates that the clientSequenceNumber for a given clientId is always increasing.
877
+ * @param message - The message to validate.
878
+ */
879
+ private validateClientSequenceNumberConsistency(message: ISequencedDocumentMessage): void {
880
+ // Once client leaves, we no longer need to track its last observed message as the client won't send messages.
881
+ if (message.type === MessageType.ClientLeave) {
882
+ const systemLeaveMessage = message as ISequencedDocumentSystemMessage;
883
+ const clientId = JSON.parse(systemLeaveMessage.data) as string;
884
+ this.lastObservedMessageByClient.delete(clientId);
885
+ }
886
+
887
+ if (message.clientId === null) {
888
+ return;
889
+ }
890
+
891
+ const lastObservedClientMessage = this.lastObservedMessageByClient.get(message.clientId);
892
+ if (
893
+ lastObservedClientMessage !== undefined &&
894
+ message.clientSequenceNumber <= lastObservedClientMessage.clientSequenceNumber
895
+ ) {
896
+ // This looks like a data corruption issue where the clientSequenceNumber for a given clientId is not
897
+ // increasing. The Fluid Service ensures that it only processes ops with contiguous increasing
898
+ // clientSequenceNumbers for a given clientId.
899
+ // So, if we see this error, it is likely a service issue.
900
+ // One example of this is if the service sequences the same op more than once. In this case, all the
901
+ // properties except the sequenceNumber will be the same.
902
+ // Note that we are not checking for gaps in clientSequenceNumber because very old clients may have gaps
903
+ // as per the op stream in the snapshot tests under test/snapshots/content.
904
+ const error = new DataCorruptionError(
905
+ "Found two messages with non-increasing clientSequenceNumber for a given client. Likely to be a service issue",
906
+ {
907
+ clientId: this.connectionManager.clientId,
908
+ sequenceNumber: message.sequenceNumber,
909
+ message1: this.comparableMessagePayload(lastObservedClientMessage),
910
+ message2: this.comparableMessagePayload(message),
911
+ },
912
+ );
913
+ this.close(error);
914
+ }
915
+ this.lastObservedMessageByClient.set(message.clientId, message);
865
916
  }
866
917
 
867
918
  private enqueueMessages(
@@ -914,6 +965,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
914
965
  duplicate++;
915
966
  } else if (message.sequenceNumber !== prev + 1) {
916
967
  gap++;
968
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- using ??= could change behavior if value is falsy
917
969
  if (firstMissing === undefined) {
918
970
  firstMissing = prev + 1;
919
971
  }
@@ -1002,6 +1054,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
1002
1054
  }
1003
1055
  }
1004
1056
  } else if (message.sequenceNumber === this.lastQueuedSequenceNumber + 1) {
1057
+ this.validateClientSequenceNumberConsistency(message);
1005
1058
  this.lastQueuedSequenceNumber = message.sequenceNumber;
1006
1059
  this.previouslyProcessedMessage = message;
1007
1060
  this._inbound.push(message);
@@ -3,14 +3,15 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import type {
7
- ILayerCompatDetails,
8
- ILayerCompatSupportRequirements,
6
+ import {
7
+ generation,
8
+ type ILayerCompatDetails,
9
+ type ILayerCompatSupportRequirements,
9
10
  } from "@fluid-internal/client-utils";
10
11
  import type { ICriticalContainerError } from "@fluidframework/container-definitions";
11
12
  import {
12
13
  validateLayerCompatibility,
13
- type ITelemetryLoggerExt,
14
+ type MonitoringContext,
14
15
  } from "@fluidframework/telemetry-utils/internal";
15
16
 
16
17
  import { pkgVersion } from "./packageVersion.js";
@@ -27,7 +28,7 @@ export const loaderCoreCompatDetails = {
27
28
  /**
28
29
  * The current generation of the Loader layer.
29
30
  */
30
- generation: 3,
31
+ generation,
31
32
  };
32
33
 
33
34
  /**
@@ -80,7 +81,7 @@ export const driverSupportRequirementsForLoader: ILayerCompatSupportRequirements
80
81
  */
81
82
  export function validateRuntimeCompatibility(
82
83
  maybeRuntimeCompatDetails: ILayerCompatDetails | undefined,
83
- logger: ITelemetryLoggerExt,
84
+ mc: MonitoringContext,
84
85
  ): void {
85
86
  validateLayerCompatibility(
86
87
  "loader",
@@ -89,7 +90,7 @@ export function validateRuntimeCompatibility(
89
90
  runtimeSupportRequirementsForLoader,
90
91
  maybeRuntimeCompatDetails,
91
92
  () => {} /* disposeFn - no op. This will be handled by the caller */,
92
- logger,
93
+ mc,
93
94
  );
94
95
  }
95
96
 
@@ -100,7 +101,7 @@ export function validateRuntimeCompatibility(
100
101
  export function validateDriverCompatibility(
101
102
  maybeDriverCompatDetails: ILayerCompatDetails | undefined,
102
103
  disposeFn: (error?: ICriticalContainerError) => void,
103
- logger: ITelemetryLoggerExt,
104
+ mc: MonitoringContext,
104
105
  ): void {
105
106
  validateLayerCompatibility(
106
107
  "loader",
@@ -109,6 +110,6 @@ export function validateDriverCompatibility(
109
110
  driverSupportRequirementsForLoader,
110
111
  maybeDriverCompatDetails,
111
112
  disposeFn,
112
- logger,
113
+ mc,
113
114
  );
114
115
  }
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-loader";
9
- export const pkgVersion = "2.74.0";
9
+ export const pkgVersion = "2.81.0-374083";