@fluidframework/driver-web-cache 2.101.0 → 2.102.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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # @fluidframework/driver-web-cache
2
2
 
3
+ ## 2.102.0
4
+
5
+ Dependency updates only.
6
+
3
7
  ## 2.101.0
4
8
 
5
9
  Dependency updates only.
package/README.md CHANGED
@@ -66,6 +66,63 @@ new FluidCache({
66
66
  for a cache entry to be used. This flag does not control when cached content is deleted since different scenarios and
67
67
  applications may have different staleness thresholds for the same data.
68
68
 
69
+ ## Atomic updates (`update`)
70
+
71
+ `FluidCache` exposes an `update` method that performs an atomic read-modify-write. The currently-cached
72
+ value is read and the updater callback decides whether — and what — to write, inside a single IndexedDB
73
+ `readwrite` transaction. This gives consistent update semantics across consumers sharing the same
74
+ underlying IndexedDB instance (e.g. multiple browser tabs racing to persist offline pending state).
75
+
76
+ ```typescript
77
+ // Conditional overwrite (active tab wins): only write if our revision is higher.
78
+ const wrote = await fluidCache.update(entry, (existing, set) => {
79
+ const existingRev = (existing as { rev?: number } | undefined)?.rev ?? -1;
80
+ if (mine.rev > existingRev) {
81
+ set(mine);
82
+ }
83
+ });
84
+
85
+ // Read-modify-write: increment a counter atomically.
86
+ await fluidCache.update(entry, (existing, set) => {
87
+ const prev = (existing as { count: number } | undefined)?.count ?? 0;
88
+ set({ count: prev + 1 });
89
+ });
90
+ ```
91
+
92
+ The `updater` callback is invoked with `(existing, set)`. `existing` is `undefined` when the cached
93
+ row would be invisible to `get` — that is, no entry exists for the key, the existing entry belongs to
94
+ a different partition, or the existing entry is older than `maxCacheItemAge`. To commit a write, call
95
+ `set(value)`; to leave the cache untouched, return without calling `set`.
96
+
97
+ Calling `set(undefined)` removes the row at the key (equivalent to `removeEntry` inside the same
98
+ atomic transaction). `get` already collapses "no entry" and "entry stored as undefined" into the same
99
+ observable result, so the delete-on-undefined semantics gives callers an atomic conditional-delete
100
+ without ambiguity for any meaningful use case.
101
+
102
+ The updater itself must be synchronous and `set` must be called from within it. IndexedDB
103
+ transactions auto-close on any non-IDB await, which would silently break the atomicity that makes
104
+ the update correct. Two guards make misuse loud rather than silent: calling `set` after the updater
105
+ has returned throws a `UsageError` at the call site (so deferred-`set` patterns like invoking it
106
+ from a `setTimeout` fail noisily); returning a thenable — for example, declaring the updater
107
+ `async` — is detected after the updater returns, aborts the transaction, and is logged under
108
+ `FluidCacheUpdateCallbackError`. If the updater calls `set` more than once, the last value wins.
109
+ If the updater throws — including after calling `set` — the transaction is aborted and the existing
110
+ row is preserved.
111
+
112
+ When `set` is called, the write (or delete) atomically replaces whatever row exists at the key,
113
+ including cross-partition or stale rows the updater saw as `undefined` (matching the unconditional
114
+ overwrite behavior of `put`). Callers that must preserve cross-partition rows should not use
115
+ `update`.
116
+
117
+ `update` returns `true` if `set` was called and the write (or delete) committed, and `false` if the
118
+ updater returned without calling `set`, threw, or an IDB error occurred.
119
+
120
+ **Compare-and-set callers:** the `false` return collapses three distinct outcomes — the updater
121
+ returned without calling `set` (a lost race), the updater threw (including the async-updater misuse
122
+ case), and the IDB write itself failed. Callers that need to distinguish these must consult
123
+ telemetry: updater-side failures log under `FluidCacheUpdateCallbackError`; IDB-write failures log
124
+ under `FluidCachePutError`. A lost compare-and-set race is not logged.
125
+
69
126
  ## Clearing cache entries
70
127
 
71
128
  Whenever any Fluid content is loaded with the web cache enabled, a task is scheduled to clear out all "stale" cache
@@ -18,6 +18,7 @@ export class FluidCache implements IPersistedCache {
18
18
  removeEntries(file: IFileEntry): Promise<void>;
19
19
  // (undocumented)
20
20
  removeEntry(entry: ICacheEntry): Promise<void>;
21
+ update(entry: ICacheEntry, updater: (existing: unknown, set: (value: unknown) => void) => void): Promise<boolean>;
21
22
  }
22
23
 
23
24
  // @beta @legacy (undocumented)
@@ -52,5 +52,69 @@ export declare class FluidCache implements IPersistedCache {
52
52
  get(cacheEntry: ICacheEntry): Promise<any>;
53
53
  private getItemFromCache;
54
54
  put(entry: ICacheEntry, value: any): Promise<void>;
55
+ /**
56
+ * Atomically reads the existing cached entry, hands it to `updater`, and writes a
57
+ * new value iff `updater` calls the supplied `set` callback. The read and the
58
+ * conditional write happen inside a single IndexedDB `readwrite` transaction, so
59
+ * the decision sees a consistent view across consumers sharing the same underlying
60
+ * IndexedDB instance (for example, multiple browser tabs racing to persist pending
61
+ * state).
62
+ *
63
+ * @remarks
64
+ * The implementation uses `transaction.store.get` + `transaction.store.put` rather
65
+ * than an IDB cursor. Both run inside the same `readwrite` transaction, so the
66
+ * atomicity guarantee is identical, and the get/put pair is materially simpler
67
+ * to reason about for a single-key update. A cursor would be the right tool if we
68
+ * needed to iterate or range-scan; for a known key we don't.
69
+ *
70
+ * @param entry - cache entry; identifies the file and the key within that file.
71
+ * @param updater - synchronous callback invoked with `(existing, set)`.
72
+ * `existing` is the currently-cached value, or `undefined` when the cached row is
73
+ * invisible under the same rules `get` applies: no entry exists for the key, the
74
+ * existing entry belongs to a different partition, or the existing entry is older
75
+ * than `maxCacheItemAge`. The updater can derive the new value from `existing`
76
+ * (read-modify-write) or ignore it entirely. To commit a write, call `set(value)`;
77
+ * to leave the cache untouched, return without calling `set`. Stored via IndexedDB
78
+ * structured clone, with the same value requirements as {@link FluidCache.put} —
79
+ * not restricted to JSON-serializable values.
80
+ *
81
+ * Calling `set(undefined)` removes the row at the key (equivalent to
82
+ * {@link FluidCache.removeEntry} inside the same atomic transaction). `get`
83
+ * already collapses "no entry" and "entry stored as undefined" into the same
84
+ * observable result, so the delete-on-undefined semantics gives callers an
85
+ * atomic conditional-delete without ambiguity for any meaningful use case.
86
+ *
87
+ * The updater itself must be synchronous and `set` must be called from within it.
88
+ * IndexedDB transactions auto-close on any non-IDB await, which would silently
89
+ * break the atomicity that makes the update correct. Two guards make misuse
90
+ * loud rather than silent: calling `set` after `updater` has returned throws a
91
+ * `UsageError` at the call site; returning a thenable (e.g. an `async` updater)
92
+ * is detected after `updater` returns, aborts the transaction, and is logged
93
+ * under `FluidCacheUpdateCallbackError`. If `updater` calls `set` more than
94
+ * once, the last value wins.
95
+ *
96
+ * When `set` is called, the write (or delete) atomically replaces whatever row
97
+ * exists at the key, including cross-partition or stale rows that the updater
98
+ * saw as `undefined`. This matches the unconditional overwrite behavior of
99
+ * `put`. Callers that must preserve cross-partition rows should not use `update`.
100
+ *
101
+ * Exceptions thrown by `updater` are logged under the dedicated
102
+ * `FluidCacheUpdateCallbackError` telemetry event (distinct from IDB write errors)
103
+ * and surfaced to the caller as a `false` return value, after aborting the
104
+ * transaction so the existing row is preserved — even if `set` was called before
105
+ * the throw.
106
+ *
107
+ * Compare-and-set callers: a `false` return collapses three distinct outcomes —
108
+ * the updater returned without calling `set`, the updater threw (including the
109
+ * async-updater misuse case above), and the IDB write itself failed. Callers
110
+ * that need to distinguish these must consult telemetry: updater-side failures
111
+ * are logged under `FluidCacheUpdateCallbackError`; IDB-write failures are
112
+ * logged under `FluidCachePutError`. A lost compare-and-set race (the updater
113
+ * returned without calling `set`) is not logged.
114
+ * @returns `true` if `updater` called `set` and the write committed; `false` if
115
+ * `updater` returned without calling `set`, threw, or an IDB error occurred. IDB
116
+ * errors are logged and not thrown, matching the behavior of `put`.
117
+ */
118
+ update(entry: ICacheEntry, updater: (existing: unknown, set: (value: unknown) => void) => void): Promise<boolean>;
55
119
  }
56
120
  //# sourceMappingURL=FluidCache.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FluidCache.d.ts","sourceRoot":"","sources":["../src/FluidCache.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAE5E,OAAO,KAAK,EACX,eAAe,EACf,UAAU,EACV,WAAW,EACX,MAAM,6CAA6C,CAAC;AA4BrD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC;;;;;;OAMG;IAEH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B;;OAEG;IACH,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAE9B;;;OAGG;IACH,eAAe,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,qBAAa,UAAW,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAE5C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAgB;IAE7C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAiB;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,EAAE,CAA+C;IACzD,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,YAAY,CAAc;gBAEtB,MAAM,EAAE,gBAAgB;YAqFtB,MAAM;IAwCpB,OAAO,CAAC,OAAO;IAMF,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB9C,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB9C,GAAG,CAAC,UAAU,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;YAmBzC,gBAAgB;IAiDjB,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;CAgC/D"}
1
+ {"version":3,"file":"FluidCache.d.ts","sourceRoot":"","sources":["../src/FluidCache.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAE5E,OAAO,KAAK,EACX,eAAe,EACf,UAAU,EACV,WAAW,EACX,MAAM,6CAA6C,CAAC;AA4BrD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC;;;;;;OAMG;IAEH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B;;OAEG;IACH,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAE9B;;;OAGG;IACH,eAAe,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,qBAAa,UAAW,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAE5C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAgB;IAE7C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAiB;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,EAAE,CAA+C;IACzD,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,YAAY,CAAc;gBAEtB,MAAM,EAAE,gBAAgB;YAqFtB,MAAM;IAwCpB,OAAO,CAAC,OAAO;IAMF,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB9C,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB9C,GAAG,CAAC,UAAU,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;YAmBzC,gBAAgB;IAiDjB,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAiC/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8DG;IACU,MAAM,CAClB,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,GACjE,OAAO,CAAC,OAAO,CAAC;CA+GnB"}
@@ -247,6 +247,167 @@ class FluidCache {
247
247
  this.closeDb(db);
248
248
  }
249
249
  }
250
+ /**
251
+ * Atomically reads the existing cached entry, hands it to `updater`, and writes a
252
+ * new value iff `updater` calls the supplied `set` callback. The read and the
253
+ * conditional write happen inside a single IndexedDB `readwrite` transaction, so
254
+ * the decision sees a consistent view across consumers sharing the same underlying
255
+ * IndexedDB instance (for example, multiple browser tabs racing to persist pending
256
+ * state).
257
+ *
258
+ * @remarks
259
+ * The implementation uses `transaction.store.get` + `transaction.store.put` rather
260
+ * than an IDB cursor. Both run inside the same `readwrite` transaction, so the
261
+ * atomicity guarantee is identical, and the get/put pair is materially simpler
262
+ * to reason about for a single-key update. A cursor would be the right tool if we
263
+ * needed to iterate or range-scan; for a known key we don't.
264
+ *
265
+ * @param entry - cache entry; identifies the file and the key within that file.
266
+ * @param updater - synchronous callback invoked with `(existing, set)`.
267
+ * `existing` is the currently-cached value, or `undefined` when the cached row is
268
+ * invisible under the same rules `get` applies: no entry exists for the key, the
269
+ * existing entry belongs to a different partition, or the existing entry is older
270
+ * than `maxCacheItemAge`. The updater can derive the new value from `existing`
271
+ * (read-modify-write) or ignore it entirely. To commit a write, call `set(value)`;
272
+ * to leave the cache untouched, return without calling `set`. Stored via IndexedDB
273
+ * structured clone, with the same value requirements as {@link FluidCache.put} —
274
+ * not restricted to JSON-serializable values.
275
+ *
276
+ * Calling `set(undefined)` removes the row at the key (equivalent to
277
+ * {@link FluidCache.removeEntry} inside the same atomic transaction). `get`
278
+ * already collapses "no entry" and "entry stored as undefined" into the same
279
+ * observable result, so the delete-on-undefined semantics gives callers an
280
+ * atomic conditional-delete without ambiguity for any meaningful use case.
281
+ *
282
+ * The updater itself must be synchronous and `set` must be called from within it.
283
+ * IndexedDB transactions auto-close on any non-IDB await, which would silently
284
+ * break the atomicity that makes the update correct. Two guards make misuse
285
+ * loud rather than silent: calling `set` after `updater` has returned throws a
286
+ * `UsageError` at the call site; returning a thenable (e.g. an `async` updater)
287
+ * is detected after `updater` returns, aborts the transaction, and is logged
288
+ * under `FluidCacheUpdateCallbackError`. If `updater` calls `set` more than
289
+ * once, the last value wins.
290
+ *
291
+ * When `set` is called, the write (or delete) atomically replaces whatever row
292
+ * exists at the key, including cross-partition or stale rows that the updater
293
+ * saw as `undefined`. This matches the unconditional overwrite behavior of
294
+ * `put`. Callers that must preserve cross-partition rows should not use `update`.
295
+ *
296
+ * Exceptions thrown by `updater` are logged under the dedicated
297
+ * `FluidCacheUpdateCallbackError` telemetry event (distinct from IDB write errors)
298
+ * and surfaced to the caller as a `false` return value, after aborting the
299
+ * transaction so the existing row is preserved — even if `set` was called before
300
+ * the throw.
301
+ *
302
+ * Compare-and-set callers: a `false` return collapses three distinct outcomes —
303
+ * the updater returned without calling `set`, the updater threw (including the
304
+ * async-updater misuse case above), and the IDB write itself failed. Callers
305
+ * that need to distinguish these must consult telemetry: updater-side failures
306
+ * are logged under `FluidCacheUpdateCallbackError`; IDB-write failures are
307
+ * logged under `FluidCachePutError`. A lost compare-and-set race (the updater
308
+ * returned without calling `set`) is not logged.
309
+ * @returns `true` if `updater` called `set` and the write committed; `false` if
310
+ * `updater` returned without calling `set`, threw, or an IDB error occurred. IDB
311
+ * errors are logged and not thrown, matching the behavior of `put`.
312
+ */
313
+ async update(entry, updater) {
314
+ let db;
315
+ try {
316
+ db = await this.openDb();
317
+ const key = (0, internal_2.getKeyForCacheEntry)(entry);
318
+ const transaction = db.transaction(FluidCacheIndexedDb_js_1.FluidDriverObjectStoreName, "readwrite");
319
+ const existing = await transaction.store.get(key);
320
+ // Surface the cached value to the updater only when the existing entry is
321
+ // visible under the same rules `get` applies: same partition and not older
322
+ // than `maxCacheItemAge`. Cross-partition and stale entries are treated as
323
+ // absent so the updater sees the same view it would under `get`+`put`.
324
+ const existingVisible = existing?.partitionKey === this.partitionKey &&
325
+ Date.now() - existing.createdTimeMs <= this.maxCacheItemAge;
326
+ const existingValue = existingVisible ? existing?.cachedObject : undefined;
327
+ // `set` is a synchronous-only commit signal. We capture the last-supplied
328
+ // value (multi-call: last wins) and a "called" flag so the value being set
329
+ // to `undefined` still counts as a write. After `updater` returns we flip
330
+ // `updaterReturned` to true; any subsequent `set` call throws a `UsageError`
331
+ // at that call site so callers who try to defer the commit (e.g. from a
332
+ // `setTimeout`) see the misuse rather than silently writing into a closed
333
+ // transaction.
334
+ let valueToWrite;
335
+ let setCalled = false;
336
+ let updaterReturned = false;
337
+ const set = (value) => {
338
+ if (updaterReturned) {
339
+ throw new internal_3.UsageError("FluidCache.update: set called after updater returned");
340
+ }
341
+ valueToWrite = value;
342
+ setCalled = true;
343
+ };
344
+ // Invoke the updater in its own try/catch so a host-supplied callback
345
+ // throwing does not get logged under `FluidCachePutError` (which is for
346
+ // IDB-write failures). On updater throw we abort the transaction so the
347
+ // existing row is preserved — even if `set` was called before the throw —
348
+ // log under the updater-specific event, and return `false` (matching the
349
+ // documented "errors are logged, not thrown" contract).
350
+ try {
351
+ const updaterResult = updater(existingValue, set);
352
+ updaterReturned = true;
353
+ // Reject async updaters: TypeScript structurally accepts
354
+ // `async (...) => Promise<void>` for the declared `() => void` parameter
355
+ // type, but an async updater that calls `set` synchronously and then
356
+ // awaits would let the IDB write commit before its eventual rejection
357
+ // surfaced — contradicting the "throw aborts the transaction" contract.
358
+ // Detect a thenable return and treat it as misuse symmetric with the
359
+ // late-`set` guard.
360
+ if ((0, internal_1.isPromiseLike)(updaterResult)) {
361
+ throw new internal_3.UsageError("FluidCache.update: updater must be synchronous (returned a thenable)");
362
+ }
363
+ }
364
+ catch (updaterError) {
365
+ updaterReturned = true;
366
+ transaction.abort();
367
+ // Await transaction settlement; aborting causes `transaction.done` to
368
+ // reject, which we swallow because the updater error is the real cause.
369
+ await transaction.done.catch(() => { });
370
+ this.logger.sendErrorEvent({
371
+ eventName: "FluidCacheUpdateCallbackError" /* FluidCacheErrorEvent.FluidCacheUpdateCallbackError */,
372
+ pkgVersion: packageVersion_js_1.pkgVersion,
373
+ }, updaterError);
374
+ return false;
375
+ }
376
+ if (!setCalled) {
377
+ await transaction.done;
378
+ return false;
379
+ }
380
+ // `set(undefined)` is treated as a delete: there is no useful distinction
381
+ // between "no entry" and "entry stored as undefined" (both surface as
382
+ // `undefined` from `get`), so we expose this as an atomic conditional-delete
383
+ // rather than persisting an undefined-valued row that would otherwise
384
+ // occupy IDB until maintenance reaped it.
385
+ if (valueToWrite === undefined) {
386
+ await transaction.store.delete(key);
387
+ }
388
+ else {
389
+ const currentTime = Date.now();
390
+ await transaction.store.put({
391
+ cachedObject: valueToWrite,
392
+ fileId: entry.file.docId,
393
+ type: entry.type,
394
+ cacheItemId: entry.key,
395
+ partitionKey: this.partitionKey,
396
+ createdTimeMs: currentTime,
397
+ lastAccessTimeMs: currentTime,
398
+ }, key);
399
+ }
400
+ await transaction.done;
401
+ return true;
402
+ }
403
+ catch (error) {
404
+ this.logger.sendErrorEvent({ eventName: "FluidCachePutError" /* FluidCacheErrorEvent.FluidCachePutError */, pkgVersion: packageVersion_js_1.pkgVersion }, error);
405
+ return false;
406
+ }
407
+ finally {
408
+ this.closeDb(db);
409
+ }
410
+ }
250
411
  }
251
412
  exports.FluidCache = FluidCache;
252
413
  //# sourceMappingURL=FluidCache.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"FluidCache.js","sourceRoot":"","sources":["../src/FluidCache.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 { assert } from \"@fluidframework/core-utils/internal\";\nimport type {\n\tIPersistedCache,\n\tIFileEntry,\n\tICacheEntry,\n} from \"@fluidframework/driver-definitions/internal\";\nimport {\n\tgetKeyForCacheEntry,\n\tmaximumCacheDurationMs,\n} from \"@fluidframework/driver-utils/internal\";\nimport type { TelemetryLoggerExt } from \"@fluidframework/telemetry-utils/internal\";\nimport { UsageError, createChildLogger } from \"@fluidframework/telemetry-utils/internal\";\nimport type { IDBPDatabase } from \"idb\";\n\nimport type { FluidCacheDBSchema } from \"./FluidCacheIndexedDb.js\";\nimport {\n\tFluidDriverObjectStoreName,\n\tgetFluidCacheIndexedDbInstance,\n} from \"./FluidCacheIndexedDb.js\";\nimport {\n\tFluidCacheErrorEvent,\n\tFluidCacheEventSubCategories,\n\tFluidCacheGenericEvent,\n} from \"./fluidCacheTelemetry.js\";\nimport { pkgVersion } from \"./packageVersion.js\";\nimport { scheduleIdleTask } from \"./scheduleIdleTask.js\";\n\n// Some browsers have a usageDetails property that will tell you more detailed information\n// on how the storage is being used\ninterface StorageQuotaUsageDetails {\n\tindexedDB: number | undefined;\n}\n\n/**\n * @legacy @beta\n */\nexport interface FluidCacheConfig {\n\t/**\n\t * A string to specify what partition of the cache you wish to use (e.g. a user id).\n\t * Null can be used to explicity indicate no partitioning, and has been chosen\n\t * vs undefined so that it is clear this is an intentional choice by the caller.\n\t * A null value should only be used when the host can ensure that the cache is not able\n\t * to be shared with multiple users.\n\t */\n\t// eslint-disable-next-line @rushstack/no-new-null\n\tpartitionKey: string | null;\n\n\t/**\n\t * A logger that can be used to get insight into cache performance and errors\n\t */\n\tlogger?: ITelemetryBaseLogger;\n\n\t/**\n\t * A value in milliseconds that determines the maximum age of a cache entry to return.\n\t * If an entry exists in the cache, but is older than this value, the cached value will not be returned.\n\t */\n\tmaxCacheItemAge: number;\n\n\t/**\n\t * Each time db is opened, it will remain open for this much time. To improve perf, if this property is set as\n\t * any number greater than 0, then db will not be closed immediately after usage. This value is in milliseconds.\n\t */\n\tcloseDbAfterMs?: number;\n}\n\n/**\n * A cache that can be used by the Fluid ODSP driver to cache data for faster performance.\n * @legacy @beta\n */\nexport class FluidCache implements IPersistedCache {\n\tprivate readonly logger: TelemetryLoggerExt;\n\n\tprivate readonly partitionKey: string | null;\n\n\tprivate readonly maxCacheItemAge: number;\n\tprivate readonly closeDbImmediately: boolean = true;\n\tprivate readonly closeDbAfterMs: number;\n\tprivate db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\tprivate dbCloseTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate dbReuseCount: number = -1;\n\n\tconstructor(config: FluidCacheConfig) {\n\t\tconst { logger, partitionKey, maxCacheItemAge, closeDbAfterMs } = config;\n\t\tthis.logger = createChildLogger({ logger });\n\t\tthis.partitionKey = partitionKey;\n\t\tif (maxCacheItemAge > maximumCacheDurationMs) {\n\t\t\tconst error = new UsageError(\n\t\t\t\t`maxCacheItemAge(${maxCacheItemAge}) cannot be greater than ${maximumCacheDurationMs}`,\n\t\t\t\t{\n\t\t\t\t\tmaxCacheItemAge,\n\t\t\t\t\tmaximumCacheDurationMs,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t},\n\t\t\t);\n\t\t\t// go with logging, rather than throwing for now\n\t\t\t// as throwing could break existing usages\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{\n\t\t\t\t\teventName: \"maxCacheItemAgeTooLarge\",\n\t\t\t\t\tsubCategory: FluidCacheEventSubCategories.FluidCache,\n\t\t\t\t},\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t\tthis.maxCacheItemAge = Math.min(maxCacheItemAge, maximumCacheDurationMs);\n\t\tthis.closeDbAfterMs = closeDbAfterMs ?? 0;\n\t\tif (this.closeDbAfterMs > 0) {\n\t\t\tthis.closeDbImmediately = false;\n\t\t}\n\n\t\tscheduleIdleTask(async () => {\n\t\t\t// Log how much storage space is currently being used by indexed db.\n\t\t\t// NOTE: This API is not supported in all browsers and it doesn't let you see the size of a specific DB.\n\t\t\t// Exception added when eslint rule was added, this should be revisited when modifying this code\n\t\t\tif (navigator.storage?.estimate) {\n\t\t\t\tconst estimate = await navigator.storage.estimate();\n\n\t\t\t\t// Some browsers have a usageDetails property that will tell you\n\t\t\t\t// more detailed information on how the storage is being used\n\t\t\t\tlet indexedDBSize: number | undefined;\n\t\t\t\tif (\"usageDetails\" in estimate) {\n\t\t\t\t\tindexedDBSize = ((estimate as any).usageDetails as StorageQuotaUsageDetails)\n\t\t\t\t\t\t.indexedDB;\n\t\t\t\t}\n\n\t\t\t\tthis.logger.sendTelemetryEvent({\n\t\t\t\t\teventName: FluidCacheGenericEvent.FluidCacheStorageInfo,\n\t\t\t\t\tsubCategory: FluidCacheEventSubCategories.FluidCache,\n\t\t\t\t\tquota: estimate.quota,\n\t\t\t\t\tusage: estimate.usage,\n\t\t\t\t\tindexedDBSize,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\n\t\tscheduleIdleTask(async () => {\n\t\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\n\t\t\t// Delete entries that have not been accessed recently to clean up space\n\t\t\ttry {\n\t\t\t\tdb = await getFluidCacheIndexedDbInstance(this.logger);\n\n\t\t\t\tconst transaction = db.transaction(FluidDriverObjectStoreName, \"readwrite\");\n\t\t\t\tconst index = transaction.store.index(\"createdTimeMs\");\n\t\t\t\t// Get items which were cached before the maxCacheItemAge.\n\t\t\t\tconst keysToDelete = await index.getAllKeys(\n\t\t\t\t\tIDBKeyRange.upperBound(Date.now() - this.maxCacheItemAge),\n\t\t\t\t);\n\n\t\t\t\tawait Promise.all(keysToDelete.map(async (key) => transaction.store.delete(key)));\n\t\t\t\tawait transaction.done;\n\t\t\t} catch (error: any) {\n\t\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t\t{\n\t\t\t\t\t\teventName: FluidCacheErrorEvent.FluidCacheDeleteOldEntriesError,\n\t\t\t\t\t\tpkgVersion,\n\t\t\t\t\t},\n\t\t\t\t\terror,\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tdb?.close();\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate async openDb(): Promise<IDBPDatabase<FluidCacheDBSchema>> {\n\t\tif (this.closeDbImmediately) {\n\t\t\treturn getFluidCacheIndexedDbInstance(this.logger);\n\t\t}\n\t\tif (this.db === undefined) {\n\t\t\tconst dbInstance = await getFluidCacheIndexedDbInstance(this.logger);\n\t\t\tif (this.db === undefined) {\n\t\t\t\t// Reset the counter on first open.\n\t\t\t\tthis.dbReuseCount = -1;\n\t\t\t\tthis.db = dbInstance;\n\t\t\t} else {\n\t\t\t\tdbInstance.close();\n\t\t\t\tthis.dbReuseCount += 1;\n\t\t\t\treturn this.db;\n\t\t\t}\n\t\t\t// Need to close the db on version change if opened.\n\t\t\tthis.db.onversionchange = (ev) => {\n\t\t\t\tthis.db?.close();\n\t\t\t\tthis.db = undefined;\n\t\t\t\tclearTimeout(this.dbCloseTimer);\n\t\t\t\tthis.dbCloseTimer = undefined;\n\t\t\t};\n\t\t\tthis.db.addEventListener(\"close\", (ev) => {\n\t\t\t\tclearTimeout(this.dbCloseTimer);\n\t\t\t\tthis.dbCloseTimer = undefined;\n\t\t\t\tthis.db = undefined;\n\t\t\t});\n\t\t\t// Schedule db close after this.closeDbAfterMs.\n\t\t\tassert(this.dbCloseTimer === undefined, 0x6c6 /* timer should not be set yet!! */);\n\t\t\tthis.dbCloseTimer = setTimeout(() => {\n\t\t\t\tthis.db?.close();\n\t\t\t\tthis.db = undefined;\n\t\t\t\tthis.dbCloseTimer = undefined;\n\t\t\t}, this.closeDbAfterMs);\n\t\t}\n\t\tassert(this.db !== undefined, 0x6c7 /* db should be intialized by now */);\n\t\tthis.dbReuseCount += 1;\n\t\treturn this.db;\n\t}\n\n\tprivate closeDb(db?: IDBPDatabase<FluidCacheDBSchema>): void {\n\t\tif (this.closeDbImmediately) {\n\t\t\tdb?.close();\n\t\t}\n\t}\n\n\tpublic async removeEntries(file: IFileEntry): Promise<void> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tdb = await this.openDb();\n\n\t\t\tconst transaction = db.transaction(FluidDriverObjectStoreName, \"readwrite\");\n\t\t\tconst index = transaction.store.index(\"fileId\");\n\n\t\t\tconst keysToDelete = await index.getAllKeys(file.docId);\n\n\t\t\tawait Promise.all(keysToDelete.map(async (key) => transaction.store.delete(key)));\n\t\t\tawait transaction.done;\n\t\t} catch (error: any) {\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{\n\t\t\t\t\teventName: FluidCacheErrorEvent.FluidCacheDeleteOldEntriesError,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t},\n\t\t\t\terror,\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.closeDb(db);\n\t\t}\n\t}\n\n\tpublic async removeEntry(entry: ICacheEntry): Promise<void> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tdb = await this.openDb();\n\n\t\t\tconst key = getKeyForCacheEntry(entry);\n\t\t\tawait db.delete(FluidDriverObjectStoreName, key);\n\t\t} catch (error: any) {\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{\n\t\t\t\t\teventName: FluidCacheErrorEvent.FluidCacheDeleteSingleEntryError,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t},\n\t\t\t\terror,\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.closeDb(db);\n\t\t}\n\t}\n\n\tpublic async get(cacheEntry: ICacheEntry): Promise<any> {\n\t\tconst startTime = performance.now();\n\n\t\tconst cachedItem = await this.getItemFromCache(cacheEntry);\n\n\t\tthis.logger.sendPerformanceEvent({\n\t\t\teventName: \"FluidCacheAccess\",\n\t\t\tcacheHit: cachedItem !== undefined,\n\t\t\ttype: cacheEntry.type,\n\t\t\tduration: performance.now() - startTime,\n\t\t\tdbOpenPerf: cachedItem?.dbOpenPerf,\n\t\t\tdbReuseCount: this.dbReuseCount,\n\t\t\tpkgVersion,\n\t\t});\n\n\t\t// Value will contain metadata like the expiry time, we just want to return the object we were asked to cache\n\t\treturn cachedItem?.cachedObject;\n\t}\n\n\tprivate async getItemFromCache(cacheEntry: ICacheEntry): Promise<any> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tconst key = getKeyForCacheEntry(cacheEntry);\n\n\t\t\tconst dbOpenStartTime = performance.now();\n\t\t\tdb = await this.openDb();\n\t\t\tconst dbOpenPerf = performance.now() - dbOpenStartTime;\n\t\t\tconst value = await db.get(FluidDriverObjectStoreName, key);\n\n\t\t\tif (!value) {\n\t\t\t\tthis.closeDb(db);\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\t// If the data does not come from the same partition, don't return it\n\t\t\tif (value.partitionKey !== this.partitionKey) {\n\t\t\t\tthis.logger.sendTelemetryEvent({\n\t\t\t\t\teventName: FluidCacheGenericEvent.FluidCachePartitionKeyMismatch,\n\t\t\t\t\tsubCategory: FluidCacheEventSubCategories.FluidCache,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t});\n\n\t\t\t\tthis.closeDb(db);\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tconst currentTime = Date.now();\n\n\t\t\t// If too much time has passed since this cache entry was used, we will also return undefined\n\t\t\tif (currentTime - value.createdTimeMs > this.maxCacheItemAge) {\n\t\t\t\tthis.closeDb(db);\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tthis.closeDb(db);\n\t\t\treturn { ...value, dbOpenPerf };\n\t\t} catch (error: any) {\n\t\t\t// We can fail to open the db for a variety of reasons,\n\t\t\t// such as the database version having upgraded underneath us. Return undefined in this case\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{ eventName: FluidCacheErrorEvent.FluidCacheGetError, pkgVersion },\n\t\t\t\terror,\n\t\t\t);\n\t\t\tthis.closeDb(db);\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tpublic async put(entry: ICacheEntry, value: any): Promise<void> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tdb = await this.openDb();\n\n\t\t\tconst currentTime = Date.now();\n\n\t\t\tawait db.put(\n\t\t\t\tFluidDriverObjectStoreName,\n\t\t\t\t{\n\t\t\t\t\tcachedObject: value,\n\t\t\t\t\tfileId: entry.file.docId,\n\t\t\t\t\ttype: entry.type,\n\t\t\t\t\tcacheItemId: entry.key,\n\t\t\t\t\tpartitionKey: this.partitionKey,\n\t\t\t\t\tcreatedTimeMs: currentTime,\n\t\t\t\t\tlastAccessTimeMs: currentTime,\n\t\t\t\t},\n\t\t\t\tgetKeyForCacheEntry(entry),\n\t\t\t);\n\t\t\tthis.closeDb(db);\n\t\t} catch (error: any) {\n\t\t\t// We can fail to open the db for a variety of reasons,\n\t\t\t// such as the database version having upgraded underneath us\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{ eventName: FluidCacheErrorEvent.FluidCachePutError, pkgVersion },\n\t\t\t\terror,\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.closeDb(db);\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"FluidCache.js","sourceRoot":"","sources":["../src/FluidCache.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 { assert, isPromiseLike } from \"@fluidframework/core-utils/internal\";\nimport type {\n\tIPersistedCache,\n\tIFileEntry,\n\tICacheEntry,\n} from \"@fluidframework/driver-definitions/internal\";\nimport {\n\tgetKeyForCacheEntry,\n\tmaximumCacheDurationMs,\n} from \"@fluidframework/driver-utils/internal\";\nimport type { TelemetryLoggerExt } from \"@fluidframework/telemetry-utils/internal\";\nimport { UsageError, createChildLogger } from \"@fluidframework/telemetry-utils/internal\";\nimport type { IDBPDatabase } from \"idb\";\n\nimport type { FluidCacheDBSchema } from \"./FluidCacheIndexedDb.js\";\nimport {\n\tFluidDriverObjectStoreName,\n\tgetFluidCacheIndexedDbInstance,\n} from \"./FluidCacheIndexedDb.js\";\nimport {\n\tFluidCacheErrorEvent,\n\tFluidCacheEventSubCategories,\n\tFluidCacheGenericEvent,\n} from \"./fluidCacheTelemetry.js\";\nimport { pkgVersion } from \"./packageVersion.js\";\nimport { scheduleIdleTask } from \"./scheduleIdleTask.js\";\n\n// Some browsers have a usageDetails property that will tell you more detailed information\n// on how the storage is being used\ninterface StorageQuotaUsageDetails {\n\tindexedDB: number | undefined;\n}\n\n/**\n * @legacy @beta\n */\nexport interface FluidCacheConfig {\n\t/**\n\t * A string to specify what partition of the cache you wish to use (e.g. a user id).\n\t * Null can be used to explicity indicate no partitioning, and has been chosen\n\t * vs undefined so that it is clear this is an intentional choice by the caller.\n\t * A null value should only be used when the host can ensure that the cache is not able\n\t * to be shared with multiple users.\n\t */\n\t// eslint-disable-next-line @rushstack/no-new-null\n\tpartitionKey: string | null;\n\n\t/**\n\t * A logger that can be used to get insight into cache performance and errors\n\t */\n\tlogger?: ITelemetryBaseLogger;\n\n\t/**\n\t * A value in milliseconds that determines the maximum age of a cache entry to return.\n\t * If an entry exists in the cache, but is older than this value, the cached value will not be returned.\n\t */\n\tmaxCacheItemAge: number;\n\n\t/**\n\t * Each time db is opened, it will remain open for this much time. To improve perf, if this property is set as\n\t * any number greater than 0, then db will not be closed immediately after usage. This value is in milliseconds.\n\t */\n\tcloseDbAfterMs?: number;\n}\n\n/**\n * A cache that can be used by the Fluid ODSP driver to cache data for faster performance.\n * @legacy @beta\n */\nexport class FluidCache implements IPersistedCache {\n\tprivate readonly logger: TelemetryLoggerExt;\n\n\tprivate readonly partitionKey: string | null;\n\n\tprivate readonly maxCacheItemAge: number;\n\tprivate readonly closeDbImmediately: boolean = true;\n\tprivate readonly closeDbAfterMs: number;\n\tprivate db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\tprivate dbCloseTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate dbReuseCount: number = -1;\n\n\tconstructor(config: FluidCacheConfig) {\n\t\tconst { logger, partitionKey, maxCacheItemAge, closeDbAfterMs } = config;\n\t\tthis.logger = createChildLogger({ logger });\n\t\tthis.partitionKey = partitionKey;\n\t\tif (maxCacheItemAge > maximumCacheDurationMs) {\n\t\t\tconst error = new UsageError(\n\t\t\t\t`maxCacheItemAge(${maxCacheItemAge}) cannot be greater than ${maximumCacheDurationMs}`,\n\t\t\t\t{\n\t\t\t\t\tmaxCacheItemAge,\n\t\t\t\t\tmaximumCacheDurationMs,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t},\n\t\t\t);\n\t\t\t// go with logging, rather than throwing for now\n\t\t\t// as throwing could break existing usages\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{\n\t\t\t\t\teventName: \"maxCacheItemAgeTooLarge\",\n\t\t\t\t\tsubCategory: FluidCacheEventSubCategories.FluidCache,\n\t\t\t\t},\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t\tthis.maxCacheItemAge = Math.min(maxCacheItemAge, maximumCacheDurationMs);\n\t\tthis.closeDbAfterMs = closeDbAfterMs ?? 0;\n\t\tif (this.closeDbAfterMs > 0) {\n\t\t\tthis.closeDbImmediately = false;\n\t\t}\n\n\t\tscheduleIdleTask(async () => {\n\t\t\t// Log how much storage space is currently being used by indexed db.\n\t\t\t// NOTE: This API is not supported in all browsers and it doesn't let you see the size of a specific DB.\n\t\t\t// Exception added when eslint rule was added, this should be revisited when modifying this code\n\t\t\tif (navigator.storage?.estimate) {\n\t\t\t\tconst estimate = await navigator.storage.estimate();\n\n\t\t\t\t// Some browsers have a usageDetails property that will tell you\n\t\t\t\t// more detailed information on how the storage is being used\n\t\t\t\tlet indexedDBSize: number | undefined;\n\t\t\t\tif (\"usageDetails\" in estimate) {\n\t\t\t\t\tindexedDBSize = ((estimate as any).usageDetails as StorageQuotaUsageDetails)\n\t\t\t\t\t\t.indexedDB;\n\t\t\t\t}\n\n\t\t\t\tthis.logger.sendTelemetryEvent({\n\t\t\t\t\teventName: FluidCacheGenericEvent.FluidCacheStorageInfo,\n\t\t\t\t\tsubCategory: FluidCacheEventSubCategories.FluidCache,\n\t\t\t\t\tquota: estimate.quota,\n\t\t\t\t\tusage: estimate.usage,\n\t\t\t\t\tindexedDBSize,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\n\t\tscheduleIdleTask(async () => {\n\t\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\n\t\t\t// Delete entries that have not been accessed recently to clean up space\n\t\t\ttry {\n\t\t\t\tdb = await getFluidCacheIndexedDbInstance(this.logger);\n\n\t\t\t\tconst transaction = db.transaction(FluidDriverObjectStoreName, \"readwrite\");\n\t\t\t\tconst index = transaction.store.index(\"createdTimeMs\");\n\t\t\t\t// Get items which were cached before the maxCacheItemAge.\n\t\t\t\tconst keysToDelete = await index.getAllKeys(\n\t\t\t\t\tIDBKeyRange.upperBound(Date.now() - this.maxCacheItemAge),\n\t\t\t\t);\n\n\t\t\t\tawait Promise.all(keysToDelete.map(async (key) => transaction.store.delete(key)));\n\t\t\t\tawait transaction.done;\n\t\t\t} catch (error: any) {\n\t\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t\t{\n\t\t\t\t\t\teventName: FluidCacheErrorEvent.FluidCacheDeleteOldEntriesError,\n\t\t\t\t\t\tpkgVersion,\n\t\t\t\t\t},\n\t\t\t\t\terror,\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tdb?.close();\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate async openDb(): Promise<IDBPDatabase<FluidCacheDBSchema>> {\n\t\tif (this.closeDbImmediately) {\n\t\t\treturn getFluidCacheIndexedDbInstance(this.logger);\n\t\t}\n\t\tif (this.db === undefined) {\n\t\t\tconst dbInstance = await getFluidCacheIndexedDbInstance(this.logger);\n\t\t\tif (this.db === undefined) {\n\t\t\t\t// Reset the counter on first open.\n\t\t\t\tthis.dbReuseCount = -1;\n\t\t\t\tthis.db = dbInstance;\n\t\t\t} else {\n\t\t\t\tdbInstance.close();\n\t\t\t\tthis.dbReuseCount += 1;\n\t\t\t\treturn this.db;\n\t\t\t}\n\t\t\t// Need to close the db on version change if opened.\n\t\t\tthis.db.onversionchange = (ev) => {\n\t\t\t\tthis.db?.close();\n\t\t\t\tthis.db = undefined;\n\t\t\t\tclearTimeout(this.dbCloseTimer);\n\t\t\t\tthis.dbCloseTimer = undefined;\n\t\t\t};\n\t\t\tthis.db.addEventListener(\"close\", (ev) => {\n\t\t\t\tclearTimeout(this.dbCloseTimer);\n\t\t\t\tthis.dbCloseTimer = undefined;\n\t\t\t\tthis.db = undefined;\n\t\t\t});\n\t\t\t// Schedule db close after this.closeDbAfterMs.\n\t\t\tassert(this.dbCloseTimer === undefined, 0x6c6 /* timer should not be set yet!! */);\n\t\t\tthis.dbCloseTimer = setTimeout(() => {\n\t\t\t\tthis.db?.close();\n\t\t\t\tthis.db = undefined;\n\t\t\t\tthis.dbCloseTimer = undefined;\n\t\t\t}, this.closeDbAfterMs);\n\t\t}\n\t\tassert(this.db !== undefined, 0x6c7 /* db should be intialized by now */);\n\t\tthis.dbReuseCount += 1;\n\t\treturn this.db;\n\t}\n\n\tprivate closeDb(db?: IDBPDatabase<FluidCacheDBSchema>): void {\n\t\tif (this.closeDbImmediately) {\n\t\t\tdb?.close();\n\t\t}\n\t}\n\n\tpublic async removeEntries(file: IFileEntry): Promise<void> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tdb = await this.openDb();\n\n\t\t\tconst transaction = db.transaction(FluidDriverObjectStoreName, \"readwrite\");\n\t\t\tconst index = transaction.store.index(\"fileId\");\n\n\t\t\tconst keysToDelete = await index.getAllKeys(file.docId);\n\n\t\t\tawait Promise.all(keysToDelete.map(async (key) => transaction.store.delete(key)));\n\t\t\tawait transaction.done;\n\t\t} catch (error: any) {\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{\n\t\t\t\t\teventName: FluidCacheErrorEvent.FluidCacheDeleteOldEntriesError,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t},\n\t\t\t\terror,\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.closeDb(db);\n\t\t}\n\t}\n\n\tpublic async removeEntry(entry: ICacheEntry): Promise<void> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tdb = await this.openDb();\n\n\t\t\tconst key = getKeyForCacheEntry(entry);\n\t\t\tawait db.delete(FluidDriverObjectStoreName, key);\n\t\t} catch (error: any) {\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{\n\t\t\t\t\teventName: FluidCacheErrorEvent.FluidCacheDeleteSingleEntryError,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t},\n\t\t\t\terror,\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.closeDb(db);\n\t\t}\n\t}\n\n\tpublic async get(cacheEntry: ICacheEntry): Promise<any> {\n\t\tconst startTime = performance.now();\n\n\t\tconst cachedItem = await this.getItemFromCache(cacheEntry);\n\n\t\tthis.logger.sendPerformanceEvent({\n\t\t\teventName: \"FluidCacheAccess\",\n\t\t\tcacheHit: cachedItem !== undefined,\n\t\t\ttype: cacheEntry.type,\n\t\t\tduration: performance.now() - startTime,\n\t\t\tdbOpenPerf: cachedItem?.dbOpenPerf,\n\t\t\tdbReuseCount: this.dbReuseCount,\n\t\t\tpkgVersion,\n\t\t});\n\n\t\t// Value will contain metadata like the expiry time, we just want to return the object we were asked to cache\n\t\treturn cachedItem?.cachedObject;\n\t}\n\n\tprivate async getItemFromCache(cacheEntry: ICacheEntry): Promise<any> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tconst key = getKeyForCacheEntry(cacheEntry);\n\n\t\t\tconst dbOpenStartTime = performance.now();\n\t\t\tdb = await this.openDb();\n\t\t\tconst dbOpenPerf = performance.now() - dbOpenStartTime;\n\t\t\tconst value = await db.get(FluidDriverObjectStoreName, key);\n\n\t\t\tif (!value) {\n\t\t\t\tthis.closeDb(db);\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\t// If the data does not come from the same partition, don't return it\n\t\t\tif (value.partitionKey !== this.partitionKey) {\n\t\t\t\tthis.logger.sendTelemetryEvent({\n\t\t\t\t\teventName: FluidCacheGenericEvent.FluidCachePartitionKeyMismatch,\n\t\t\t\t\tsubCategory: FluidCacheEventSubCategories.FluidCache,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t});\n\n\t\t\t\tthis.closeDb(db);\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tconst currentTime = Date.now();\n\n\t\t\t// If too much time has passed since this cache entry was used, we will also return undefined\n\t\t\tif (currentTime - value.createdTimeMs > this.maxCacheItemAge) {\n\t\t\t\tthis.closeDb(db);\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tthis.closeDb(db);\n\t\t\treturn { ...value, dbOpenPerf };\n\t\t} catch (error: any) {\n\t\t\t// We can fail to open the db for a variety of reasons,\n\t\t\t// such as the database version having upgraded underneath us. Return undefined in this case\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{ eventName: FluidCacheErrorEvent.FluidCacheGetError, pkgVersion },\n\t\t\t\terror,\n\t\t\t);\n\t\t\tthis.closeDb(db);\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tpublic async put(entry: ICacheEntry, value: any): Promise<void> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tdb = await this.openDb();\n\n\t\t\tconst currentTime = Date.now();\n\n\t\t\tawait db.put(\n\t\t\t\tFluidDriverObjectStoreName,\n\t\t\t\t{\n\t\t\t\t\tcachedObject: value,\n\t\t\t\t\tfileId: entry.file.docId,\n\t\t\t\t\ttype: entry.type,\n\t\t\t\t\tcacheItemId: entry.key,\n\t\t\t\t\tpartitionKey: this.partitionKey,\n\t\t\t\t\tcreatedTimeMs: currentTime,\n\t\t\t\t\tlastAccessTimeMs: currentTime,\n\t\t\t\t},\n\t\t\t\tgetKeyForCacheEntry(entry),\n\t\t\t);\n\t\t\tthis.closeDb(db);\n\t\t} catch (error: any) {\n\t\t\t// We can fail to open the db for a variety of reasons,\n\t\t\t// such as the database version having upgraded underneath us\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{ eventName: FluidCacheErrorEvent.FluidCachePutError, pkgVersion },\n\t\t\t\terror,\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.closeDb(db);\n\t\t}\n\t}\n\n\t/**\n\t * Atomically reads the existing cached entry, hands it to `updater`, and writes a\n\t * new value iff `updater` calls the supplied `set` callback. The read and the\n\t * conditional write happen inside a single IndexedDB `readwrite` transaction, so\n\t * the decision sees a consistent view across consumers sharing the same underlying\n\t * IndexedDB instance (for example, multiple browser tabs racing to persist pending\n\t * state).\n\t *\n\t * @remarks\n\t * The implementation uses `transaction.store.get` + `transaction.store.put` rather\n\t * than an IDB cursor. Both run inside the same `readwrite` transaction, so the\n\t * atomicity guarantee is identical, and the get/put pair is materially simpler\n\t * to reason about for a single-key update. A cursor would be the right tool if we\n\t * needed to iterate or range-scan; for a known key we don't.\n\t *\n\t * @param entry - cache entry; identifies the file and the key within that file.\n\t * @param updater - synchronous callback invoked with `(existing, set)`.\n\t * `existing` is the currently-cached value, or `undefined` when the cached row is\n\t * invisible under the same rules `get` applies: no entry exists for the key, the\n\t * existing entry belongs to a different partition, or the existing entry is older\n\t * than `maxCacheItemAge`. The updater can derive the new value from `existing`\n\t * (read-modify-write) or ignore it entirely. To commit a write, call `set(value)`;\n\t * to leave the cache untouched, return without calling `set`. Stored via IndexedDB\n\t * structured clone, with the same value requirements as {@link FluidCache.put} —\n\t * not restricted to JSON-serializable values.\n\t *\n\t * Calling `set(undefined)` removes the row at the key (equivalent to\n\t * {@link FluidCache.removeEntry} inside the same atomic transaction). `get`\n\t * already collapses \"no entry\" and \"entry stored as undefined\" into the same\n\t * observable result, so the delete-on-undefined semantics gives callers an\n\t * atomic conditional-delete without ambiguity for any meaningful use case.\n\t *\n\t * The updater itself must be synchronous and `set` must be called from within it.\n\t * IndexedDB transactions auto-close on any non-IDB await, which would silently\n\t * break the atomicity that makes the update correct. Two guards make misuse\n\t * loud rather than silent: calling `set` after `updater` has returned throws a\n\t * `UsageError` at the call site; returning a thenable (e.g. an `async` updater)\n\t * is detected after `updater` returns, aborts the transaction, and is logged\n\t * under `FluidCacheUpdateCallbackError`. If `updater` calls `set` more than\n\t * once, the last value wins.\n\t *\n\t * When `set` is called, the write (or delete) atomically replaces whatever row\n\t * exists at the key, including cross-partition or stale rows that the updater\n\t * saw as `undefined`. This matches the unconditional overwrite behavior of\n\t * `put`. Callers that must preserve cross-partition rows should not use `update`.\n\t *\n\t * Exceptions thrown by `updater` are logged under the dedicated\n\t * `FluidCacheUpdateCallbackError` telemetry event (distinct from IDB write errors)\n\t * and surfaced to the caller as a `false` return value, after aborting the\n\t * transaction so the existing row is preserved — even if `set` was called before\n\t * the throw.\n\t *\n\t * Compare-and-set callers: a `false` return collapses three distinct outcomes —\n\t * the updater returned without calling `set`, the updater threw (including the\n\t * async-updater misuse case above), and the IDB write itself failed. Callers\n\t * that need to distinguish these must consult telemetry: updater-side failures\n\t * are logged under `FluidCacheUpdateCallbackError`; IDB-write failures are\n\t * logged under `FluidCachePutError`. A lost compare-and-set race (the updater\n\t * returned without calling `set`) is not logged.\n\t * @returns `true` if `updater` called `set` and the write committed; `false` if\n\t * `updater` returned without calling `set`, threw, or an IDB error occurred. IDB\n\t * errors are logged and not thrown, matching the behavior of `put`.\n\t */\n\tpublic async update(\n\t\tentry: ICacheEntry,\n\t\tupdater: (existing: unknown, set: (value: unknown) => void) => void,\n\t): Promise<boolean> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tdb = await this.openDb();\n\n\t\t\tconst key = getKeyForCacheEntry(entry);\n\t\t\tconst transaction = db.transaction(FluidDriverObjectStoreName, \"readwrite\");\n\t\t\tconst existing = await transaction.store.get(key);\n\t\t\t// Surface the cached value to the updater only when the existing entry is\n\t\t\t// visible under the same rules `get` applies: same partition and not older\n\t\t\t// than `maxCacheItemAge`. Cross-partition and stale entries are treated as\n\t\t\t// absent so the updater sees the same view it would under `get`+`put`.\n\t\t\tconst existingVisible =\n\t\t\t\texisting?.partitionKey === this.partitionKey &&\n\t\t\t\tDate.now() - existing.createdTimeMs <= this.maxCacheItemAge;\n\t\t\tconst existingValue = existingVisible ? existing?.cachedObject : undefined;\n\n\t\t\t// `set` is a synchronous-only commit signal. We capture the last-supplied\n\t\t\t// value (multi-call: last wins) and a \"called\" flag so the value being set\n\t\t\t// to `undefined` still counts as a write. After `updater` returns we flip\n\t\t\t// `updaterReturned` to true; any subsequent `set` call throws a `UsageError`\n\t\t\t// at that call site so callers who try to defer the commit (e.g. from a\n\t\t\t// `setTimeout`) see the misuse rather than silently writing into a closed\n\t\t\t// transaction.\n\t\t\tlet valueToWrite: unknown;\n\t\t\tlet setCalled = false;\n\t\t\tlet updaterReturned = false;\n\t\t\tconst set = (value: unknown): void => {\n\t\t\t\tif (updaterReturned) {\n\t\t\t\t\tthrow new UsageError(\"FluidCache.update: set called after updater returned\");\n\t\t\t\t}\n\t\t\t\tvalueToWrite = value;\n\t\t\t\tsetCalled = true;\n\t\t\t};\n\n\t\t\t// Invoke the updater in its own try/catch so a host-supplied callback\n\t\t\t// throwing does not get logged under `FluidCachePutError` (which is for\n\t\t\t// IDB-write failures). On updater throw we abort the transaction so the\n\t\t\t// existing row is preserved — even if `set` was called before the throw —\n\t\t\t// log under the updater-specific event, and return `false` (matching the\n\t\t\t// documented \"errors are logged, not thrown\" contract).\n\t\t\ttry {\n\t\t\t\tconst updaterResult = updater(existingValue, set);\n\t\t\t\tupdaterReturned = true;\n\t\t\t\t// Reject async updaters: TypeScript structurally accepts\n\t\t\t\t// `async (...) => Promise<void>` for the declared `() => void` parameter\n\t\t\t\t// type, but an async updater that calls `set` synchronously and then\n\t\t\t\t// awaits would let the IDB write commit before its eventual rejection\n\t\t\t\t// surfaced — contradicting the \"throw aborts the transaction\" contract.\n\t\t\t\t// Detect a thenable return and treat it as misuse symmetric with the\n\t\t\t\t// late-`set` guard.\n\t\t\t\tif (isPromiseLike(updaterResult)) {\n\t\t\t\t\tthrow new UsageError(\n\t\t\t\t\t\t\"FluidCache.update: updater must be synchronous (returned a thenable)\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch (updaterError: any) {\n\t\t\t\tupdaterReturned = true;\n\t\t\t\ttransaction.abort();\n\t\t\t\t// Await transaction settlement; aborting causes `transaction.done` to\n\t\t\t\t// reject, which we swallow because the updater error is the real cause.\n\t\t\t\tawait transaction.done.catch(() => {});\n\t\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t\t{\n\t\t\t\t\t\teventName: FluidCacheErrorEvent.FluidCacheUpdateCallbackError,\n\t\t\t\t\t\tpkgVersion,\n\t\t\t\t\t},\n\t\t\t\t\tupdaterError,\n\t\t\t\t);\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tif (!setCalled) {\n\t\t\t\tawait transaction.done;\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// `set(undefined)` is treated as a delete: there is no useful distinction\n\t\t\t// between \"no entry\" and \"entry stored as undefined\" (both surface as\n\t\t\t// `undefined` from `get`), so we expose this as an atomic conditional-delete\n\t\t\t// rather than persisting an undefined-valued row that would otherwise\n\t\t\t// occupy IDB until maintenance reaped it.\n\t\t\tif (valueToWrite === undefined) {\n\t\t\t\tawait transaction.store.delete(key);\n\t\t\t} else {\n\t\t\t\tconst currentTime = Date.now();\n\t\t\t\tawait transaction.store.put(\n\t\t\t\t\t{\n\t\t\t\t\t\tcachedObject: valueToWrite,\n\t\t\t\t\t\tfileId: entry.file.docId,\n\t\t\t\t\t\ttype: entry.type,\n\t\t\t\t\t\tcacheItemId: entry.key,\n\t\t\t\t\t\tpartitionKey: this.partitionKey,\n\t\t\t\t\t\tcreatedTimeMs: currentTime,\n\t\t\t\t\t\tlastAccessTimeMs: currentTime,\n\t\t\t\t\t},\n\t\t\t\t\tkey,\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait transaction.done;\n\t\t\treturn true;\n\t\t} catch (error: any) {\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{ eventName: FluidCacheErrorEvent.FluidCachePutError, pkgVersion },\n\t\t\t\terror,\n\t\t\t);\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tthis.closeDb(db);\n\t\t}\n\t}\n}\n"]}
@@ -11,6 +11,7 @@ export declare const enum FluidCacheErrorEvent {
11
11
  "FluidCacheDeleteSingleEntryError" = "FluidCacheDeleteSingleEntryError",
12
12
  "FluidCacheGetError" = "FluidCacheGetError",
13
13
  "FluidCachePutError" = "FluidCachePutError",
14
+ "FluidCacheUpdateCallbackError" = "FluidCacheUpdateCallbackError",
14
15
  "FluidCacheUpdateUsageError" = "FluidCacheUpdateUsageError",
15
16
  "FluidCacheDeleteOldDbError" = "FluidCacheDeleteOldDbError"
16
17
  }
@@ -1 +1 @@
1
- {"version":3,"file":"fluidCacheTelemetry.d.ts","sourceRoot":"","sources":["../src/fluidCacheTelemetry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,0BAAkB,sBAAsB;IACvC,uBAAuB,0BAA0B;IACjD,gCAAgC,mCAAmC;CACnE;AAED,0BAAkB,oBAAoB;IACrC,iCAAiC,oCAAoC;IACrE,kCAAkC,qCAAqC;IACvE,oBAAoB,uBAAuB;IAC3C,oBAAoB,uBAAuB;IAC3C,4BAA4B,+BAA+B;IAC3D,4BAA4B,+BAA+B;CAC3D;AAED,0BAAkB,4BAA4B;IAC7C,YAAY,eAAe;CAC3B"}
1
+ {"version":3,"file":"fluidCacheTelemetry.d.ts","sourceRoot":"","sources":["../src/fluidCacheTelemetry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,0BAAkB,sBAAsB;IACvC,uBAAuB,0BAA0B;IACjD,gCAAgC,mCAAmC;CACnE;AAED,0BAAkB,oBAAoB;IACrC,iCAAiC,oCAAoC;IACrE,kCAAkC,qCAAqC;IACvE,oBAAoB,uBAAuB;IAC3C,oBAAoB,uBAAuB;IAC3C,+BAA+B,kCAAkC;IACjE,4BAA4B,+BAA+B;IAC3D,4BAA4B,+BAA+B;CAC3D;AAED,0BAAkB,4BAA4B;IAC7C,YAAY,eAAe;CAC3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"fluidCacheTelemetry.js","sourceRoot":"","sources":["../src/fluidCacheTelemetry.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nexport const enum FluidCacheGenericEvent {\n\t\"FluidCacheStorageInfo\" = \"FluidCacheStorageInfo\",\n\t\"FluidCachePartitionKeyMismatch\" = \"FluidCachePartitionKeyMismatch\",\n}\n\nexport const enum FluidCacheErrorEvent {\n\t\"FluidCacheDeleteOldEntriesError\" = \"FluidCacheDeleteOldEntriesError\",\n\t\"FluidCacheDeleteSingleEntryError\" = \"FluidCacheDeleteSingleEntryError\",\n\t\"FluidCacheGetError\" = \"FluidCacheGetError\",\n\t\"FluidCachePutError\" = \"FluidCachePutError\",\n\t\"FluidCacheUpdateUsageError\" = \"FluidCacheUpdateUsageError\",\n\t\"FluidCacheDeleteOldDbError\" = \"FluidCacheDeleteOldDbError\",\n}\n\nexport const enum FluidCacheEventSubCategories {\n\t\"FluidCache\" = \"FluidCache\",\n}\n"]}
1
+ {"version":3,"file":"fluidCacheTelemetry.js","sourceRoot":"","sources":["../src/fluidCacheTelemetry.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nexport const enum FluidCacheGenericEvent {\n\t\"FluidCacheStorageInfo\" = \"FluidCacheStorageInfo\",\n\t\"FluidCachePartitionKeyMismatch\" = \"FluidCachePartitionKeyMismatch\",\n}\n\nexport const enum FluidCacheErrorEvent {\n\t\"FluidCacheDeleteOldEntriesError\" = \"FluidCacheDeleteOldEntriesError\",\n\t\"FluidCacheDeleteSingleEntryError\" = \"FluidCacheDeleteSingleEntryError\",\n\t\"FluidCacheGetError\" = \"FluidCacheGetError\",\n\t\"FluidCachePutError\" = \"FluidCachePutError\",\n\t\"FluidCacheUpdateCallbackError\" = \"FluidCacheUpdateCallbackError\",\n\t\"FluidCacheUpdateUsageError\" = \"FluidCacheUpdateUsageError\",\n\t\"FluidCacheDeleteOldDbError\" = \"FluidCacheDeleteOldDbError\",\n}\n\nexport const enum FluidCacheEventSubCategories {\n\t\"FluidCache\" = \"FluidCache\",\n}\n"]}
@@ -5,5 +5,5 @@
5
5
  * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
6
6
  */
7
7
  export declare const pkgName = "@fluidframework/driver-web-cache";
8
- export declare const pkgVersion = "2.101.0";
8
+ export declare const pkgVersion = "2.102.0";
9
9
  //# sourceMappingURL=packageVersion.d.ts.map
@@ -8,5 +8,5 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.pkgVersion = exports.pkgName = void 0;
10
10
  exports.pkgName = "@fluidframework/driver-web-cache";
11
- exports.pkgVersion = "2.101.0";
11
+ exports.pkgVersion = "2.102.0";
12
12
  //# sourceMappingURL=packageVersion.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"packageVersion.js","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEU,QAAA,OAAO,GAAG,kCAAkC,CAAC;AAC7C,QAAA,UAAU,GAAG,SAAS,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/driver-web-cache\";\nexport const pkgVersion = \"2.101.0\";\n"]}
1
+ {"version":3,"file":"packageVersion.js","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEU,QAAA,OAAO,GAAG,kCAAkC,CAAC;AAC7C,QAAA,UAAU,GAAG,SAAS,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/driver-web-cache\";\nexport const pkgVersion = \"2.102.0\";\n"]}
@@ -52,5 +52,69 @@ export declare class FluidCache implements IPersistedCache {
52
52
  get(cacheEntry: ICacheEntry): Promise<any>;
53
53
  private getItemFromCache;
54
54
  put(entry: ICacheEntry, value: any): Promise<void>;
55
+ /**
56
+ * Atomically reads the existing cached entry, hands it to `updater`, and writes a
57
+ * new value iff `updater` calls the supplied `set` callback. The read and the
58
+ * conditional write happen inside a single IndexedDB `readwrite` transaction, so
59
+ * the decision sees a consistent view across consumers sharing the same underlying
60
+ * IndexedDB instance (for example, multiple browser tabs racing to persist pending
61
+ * state).
62
+ *
63
+ * @remarks
64
+ * The implementation uses `transaction.store.get` + `transaction.store.put` rather
65
+ * than an IDB cursor. Both run inside the same `readwrite` transaction, so the
66
+ * atomicity guarantee is identical, and the get/put pair is materially simpler
67
+ * to reason about for a single-key update. A cursor would be the right tool if we
68
+ * needed to iterate or range-scan; for a known key we don't.
69
+ *
70
+ * @param entry - cache entry; identifies the file and the key within that file.
71
+ * @param updater - synchronous callback invoked with `(existing, set)`.
72
+ * `existing` is the currently-cached value, or `undefined` when the cached row is
73
+ * invisible under the same rules `get` applies: no entry exists for the key, the
74
+ * existing entry belongs to a different partition, or the existing entry is older
75
+ * than `maxCacheItemAge`. The updater can derive the new value from `existing`
76
+ * (read-modify-write) or ignore it entirely. To commit a write, call `set(value)`;
77
+ * to leave the cache untouched, return without calling `set`. Stored via IndexedDB
78
+ * structured clone, with the same value requirements as {@link FluidCache.put} —
79
+ * not restricted to JSON-serializable values.
80
+ *
81
+ * Calling `set(undefined)` removes the row at the key (equivalent to
82
+ * {@link FluidCache.removeEntry} inside the same atomic transaction). `get`
83
+ * already collapses "no entry" and "entry stored as undefined" into the same
84
+ * observable result, so the delete-on-undefined semantics gives callers an
85
+ * atomic conditional-delete without ambiguity for any meaningful use case.
86
+ *
87
+ * The updater itself must be synchronous and `set` must be called from within it.
88
+ * IndexedDB transactions auto-close on any non-IDB await, which would silently
89
+ * break the atomicity that makes the update correct. Two guards make misuse
90
+ * loud rather than silent: calling `set` after `updater` has returned throws a
91
+ * `UsageError` at the call site; returning a thenable (e.g. an `async` updater)
92
+ * is detected after `updater` returns, aborts the transaction, and is logged
93
+ * under `FluidCacheUpdateCallbackError`. If `updater` calls `set` more than
94
+ * once, the last value wins.
95
+ *
96
+ * When `set` is called, the write (or delete) atomically replaces whatever row
97
+ * exists at the key, including cross-partition or stale rows that the updater
98
+ * saw as `undefined`. This matches the unconditional overwrite behavior of
99
+ * `put`. Callers that must preserve cross-partition rows should not use `update`.
100
+ *
101
+ * Exceptions thrown by `updater` are logged under the dedicated
102
+ * `FluidCacheUpdateCallbackError` telemetry event (distinct from IDB write errors)
103
+ * and surfaced to the caller as a `false` return value, after aborting the
104
+ * transaction so the existing row is preserved — even if `set` was called before
105
+ * the throw.
106
+ *
107
+ * Compare-and-set callers: a `false` return collapses three distinct outcomes —
108
+ * the updater returned without calling `set`, the updater threw (including the
109
+ * async-updater misuse case above), and the IDB write itself failed. Callers
110
+ * that need to distinguish these must consult telemetry: updater-side failures
111
+ * are logged under `FluidCacheUpdateCallbackError`; IDB-write failures are
112
+ * logged under `FluidCachePutError`. A lost compare-and-set race (the updater
113
+ * returned without calling `set`) is not logged.
114
+ * @returns `true` if `updater` called `set` and the write committed; `false` if
115
+ * `updater` returned without calling `set`, threw, or an IDB error occurred. IDB
116
+ * errors are logged and not thrown, matching the behavior of `put`.
117
+ */
118
+ update(entry: ICacheEntry, updater: (existing: unknown, set: (value: unknown) => void) => void): Promise<boolean>;
55
119
  }
56
120
  //# sourceMappingURL=FluidCache.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FluidCache.d.ts","sourceRoot":"","sources":["../src/FluidCache.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAE5E,OAAO,KAAK,EACX,eAAe,EACf,UAAU,EACV,WAAW,EACX,MAAM,6CAA6C,CAAC;AA4BrD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC;;;;;;OAMG;IAEH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B;;OAEG;IACH,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAE9B;;;OAGG;IACH,eAAe,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,qBAAa,UAAW,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAE5C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAgB;IAE7C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAiB;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,EAAE,CAA+C;IACzD,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,YAAY,CAAc;gBAEtB,MAAM,EAAE,gBAAgB;YAqFtB,MAAM;IAwCpB,OAAO,CAAC,OAAO;IAMF,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB9C,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB9C,GAAG,CAAC,UAAU,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;YAmBzC,gBAAgB;IAiDjB,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;CAgC/D"}
1
+ {"version":3,"file":"FluidCache.d.ts","sourceRoot":"","sources":["../src/FluidCache.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAE5E,OAAO,KAAK,EACX,eAAe,EACf,UAAU,EACV,WAAW,EACX,MAAM,6CAA6C,CAAC;AA4BrD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC;;;;;;OAMG;IAEH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B;;OAEG;IACH,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAE9B;;;OAGG;IACH,eAAe,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,qBAAa,UAAW,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAE5C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAgB;IAE7C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAiB;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,EAAE,CAA+C;IACzD,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,YAAY,CAAc;gBAEtB,MAAM,EAAE,gBAAgB;YAqFtB,MAAM;IAwCpB,OAAO,CAAC,OAAO;IAMF,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB9C,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB9C,GAAG,CAAC,UAAU,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;YAmBzC,gBAAgB;IAiDjB,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAiC/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8DG;IACU,MAAM,CAClB,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,GACjE,OAAO,CAAC,OAAO,CAAC;CA+GnB"}
package/lib/FluidCache.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
- import { assert } from "@fluidframework/core-utils/internal";
5
+ import { assert, isPromiseLike } from "@fluidframework/core-utils/internal";
6
6
  import { getKeyForCacheEntry, maximumCacheDurationMs, } from "@fluidframework/driver-utils/internal";
7
7
  import { UsageError, createChildLogger } from "@fluidframework/telemetry-utils/internal";
8
8
  import { FluidDriverObjectStoreName, getFluidCacheIndexedDbInstance, } from "./FluidCacheIndexedDb.js";
@@ -244,5 +244,166 @@ export class FluidCache {
244
244
  this.closeDb(db);
245
245
  }
246
246
  }
247
+ /**
248
+ * Atomically reads the existing cached entry, hands it to `updater`, and writes a
249
+ * new value iff `updater` calls the supplied `set` callback. The read and the
250
+ * conditional write happen inside a single IndexedDB `readwrite` transaction, so
251
+ * the decision sees a consistent view across consumers sharing the same underlying
252
+ * IndexedDB instance (for example, multiple browser tabs racing to persist pending
253
+ * state).
254
+ *
255
+ * @remarks
256
+ * The implementation uses `transaction.store.get` + `transaction.store.put` rather
257
+ * than an IDB cursor. Both run inside the same `readwrite` transaction, so the
258
+ * atomicity guarantee is identical, and the get/put pair is materially simpler
259
+ * to reason about for a single-key update. A cursor would be the right tool if we
260
+ * needed to iterate or range-scan; for a known key we don't.
261
+ *
262
+ * @param entry - cache entry; identifies the file and the key within that file.
263
+ * @param updater - synchronous callback invoked with `(existing, set)`.
264
+ * `existing` is the currently-cached value, or `undefined` when the cached row is
265
+ * invisible under the same rules `get` applies: no entry exists for the key, the
266
+ * existing entry belongs to a different partition, or the existing entry is older
267
+ * than `maxCacheItemAge`. The updater can derive the new value from `existing`
268
+ * (read-modify-write) or ignore it entirely. To commit a write, call `set(value)`;
269
+ * to leave the cache untouched, return without calling `set`. Stored via IndexedDB
270
+ * structured clone, with the same value requirements as {@link FluidCache.put} —
271
+ * not restricted to JSON-serializable values.
272
+ *
273
+ * Calling `set(undefined)` removes the row at the key (equivalent to
274
+ * {@link FluidCache.removeEntry} inside the same atomic transaction). `get`
275
+ * already collapses "no entry" and "entry stored as undefined" into the same
276
+ * observable result, so the delete-on-undefined semantics gives callers an
277
+ * atomic conditional-delete without ambiguity for any meaningful use case.
278
+ *
279
+ * The updater itself must be synchronous and `set` must be called from within it.
280
+ * IndexedDB transactions auto-close on any non-IDB await, which would silently
281
+ * break the atomicity that makes the update correct. Two guards make misuse
282
+ * loud rather than silent: calling `set` after `updater` has returned throws a
283
+ * `UsageError` at the call site; returning a thenable (e.g. an `async` updater)
284
+ * is detected after `updater` returns, aborts the transaction, and is logged
285
+ * under `FluidCacheUpdateCallbackError`. If `updater` calls `set` more than
286
+ * once, the last value wins.
287
+ *
288
+ * When `set` is called, the write (or delete) atomically replaces whatever row
289
+ * exists at the key, including cross-partition or stale rows that the updater
290
+ * saw as `undefined`. This matches the unconditional overwrite behavior of
291
+ * `put`. Callers that must preserve cross-partition rows should not use `update`.
292
+ *
293
+ * Exceptions thrown by `updater` are logged under the dedicated
294
+ * `FluidCacheUpdateCallbackError` telemetry event (distinct from IDB write errors)
295
+ * and surfaced to the caller as a `false` return value, after aborting the
296
+ * transaction so the existing row is preserved — even if `set` was called before
297
+ * the throw.
298
+ *
299
+ * Compare-and-set callers: a `false` return collapses three distinct outcomes —
300
+ * the updater returned without calling `set`, the updater threw (including the
301
+ * async-updater misuse case above), and the IDB write itself failed. Callers
302
+ * that need to distinguish these must consult telemetry: updater-side failures
303
+ * are logged under `FluidCacheUpdateCallbackError`; IDB-write failures are
304
+ * logged under `FluidCachePutError`. A lost compare-and-set race (the updater
305
+ * returned without calling `set`) is not logged.
306
+ * @returns `true` if `updater` called `set` and the write committed; `false` if
307
+ * `updater` returned without calling `set`, threw, or an IDB error occurred. IDB
308
+ * errors are logged and not thrown, matching the behavior of `put`.
309
+ */
310
+ async update(entry, updater) {
311
+ let db;
312
+ try {
313
+ db = await this.openDb();
314
+ const key = getKeyForCacheEntry(entry);
315
+ const transaction = db.transaction(FluidDriverObjectStoreName, "readwrite");
316
+ const existing = await transaction.store.get(key);
317
+ // Surface the cached value to the updater only when the existing entry is
318
+ // visible under the same rules `get` applies: same partition and not older
319
+ // than `maxCacheItemAge`. Cross-partition and stale entries are treated as
320
+ // absent so the updater sees the same view it would under `get`+`put`.
321
+ const existingVisible = existing?.partitionKey === this.partitionKey &&
322
+ Date.now() - existing.createdTimeMs <= this.maxCacheItemAge;
323
+ const existingValue = existingVisible ? existing?.cachedObject : undefined;
324
+ // `set` is a synchronous-only commit signal. We capture the last-supplied
325
+ // value (multi-call: last wins) and a "called" flag so the value being set
326
+ // to `undefined` still counts as a write. After `updater` returns we flip
327
+ // `updaterReturned` to true; any subsequent `set` call throws a `UsageError`
328
+ // at that call site so callers who try to defer the commit (e.g. from a
329
+ // `setTimeout`) see the misuse rather than silently writing into a closed
330
+ // transaction.
331
+ let valueToWrite;
332
+ let setCalled = false;
333
+ let updaterReturned = false;
334
+ const set = (value) => {
335
+ if (updaterReturned) {
336
+ throw new UsageError("FluidCache.update: set called after updater returned");
337
+ }
338
+ valueToWrite = value;
339
+ setCalled = true;
340
+ };
341
+ // Invoke the updater in its own try/catch so a host-supplied callback
342
+ // throwing does not get logged under `FluidCachePutError` (which is for
343
+ // IDB-write failures). On updater throw we abort the transaction so the
344
+ // existing row is preserved — even if `set` was called before the throw —
345
+ // log under the updater-specific event, and return `false` (matching the
346
+ // documented "errors are logged, not thrown" contract).
347
+ try {
348
+ const updaterResult = updater(existingValue, set);
349
+ updaterReturned = true;
350
+ // Reject async updaters: TypeScript structurally accepts
351
+ // `async (...) => Promise<void>` for the declared `() => void` parameter
352
+ // type, but an async updater that calls `set` synchronously and then
353
+ // awaits would let the IDB write commit before its eventual rejection
354
+ // surfaced — contradicting the "throw aborts the transaction" contract.
355
+ // Detect a thenable return and treat it as misuse symmetric with the
356
+ // late-`set` guard.
357
+ if (isPromiseLike(updaterResult)) {
358
+ throw new UsageError("FluidCache.update: updater must be synchronous (returned a thenable)");
359
+ }
360
+ }
361
+ catch (updaterError) {
362
+ updaterReturned = true;
363
+ transaction.abort();
364
+ // Await transaction settlement; aborting causes `transaction.done` to
365
+ // reject, which we swallow because the updater error is the real cause.
366
+ await transaction.done.catch(() => { });
367
+ this.logger.sendErrorEvent({
368
+ eventName: "FluidCacheUpdateCallbackError" /* FluidCacheErrorEvent.FluidCacheUpdateCallbackError */,
369
+ pkgVersion,
370
+ }, updaterError);
371
+ return false;
372
+ }
373
+ if (!setCalled) {
374
+ await transaction.done;
375
+ return false;
376
+ }
377
+ // `set(undefined)` is treated as a delete: there is no useful distinction
378
+ // between "no entry" and "entry stored as undefined" (both surface as
379
+ // `undefined` from `get`), so we expose this as an atomic conditional-delete
380
+ // rather than persisting an undefined-valued row that would otherwise
381
+ // occupy IDB until maintenance reaped it.
382
+ if (valueToWrite === undefined) {
383
+ await transaction.store.delete(key);
384
+ }
385
+ else {
386
+ const currentTime = Date.now();
387
+ await transaction.store.put({
388
+ cachedObject: valueToWrite,
389
+ fileId: entry.file.docId,
390
+ type: entry.type,
391
+ cacheItemId: entry.key,
392
+ partitionKey: this.partitionKey,
393
+ createdTimeMs: currentTime,
394
+ lastAccessTimeMs: currentTime,
395
+ }, key);
396
+ }
397
+ await transaction.done;
398
+ return true;
399
+ }
400
+ catch (error) {
401
+ this.logger.sendErrorEvent({ eventName: "FluidCachePutError" /* FluidCacheErrorEvent.FluidCachePutError */, pkgVersion }, error);
402
+ return false;
403
+ }
404
+ finally {
405
+ this.closeDb(db);
406
+ }
407
+ }
247
408
  }
248
409
  //# sourceMappingURL=FluidCache.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"FluidCache.js","sourceRoot":"","sources":["../src/FluidCache.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 { assert } from \"@fluidframework/core-utils/internal\";\nimport type {\n\tIPersistedCache,\n\tIFileEntry,\n\tICacheEntry,\n} from \"@fluidframework/driver-definitions/internal\";\nimport {\n\tgetKeyForCacheEntry,\n\tmaximumCacheDurationMs,\n} from \"@fluidframework/driver-utils/internal\";\nimport type { TelemetryLoggerExt } from \"@fluidframework/telemetry-utils/internal\";\nimport { UsageError, createChildLogger } from \"@fluidframework/telemetry-utils/internal\";\nimport type { IDBPDatabase } from \"idb\";\n\nimport type { FluidCacheDBSchema } from \"./FluidCacheIndexedDb.js\";\nimport {\n\tFluidDriverObjectStoreName,\n\tgetFluidCacheIndexedDbInstance,\n} from \"./FluidCacheIndexedDb.js\";\nimport {\n\tFluidCacheErrorEvent,\n\tFluidCacheEventSubCategories,\n\tFluidCacheGenericEvent,\n} from \"./fluidCacheTelemetry.js\";\nimport { pkgVersion } from \"./packageVersion.js\";\nimport { scheduleIdleTask } from \"./scheduleIdleTask.js\";\n\n// Some browsers have a usageDetails property that will tell you more detailed information\n// on how the storage is being used\ninterface StorageQuotaUsageDetails {\n\tindexedDB: number | undefined;\n}\n\n/**\n * @legacy @beta\n */\nexport interface FluidCacheConfig {\n\t/**\n\t * A string to specify what partition of the cache you wish to use (e.g. a user id).\n\t * Null can be used to explicity indicate no partitioning, and has been chosen\n\t * vs undefined so that it is clear this is an intentional choice by the caller.\n\t * A null value should only be used when the host can ensure that the cache is not able\n\t * to be shared with multiple users.\n\t */\n\t// eslint-disable-next-line @rushstack/no-new-null\n\tpartitionKey: string | null;\n\n\t/**\n\t * A logger that can be used to get insight into cache performance and errors\n\t */\n\tlogger?: ITelemetryBaseLogger;\n\n\t/**\n\t * A value in milliseconds that determines the maximum age of a cache entry to return.\n\t * If an entry exists in the cache, but is older than this value, the cached value will not be returned.\n\t */\n\tmaxCacheItemAge: number;\n\n\t/**\n\t * Each time db is opened, it will remain open for this much time. To improve perf, if this property is set as\n\t * any number greater than 0, then db will not be closed immediately after usage. This value is in milliseconds.\n\t */\n\tcloseDbAfterMs?: number;\n}\n\n/**\n * A cache that can be used by the Fluid ODSP driver to cache data for faster performance.\n * @legacy @beta\n */\nexport class FluidCache implements IPersistedCache {\n\tprivate readonly logger: TelemetryLoggerExt;\n\n\tprivate readonly partitionKey: string | null;\n\n\tprivate readonly maxCacheItemAge: number;\n\tprivate readonly closeDbImmediately: boolean = true;\n\tprivate readonly closeDbAfterMs: number;\n\tprivate db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\tprivate dbCloseTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate dbReuseCount: number = -1;\n\n\tconstructor(config: FluidCacheConfig) {\n\t\tconst { logger, partitionKey, maxCacheItemAge, closeDbAfterMs } = config;\n\t\tthis.logger = createChildLogger({ logger });\n\t\tthis.partitionKey = partitionKey;\n\t\tif (maxCacheItemAge > maximumCacheDurationMs) {\n\t\t\tconst error = new UsageError(\n\t\t\t\t`maxCacheItemAge(${maxCacheItemAge}) cannot be greater than ${maximumCacheDurationMs}`,\n\t\t\t\t{\n\t\t\t\t\tmaxCacheItemAge,\n\t\t\t\t\tmaximumCacheDurationMs,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t},\n\t\t\t);\n\t\t\t// go with logging, rather than throwing for now\n\t\t\t// as throwing could break existing usages\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{\n\t\t\t\t\teventName: \"maxCacheItemAgeTooLarge\",\n\t\t\t\t\tsubCategory: FluidCacheEventSubCategories.FluidCache,\n\t\t\t\t},\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t\tthis.maxCacheItemAge = Math.min(maxCacheItemAge, maximumCacheDurationMs);\n\t\tthis.closeDbAfterMs = closeDbAfterMs ?? 0;\n\t\tif (this.closeDbAfterMs > 0) {\n\t\t\tthis.closeDbImmediately = false;\n\t\t}\n\n\t\tscheduleIdleTask(async () => {\n\t\t\t// Log how much storage space is currently being used by indexed db.\n\t\t\t// NOTE: This API is not supported in all browsers and it doesn't let you see the size of a specific DB.\n\t\t\t// Exception added when eslint rule was added, this should be revisited when modifying this code\n\t\t\tif (navigator.storage?.estimate) {\n\t\t\t\tconst estimate = await navigator.storage.estimate();\n\n\t\t\t\t// Some browsers have a usageDetails property that will tell you\n\t\t\t\t// more detailed information on how the storage is being used\n\t\t\t\tlet indexedDBSize: number | undefined;\n\t\t\t\tif (\"usageDetails\" in estimate) {\n\t\t\t\t\tindexedDBSize = ((estimate as any).usageDetails as StorageQuotaUsageDetails)\n\t\t\t\t\t\t.indexedDB;\n\t\t\t\t}\n\n\t\t\t\tthis.logger.sendTelemetryEvent({\n\t\t\t\t\teventName: FluidCacheGenericEvent.FluidCacheStorageInfo,\n\t\t\t\t\tsubCategory: FluidCacheEventSubCategories.FluidCache,\n\t\t\t\t\tquota: estimate.quota,\n\t\t\t\t\tusage: estimate.usage,\n\t\t\t\t\tindexedDBSize,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\n\t\tscheduleIdleTask(async () => {\n\t\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\n\t\t\t// Delete entries that have not been accessed recently to clean up space\n\t\t\ttry {\n\t\t\t\tdb = await getFluidCacheIndexedDbInstance(this.logger);\n\n\t\t\t\tconst transaction = db.transaction(FluidDriverObjectStoreName, \"readwrite\");\n\t\t\t\tconst index = transaction.store.index(\"createdTimeMs\");\n\t\t\t\t// Get items which were cached before the maxCacheItemAge.\n\t\t\t\tconst keysToDelete = await index.getAllKeys(\n\t\t\t\t\tIDBKeyRange.upperBound(Date.now() - this.maxCacheItemAge),\n\t\t\t\t);\n\n\t\t\t\tawait Promise.all(keysToDelete.map(async (key) => transaction.store.delete(key)));\n\t\t\t\tawait transaction.done;\n\t\t\t} catch (error: any) {\n\t\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t\t{\n\t\t\t\t\t\teventName: FluidCacheErrorEvent.FluidCacheDeleteOldEntriesError,\n\t\t\t\t\t\tpkgVersion,\n\t\t\t\t\t},\n\t\t\t\t\terror,\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tdb?.close();\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate async openDb(): Promise<IDBPDatabase<FluidCacheDBSchema>> {\n\t\tif (this.closeDbImmediately) {\n\t\t\treturn getFluidCacheIndexedDbInstance(this.logger);\n\t\t}\n\t\tif (this.db === undefined) {\n\t\t\tconst dbInstance = await getFluidCacheIndexedDbInstance(this.logger);\n\t\t\tif (this.db === undefined) {\n\t\t\t\t// Reset the counter on first open.\n\t\t\t\tthis.dbReuseCount = -1;\n\t\t\t\tthis.db = dbInstance;\n\t\t\t} else {\n\t\t\t\tdbInstance.close();\n\t\t\t\tthis.dbReuseCount += 1;\n\t\t\t\treturn this.db;\n\t\t\t}\n\t\t\t// Need to close the db on version change if opened.\n\t\t\tthis.db.onversionchange = (ev) => {\n\t\t\t\tthis.db?.close();\n\t\t\t\tthis.db = undefined;\n\t\t\t\tclearTimeout(this.dbCloseTimer);\n\t\t\t\tthis.dbCloseTimer = undefined;\n\t\t\t};\n\t\t\tthis.db.addEventListener(\"close\", (ev) => {\n\t\t\t\tclearTimeout(this.dbCloseTimer);\n\t\t\t\tthis.dbCloseTimer = undefined;\n\t\t\t\tthis.db = undefined;\n\t\t\t});\n\t\t\t// Schedule db close after this.closeDbAfterMs.\n\t\t\tassert(this.dbCloseTimer === undefined, 0x6c6 /* timer should not be set yet!! */);\n\t\t\tthis.dbCloseTimer = setTimeout(() => {\n\t\t\t\tthis.db?.close();\n\t\t\t\tthis.db = undefined;\n\t\t\t\tthis.dbCloseTimer = undefined;\n\t\t\t}, this.closeDbAfterMs);\n\t\t}\n\t\tassert(this.db !== undefined, 0x6c7 /* db should be intialized by now */);\n\t\tthis.dbReuseCount += 1;\n\t\treturn this.db;\n\t}\n\n\tprivate closeDb(db?: IDBPDatabase<FluidCacheDBSchema>): void {\n\t\tif (this.closeDbImmediately) {\n\t\t\tdb?.close();\n\t\t}\n\t}\n\n\tpublic async removeEntries(file: IFileEntry): Promise<void> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tdb = await this.openDb();\n\n\t\t\tconst transaction = db.transaction(FluidDriverObjectStoreName, \"readwrite\");\n\t\t\tconst index = transaction.store.index(\"fileId\");\n\n\t\t\tconst keysToDelete = await index.getAllKeys(file.docId);\n\n\t\t\tawait Promise.all(keysToDelete.map(async (key) => transaction.store.delete(key)));\n\t\t\tawait transaction.done;\n\t\t} catch (error: any) {\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{\n\t\t\t\t\teventName: FluidCacheErrorEvent.FluidCacheDeleteOldEntriesError,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t},\n\t\t\t\terror,\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.closeDb(db);\n\t\t}\n\t}\n\n\tpublic async removeEntry(entry: ICacheEntry): Promise<void> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tdb = await this.openDb();\n\n\t\t\tconst key = getKeyForCacheEntry(entry);\n\t\t\tawait db.delete(FluidDriverObjectStoreName, key);\n\t\t} catch (error: any) {\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{\n\t\t\t\t\teventName: FluidCacheErrorEvent.FluidCacheDeleteSingleEntryError,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t},\n\t\t\t\terror,\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.closeDb(db);\n\t\t}\n\t}\n\n\tpublic async get(cacheEntry: ICacheEntry): Promise<any> {\n\t\tconst startTime = performance.now();\n\n\t\tconst cachedItem = await this.getItemFromCache(cacheEntry);\n\n\t\tthis.logger.sendPerformanceEvent({\n\t\t\teventName: \"FluidCacheAccess\",\n\t\t\tcacheHit: cachedItem !== undefined,\n\t\t\ttype: cacheEntry.type,\n\t\t\tduration: performance.now() - startTime,\n\t\t\tdbOpenPerf: cachedItem?.dbOpenPerf,\n\t\t\tdbReuseCount: this.dbReuseCount,\n\t\t\tpkgVersion,\n\t\t});\n\n\t\t// Value will contain metadata like the expiry time, we just want to return the object we were asked to cache\n\t\treturn cachedItem?.cachedObject;\n\t}\n\n\tprivate async getItemFromCache(cacheEntry: ICacheEntry): Promise<any> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tconst key = getKeyForCacheEntry(cacheEntry);\n\n\t\t\tconst dbOpenStartTime = performance.now();\n\t\t\tdb = await this.openDb();\n\t\t\tconst dbOpenPerf = performance.now() - dbOpenStartTime;\n\t\t\tconst value = await db.get(FluidDriverObjectStoreName, key);\n\n\t\t\tif (!value) {\n\t\t\t\tthis.closeDb(db);\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\t// If the data does not come from the same partition, don't return it\n\t\t\tif (value.partitionKey !== this.partitionKey) {\n\t\t\t\tthis.logger.sendTelemetryEvent({\n\t\t\t\t\teventName: FluidCacheGenericEvent.FluidCachePartitionKeyMismatch,\n\t\t\t\t\tsubCategory: FluidCacheEventSubCategories.FluidCache,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t});\n\n\t\t\t\tthis.closeDb(db);\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tconst currentTime = Date.now();\n\n\t\t\t// If too much time has passed since this cache entry was used, we will also return undefined\n\t\t\tif (currentTime - value.createdTimeMs > this.maxCacheItemAge) {\n\t\t\t\tthis.closeDb(db);\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tthis.closeDb(db);\n\t\t\treturn { ...value, dbOpenPerf };\n\t\t} catch (error: any) {\n\t\t\t// We can fail to open the db for a variety of reasons,\n\t\t\t// such as the database version having upgraded underneath us. Return undefined in this case\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{ eventName: FluidCacheErrorEvent.FluidCacheGetError, pkgVersion },\n\t\t\t\terror,\n\t\t\t);\n\t\t\tthis.closeDb(db);\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tpublic async put(entry: ICacheEntry, value: any): Promise<void> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tdb = await this.openDb();\n\n\t\t\tconst currentTime = Date.now();\n\n\t\t\tawait db.put(\n\t\t\t\tFluidDriverObjectStoreName,\n\t\t\t\t{\n\t\t\t\t\tcachedObject: value,\n\t\t\t\t\tfileId: entry.file.docId,\n\t\t\t\t\ttype: entry.type,\n\t\t\t\t\tcacheItemId: entry.key,\n\t\t\t\t\tpartitionKey: this.partitionKey,\n\t\t\t\t\tcreatedTimeMs: currentTime,\n\t\t\t\t\tlastAccessTimeMs: currentTime,\n\t\t\t\t},\n\t\t\t\tgetKeyForCacheEntry(entry),\n\t\t\t);\n\t\t\tthis.closeDb(db);\n\t\t} catch (error: any) {\n\t\t\t// We can fail to open the db for a variety of reasons,\n\t\t\t// such as the database version having upgraded underneath us\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{ eventName: FluidCacheErrorEvent.FluidCachePutError, pkgVersion },\n\t\t\t\terror,\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.closeDb(db);\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"FluidCache.js","sourceRoot":"","sources":["../src/FluidCache.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AAM5E,OAAO,EACN,mBAAmB,EACnB,sBAAsB,GACtB,MAAM,uCAAuC,CAAC;AAE/C,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,0CAA0C,CAAC;AAIzF,OAAO,EACN,0BAA0B,EAC1B,8BAA8B,GAC9B,MAAM,0BAA0B,CAAC;AAMlC,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAwCzD;;;GAGG;AACH,MAAM,OAAO,UAAU;IAYtB,YAAY,MAAwB;QANnB,uBAAkB,GAAY,IAAI,CAAC;QAI5C,iBAAY,GAAW,CAAC,CAAC,CAAC;QAGjC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,GAAG,MAAM,CAAC;QACzE,IAAI,CAAC,MAAM,GAAG,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;QAC5C,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,eAAe,GAAG,sBAAsB,EAAE,CAAC;YAC9C,MAAM,KAAK,GAAG,IAAI,UAAU,CAC3B,mBAAmB,eAAe,4BAA4B,sBAAsB,EAAE,EACtF;gBACC,eAAe;gBACf,sBAAsB;gBACtB,UAAU;aACV,CACD,CAAC;YACF,gDAAgD;YAChD,0CAA0C;YAC1C,IAAI,CAAC,MAAM,CAAC,cAAc,CACzB;gBACC,SAAS,EAAE,yBAAyB;gBACpC,WAAW,4DAAyC;aACpD,EACD,KAAK,CACL,CAAC;QACH,CAAC;QACD,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAAC;QACzE,IAAI,CAAC,cAAc,GAAG,cAAc,IAAI,CAAC,CAAC;QAC1C,IAAI,IAAI,CAAC,cAAc,GAAG,CAAC,EAAE,CAAC;YAC7B,IAAI,CAAC,kBAAkB,GAAG,KAAK,CAAC;QACjC,CAAC;QAED,gBAAgB,CAAC,KAAK,IAAI,EAAE;YAC3B,oEAAoE;YACpE,wGAAwG;YACxG,gGAAgG;YAChG,IAAI,SAAS,CAAC,OAAO,EAAE,QAAQ,EAAE,CAAC;gBACjC,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;gBAEpD,gEAAgE;gBAChE,6DAA6D;gBAC7D,IAAI,aAAiC,CAAC;gBACtC,IAAI,cAAc,IAAI,QAAQ,EAAE,CAAC;oBAChC,aAAa,GAAK,QAAgB,CAAC,YAAyC;yBAC1E,SAAS,CAAC;gBACb,CAAC;gBAED,IAAI,CAAC,MAAM,CAAC,kBAAkB,CAAC;oBAC9B,SAAS,4EAA8C;oBACvD,WAAW,4DAAyC;oBACpD,KAAK,EAAE,QAAQ,CAAC,KAAK;oBACrB,KAAK,EAAE,QAAQ,CAAC,KAAK;oBACrB,aAAa;oBACb,UAAU;iBACV,CAAC,CAAC;YACJ,CAAC;QACF,CAAC,CAAC,CAAC;QAEH,gBAAgB,CAAC,KAAK,IAAI,EAAE;YAC3B,IAAI,EAAgD,CAAC;YAErD,wEAAwE;YACxE,IAAI,CAAC;gBACJ,EAAE,GAAG,MAAM,8BAA8B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAEvD,MAAM,WAAW,GAAG,EAAE,CAAC,WAAW,CAAC,0BAA0B,EAAE,WAAW,CAAC,CAAC;gBAC5E,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;gBACvD,0DAA0D;gBAC1D,MAAM,YAAY,GAAG,MAAM,KAAK,CAAC,UAAU,CAC1C,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,eAAe,CAAC,CACzD,CAAC;gBAEF,MAAM,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBAClF,MAAM,WAAW,CAAC,IAAI,CAAC;YACxB,CAAC;YAAC,OAAO,KAAU,EAAE,CAAC;gBACrB,IAAI,CAAC,MAAM,CAAC,cAAc,CACzB;oBACC,SAAS,8FAAsD;oBAC/D,UAAU;iBACV,EACD,KAAK,CACL,CAAC;YACH,CAAC;oBAAS,CAAC;gBACV,EAAE,EAAE,KAAK,EAAE,CAAC;YACb,CAAC;QACF,CAAC,CAAC,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,MAAM;QACnB,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC7B,OAAO,8BAA8B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACpD,CAAC;QACD,IAAI,IAAI,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,UAAU,GAAG,MAAM,8BAA8B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACrE,IAAI,IAAI,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;gBAC3B,mCAAmC;gBACnC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC;gBACvB,IAAI,CAAC,EAAE,GAAG,UAAU,CAAC;YACtB,CAAC;iBAAM,CAAC;gBACP,UAAU,CAAC,KAAK,EAAE,CAAC;gBACnB,IAAI,CAAC,YAAY,IAAI,CAAC,CAAC;gBACvB,OAAO,IAAI,CAAC,EAAE,CAAC;YAChB,CAAC;YACD,oDAAoD;YACpD,IAAI,CAAC,EAAE,CAAC,eAAe,GAAG,CAAC,EAAE,EAAE,EAAE;gBAChC,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC;gBACjB,IAAI,CAAC,EAAE,GAAG,SAAS,CAAC;gBACpB,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;gBAChC,IAAI,CAAC,YAAY,GAAG,SAAS,CAAC;YAC/B,CAAC,CAAC;YACF,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE;gBACxC,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;gBAChC,IAAI,CAAC,YAAY,GAAG,SAAS,CAAC;gBAC9B,IAAI,CAAC,EAAE,GAAG,SAAS,CAAC;YACrB,CAAC,CAAC,CAAC;YACH,+CAA+C;YAC/C,MAAM,CAAC,IAAI,CAAC,YAAY,KAAK,SAAS,EAAE,KAAK,CAAC,mCAAmC,CAAC,CAAC;YACnF,IAAI,CAAC,YAAY,GAAG,UAAU,CAAC,GAAG,EAAE;gBACnC,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC;gBACjB,IAAI,CAAC,EAAE,GAAG,SAAS,CAAC;gBACpB,IAAI,CAAC,YAAY,GAAG,SAAS,CAAC;YAC/B,CAAC,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;QACzB,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,SAAS,EAAE,KAAK,CAAC,oCAAoC,CAAC,CAAC;QAC1E,IAAI,CAAC,YAAY,IAAI,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC,EAAE,CAAC;IAChB,CAAC;IAEO,OAAO,CAAC,EAAqC;QACpD,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC7B,EAAE,EAAE,KAAK,EAAE,CAAC;QACb,CAAC;IACF,CAAC;IAEM,KAAK,CAAC,aAAa,CAAC,IAAgB;QAC1C,IAAI,EAAgD,CAAC;QACrD,IAAI,CAAC;YACJ,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YAEzB,MAAM,WAAW,GAAG,EAAE,CAAC,WAAW,CAAC,0BAA0B,EAAE,WAAW,CAAC,CAAC;YAC5E,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YAEhD,MAAM,YAAY,GAAG,MAAM,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAExD,MAAM,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAClF,MAAM,WAAW,CAAC,IAAI,CAAC;QACxB,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACrB,IAAI,CAAC,MAAM,CAAC,cAAc,CACzB;gBACC,SAAS,8FAAsD;gBAC/D,UAAU;aACV,EACD,KAAK,CACL,CAAC;QACH,CAAC;gBAAS,CAAC;YACV,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAClB,CAAC;IACF,CAAC;IAEM,KAAK,CAAC,WAAW,CAAC,KAAkB;QAC1C,IAAI,EAAgD,CAAC;QACrD,IAAI,CAAC;YACJ,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YAEzB,MAAM,GAAG,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;YACvC,MAAM,EAAE,CAAC,MAAM,CAAC,0BAA0B,EAAE,GAAG,CAAC,CAAC;QAClD,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACrB,IAAI,CAAC,MAAM,CAAC,cAAc,CACzB;gBACC,SAAS,gGAAuD;gBAChE,UAAU;aACV,EACD,KAAK,CACL,CAAC;QACH,CAAC;gBAAS,CAAC;YACV,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAClB,CAAC;IACF,CAAC;IAEM,KAAK,CAAC,GAAG,CAAC,UAAuB;QACvC,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAEpC,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;QAE3D,IAAI,CAAC,MAAM,CAAC,oBAAoB,CAAC;YAChC,SAAS,EAAE,kBAAkB;YAC7B,QAAQ,EAAE,UAAU,KAAK,SAAS;YAClC,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,QAAQ,EAAE,WAAW,CAAC,GAAG,EAAE,GAAG,SAAS;YACvC,UAAU,EAAE,UAAU,EAAE,UAAU;YAClC,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,UAAU;SACV,CAAC,CAAC;QAEH,6GAA6G;QAC7G,OAAO,UAAU,EAAE,YAAY,CAAC;IACjC,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,UAAuB;QACrD,IAAI,EAAgD,CAAC;QACrD,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;YAE5C,MAAM,eAAe,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;YAC1C,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YACzB,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,eAAe,CAAC;YACvD,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,0BAA0B,EAAE,GAAG,CAAC,CAAC;YAE5D,IAAI,CAAC,KAAK,EAAE,CAAC;gBACZ,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBACjB,OAAO,SAAS,CAAC;YAClB,CAAC;YAED,qEAAqE;YACrE,IAAI,KAAK,CAAC,YAAY,KAAK,IAAI,CAAC,YAAY,EAAE,CAAC;gBAC9C,IAAI,CAAC,MAAM,CAAC,kBAAkB,CAAC;oBAC9B,SAAS,8FAAuD;oBAChE,WAAW,4DAAyC;oBACpD,UAAU;iBACV,CAAC,CAAC;gBAEH,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBACjB,OAAO,SAAS,CAAC;YAClB,CAAC;YAED,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAE/B,6FAA6F;YAC7F,IAAI,WAAW,GAAG,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;gBAC9D,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBACjB,OAAO,SAAS,CAAC;YAClB,CAAC;YAED,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACjB,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,CAAC;QACjC,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACrB,uDAAuD;YACvD,4FAA4F;YAC5F,IAAI,CAAC,MAAM,CAAC,cAAc,CACzB,EAAE,SAAS,oEAAyC,EAAE,UAAU,EAAE,EAClE,KAAK,CACL,CAAC;YACF,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACjB,OAAO,SAAS,CAAC;QAClB,CAAC;IACF,CAAC;IAEM,KAAK,CAAC,GAAG,CAAC,KAAkB,EAAE,KAAU;QAC9C,IAAI,EAAgD,CAAC;QACrD,IAAI,CAAC;YACJ,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YAEzB,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAE/B,MAAM,EAAE,CAAC,GAAG,CACX,0BAA0B,EAC1B;gBACC,YAAY,EAAE,KAAK;gBACnB,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK;gBACxB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,WAAW,EAAE,KAAK,CAAC,GAAG;gBACtB,YAAY,EAAE,IAAI,CAAC,YAAY;gBAC/B,aAAa,EAAE,WAAW;gBAC1B,gBAAgB,EAAE,WAAW;aAC7B,EACD,mBAAmB,CAAC,KAAK,CAAC,CAC1B,CAAC;YACF,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAClB,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACrB,uDAAuD;YACvD,6DAA6D;YAC7D,IAAI,CAAC,MAAM,CAAC,cAAc,CACzB,EAAE,SAAS,oEAAyC,EAAE,UAAU,EAAE,EAClE,KAAK,CACL,CAAC;QACH,CAAC;gBAAS,CAAC;YACV,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAClB,CAAC;IACF,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8DG;IACI,KAAK,CAAC,MAAM,CAClB,KAAkB,EAClB,OAAmE;QAEnE,IAAI,EAAgD,CAAC;QACrD,IAAI,CAAC;YACJ,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YAEzB,MAAM,GAAG,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;YACvC,MAAM,WAAW,GAAG,EAAE,CAAC,WAAW,CAAC,0BAA0B,EAAE,WAAW,CAAC,CAAC;YAC5E,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAClD,0EAA0E;YAC1E,2EAA2E;YAC3E,2EAA2E;YAC3E,uEAAuE;YACvE,MAAM,eAAe,GACpB,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC,YAAY;gBAC5C,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,aAAa,IAAI,IAAI,CAAC,eAAe,CAAC;YAC7D,MAAM,aAAa,GAAG,eAAe,CAAC,CAAC,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC;YAE3E,0EAA0E;YAC1E,2EAA2E;YAC3E,0EAA0E;YAC1E,6EAA6E;YAC7E,wEAAwE;YACxE,0EAA0E;YAC1E,eAAe;YACf,IAAI,YAAqB,CAAC;YAC1B,IAAI,SAAS,GAAG,KAAK,CAAC;YACtB,IAAI,eAAe,GAAG,KAAK,CAAC;YAC5B,MAAM,GAAG,GAAG,CAAC,KAAc,EAAQ,EAAE;gBACpC,IAAI,eAAe,EAAE,CAAC;oBACrB,MAAM,IAAI,UAAU,CAAC,sDAAsD,CAAC,CAAC;gBAC9E,CAAC;gBACD,YAAY,GAAG,KAAK,CAAC;gBACrB,SAAS,GAAG,IAAI,CAAC;YAClB,CAAC,CAAC;YAEF,sEAAsE;YACtE,wEAAwE;YACxE,wEAAwE;YACxE,0EAA0E;YAC1E,yEAAyE;YACzE,wDAAwD;YACxD,IAAI,CAAC;gBACJ,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;gBAClD,eAAe,GAAG,IAAI,CAAC;gBACvB,yDAAyD;gBACzD,yEAAyE;gBACzE,qEAAqE;gBACrE,sEAAsE;gBACtE,wEAAwE;gBACxE,qEAAqE;gBACrE,oBAAoB;gBACpB,IAAI,aAAa,CAAC,aAAa,CAAC,EAAE,CAAC;oBAClC,MAAM,IAAI,UAAU,CACnB,sEAAsE,CACtE,CAAC;gBACH,CAAC;YACF,CAAC;YAAC,OAAO,YAAiB,EAAE,CAAC;gBAC5B,eAAe,GAAG,IAAI,CAAC;gBACvB,WAAW,CAAC,KAAK,EAAE,CAAC;gBACpB,sEAAsE;gBACtE,wEAAwE;gBACxE,MAAM,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;gBACvC,IAAI,CAAC,MAAM,CAAC,cAAc,CACzB;oBACC,SAAS,0FAAoD;oBAC7D,UAAU;iBACV,EACD,YAAY,CACZ,CAAC;gBACF,OAAO,KAAK,CAAC;YACd,CAAC;YAED,IAAI,CAAC,SAAS,EAAE,CAAC;gBAChB,MAAM,WAAW,CAAC,IAAI,CAAC;gBACvB,OAAO,KAAK,CAAC;YACd,CAAC;YAED,0EAA0E;YAC1E,sEAAsE;YACtE,6EAA6E;YAC7E,sEAAsE;YACtE,0CAA0C;YAC1C,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;gBAChC,MAAM,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACrC,CAAC;iBAAM,CAAC;gBACP,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC/B,MAAM,WAAW,CAAC,KAAK,CAAC,GAAG,CAC1B;oBACC,YAAY,EAAE,YAAY;oBAC1B,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK;oBACxB,IAAI,EAAE,KAAK,CAAC,IAAI;oBAChB,WAAW,EAAE,KAAK,CAAC,GAAG;oBACtB,YAAY,EAAE,IAAI,CAAC,YAAY;oBAC/B,aAAa,EAAE,WAAW;oBAC1B,gBAAgB,EAAE,WAAW;iBAC7B,EACD,GAAG,CACH,CAAC;YACH,CAAC;YACD,MAAM,WAAW,CAAC,IAAI,CAAC;YACvB,OAAO,IAAI,CAAC;QACb,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACrB,IAAI,CAAC,MAAM,CAAC,cAAc,CACzB,EAAE,SAAS,oEAAyC,EAAE,UAAU,EAAE,EAClE,KAAK,CACL,CAAC;YACF,OAAO,KAAK,CAAC;QACd,CAAC;gBAAS,CAAC;YACV,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAClB,CAAC;IACF,CAAC;CACD","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 { assert, isPromiseLike } from \"@fluidframework/core-utils/internal\";\nimport type {\n\tIPersistedCache,\n\tIFileEntry,\n\tICacheEntry,\n} from \"@fluidframework/driver-definitions/internal\";\nimport {\n\tgetKeyForCacheEntry,\n\tmaximumCacheDurationMs,\n} from \"@fluidframework/driver-utils/internal\";\nimport type { TelemetryLoggerExt } from \"@fluidframework/telemetry-utils/internal\";\nimport { UsageError, createChildLogger } from \"@fluidframework/telemetry-utils/internal\";\nimport type { IDBPDatabase } from \"idb\";\n\nimport type { FluidCacheDBSchema } from \"./FluidCacheIndexedDb.js\";\nimport {\n\tFluidDriverObjectStoreName,\n\tgetFluidCacheIndexedDbInstance,\n} from \"./FluidCacheIndexedDb.js\";\nimport {\n\tFluidCacheErrorEvent,\n\tFluidCacheEventSubCategories,\n\tFluidCacheGenericEvent,\n} from \"./fluidCacheTelemetry.js\";\nimport { pkgVersion } from \"./packageVersion.js\";\nimport { scheduleIdleTask } from \"./scheduleIdleTask.js\";\n\n// Some browsers have a usageDetails property that will tell you more detailed information\n// on how the storage is being used\ninterface StorageQuotaUsageDetails {\n\tindexedDB: number | undefined;\n}\n\n/**\n * @legacy @beta\n */\nexport interface FluidCacheConfig {\n\t/**\n\t * A string to specify what partition of the cache you wish to use (e.g. a user id).\n\t * Null can be used to explicity indicate no partitioning, and has been chosen\n\t * vs undefined so that it is clear this is an intentional choice by the caller.\n\t * A null value should only be used when the host can ensure that the cache is not able\n\t * to be shared with multiple users.\n\t */\n\t// eslint-disable-next-line @rushstack/no-new-null\n\tpartitionKey: string | null;\n\n\t/**\n\t * A logger that can be used to get insight into cache performance and errors\n\t */\n\tlogger?: ITelemetryBaseLogger;\n\n\t/**\n\t * A value in milliseconds that determines the maximum age of a cache entry to return.\n\t * If an entry exists in the cache, but is older than this value, the cached value will not be returned.\n\t */\n\tmaxCacheItemAge: number;\n\n\t/**\n\t * Each time db is opened, it will remain open for this much time. To improve perf, if this property is set as\n\t * any number greater than 0, then db will not be closed immediately after usage. This value is in milliseconds.\n\t */\n\tcloseDbAfterMs?: number;\n}\n\n/**\n * A cache that can be used by the Fluid ODSP driver to cache data for faster performance.\n * @legacy @beta\n */\nexport class FluidCache implements IPersistedCache {\n\tprivate readonly logger: TelemetryLoggerExt;\n\n\tprivate readonly partitionKey: string | null;\n\n\tprivate readonly maxCacheItemAge: number;\n\tprivate readonly closeDbImmediately: boolean = true;\n\tprivate readonly closeDbAfterMs: number;\n\tprivate db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\tprivate dbCloseTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate dbReuseCount: number = -1;\n\n\tconstructor(config: FluidCacheConfig) {\n\t\tconst { logger, partitionKey, maxCacheItemAge, closeDbAfterMs } = config;\n\t\tthis.logger = createChildLogger({ logger });\n\t\tthis.partitionKey = partitionKey;\n\t\tif (maxCacheItemAge > maximumCacheDurationMs) {\n\t\t\tconst error = new UsageError(\n\t\t\t\t`maxCacheItemAge(${maxCacheItemAge}) cannot be greater than ${maximumCacheDurationMs}`,\n\t\t\t\t{\n\t\t\t\t\tmaxCacheItemAge,\n\t\t\t\t\tmaximumCacheDurationMs,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t},\n\t\t\t);\n\t\t\t// go with logging, rather than throwing for now\n\t\t\t// as throwing could break existing usages\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{\n\t\t\t\t\teventName: \"maxCacheItemAgeTooLarge\",\n\t\t\t\t\tsubCategory: FluidCacheEventSubCategories.FluidCache,\n\t\t\t\t},\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t\tthis.maxCacheItemAge = Math.min(maxCacheItemAge, maximumCacheDurationMs);\n\t\tthis.closeDbAfterMs = closeDbAfterMs ?? 0;\n\t\tif (this.closeDbAfterMs > 0) {\n\t\t\tthis.closeDbImmediately = false;\n\t\t}\n\n\t\tscheduleIdleTask(async () => {\n\t\t\t// Log how much storage space is currently being used by indexed db.\n\t\t\t// NOTE: This API is not supported in all browsers and it doesn't let you see the size of a specific DB.\n\t\t\t// Exception added when eslint rule was added, this should be revisited when modifying this code\n\t\t\tif (navigator.storage?.estimate) {\n\t\t\t\tconst estimate = await navigator.storage.estimate();\n\n\t\t\t\t// Some browsers have a usageDetails property that will tell you\n\t\t\t\t// more detailed information on how the storage is being used\n\t\t\t\tlet indexedDBSize: number | undefined;\n\t\t\t\tif (\"usageDetails\" in estimate) {\n\t\t\t\t\tindexedDBSize = ((estimate as any).usageDetails as StorageQuotaUsageDetails)\n\t\t\t\t\t\t.indexedDB;\n\t\t\t\t}\n\n\t\t\t\tthis.logger.sendTelemetryEvent({\n\t\t\t\t\teventName: FluidCacheGenericEvent.FluidCacheStorageInfo,\n\t\t\t\t\tsubCategory: FluidCacheEventSubCategories.FluidCache,\n\t\t\t\t\tquota: estimate.quota,\n\t\t\t\t\tusage: estimate.usage,\n\t\t\t\t\tindexedDBSize,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\n\t\tscheduleIdleTask(async () => {\n\t\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\n\t\t\t// Delete entries that have not been accessed recently to clean up space\n\t\t\ttry {\n\t\t\t\tdb = await getFluidCacheIndexedDbInstance(this.logger);\n\n\t\t\t\tconst transaction = db.transaction(FluidDriverObjectStoreName, \"readwrite\");\n\t\t\t\tconst index = transaction.store.index(\"createdTimeMs\");\n\t\t\t\t// Get items which were cached before the maxCacheItemAge.\n\t\t\t\tconst keysToDelete = await index.getAllKeys(\n\t\t\t\t\tIDBKeyRange.upperBound(Date.now() - this.maxCacheItemAge),\n\t\t\t\t);\n\n\t\t\t\tawait Promise.all(keysToDelete.map(async (key) => transaction.store.delete(key)));\n\t\t\t\tawait transaction.done;\n\t\t\t} catch (error: any) {\n\t\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t\t{\n\t\t\t\t\t\teventName: FluidCacheErrorEvent.FluidCacheDeleteOldEntriesError,\n\t\t\t\t\t\tpkgVersion,\n\t\t\t\t\t},\n\t\t\t\t\terror,\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tdb?.close();\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate async openDb(): Promise<IDBPDatabase<FluidCacheDBSchema>> {\n\t\tif (this.closeDbImmediately) {\n\t\t\treturn getFluidCacheIndexedDbInstance(this.logger);\n\t\t}\n\t\tif (this.db === undefined) {\n\t\t\tconst dbInstance = await getFluidCacheIndexedDbInstance(this.logger);\n\t\t\tif (this.db === undefined) {\n\t\t\t\t// Reset the counter on first open.\n\t\t\t\tthis.dbReuseCount = -1;\n\t\t\t\tthis.db = dbInstance;\n\t\t\t} else {\n\t\t\t\tdbInstance.close();\n\t\t\t\tthis.dbReuseCount += 1;\n\t\t\t\treturn this.db;\n\t\t\t}\n\t\t\t// Need to close the db on version change if opened.\n\t\t\tthis.db.onversionchange = (ev) => {\n\t\t\t\tthis.db?.close();\n\t\t\t\tthis.db = undefined;\n\t\t\t\tclearTimeout(this.dbCloseTimer);\n\t\t\t\tthis.dbCloseTimer = undefined;\n\t\t\t};\n\t\t\tthis.db.addEventListener(\"close\", (ev) => {\n\t\t\t\tclearTimeout(this.dbCloseTimer);\n\t\t\t\tthis.dbCloseTimer = undefined;\n\t\t\t\tthis.db = undefined;\n\t\t\t});\n\t\t\t// Schedule db close after this.closeDbAfterMs.\n\t\t\tassert(this.dbCloseTimer === undefined, 0x6c6 /* timer should not be set yet!! */);\n\t\t\tthis.dbCloseTimer = setTimeout(() => {\n\t\t\t\tthis.db?.close();\n\t\t\t\tthis.db = undefined;\n\t\t\t\tthis.dbCloseTimer = undefined;\n\t\t\t}, this.closeDbAfterMs);\n\t\t}\n\t\tassert(this.db !== undefined, 0x6c7 /* db should be intialized by now */);\n\t\tthis.dbReuseCount += 1;\n\t\treturn this.db;\n\t}\n\n\tprivate closeDb(db?: IDBPDatabase<FluidCacheDBSchema>): void {\n\t\tif (this.closeDbImmediately) {\n\t\t\tdb?.close();\n\t\t}\n\t}\n\n\tpublic async removeEntries(file: IFileEntry): Promise<void> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tdb = await this.openDb();\n\n\t\t\tconst transaction = db.transaction(FluidDriverObjectStoreName, \"readwrite\");\n\t\t\tconst index = transaction.store.index(\"fileId\");\n\n\t\t\tconst keysToDelete = await index.getAllKeys(file.docId);\n\n\t\t\tawait Promise.all(keysToDelete.map(async (key) => transaction.store.delete(key)));\n\t\t\tawait transaction.done;\n\t\t} catch (error: any) {\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{\n\t\t\t\t\teventName: FluidCacheErrorEvent.FluidCacheDeleteOldEntriesError,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t},\n\t\t\t\terror,\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.closeDb(db);\n\t\t}\n\t}\n\n\tpublic async removeEntry(entry: ICacheEntry): Promise<void> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tdb = await this.openDb();\n\n\t\t\tconst key = getKeyForCacheEntry(entry);\n\t\t\tawait db.delete(FluidDriverObjectStoreName, key);\n\t\t} catch (error: any) {\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{\n\t\t\t\t\teventName: FluidCacheErrorEvent.FluidCacheDeleteSingleEntryError,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t},\n\t\t\t\terror,\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.closeDb(db);\n\t\t}\n\t}\n\n\tpublic async get(cacheEntry: ICacheEntry): Promise<any> {\n\t\tconst startTime = performance.now();\n\n\t\tconst cachedItem = await this.getItemFromCache(cacheEntry);\n\n\t\tthis.logger.sendPerformanceEvent({\n\t\t\teventName: \"FluidCacheAccess\",\n\t\t\tcacheHit: cachedItem !== undefined,\n\t\t\ttype: cacheEntry.type,\n\t\t\tduration: performance.now() - startTime,\n\t\t\tdbOpenPerf: cachedItem?.dbOpenPerf,\n\t\t\tdbReuseCount: this.dbReuseCount,\n\t\t\tpkgVersion,\n\t\t});\n\n\t\t// Value will contain metadata like the expiry time, we just want to return the object we were asked to cache\n\t\treturn cachedItem?.cachedObject;\n\t}\n\n\tprivate async getItemFromCache(cacheEntry: ICacheEntry): Promise<any> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tconst key = getKeyForCacheEntry(cacheEntry);\n\n\t\t\tconst dbOpenStartTime = performance.now();\n\t\t\tdb = await this.openDb();\n\t\t\tconst dbOpenPerf = performance.now() - dbOpenStartTime;\n\t\t\tconst value = await db.get(FluidDriverObjectStoreName, key);\n\n\t\t\tif (!value) {\n\t\t\t\tthis.closeDb(db);\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\t// If the data does not come from the same partition, don't return it\n\t\t\tif (value.partitionKey !== this.partitionKey) {\n\t\t\t\tthis.logger.sendTelemetryEvent({\n\t\t\t\t\teventName: FluidCacheGenericEvent.FluidCachePartitionKeyMismatch,\n\t\t\t\t\tsubCategory: FluidCacheEventSubCategories.FluidCache,\n\t\t\t\t\tpkgVersion,\n\t\t\t\t});\n\n\t\t\t\tthis.closeDb(db);\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tconst currentTime = Date.now();\n\n\t\t\t// If too much time has passed since this cache entry was used, we will also return undefined\n\t\t\tif (currentTime - value.createdTimeMs > this.maxCacheItemAge) {\n\t\t\t\tthis.closeDb(db);\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tthis.closeDb(db);\n\t\t\treturn { ...value, dbOpenPerf };\n\t\t} catch (error: any) {\n\t\t\t// We can fail to open the db for a variety of reasons,\n\t\t\t// such as the database version having upgraded underneath us. Return undefined in this case\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{ eventName: FluidCacheErrorEvent.FluidCacheGetError, pkgVersion },\n\t\t\t\terror,\n\t\t\t);\n\t\t\tthis.closeDb(db);\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tpublic async put(entry: ICacheEntry, value: any): Promise<void> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tdb = await this.openDb();\n\n\t\t\tconst currentTime = Date.now();\n\n\t\t\tawait db.put(\n\t\t\t\tFluidDriverObjectStoreName,\n\t\t\t\t{\n\t\t\t\t\tcachedObject: value,\n\t\t\t\t\tfileId: entry.file.docId,\n\t\t\t\t\ttype: entry.type,\n\t\t\t\t\tcacheItemId: entry.key,\n\t\t\t\t\tpartitionKey: this.partitionKey,\n\t\t\t\t\tcreatedTimeMs: currentTime,\n\t\t\t\t\tlastAccessTimeMs: currentTime,\n\t\t\t\t},\n\t\t\t\tgetKeyForCacheEntry(entry),\n\t\t\t);\n\t\t\tthis.closeDb(db);\n\t\t} catch (error: any) {\n\t\t\t// We can fail to open the db for a variety of reasons,\n\t\t\t// such as the database version having upgraded underneath us\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{ eventName: FluidCacheErrorEvent.FluidCachePutError, pkgVersion },\n\t\t\t\terror,\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.closeDb(db);\n\t\t}\n\t}\n\n\t/**\n\t * Atomically reads the existing cached entry, hands it to `updater`, and writes a\n\t * new value iff `updater` calls the supplied `set` callback. The read and the\n\t * conditional write happen inside a single IndexedDB `readwrite` transaction, so\n\t * the decision sees a consistent view across consumers sharing the same underlying\n\t * IndexedDB instance (for example, multiple browser tabs racing to persist pending\n\t * state).\n\t *\n\t * @remarks\n\t * The implementation uses `transaction.store.get` + `transaction.store.put` rather\n\t * than an IDB cursor. Both run inside the same `readwrite` transaction, so the\n\t * atomicity guarantee is identical, and the get/put pair is materially simpler\n\t * to reason about for a single-key update. A cursor would be the right tool if we\n\t * needed to iterate or range-scan; for a known key we don't.\n\t *\n\t * @param entry - cache entry; identifies the file and the key within that file.\n\t * @param updater - synchronous callback invoked with `(existing, set)`.\n\t * `existing` is the currently-cached value, or `undefined` when the cached row is\n\t * invisible under the same rules `get` applies: no entry exists for the key, the\n\t * existing entry belongs to a different partition, or the existing entry is older\n\t * than `maxCacheItemAge`. The updater can derive the new value from `existing`\n\t * (read-modify-write) or ignore it entirely. To commit a write, call `set(value)`;\n\t * to leave the cache untouched, return without calling `set`. Stored via IndexedDB\n\t * structured clone, with the same value requirements as {@link FluidCache.put} —\n\t * not restricted to JSON-serializable values.\n\t *\n\t * Calling `set(undefined)` removes the row at the key (equivalent to\n\t * {@link FluidCache.removeEntry} inside the same atomic transaction). `get`\n\t * already collapses \"no entry\" and \"entry stored as undefined\" into the same\n\t * observable result, so the delete-on-undefined semantics gives callers an\n\t * atomic conditional-delete without ambiguity for any meaningful use case.\n\t *\n\t * The updater itself must be synchronous and `set` must be called from within it.\n\t * IndexedDB transactions auto-close on any non-IDB await, which would silently\n\t * break the atomicity that makes the update correct. Two guards make misuse\n\t * loud rather than silent: calling `set` after `updater` has returned throws a\n\t * `UsageError` at the call site; returning a thenable (e.g. an `async` updater)\n\t * is detected after `updater` returns, aborts the transaction, and is logged\n\t * under `FluidCacheUpdateCallbackError`. If `updater` calls `set` more than\n\t * once, the last value wins.\n\t *\n\t * When `set` is called, the write (or delete) atomically replaces whatever row\n\t * exists at the key, including cross-partition or stale rows that the updater\n\t * saw as `undefined`. This matches the unconditional overwrite behavior of\n\t * `put`. Callers that must preserve cross-partition rows should not use `update`.\n\t *\n\t * Exceptions thrown by `updater` are logged under the dedicated\n\t * `FluidCacheUpdateCallbackError` telemetry event (distinct from IDB write errors)\n\t * and surfaced to the caller as a `false` return value, after aborting the\n\t * transaction so the existing row is preserved — even if `set` was called before\n\t * the throw.\n\t *\n\t * Compare-and-set callers: a `false` return collapses three distinct outcomes —\n\t * the updater returned without calling `set`, the updater threw (including the\n\t * async-updater misuse case above), and the IDB write itself failed. Callers\n\t * that need to distinguish these must consult telemetry: updater-side failures\n\t * are logged under `FluidCacheUpdateCallbackError`; IDB-write failures are\n\t * logged under `FluidCachePutError`. A lost compare-and-set race (the updater\n\t * returned without calling `set`) is not logged.\n\t * @returns `true` if `updater` called `set` and the write committed; `false` if\n\t * `updater` returned without calling `set`, threw, or an IDB error occurred. IDB\n\t * errors are logged and not thrown, matching the behavior of `put`.\n\t */\n\tpublic async update(\n\t\tentry: ICacheEntry,\n\t\tupdater: (existing: unknown, set: (value: unknown) => void) => void,\n\t): Promise<boolean> {\n\t\tlet db: IDBPDatabase<FluidCacheDBSchema> | undefined;\n\t\ttry {\n\t\t\tdb = await this.openDb();\n\n\t\t\tconst key = getKeyForCacheEntry(entry);\n\t\t\tconst transaction = db.transaction(FluidDriverObjectStoreName, \"readwrite\");\n\t\t\tconst existing = await transaction.store.get(key);\n\t\t\t// Surface the cached value to the updater only when the existing entry is\n\t\t\t// visible under the same rules `get` applies: same partition and not older\n\t\t\t// than `maxCacheItemAge`. Cross-partition and stale entries are treated as\n\t\t\t// absent so the updater sees the same view it would under `get`+`put`.\n\t\t\tconst existingVisible =\n\t\t\t\texisting?.partitionKey === this.partitionKey &&\n\t\t\t\tDate.now() - existing.createdTimeMs <= this.maxCacheItemAge;\n\t\t\tconst existingValue = existingVisible ? existing?.cachedObject : undefined;\n\n\t\t\t// `set` is a synchronous-only commit signal. We capture the last-supplied\n\t\t\t// value (multi-call: last wins) and a \"called\" flag so the value being set\n\t\t\t// to `undefined` still counts as a write. After `updater` returns we flip\n\t\t\t// `updaterReturned` to true; any subsequent `set` call throws a `UsageError`\n\t\t\t// at that call site so callers who try to defer the commit (e.g. from a\n\t\t\t// `setTimeout`) see the misuse rather than silently writing into a closed\n\t\t\t// transaction.\n\t\t\tlet valueToWrite: unknown;\n\t\t\tlet setCalled = false;\n\t\t\tlet updaterReturned = false;\n\t\t\tconst set = (value: unknown): void => {\n\t\t\t\tif (updaterReturned) {\n\t\t\t\t\tthrow new UsageError(\"FluidCache.update: set called after updater returned\");\n\t\t\t\t}\n\t\t\t\tvalueToWrite = value;\n\t\t\t\tsetCalled = true;\n\t\t\t};\n\n\t\t\t// Invoke the updater in its own try/catch so a host-supplied callback\n\t\t\t// throwing does not get logged under `FluidCachePutError` (which is for\n\t\t\t// IDB-write failures). On updater throw we abort the transaction so the\n\t\t\t// existing row is preserved — even if `set` was called before the throw —\n\t\t\t// log under the updater-specific event, and return `false` (matching the\n\t\t\t// documented \"errors are logged, not thrown\" contract).\n\t\t\ttry {\n\t\t\t\tconst updaterResult = updater(existingValue, set);\n\t\t\t\tupdaterReturned = true;\n\t\t\t\t// Reject async updaters: TypeScript structurally accepts\n\t\t\t\t// `async (...) => Promise<void>` for the declared `() => void` parameter\n\t\t\t\t// type, but an async updater that calls `set` synchronously and then\n\t\t\t\t// awaits would let the IDB write commit before its eventual rejection\n\t\t\t\t// surfaced — contradicting the \"throw aborts the transaction\" contract.\n\t\t\t\t// Detect a thenable return and treat it as misuse symmetric with the\n\t\t\t\t// late-`set` guard.\n\t\t\t\tif (isPromiseLike(updaterResult)) {\n\t\t\t\t\tthrow new UsageError(\n\t\t\t\t\t\t\"FluidCache.update: updater must be synchronous (returned a thenable)\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch (updaterError: any) {\n\t\t\t\tupdaterReturned = true;\n\t\t\t\ttransaction.abort();\n\t\t\t\t// Await transaction settlement; aborting causes `transaction.done` to\n\t\t\t\t// reject, which we swallow because the updater error is the real cause.\n\t\t\t\tawait transaction.done.catch(() => {});\n\t\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t\t{\n\t\t\t\t\t\teventName: FluidCacheErrorEvent.FluidCacheUpdateCallbackError,\n\t\t\t\t\t\tpkgVersion,\n\t\t\t\t\t},\n\t\t\t\t\tupdaterError,\n\t\t\t\t);\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tif (!setCalled) {\n\t\t\t\tawait transaction.done;\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// `set(undefined)` is treated as a delete: there is no useful distinction\n\t\t\t// between \"no entry\" and \"entry stored as undefined\" (both surface as\n\t\t\t// `undefined` from `get`), so we expose this as an atomic conditional-delete\n\t\t\t// rather than persisting an undefined-valued row that would otherwise\n\t\t\t// occupy IDB until maintenance reaped it.\n\t\t\tif (valueToWrite === undefined) {\n\t\t\t\tawait transaction.store.delete(key);\n\t\t\t} else {\n\t\t\t\tconst currentTime = Date.now();\n\t\t\t\tawait transaction.store.put(\n\t\t\t\t\t{\n\t\t\t\t\t\tcachedObject: valueToWrite,\n\t\t\t\t\t\tfileId: entry.file.docId,\n\t\t\t\t\t\ttype: entry.type,\n\t\t\t\t\t\tcacheItemId: entry.key,\n\t\t\t\t\t\tpartitionKey: this.partitionKey,\n\t\t\t\t\t\tcreatedTimeMs: currentTime,\n\t\t\t\t\t\tlastAccessTimeMs: currentTime,\n\t\t\t\t\t},\n\t\t\t\t\tkey,\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait transaction.done;\n\t\t\treturn true;\n\t\t} catch (error: any) {\n\t\t\tthis.logger.sendErrorEvent(\n\t\t\t\t{ eventName: FluidCacheErrorEvent.FluidCachePutError, pkgVersion },\n\t\t\t\terror,\n\t\t\t);\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tthis.closeDb(db);\n\t\t}\n\t}\n}\n"]}
@@ -11,6 +11,7 @@ export declare const enum FluidCacheErrorEvent {
11
11
  "FluidCacheDeleteSingleEntryError" = "FluidCacheDeleteSingleEntryError",
12
12
  "FluidCacheGetError" = "FluidCacheGetError",
13
13
  "FluidCachePutError" = "FluidCachePutError",
14
+ "FluidCacheUpdateCallbackError" = "FluidCacheUpdateCallbackError",
14
15
  "FluidCacheUpdateUsageError" = "FluidCacheUpdateUsageError",
15
16
  "FluidCacheDeleteOldDbError" = "FluidCacheDeleteOldDbError"
16
17
  }
@@ -1 +1 @@
1
- {"version":3,"file":"fluidCacheTelemetry.d.ts","sourceRoot":"","sources":["../src/fluidCacheTelemetry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,0BAAkB,sBAAsB;IACvC,uBAAuB,0BAA0B;IACjD,gCAAgC,mCAAmC;CACnE;AAED,0BAAkB,oBAAoB;IACrC,iCAAiC,oCAAoC;IACrE,kCAAkC,qCAAqC;IACvE,oBAAoB,uBAAuB;IAC3C,oBAAoB,uBAAuB;IAC3C,4BAA4B,+BAA+B;IAC3D,4BAA4B,+BAA+B;CAC3D;AAED,0BAAkB,4BAA4B;IAC7C,YAAY,eAAe;CAC3B"}
1
+ {"version":3,"file":"fluidCacheTelemetry.d.ts","sourceRoot":"","sources":["../src/fluidCacheTelemetry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,0BAAkB,sBAAsB;IACvC,uBAAuB,0BAA0B;IACjD,gCAAgC,mCAAmC;CACnE;AAED,0BAAkB,oBAAoB;IACrC,iCAAiC,oCAAoC;IACrE,kCAAkC,qCAAqC;IACvE,oBAAoB,uBAAuB;IAC3C,oBAAoB,uBAAuB;IAC3C,+BAA+B,kCAAkC;IACjE,4BAA4B,+BAA+B;IAC3D,4BAA4B,+BAA+B;CAC3D;AAED,0BAAkB,4BAA4B;IAC7C,YAAY,eAAe;CAC3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"fluidCacheTelemetry.js","sourceRoot":"","sources":["../src/fluidCacheTelemetry.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nexport const enum FluidCacheGenericEvent {\n\t\"FluidCacheStorageInfo\" = \"FluidCacheStorageInfo\",\n\t\"FluidCachePartitionKeyMismatch\" = \"FluidCachePartitionKeyMismatch\",\n}\n\nexport const enum FluidCacheErrorEvent {\n\t\"FluidCacheDeleteOldEntriesError\" = \"FluidCacheDeleteOldEntriesError\",\n\t\"FluidCacheDeleteSingleEntryError\" = \"FluidCacheDeleteSingleEntryError\",\n\t\"FluidCacheGetError\" = \"FluidCacheGetError\",\n\t\"FluidCachePutError\" = \"FluidCachePutError\",\n\t\"FluidCacheUpdateUsageError\" = \"FluidCacheUpdateUsageError\",\n\t\"FluidCacheDeleteOldDbError\" = \"FluidCacheDeleteOldDbError\",\n}\n\nexport const enum FluidCacheEventSubCategories {\n\t\"FluidCache\" = \"FluidCache\",\n}\n"]}
1
+ {"version":3,"file":"fluidCacheTelemetry.js","sourceRoot":"","sources":["../src/fluidCacheTelemetry.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nexport const enum FluidCacheGenericEvent {\n\t\"FluidCacheStorageInfo\" = \"FluidCacheStorageInfo\",\n\t\"FluidCachePartitionKeyMismatch\" = \"FluidCachePartitionKeyMismatch\",\n}\n\nexport const enum FluidCacheErrorEvent {\n\t\"FluidCacheDeleteOldEntriesError\" = \"FluidCacheDeleteOldEntriesError\",\n\t\"FluidCacheDeleteSingleEntryError\" = \"FluidCacheDeleteSingleEntryError\",\n\t\"FluidCacheGetError\" = \"FluidCacheGetError\",\n\t\"FluidCachePutError\" = \"FluidCachePutError\",\n\t\"FluidCacheUpdateCallbackError\" = \"FluidCacheUpdateCallbackError\",\n\t\"FluidCacheUpdateUsageError\" = \"FluidCacheUpdateUsageError\",\n\t\"FluidCacheDeleteOldDbError\" = \"FluidCacheDeleteOldDbError\",\n}\n\nexport const enum FluidCacheEventSubCategories {\n\t\"FluidCache\" = \"FluidCache\",\n}\n"]}
@@ -5,5 +5,5 @@
5
5
  * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
6
6
  */
7
7
  export declare const pkgName = "@fluidframework/driver-web-cache";
8
- export declare const pkgVersion = "2.101.0";
8
+ export declare const pkgVersion = "2.102.0";
9
9
  //# sourceMappingURL=packageVersion.d.ts.map
@@ -5,5 +5,5 @@
5
5
  * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
6
6
  */
7
7
  export const pkgName = "@fluidframework/driver-web-cache";
8
- export const pkgVersion = "2.101.0";
8
+ export const pkgVersion = "2.102.0";
9
9
  //# sourceMappingURL=packageVersion.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"packageVersion.js","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,OAAO,GAAG,kCAAkC,CAAC;AAC1D,MAAM,CAAC,MAAM,UAAU,GAAG,SAAS,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/driver-web-cache\";\nexport const pkgVersion = \"2.101.0\";\n"]}
1
+ {"version":3,"file":"packageVersion.js","sourceRoot":"","sources":["../src/packageVersion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,OAAO,GAAG,kCAAkC,CAAC;AAC1D,MAAM,CAAC,MAAM,UAAU,GAAG,SAAS,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n *\n * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY\n */\n\nexport const pkgName = \"@fluidframework/driver-web-cache\";\nexport const pkgVersion = \"2.102.0\";\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/driver-web-cache",
3
- "version": "2.101.0",
3
+ "version": "2.102.0",
4
4
  "description": "Implementation of the driver caching API for a web browser",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -47,21 +47,21 @@
47
47
  "main": "lib/index.js",
48
48
  "types": "lib/public.d.ts",
49
49
  "dependencies": {
50
- "@fluidframework/core-interfaces": "~2.101.0",
51
- "@fluidframework/core-utils": "~2.101.0",
52
- "@fluidframework/driver-definitions": "~2.101.0",
53
- "@fluidframework/driver-utils": "~2.101.0",
54
- "@fluidframework/telemetry-utils": "~2.101.0",
50
+ "@fluidframework/core-interfaces": "~2.102.0",
51
+ "@fluidframework/core-utils": "~2.102.0",
52
+ "@fluidframework/driver-definitions": "~2.102.0",
53
+ "@fluidframework/driver-utils": "~2.102.0",
54
+ "@fluidframework/telemetry-utils": "~2.102.0",
55
55
  "idb": "^6.1.2"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@arethetypeswrong/cli": "^0.18.2",
59
59
  "@biomejs/biome": "~2.4.5",
60
- "@fluid-internal/mocha-test-setup": "~2.101.0",
60
+ "@fluid-internal/mocha-test-setup": "~2.102.0",
61
61
  "@fluid-tools/build-cli": "^0.65.0",
62
62
  "@fluidframework/build-common": "^2.0.3",
63
63
  "@fluidframework/build-tools": "^0.65.0",
64
- "@fluidframework/driver-web-cache-previous": "npm:@fluidframework/driver-web-cache@2.92.0",
64
+ "@fluidframework/driver-web-cache-previous": "npm:@fluidframework/driver-web-cache@2.101.0",
65
65
  "@fluidframework/eslint-config-fluid": "^9.0.0",
66
66
  "@microsoft/api-extractor": "7.58.1",
67
67
  "@types/mocha": "^10.0.10",
@@ -79,7 +79,11 @@
79
79
  "typescript": "~5.4.5"
80
80
  },
81
81
  "typeValidation": {
82
- "broken": {},
82
+ "broken": {
83
+ "Class_FluidCache": {
84
+ "forwardCompat": false
85
+ }
86
+ },
83
87
  "entrypoint": "legacy"
84
88
  },
85
89
  "scripts": {
package/src/FluidCache.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { ITelemetryBaseLogger } from "@fluidframework/core-interfaces";
7
- import { assert } from "@fluidframework/core-utils/internal";
7
+ import { assert, isPromiseLike } from "@fluidframework/core-utils/internal";
8
8
  import type {
9
9
  IPersistedCache,
10
10
  IFileEntry,
@@ -361,4 +361,182 @@ export class FluidCache implements IPersistedCache {
361
361
  this.closeDb(db);
362
362
  }
363
363
  }
364
+
365
+ /**
366
+ * Atomically reads the existing cached entry, hands it to `updater`, and writes a
367
+ * new value iff `updater` calls the supplied `set` callback. The read and the
368
+ * conditional write happen inside a single IndexedDB `readwrite` transaction, so
369
+ * the decision sees a consistent view across consumers sharing the same underlying
370
+ * IndexedDB instance (for example, multiple browser tabs racing to persist pending
371
+ * state).
372
+ *
373
+ * @remarks
374
+ * The implementation uses `transaction.store.get` + `transaction.store.put` rather
375
+ * than an IDB cursor. Both run inside the same `readwrite` transaction, so the
376
+ * atomicity guarantee is identical, and the get/put pair is materially simpler
377
+ * to reason about for a single-key update. A cursor would be the right tool if we
378
+ * needed to iterate or range-scan; for a known key we don't.
379
+ *
380
+ * @param entry - cache entry; identifies the file and the key within that file.
381
+ * @param updater - synchronous callback invoked with `(existing, set)`.
382
+ * `existing` is the currently-cached value, or `undefined` when the cached row is
383
+ * invisible under the same rules `get` applies: no entry exists for the key, the
384
+ * existing entry belongs to a different partition, or the existing entry is older
385
+ * than `maxCacheItemAge`. The updater can derive the new value from `existing`
386
+ * (read-modify-write) or ignore it entirely. To commit a write, call `set(value)`;
387
+ * to leave the cache untouched, return without calling `set`. Stored via IndexedDB
388
+ * structured clone, with the same value requirements as {@link FluidCache.put} —
389
+ * not restricted to JSON-serializable values.
390
+ *
391
+ * Calling `set(undefined)` removes the row at the key (equivalent to
392
+ * {@link FluidCache.removeEntry} inside the same atomic transaction). `get`
393
+ * already collapses "no entry" and "entry stored as undefined" into the same
394
+ * observable result, so the delete-on-undefined semantics gives callers an
395
+ * atomic conditional-delete without ambiguity for any meaningful use case.
396
+ *
397
+ * The updater itself must be synchronous and `set` must be called from within it.
398
+ * IndexedDB transactions auto-close on any non-IDB await, which would silently
399
+ * break the atomicity that makes the update correct. Two guards make misuse
400
+ * loud rather than silent: calling `set` after `updater` has returned throws a
401
+ * `UsageError` at the call site; returning a thenable (e.g. an `async` updater)
402
+ * is detected after `updater` returns, aborts the transaction, and is logged
403
+ * under `FluidCacheUpdateCallbackError`. If `updater` calls `set` more than
404
+ * once, the last value wins.
405
+ *
406
+ * When `set` is called, the write (or delete) atomically replaces whatever row
407
+ * exists at the key, including cross-partition or stale rows that the updater
408
+ * saw as `undefined`. This matches the unconditional overwrite behavior of
409
+ * `put`. Callers that must preserve cross-partition rows should not use `update`.
410
+ *
411
+ * Exceptions thrown by `updater` are logged under the dedicated
412
+ * `FluidCacheUpdateCallbackError` telemetry event (distinct from IDB write errors)
413
+ * and surfaced to the caller as a `false` return value, after aborting the
414
+ * transaction so the existing row is preserved — even if `set` was called before
415
+ * the throw.
416
+ *
417
+ * Compare-and-set callers: a `false` return collapses three distinct outcomes —
418
+ * the updater returned without calling `set`, the updater threw (including the
419
+ * async-updater misuse case above), and the IDB write itself failed. Callers
420
+ * that need to distinguish these must consult telemetry: updater-side failures
421
+ * are logged under `FluidCacheUpdateCallbackError`; IDB-write failures are
422
+ * logged under `FluidCachePutError`. A lost compare-and-set race (the updater
423
+ * returned without calling `set`) is not logged.
424
+ * @returns `true` if `updater` called `set` and the write committed; `false` if
425
+ * `updater` returned without calling `set`, threw, or an IDB error occurred. IDB
426
+ * errors are logged and not thrown, matching the behavior of `put`.
427
+ */
428
+ public async update(
429
+ entry: ICacheEntry,
430
+ updater: (existing: unknown, set: (value: unknown) => void) => void,
431
+ ): Promise<boolean> {
432
+ let db: IDBPDatabase<FluidCacheDBSchema> | undefined;
433
+ try {
434
+ db = await this.openDb();
435
+
436
+ const key = getKeyForCacheEntry(entry);
437
+ const transaction = db.transaction(FluidDriverObjectStoreName, "readwrite");
438
+ const existing = await transaction.store.get(key);
439
+ // Surface the cached value to the updater only when the existing entry is
440
+ // visible under the same rules `get` applies: same partition and not older
441
+ // than `maxCacheItemAge`. Cross-partition and stale entries are treated as
442
+ // absent so the updater sees the same view it would under `get`+`put`.
443
+ const existingVisible =
444
+ existing?.partitionKey === this.partitionKey &&
445
+ Date.now() - existing.createdTimeMs <= this.maxCacheItemAge;
446
+ const existingValue = existingVisible ? existing?.cachedObject : undefined;
447
+
448
+ // `set` is a synchronous-only commit signal. We capture the last-supplied
449
+ // value (multi-call: last wins) and a "called" flag so the value being set
450
+ // to `undefined` still counts as a write. After `updater` returns we flip
451
+ // `updaterReturned` to true; any subsequent `set` call throws a `UsageError`
452
+ // at that call site so callers who try to defer the commit (e.g. from a
453
+ // `setTimeout`) see the misuse rather than silently writing into a closed
454
+ // transaction.
455
+ let valueToWrite: unknown;
456
+ let setCalled = false;
457
+ let updaterReturned = false;
458
+ const set = (value: unknown): void => {
459
+ if (updaterReturned) {
460
+ throw new UsageError("FluidCache.update: set called after updater returned");
461
+ }
462
+ valueToWrite = value;
463
+ setCalled = true;
464
+ };
465
+
466
+ // Invoke the updater in its own try/catch so a host-supplied callback
467
+ // throwing does not get logged under `FluidCachePutError` (which is for
468
+ // IDB-write failures). On updater throw we abort the transaction so the
469
+ // existing row is preserved — even if `set` was called before the throw —
470
+ // log under the updater-specific event, and return `false` (matching the
471
+ // documented "errors are logged, not thrown" contract).
472
+ try {
473
+ const updaterResult = updater(existingValue, set);
474
+ updaterReturned = true;
475
+ // Reject async updaters: TypeScript structurally accepts
476
+ // `async (...) => Promise<void>` for the declared `() => void` parameter
477
+ // type, but an async updater that calls `set` synchronously and then
478
+ // awaits would let the IDB write commit before its eventual rejection
479
+ // surfaced — contradicting the "throw aborts the transaction" contract.
480
+ // Detect a thenable return and treat it as misuse symmetric with the
481
+ // late-`set` guard.
482
+ if (isPromiseLike(updaterResult)) {
483
+ throw new UsageError(
484
+ "FluidCache.update: updater must be synchronous (returned a thenable)",
485
+ );
486
+ }
487
+ } catch (updaterError: any) {
488
+ updaterReturned = true;
489
+ transaction.abort();
490
+ // Await transaction settlement; aborting causes `transaction.done` to
491
+ // reject, which we swallow because the updater error is the real cause.
492
+ await transaction.done.catch(() => {});
493
+ this.logger.sendErrorEvent(
494
+ {
495
+ eventName: FluidCacheErrorEvent.FluidCacheUpdateCallbackError,
496
+ pkgVersion,
497
+ },
498
+ updaterError,
499
+ );
500
+ return false;
501
+ }
502
+
503
+ if (!setCalled) {
504
+ await transaction.done;
505
+ return false;
506
+ }
507
+
508
+ // `set(undefined)` is treated as a delete: there is no useful distinction
509
+ // between "no entry" and "entry stored as undefined" (both surface as
510
+ // `undefined` from `get`), so we expose this as an atomic conditional-delete
511
+ // rather than persisting an undefined-valued row that would otherwise
512
+ // occupy IDB until maintenance reaped it.
513
+ if (valueToWrite === undefined) {
514
+ await transaction.store.delete(key);
515
+ } else {
516
+ const currentTime = Date.now();
517
+ await transaction.store.put(
518
+ {
519
+ cachedObject: valueToWrite,
520
+ fileId: entry.file.docId,
521
+ type: entry.type,
522
+ cacheItemId: entry.key,
523
+ partitionKey: this.partitionKey,
524
+ createdTimeMs: currentTime,
525
+ lastAccessTimeMs: currentTime,
526
+ },
527
+ key,
528
+ );
529
+ }
530
+ await transaction.done;
531
+ return true;
532
+ } catch (error: any) {
533
+ this.logger.sendErrorEvent(
534
+ { eventName: FluidCacheErrorEvent.FluidCachePutError, pkgVersion },
535
+ error,
536
+ );
537
+ return false;
538
+ } finally {
539
+ this.closeDb(db);
540
+ }
541
+ }
364
542
  }
@@ -13,6 +13,7 @@ export const enum FluidCacheErrorEvent {
13
13
  "FluidCacheDeleteSingleEntryError" = "FluidCacheDeleteSingleEntryError",
14
14
  "FluidCacheGetError" = "FluidCacheGetError",
15
15
  "FluidCachePutError" = "FluidCachePutError",
16
+ "FluidCacheUpdateCallbackError" = "FluidCacheUpdateCallbackError",
16
17
  "FluidCacheUpdateUsageError" = "FluidCacheUpdateUsageError",
17
18
  "FluidCacheDeleteOldDbError" = "FluidCacheDeleteOldDbError",
18
19
  }
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/driver-web-cache";
9
- export const pkgVersion = "2.101.0";
9
+ export const pkgVersion = "2.102.0";