@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 +4 -0
- package/README.md +57 -0
- package/api-report/driver-web-cache.legacy.beta.api.md +1 -0
- package/dist/FluidCache.d.ts +64 -0
- package/dist/FluidCache.d.ts.map +1 -1
- package/dist/FluidCache.js +161 -0
- package/dist/FluidCache.js.map +1 -1
- package/dist/fluidCacheTelemetry.d.ts +1 -0
- package/dist/fluidCacheTelemetry.d.ts.map +1 -1
- package/dist/fluidCacheTelemetry.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/lib/FluidCache.d.ts +64 -0
- package/lib/FluidCache.d.ts.map +1 -1
- package/lib/FluidCache.js +162 -1
- package/lib/FluidCache.js.map +1 -1
- package/lib/fluidCacheTelemetry.d.ts +1 -0
- package/lib/fluidCacheTelemetry.d.ts.map +1 -1
- package/lib/fluidCacheTelemetry.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/package.json +13 -9
- package/src/FluidCache.ts +179 -1
- package/src/fluidCacheTelemetry.ts +1 -0
- package/src/packageVersion.ts +1 -1
package/CHANGELOG.md
CHANGED
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)
|
package/dist/FluidCache.d.ts
CHANGED
|
@@ -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
|
package/dist/FluidCache.d.ts.map
CHANGED
|
@@ -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;
|
|
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/dist/FluidCache.js
CHANGED
|
@@ -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
|
package/dist/FluidCache.js.map
CHANGED
|
@@ -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"]}
|
package/dist/packageVersion.d.ts
CHANGED
|
@@ -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.
|
|
8
|
+
export declare const pkgVersion = "2.102.0";
|
|
9
9
|
//# sourceMappingURL=packageVersion.d.ts.map
|
package/dist/packageVersion.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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"]}
|
package/lib/FluidCache.d.ts
CHANGED
|
@@ -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
|
package/lib/FluidCache.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
package/lib/FluidCache.js.map
CHANGED
|
@@ -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"]}
|
package/lib/packageVersion.d.ts
CHANGED
|
@@ -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.
|
|
8
|
+
export declare const pkgVersion = "2.102.0";
|
|
9
9
|
//# sourceMappingURL=packageVersion.d.ts.map
|
package/lib/packageVersion.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
51
|
-
"@fluidframework/core-utils": "~2.
|
|
52
|
-
"@fluidframework/driver-definitions": "~2.
|
|
53
|
-
"@fluidframework/driver-utils": "~2.
|
|
54
|
-
"@fluidframework/telemetry-utils": "~2.
|
|
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.
|
|
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.
|
|
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
|
}
|
package/src/packageVersion.ts
CHANGED