@nxtedition/cache 2.1.12 → 2.1.14
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/README.md +33 -27
- package/lib/index.d.ts +2 -10
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +541 -897
- package/lib/index.js.map +1 -0
- package/lib/memory.d.ts +1 -0
- package/lib/memory.d.ts.map +1 -0
- package/lib/memory.js +83 -118
- package/lib/memory.js.map +1 -0
- package/package.json +10 -12
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# @nxtedition/cache
|
|
2
2
|
|
|
3
|
-
A two-tier async cache with SQLite persistence, in-memory pseudo-LRU, stale-while-revalidate, cross-
|
|
3
|
+
A two-tier async cache with SQLite persistence, in-memory pseudo-LRU, stale-while-revalidate, cross-thread deduplication via `SharedArrayBuffer`, and automatic request coalescing.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Two-tier storage** — In-memory cache backed by SQLite on disk
|
|
8
8
|
- **Stale-while-revalidate** — Serve stale data synchronously while refreshing in the background
|
|
9
|
-
- **Request coalescing** — Concurrent fetches for the same key share a single in-flight
|
|
10
|
-
- **Cross-
|
|
9
|
+
- **Request coalescing** — Concurrent fetches for the same key share a single in-flight `Promise`
|
|
10
|
+
- **Cross-thread locking** — `SharedArrayBuffer` + `Atomics.compareExchange` / `Atomics.waitAsync` prevent redundant `valueSelector` calls across worker threads in the same process that share the same `location`
|
|
11
11
|
- **Async value resolution** — Transparently fetches missing values via a user-defined `valueSelector`
|
|
12
12
|
- **Binary support** — Store and retrieve `Buffer` / `Uint8Array` alongside JSON values
|
|
13
13
|
- **Size-bounded storage** — Configurable max database size with automatic eviction of oldest entries
|
|
@@ -67,14 +67,14 @@ if (result.async) {
|
|
|
67
67
|
|
|
68
68
|
#### `CacheOptions`
|
|
69
69
|
|
|
70
|
-
| Option | Type | Default
|
|
71
|
-
| ------------ | ---------------------------------- |
|
|
72
|
-
| `ttl` | `number \| (value, key) => number` | `MAX_SAFE_INTEGER`
|
|
73
|
-
| `stale` | `number \| (value, key) => number` | `MAX_SAFE_INTEGER`
|
|
74
|
-
| `memory` | `MemoryOptions \| false \| null` | `{ maxSize: 16MB, maxCount: 16384 }`
|
|
75
|
-
| `database` | `DatabaseOptions \| false \| null` | `{ timeout: 20, maxSize: 128MB }`
|
|
76
|
-
| `lock` | `
|
|
77
|
-
| `serializer` | `Serializer<V>` | JSON + ArrayBufferView passthrough
|
|
70
|
+
| Option | Type | Default | Description |
|
|
71
|
+
| ------------ | ---------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
72
|
+
| `ttl` | `number \| (value, key) => number` | `MAX_SAFE_INTEGER` | Time-to-live in milliseconds. After this, the entry is stale. |
|
|
73
|
+
| `stale` | `number \| (value, key) => number` | `MAX_SAFE_INTEGER` | Stale-while-revalidate window in ms. After `ttl + stale`, the entry is purged. |
|
|
74
|
+
| `memory` | `MemoryOptions \| false \| null` | `{ maxSize: 16MB, maxCount: 16384 }` | In-memory cache config, or `false`/`null` to disable. |
|
|
75
|
+
| `database` | `DatabaseOptions \| false \| null` | `{ timeout: 20, maxSize: 128MB }` | SQLite config, or `false`/`null` to disable persistence. |
|
|
76
|
+
| `lock` | `false \| null` | enabled | Pass `false`/`null` to disable cross-thread SAB locking (see [Cross-Thread Locking](#cross-thread-locking)). Always off for `':memory:'`. |
|
|
77
|
+
| `serializer` | `Serializer<V>` | JSON + ArrayBufferView passthrough | Custom `{ serialize, deserialize }` for value encoding. |
|
|
78
78
|
|
|
79
79
|
#### `MemoryOptions`
|
|
80
80
|
|
|
@@ -92,15 +92,6 @@ The in-memory tier uses a random-two-choice eviction strategy: when the cache is
|
|
|
92
92
|
| `timeout` | `number` | `20` | SQLite busy timeout in milliseconds. |
|
|
93
93
|
| `maxSize` | `number` | `128 * 1024 * 1024` (128 MB) | Maximum database file size. Oldest entries are evicted when full. |
|
|
94
94
|
|
|
95
|
-
#### `LockOptions`
|
|
96
|
-
|
|
97
|
-
Cross-process/thread locking prevents multiple processes or worker threads from computing the same value simultaneously. The lock timeout is adaptive — it uses an exponential moving average (EMA) of `valueSelector` durations to estimate how long to wait before taking over a lock.
|
|
98
|
-
|
|
99
|
-
| Option | Type | Default | Description |
|
|
100
|
-
| ------------ | -------- | ------- | -------------------------------------------------------------------------- |
|
|
101
|
-
| `minTimeout` | `number` | `1` | Minimum lock timeout in ms. Also the starting timeout before EMA warms up. |
|
|
102
|
-
| `maxTimeout` | `number` | `1000` | Maximum lock timeout in ms. Caps the EMA-derived timeout. |
|
|
103
|
-
|
|
104
95
|
#### `Serializer<V>`
|
|
105
96
|
|
|
106
97
|
| Method | Signature | Description |
|
|
@@ -149,7 +140,7 @@ Remove an entry from both memory and SQLite. Also cancels any in-flight deduplic
|
|
|
149
140
|
|
|
150
141
|
#### `cache.purgeStale(): void`
|
|
151
142
|
|
|
152
|
-
Remove all expired entries (past `ttl + stale`) from both the in-memory cache and SQLite
|
|
143
|
+
Remove all expired entries (past `ttl + stale`) from both the in-memory cache and SQLite, and run `PRAGMA wal_checkpoint(TRUNCATE)` + `PRAGMA optimize`.
|
|
153
144
|
|
|
154
145
|
#### `cache.close(): void`
|
|
155
146
|
|
|
@@ -163,7 +154,6 @@ Returns runtime statistics:
|
|
|
163
154
|
|
|
164
155
|
```ts
|
|
165
156
|
{
|
|
166
|
-
lock: { timeout, mean, stddev } | undefined,
|
|
167
157
|
dedupe: { size },
|
|
168
158
|
memory: { size, maxSize, count, maxCount } | undefined,
|
|
169
159
|
database: { location, size } | undefined,
|
|
@@ -199,13 +189,29 @@ Once the stale window expires, the entry is purged entirely and the next `get()`
|
|
|
199
189
|
+ bg refresh
|
|
200
190
|
```
|
|
201
191
|
|
|
202
|
-
## Cross-
|
|
192
|
+
## Cross-Thread Locking
|
|
193
|
+
|
|
194
|
+
Worker threads in the same process that pass the same `location` to `new Cache(...)` share a `SharedArrayBuffer` (acquired via `@nxtedition/shared`'s `getOrCreate` registry). That buffer is treated as a hash table of per-key `Int32` mutex slots:
|
|
195
|
+
|
|
196
|
+
- Acquire is `Atomics.compareExchange(slot, 0, 1)`. The winner runs `valueSelector`; losers `Atomics.waitAsync` for a notify (with a 1 s timeout that re-enters `#load` — a guard against missed notifies and holder crashes).
|
|
197
|
+
- On completion the holder does `Atomics.sub(slot, 1)` + `Atomics.notify`, waking any waiters which then re-enter `get()` and read the freshly-cached value.
|
|
198
|
+
- Exception paths (sync `valueSelector` throw, async rejection, serializer/ttl/stale throwing inside `#set`) all run the release under a `try/finally`, so a buggy user callback can never wedge a slot.
|
|
199
|
+
|
|
200
|
+
When the `location` is `':memory:'`, the cache is inherently per-instance (the SQLite DB isn't shared), so the SAB is skipped entirely and all coordination falls back to the instance-local `#dedupe` Map.
|
|
201
|
+
|
|
202
|
+
Pass `{ lock: false }` (or `lock: null`) to opt out of the SAB lock on any cache — the instance-local `#dedupe` Map still coalesces concurrent `get()` calls on the same key, but sibling threads hitting the same DB file will each call `valueSelector` independently.
|
|
203
|
+
|
|
204
|
+
There is **no cross-process coordination** — two separate Node processes pointed at the same SQLite file may both run `valueSelector` for the same key. If you need cross-process dedup, do it at a layer above (e.g. a request-coalescing service).
|
|
205
|
+
|
|
206
|
+
### When to disable the lock
|
|
203
207
|
|
|
204
|
-
|
|
208
|
+
- `':memory:'` — already disabled for you; nothing to configure.
|
|
209
|
+
- Single-threaded app that doesn't spawn workers — disable to skip the cheap SAB round-trip on every `get()` cache miss.
|
|
210
|
+
- Workload where each worker uses a disjoint keyspace — the SAB adds overhead without any dedupe benefit.
|
|
205
211
|
|
|
206
|
-
|
|
212
|
+
### Counter overflow
|
|
207
213
|
|
|
208
|
-
|
|
214
|
+
Each slot is an `Int32`. `refresh()` uses `Atomics.add(slot, 1)` rather than a CAS (so concurrent refreshes stack), with an overflow guard at `0xfffffff` — if the counter ever reaches that value the call throws `Error: lock counter overflow` and rolls the increment back, so a wedged slot can't silently wrap. In practice this is unreachable: `0xfffffff` = ~268 M in-flight refreshes, which would OOM long before hitting the guard.
|
|
209
215
|
|
|
210
216
|
## Batched Writes
|
|
211
217
|
|
|
@@ -215,7 +221,7 @@ If the database is full (`SQLITE_FULL`), the cache evicts the 256 oldest entries
|
|
|
215
221
|
|
|
216
222
|
## Error Handling
|
|
217
223
|
|
|
218
|
-
`Cache` extends `EventEmitter`. Non-fatal errors (SQLite failures,
|
|
224
|
+
`Cache` extends `EventEmitter`. Non-fatal errors (SQLite failures, stale revalidation failures, background-refresh rejections from the SWR fire-and-forget path) are emitted as `'error'` events when a listener is attached. If no `'error'` listener is registered, errors are surfaced via `process.emitWarning()` instead, avoiding unhandled crashes.
|
|
219
225
|
|
|
220
226
|
## Off-Peak Purge
|
|
221
227
|
|
package/lib/index.d.ts
CHANGED
|
@@ -5,10 +5,6 @@ export interface DatabaseOptions {
|
|
|
5
5
|
timeout?: number;
|
|
6
6
|
maxSize?: number;
|
|
7
7
|
}
|
|
8
|
-
export interface LockOptions {
|
|
9
|
-
minTimeout?: number;
|
|
10
|
-
maxTimeout?: number;
|
|
11
|
-
}
|
|
12
8
|
export interface Serializer<V> {
|
|
13
9
|
serialize: (value: V) => Buffer | Uint8Array | string;
|
|
14
10
|
deserialize: (data: Buffer | string) => V;
|
|
@@ -16,8 +12,8 @@ export interface Serializer<V> {
|
|
|
16
12
|
export interface CacheOptions<V> {
|
|
17
13
|
ttl?: number | ((value: V, key: string) => number);
|
|
18
14
|
stale?: number | ((value: V, key: string) => number);
|
|
19
|
-
lock?: LockOptions | false | null;
|
|
20
15
|
memory?: MemoryOptions | false | null;
|
|
16
|
+
lock?: false | null;
|
|
21
17
|
database?: DatabaseOptions | false | null;
|
|
22
18
|
serializer?: Serializer<V>;
|
|
23
19
|
}
|
|
@@ -37,11 +33,6 @@ export declare class Cache<V = unknown, A extends unknown[] = [string]> extends
|
|
|
37
33
|
#private;
|
|
38
34
|
constructor(location: string, valueSelector?: (...args: A) => V | PromiseLike<V>, keySelector?: (...args: A) => string, opts?: CacheOptions<V>);
|
|
39
35
|
get stats(): {
|
|
40
|
-
lock: {
|
|
41
|
-
timeout: number;
|
|
42
|
-
mean: number;
|
|
43
|
-
stddev: number;
|
|
44
|
-
} | undefined;
|
|
45
36
|
dedupe: {
|
|
46
37
|
size: number;
|
|
47
38
|
};
|
|
@@ -68,3 +59,4 @@ export declare class Cache<V = unknown, A extends unknown[] = [string]> extends
|
|
|
68
59
|
export declare const AsyncCache: typeof Cache;
|
|
69
60
|
/** @deprecated Use `CacheOptions` instead. */
|
|
70
61
|
export type AsyncCacheOptions<V> = CacheOptions<V>;
|
|
62
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAO1C,OAAO,EAAsC,KAAK,aAAa,EAAE,MAAM,aAAa,CAAA;AAEpF,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AA2BhD,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AACD,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,GAAG,UAAU,GAAG,MAAM,CAAA;IACrD,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,KAAK,CAAC,CAAA;CAC1C;AAaD,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,CAAA;IAClD,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,CAAA;IACpD,MAAM,CAAC,EAAE,aAAa,GAAG,KAAK,GAAG,IAAI,CAAA;IACrC,IAAI,CAAC,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,QAAQ,CAAC,EAAE,eAAe,GAAG,KAAK,GAAG,IAAI,CAAA;IACzC,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;CAC3B;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,IACrB;IAAE,KAAK,EAAE,CAAC,GAAG,SAAS,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GACtC;IAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAAC,KAAK,EAAE,IAAI,CAAA;CAAE,CAAA;AAEtC,OAAO,CAAC,MAAM,CAAC;IAEb,IAAI,WAAW,EAAE,OAAO,CAAC;QAAE,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC,EAAE,CAAA;CACtD;AAMD,qBAAa,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,SAAS,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAE,SAAQ,YAAY;;gBAoChF,QAAQ,EAAE,MAAM,EAChB,aAAa,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,EAClD,WAAW,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,MAAM,EACpC,IAAI,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC;IAoIxB,IAAI,KAAK;;;;;;;;;;;sBACmB,MAAM;kBAAQ,MAAM,GAAG,SAAS;;MAkB3D;IAED,CAAC,MAAM,CAAC,OAAO,CAAC;IAIhB,KAAK,IAAI,IAAI;IA2Bb,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC;IAO/B,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC;IAOhC,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC;IA4BnC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,IAAI;IAOxB,UAAU,IAAI,IAAI;CA+VnB;AAoBD,uCAAuC;AACvC,eAAO,MAAM,UAAU,cAAQ,CAAA;AAC/B,8CAA8C;AAC9C,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,CAAA"}
|