@warlock.js/cache 5.29.0 → 5.31.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 +31 -7
- package/README.md +12 -21
- package/cjs/index.cjs +134 -71
- package/cjs/index.cjs.map +1 -1
- package/esm/cache-manager.d.mts +4 -2
- package/esm/cache-manager.d.mts.map +1 -1
- package/esm/cache-manager.mjs +3 -3
- package/esm/cache-manager.mjs.map +1 -1
- package/esm/cached/auto-key.d.mts +1 -1
- package/esm/cached/auto-key.d.mts.map +1 -1
- package/esm/cached/auto-key.mjs +1 -1
- package/esm/cached/auto-key.mjs.map +1 -1
- package/esm/cached/cached.d.mts +1 -1
- package/esm/cached/cached.d.mts.map +1 -1
- package/esm/cached/cached.mjs +1 -1
- package/esm/cached/cached.mjs.map +1 -1
- package/esm/cached/normalize-args.d.mts +1 -1
- package/esm/cached/normalize-args.d.mts.map +1 -1
- package/esm/cached/normalize-args.mjs +1 -1
- package/esm/cached/normalize-args.mjs.map +1 -1
- package/esm/drivers/base-cache-driver.d.mts +14 -6
- package/esm/drivers/base-cache-driver.d.mts.map +1 -1
- package/esm/drivers/base-cache-driver.mjs +32 -15
- package/esm/drivers/base-cache-driver.mjs.map +1 -1
- package/esm/drivers/file-cache-driver.d.mts +1 -1
- package/esm/drivers/file-cache-driver.d.mts.map +1 -1
- package/esm/drivers/file-cache-driver.mjs +2 -2
- package/esm/drivers/file-cache-driver.mjs.map +1 -1
- package/esm/drivers/in-memory-tag-index.d.mts +1 -1
- package/esm/drivers/in-memory-tag-index.d.mts.map +1 -1
- package/esm/drivers/in-memory-tag-index.mjs +1 -1
- package/esm/drivers/in-memory-tag-index.mjs.map +1 -1
- package/esm/drivers/index.d.mts +1 -1
- package/esm/drivers/index.mjs +1 -1
- package/esm/drivers/lru-memory-cache-driver.d.mts +4 -2
- package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/lru-memory-cache-driver.mjs +9 -6
- package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-cache-driver.d.mts +4 -2
- package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-cache-driver.mjs +12 -5
- package/esm/drivers/memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.d.mts +1 -1
- package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.mjs +1 -1
- package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -1
- package/esm/drivers/mock-cache-driver.d.mts +4 -2
- package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
- package/esm/drivers/mock-cache-driver.mjs +14 -6
- package/esm/drivers/mock-cache-driver.mjs.map +1 -1
- package/esm/drivers/null-cache-driver.d.mts +5 -1
- package/esm/drivers/null-cache-driver.d.mts.map +1 -1
- package/esm/drivers/null-cache-driver.mjs +17 -10
- package/esm/drivers/null-cache-driver.mjs.map +1 -1
- package/esm/drivers/pg-cache-driver.d.mts +4 -2
- package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
- package/esm/drivers/pg-cache-driver.mjs +9 -5
- package/esm/drivers/pg-cache-driver.mjs.map +1 -1
- package/esm/drivers/redis-cache-driver.d.mts +17 -3
- package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
- package/esm/drivers/redis-cache-driver.mjs +26 -5
- package/esm/drivers/redis-cache-driver.mjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/index.mjs +2 -2
- package/esm/list/memory-cache-list.d.mts +1 -1
- package/esm/list/memory-cache-list.d.mts.map +1 -1
- package/esm/list/memory-cache-list.mjs +1 -1
- package/esm/list/memory-cache-list.mjs.map +1 -1
- package/esm/metrics.d.mts +1 -1
- package/esm/metrics.d.mts.map +1 -1
- package/esm/metrics.mjs +1 -1
- package/esm/metrics.mjs.map +1 -1
- package/esm/scoped-cache.d.mts +4 -2
- package/esm/scoped-cache.d.mts.map +1 -1
- package/esm/scoped-cache.mjs +3 -3
- package/esm/scoped-cache.mjs.map +1 -1
- package/esm/tagged-cache.d.mts +4 -2
- package/esm/tagged-cache.d.mts.map +1 -1
- package/esm/tagged-cache.mjs +3 -3
- package/esm/tagged-cache.mjs.map +1 -1
- package/esm/tagged-scoped-cache.d.mts +4 -2
- package/esm/tagged-scoped-cache.d.mts.map +1 -1
- package/esm/tagged-scoped-cache.mjs +3 -3
- package/esm/tagged-scoped-cache.mjs.map +1 -1
- package/esm/types.d.mts +17 -6
- package/esm/types.d.mts.map +1 -1
- package/esm/types.mjs +1 -1
- package/esm/types.mjs.map +1 -1
- package/esm/utils.d.mts +1 -1
- package/esm/utils.d.mts.map +1 -1
- package/esm/utils.mjs +1 -1
- package/esm/utils.mjs.map +1 -1
- package/llms-full.txt +231 -224
- package/llms.txt +1 -1
- package/package.json +5 -4
- package/skills/apply-cache-patterns/SKILL.md +3 -2
- package/skills/cache-basics/SKILL.md +26 -24
- package/skills/configure-pg-cache/SKILL.md +2 -2
- package/skills/configure-set-options/SKILL.md +14 -14
- package/skills/handle-cache-errors/SKILL.md +8 -8
- package/skills/index.md +4 -1
- package/skills/observe-cache/SKILL.md +8 -8
- package/skills/pick-cache-driver/SKILL.md +24 -23
- package/skills/test-cache-code/SKILL.md +15 -10
- package/skills/use-cache-atomic/SKILL.md +17 -11
- package/skills/use-cache-bulk/SKILL.md +8 -5
- package/skills/use-cache-list/SKILL.md +13 -13
- package/skills/use-cache-lock/SKILL.md +11 -17
- package/skills/use-cache-namespace/SKILL.md +11 -13
- package/skills/use-cache-similarity/SKILL.md +9 -9
- package/skills/use-cache-tags/SKILL.md +13 -13
- package/skills/use-cache-update-merge/SKILL.md +10 -10
- package/skills/use-cache-utils/SKILL.md +17 -17
- package/skills/use-cached-hof/SKILL.md +8 -7
- package/skills/use-swr/SKILL.md +14 -18
package/llms-full.txt
CHANGED
|
@@ -19,7 +19,7 @@ Common shapes — the "general patterns" file. Specialized shapes live in their
|
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
21
|
const user = await cache.remember(`user:${id}`, "1h", async () => {
|
|
22
|
-
return db.users.find(id);
|
|
22
|
+
return db.users.find(id); // runs only on cache miss
|
|
23
23
|
});
|
|
24
24
|
```
|
|
25
25
|
|
|
@@ -89,8 +89,9 @@ await cache.set("user:1", user, "1h");
|
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
Clear a tenant out:
|
|
92
|
+
|
|
92
93
|
```ts
|
|
93
|
-
await cache.removeNamespace("");
|
|
94
|
+
await cache.removeNamespace(""); // when globalPrefix is set, flush scopes to it
|
|
94
95
|
// or
|
|
95
96
|
await cache.tags([`tenant-${tenantId}`]).invalidate();
|
|
96
97
|
```
|
|
@@ -171,20 +172,22 @@ cache.setCacheConfigurations(config);
|
|
|
171
172
|
await cache.init();
|
|
172
173
|
```
|
|
173
174
|
|
|
175
|
+
`logging` turns the per-operation key log (`<key> fetched`, `<key> not found`, ...) on or off. Email addresses inside logged keys are masked (`user.member@example.com` logs as `u***@example.com`), in operation lines and in error lines (which are written even with logging off); other personal data in a key is logged as is, so keep it out of keys or turn logging off in production.
|
|
176
|
+
|
|
174
177
|
## Primary ops
|
|
175
178
|
|
|
176
179
|
```ts
|
|
177
180
|
// Set + get
|
|
178
181
|
await cache.set("user.1", user, "1h");
|
|
179
|
-
const cached = await cache.get<User>("user.1");
|
|
182
|
+
const cached = await cache.get<User>("user.1"); // User | null
|
|
180
183
|
|
|
181
184
|
// Presence + read-and-delete
|
|
182
|
-
const exists = await cache.has("user.1");
|
|
183
|
-
const taken = await cache.pull<User>("user.1");
|
|
185
|
+
const exists = await cache.has("user.1"); // boolean
|
|
186
|
+
const taken = await cache.pull<User>("user.1"); // returns then removes
|
|
184
187
|
|
|
185
188
|
// Remove + flush
|
|
186
189
|
await cache.remove("user.1");
|
|
187
|
-
await cache.flush();
|
|
190
|
+
await cache.flush(); // wipe everything (current driver)
|
|
188
191
|
|
|
189
192
|
// Many at once — array positionally aligned with the keys (null for misses)
|
|
190
193
|
const [u1, u2, u3] = await cache.many(["user.1", "user.2", "user.3"]);
|
|
@@ -194,9 +197,9 @@ const [u1, u2, u3] = await cache.many(["user.1", "user.2", "user.3"]);
|
|
|
194
197
|
await cache.forever("config.version", "1.2.3");
|
|
195
198
|
|
|
196
199
|
// Counters
|
|
197
|
-
await cache.increment("post.42.views");
|
|
198
|
-
await cache.increment("post.42.views", 10);
|
|
199
|
-
await cache.decrement("inventory.sku-x");
|
|
200
|
+
await cache.increment("post.42.views"); // +1, returns new value
|
|
201
|
+
await cache.increment("post.42.views", 10); // +10
|
|
202
|
+
await cache.decrement("inventory.sku-x"); // -1
|
|
200
203
|
|
|
201
204
|
// Memoize an expensive function
|
|
202
205
|
const user = await cache.remember("user.1", "1h", async () => db.users.find(1));
|
|
@@ -204,23 +207,23 @@ const user = await cache.remember("user.1", "1h", async () => db.users.find(1));
|
|
|
204
207
|
|
|
205
208
|
## Pick a skill
|
|
206
209
|
|
|
207
|
-
| If the task is about…
|
|
208
|
-
|
|
|
209
|
-
| Choosing a driver, configuring it, or understanding what each one does best
|
|
210
|
-
| The `set` options object (`ttl`, `expiresAt`, `tags`, `onConflict`, `driver`, `vector`)
|
|
211
|
-
| Memoization with `remember()`, counters, negative caching, per-tenant scoping, TTL constants | The `apply-cache-patterns` topic
|
|
212
|
-
| Scoped handles via `cache.namespace(prefix, options?)`
|
|
213
|
-
| Tag-based invalidation — `cache.tags([...]).invalidate()`
|
|
214
|
-
| Stale-while-revalidate — `cache.swr(...)`
|
|
215
|
-
| Wrapping a function with `cached()` — HOF memoization with `.invalidate()`
|
|
216
|
-
| Distributed locks — `cache.lock(key, ttl, fn)` with auto-release
|
|
217
|
-
| Queues, recent-N buffers, `push`/`shift`/`trim` — the list sub-API
|
|
218
|
-
| Atomic read-modify-write via `update()` and `merge()`
|
|
219
|
-
| Similarity retrieval — `set({ vector })` + `cache.similar(...)`
|
|
220
|
-
| Postgres driver setup (KV-only or with pgvector)
|
|
221
|
-
| `cache.metrics()` aggregate snapshot + event bus for per-event reactions
|
|
222
|
-
| Error classes (`CacheConfigurationError`, `CacheUnsupportedError`, etc.)
|
|
223
|
-
| Tests that touch cache code paths — `MockCacheDriver`, `MemoryCacheDriver`
|
|
210
|
+
| If the task is about… | Load |
|
|
211
|
+
| -------------------------------------------------------------------------------------------- | ---------------------------------- |
|
|
212
|
+
| Choosing a driver, configuring it, or understanding what each one does best | The `pick-cache-driver` topic |
|
|
213
|
+
| The `set` options object (`ttl`, `expiresAt`, `tags`, `onConflict`, `driver`, `vector`) | The `configure-set-options` topic |
|
|
214
|
+
| Memoization with `remember()`, counters, negative caching, per-tenant scoping, TTL constants | The `apply-cache-patterns` topic |
|
|
215
|
+
| Scoped handles via `cache.namespace(prefix, options?)` | The `use-cache-namespace` topic |
|
|
216
|
+
| Tag-based invalidation — `cache.tags([...]).invalidate()` | The `use-cache-tags` topic |
|
|
217
|
+
| Stale-while-revalidate — `cache.swr(...)` | The `use-swr` topic |
|
|
218
|
+
| Wrapping a function with `cached()` — HOF memoization with `.invalidate()` | The `use-cached-hof` topic |
|
|
219
|
+
| Distributed locks — `cache.lock(key, ttl, fn)` with auto-release | The `use-cache-lock` topic |
|
|
220
|
+
| Queues, recent-N buffers, `push`/`shift`/`trim` — the list sub-API | The `use-cache-list` topic |
|
|
221
|
+
| Atomic read-modify-write via `update()` and `merge()` | The `use-cache-update-merge` topic |
|
|
222
|
+
| Similarity retrieval — `set({ vector })` + `cache.similar(...)` | The `use-cache-similarity` topic |
|
|
223
|
+
| Postgres driver setup (KV-only or with pgvector) | The `configure-pg-cache` topic |
|
|
224
|
+
| `cache.metrics()` aggregate snapshot + event bus for per-event reactions | The `observe-cache` topic |
|
|
225
|
+
| Error classes (`CacheConfigurationError`, `CacheUnsupportedError`, etc.) | The `handle-cache-errors` topic |
|
|
226
|
+
| Tests that touch cache code paths — `MockCacheDriver`, `MemoryCacheDriver` | The `test-cache-code` topic |
|
|
224
227
|
|
|
225
228
|
## Things NOT to do
|
|
226
229
|
|
|
@@ -278,8 +281,8 @@ cache.setCacheConfigurations({
|
|
|
278
281
|
options: {
|
|
279
282
|
pg: {
|
|
280
283
|
client: pool,
|
|
281
|
-
table: "warlock_cache",
|
|
282
|
-
ttl: "1h",
|
|
284
|
+
table: "warlock_cache", // optional, default
|
|
285
|
+
ttl: "1h", // optional default
|
|
283
286
|
globalPrefix: "prod-app",
|
|
284
287
|
},
|
|
285
288
|
},
|
|
@@ -373,7 +376,7 @@ description: 'Configure cache.set''s third argument — ttl, expiresAt, tags, on
|
|
|
373
376
|
await cache.set("name", "Jane", 600);
|
|
374
377
|
|
|
375
378
|
// 2. String — human-readable duration, parsed via `ms`
|
|
376
|
-
await cache.set("name", "Jane", "10m");
|
|
379
|
+
await cache.set("name", "Jane", "10m"); // "1s", "30m", "1h", "7d", "2 weeks"
|
|
377
380
|
|
|
378
381
|
// 3. Options object
|
|
379
382
|
await cache.set("user:1", user, {
|
|
@@ -387,14 +390,14 @@ await cache.set("user:1", user, {
|
|
|
387
390
|
|
|
388
391
|
## Option keys
|
|
389
392
|
|
|
390
|
-
| Key
|
|
391
|
-
|
|
|
392
|
-
| `ttl`
|
|
393
|
-
| `expiresAt`
|
|
394
|
-
| `tags`
|
|
395
|
-
| `onConflict` | `"create" \| "update" \| "upsert"` | See below. Default `"upsert"`.
|
|
396
|
-
| `driver`
|
|
397
|
-
| `vector`
|
|
393
|
+
| Key | Type | Notes |
|
|
394
|
+
| ------------ | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
395
|
+
| `ttl` | `number \| string` | Relative expiry. Mutually exclusive with `expiresAt`. |
|
|
396
|
+
| `expiresAt` | `number \| Date` | Absolute deadline (epoch ms or Date). Must be in the future. Mutually exclusive with `ttl`. |
|
|
397
|
+
| `tags` | `string[]` | Inline equivalent of `cache.tags([...]).set(...)`. See the `use-cache-tags` topic. |
|
|
398
|
+
| `onConflict` | `"create" \| "update" \| "upsert"` | See below. Default `"upsert"`. |
|
|
399
|
+
| `driver` | `string` | Per-call driver override by registered name. |
|
|
400
|
+
| `vector` | `number[]` | Embedding indexed alongside the entry for `cache.similar()` (see the `use-cache-similarity` topic). Drivers without similarity support throw `CacheUnsupportedError`. |
|
|
398
401
|
|
|
399
402
|
## TTL parsing
|
|
400
403
|
|
|
@@ -430,8 +433,8 @@ Conditional writes (`"create"` / `"update"`) return a `CacheSetResult`; uncondit
|
|
|
430
433
|
Both of these throw `CacheConfigurationError`:
|
|
431
434
|
|
|
432
435
|
```ts
|
|
433
|
-
await cache.set("k", v, { ttl: "1h", expiresAt: Date.now() + 1000 });
|
|
434
|
-
await cache.set("k", v, { expiresAt: Date.now() - 1000 });
|
|
436
|
+
await cache.set("k", v, { ttl: "1h", expiresAt: Date.now() + 1000 }); // both set
|
|
437
|
+
await cache.set("k", v, { expiresAt: Date.now() - 1000 }); // past deadline
|
|
435
438
|
```
|
|
436
439
|
|
|
437
440
|
## Inline tags vs `cache.tags([...]).set(...)`
|
|
@@ -445,7 +448,7 @@ await cache.set("user:1", user, { tags: ["users", "tenant-42"] });
|
|
|
445
448
|
// Fluent (useful when you already have a tagged instance)
|
|
446
449
|
const users = cache.tags(["users"]);
|
|
447
450
|
await users.set("user:1", user);
|
|
448
|
-
await users.invalidate();
|
|
451
|
+
await users.invalidate(); // drops every key tagged "users"
|
|
449
452
|
```
|
|
450
453
|
|
|
451
454
|
Inline tag semantics are **additive**, never replace. A subsequent `set("user:1", ...)` with no `tags` leaves previous associations intact (the tag index still points to the key), and a `set(..., { tags: [...] })` only appends the key to those tags' index entries — it never removes the key from tags it was bound to earlier. To drop stale bindings, invalidate the old tag explicitly via `cache.tags([...]).invalidate()`.
|
|
@@ -455,8 +458,8 @@ Inline tag semantics are **additive**, never replace. A subsequent `set("user:1"
|
|
|
455
458
|
Every call site using the old positional-TTL shape keeps working:
|
|
456
459
|
|
|
457
460
|
```ts
|
|
458
|
-
await cache.set("k", v);
|
|
459
|
-
await cache.set("k", v, 3600);
|
|
461
|
+
await cache.set("k", v); // no TTL — driver default
|
|
462
|
+
await cache.set("k", v, 3600); // seconds
|
|
460
463
|
await cache.set("k", v, undefined); // same as no TTL
|
|
461
464
|
```
|
|
462
465
|
|
|
@@ -483,14 +486,14 @@ import {
|
|
|
483
486
|
} from "@warlock.js/cache";
|
|
484
487
|
```
|
|
485
488
|
|
|
486
|
-
| Class
|
|
487
|
-
|
|
|
488
|
-
| `CacheError`
|
|
489
|
-
| `CacheConfigurationError`
|
|
490
|
-
| `CacheConnectionError`
|
|
491
|
-
| `CacheDriverNotInitializedError`
|
|
492
|
-
| `CacheUnsupportedError`
|
|
493
|
-
| `CacheConcurrencyError`
|
|
489
|
+
| Class | When it's thrown | How to react |
|
|
490
|
+
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
491
|
+
| `CacheError` | Abstract base — don't throw directly, match against it to catch any cache error | `catch (e) { if (e instanceof CacheError) … }` |
|
|
492
|
+
| `CacheConfigurationError` | Bad TTL string, `ttl` + `expiresAt` together, `expiresAt` in the past, missing required driver option (e.g. redis without url/host, file without directory), attempting to use an unregistered driver name | Fix the config / call site. Never catch at runtime — this is a programmer error, not a user-facing one. |
|
|
493
|
+
| `CacheConnectionError` | Declared for driver connection failures. Not thrown by any built-in driver today (Redis currently logs the error and emits an `"error"` event instead of throwing on failed connect). | Reserved for future use. |
|
|
494
|
+
| `CacheDriverNotInitializedError` | Any data op called before `cache.init()` / `cache.use()` | Call `cache.init()` at app startup. Tests often forget this — add a `beforeEach`. |
|
|
495
|
+
| `CacheUnsupportedError` | Driver doesn't implement the requested op. Today: `update` / `merge` on the file driver; `set({ vector })` and `similar()` on file / redis / pg-without-`vector`-config. | Switch driver (memory family for dev similarity, `pg` with `vector` config for production), or queue the op. |
|
|
496
|
+
| `CacheConcurrencyError` | Declared for future optimistic-concurrency exhaustion on Redis `update()` | Not thrown today. Reserved for the v2.1 `WATCH`/`MULTI` implementation. |
|
|
494
497
|
| `CacheError` (file driver, path containment) | A key/namespace, after percent-encoding, still resolves outside the cache root — `"Cache key resolves outside the cache directory: ..."`. Last line of defense against path traversal on top of the encoding step; should not happen in normal use. | Treat as a programmer/attacker-input error — don't retry; investigate the key source. See the `pick-cache-driver` topic for the encoding contract. |
|
|
495
498
|
|
|
496
499
|
## Special case — `setNX` unsupported
|
|
@@ -611,15 +614,15 @@ Listeners attached via `cache.on(...)` survive driver switches the same way the
|
|
|
611
614
|
|
|
612
615
|
## Which one to reach for
|
|
613
616
|
|
|
614
|
-
| Goal
|
|
615
|
-
|
|
616
|
-
| Show hit rate / latency in a dashboard
|
|
617
|
-
| Page on cache errors
|
|
618
|
-
| Periodic export to Prometheus / StatsD
|
|
619
|
-
| Audit log of every removal
|
|
617
|
+
| Goal | Use |
|
|
618
|
+
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
619
|
+
| Show hit rate / latency in a dashboard | `cache.metrics()` |
|
|
620
|
+
| Page on cache errors | `cache.on("error", ...)` |
|
|
621
|
+
| Periodic export to Prometheus / StatsD | `cache.metrics()` + `setInterval` + `resetMetrics()` |
|
|
622
|
+
| Audit log of every removal | `cache.on("removed", ...)` |
|
|
620
623
|
| See which tag invalidations ran, what they removed and how long they took | `cache.on("invalidated", ({ tags, keys, durationMs }) => ...)` — once per `tags([...]).invalidate()` |
|
|
621
|
-
| Detect a specific anti-pattern (e.g. always-miss key)
|
|
622
|
-
| Debug "is the cache being hit at all?" in dev
|
|
624
|
+
| Detect a specific anti-pattern (e.g. always-miss key) | `cache.on("miss", ...)` |
|
|
625
|
+
| Debug "is the cache being hit at all?" in dev | `cache.metrics()` once at the end of a flow |
|
|
623
626
|
|
|
624
627
|
Both layers can coexist — events fire whether the metrics collector is attached or not.
|
|
625
628
|
|
|
@@ -749,30 +752,30 @@ description: 'Pick a cache driver — null / memory / memoryExtended / lru / fil
|
|
|
749
752
|
|
|
750
753
|
Seven production drivers + a mock driver ship in-box. Pick by durability, scope, and workload.
|
|
751
754
|
|
|
752
|
-
| Driver
|
|
753
|
-
|
|
|
754
|
-
| `null`
|
|
755
|
-
| `memory`
|
|
756
|
-
| `memoryExtended` | Single process | No
|
|
757
|
-
| `lru`
|
|
758
|
-
| `file`
|
|
759
|
-
| `redis`
|
|
760
|
-
| `pg`
|
|
755
|
+
| Driver | Process scope | Persists on restart | Good for | Avoid when |
|
|
756
|
+
| ---------------- | -------------- | ---------------------- | ------------------------------------------------------------- | ------------------------------------------- |
|
|
757
|
+
| `null` | — | — | Disabling cache in tests; feature-flagging off | You actually want caching |
|
|
758
|
+
| `memory` | Single process | No | Hot in-process data with default TTL; smallest latency | Multi-process / multi-node |
|
|
759
|
+
| `memoryExtended` | Single process | No | Sliding-window TTL (TTL resets on every read) | Any multi-process deploy |
|
|
760
|
+
| `lru` | Single process | No | Bounded in-memory caches (capacity-based eviction) | Need cross-process sharing |
|
|
761
|
+
| `file` | Single host | Yes | Build artefacts, local dev persistence across restarts | Concurrency (no locks); multi-host |
|
|
762
|
+
| `redis` | Shared | Yes (Redis-managed) | Anything shared across processes / nodes | Single-process-only workload — overkill |
|
|
763
|
+
| `pg` | Shared | Yes (Postgres-managed) | You already run Postgres; semantic caching / RAG via pgvector | High-throughput hot reads (Redis is faster) |
|
|
761
764
|
|
|
762
765
|
## Capability matrix
|
|
763
766
|
|
|
764
|
-
| Capability
|
|
765
|
-
|
|
|
766
|
-
| `set` / `get` / `remove` / `flush`
|
|
767
|
-
| TTL (number or string; numeric strings = seconds) |
|
|
768
|
-
| Sliding TTL on read
|
|
769
|
-
| `removeNamespace`
|
|
770
|
-
| `onConflict: "create"` / `"update"`
|
|
771
|
-
| `increment` / `decrement` / `pull`
|
|
772
|
-
| `update()` / `merge()`
|
|
773
|
-
| List sub-API
|
|
774
|
-
| Tagged invalidation
|
|
775
|
-
| `similar()` / `set({ vector })`
|
|
767
|
+
| Capability | null | memory | memoryExt | lru | file | redis | pg |
|
|
768
|
+
| ------------------------------------------------- | :-----------------: | :-----------: | :-----------: | :-------------: | :------------------: | :---------------------------------------------------: | :---------------------------------: |
|
|
769
|
+
| `set` / `get` / `remove` / `flush` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
770
|
+
| TTL (number or string; numeric strings = seconds) | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
771
|
+
| Sliding TTL on read | — | — | ✓ | — | — | — | — |
|
|
772
|
+
| `removeNamespace` | noop | ✓ | ✓ | ✓ (prefix-scan) | ✓ | ✓ | ✓ (LIKE prefix) |
|
|
773
|
+
| `onConflict: "create"` / `"update"` | noop | emulated | emulated | emulated | emulated | native `NX`/`XX` | native (INSERT ON CONFLICT) |
|
|
774
|
+
| `increment` / `decrement` / `pull` | — | one process | one process | one process | one process | **cross-server** | **cross-server** |
|
|
775
|
+
| `update()` / `merge()` | ✓ | one process | one process | one process | ✗ throws | **cross-server** (compare-and-set) | **cross-server** (compare-and-set) |
|
|
776
|
+
| List sub-API | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (O(n) JSON blob today; native LPUSH/LRANGE in v2.1) | ✓ |
|
|
777
|
+
| Tagged invalidation | noop | ✓ | ✓ | ✓ | ✓ (serialized index) | ✓ (native SET) | ✓ (JSON-array merge) |
|
|
778
|
+
| `similar()` / `set({ vector })` | returns `[]` / noop | ✓ brute force | ✓ brute force | ✓ brute force | ✗ throws | ✗ throws (Phase 2 backlog) | ✓ (with `vector` config — pgvector) |
|
|
776
779
|
|
|
777
780
|
## Multi-server: use redis or pg
|
|
778
781
|
|
|
@@ -844,13 +847,14 @@ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
|
844
847
|
cache.setCacheConfigurations({
|
|
845
848
|
default: "pg",
|
|
846
849
|
drivers: { pg: PgCacheDriver },
|
|
847
|
-
options: { pg: { table: "cache" } },
|
|
850
|
+
options: { pg: { table: "cache" } }, // static
|
|
848
851
|
});
|
|
849
852
|
|
|
850
|
-
await cache.use("pg", { client: pool });
|
|
853
|
+
await cache.use("pg", { client: pool }); // runtime — skip init() in this case
|
|
851
854
|
```
|
|
852
855
|
|
|
853
856
|
Constraints:
|
|
857
|
+
|
|
854
858
|
- The driver name must be registered in `setCacheConfigurations({ drivers })` — runtime options don't bypass registration.
|
|
855
859
|
- Once a driver is loaded, calling `use`/`load`/`driver` again with **non-empty** new options throws `CacheConfigurationError`. Register a second driver name if you need a different config.
|
|
856
860
|
- Calling without options (or with `{}`) on an already-loaded driver returns the cached instance silently.
|
|
@@ -938,7 +942,7 @@ When you want the real read/write semantics (eviction, TTL expiry, similarity sc
|
|
|
938
942
|
|
|
939
943
|
`MockCacheDriver` does NOT implement `similar()` — vector writes are recorded into the call log but nearest-neighbor scoring is not available. Tests that call `cache.similar(...)` should use the memory driver.
|
|
940
944
|
|
|
941
|
-
## Strategy 3 — `NullCacheDriver` when you need cache
|
|
945
|
+
## Strategy 3 — `NullCacheDriver` when you need cache _off_
|
|
942
946
|
|
|
943
947
|
Use `NullCacheDriver` to disable caching entirely for code paths that should still work without a cache (graceful-degradation tests):
|
|
944
948
|
|
|
@@ -963,7 +967,9 @@ import { vi } from "vitest";
|
|
|
963
967
|
class FakeRedisClient {
|
|
964
968
|
public store = new Map<string, string>();
|
|
965
969
|
private expires = new Map<string, number>();
|
|
966
|
-
public on() {
|
|
970
|
+
public on() {
|
|
971
|
+
return this;
|
|
972
|
+
}
|
|
967
973
|
public async connect() {}
|
|
968
974
|
public async quit() {}
|
|
969
975
|
public async set(key: string, value: string, opts?: { EX?: number; NX?: boolean; XX?: boolean }) {
|
|
@@ -991,13 +997,18 @@ class FakeRedisClient {
|
|
|
991
997
|
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
992
998
|
return [...this.store.keys()].filter((k) => regex.test(k));
|
|
993
999
|
}
|
|
994
|
-
public async flushAll() {
|
|
1000
|
+
public async flushAll() {
|
|
1001
|
+
this.store.clear();
|
|
1002
|
+
this.expires.clear();
|
|
1003
|
+
}
|
|
995
1004
|
public async incrBy(key: string, n: number) {
|
|
996
1005
|
const next = Number(this.store.get(key) ?? 0) + n;
|
|
997
1006
|
this.store.set(key, String(next));
|
|
998
1007
|
return next;
|
|
999
1008
|
}
|
|
1000
|
-
public async decrBy(key: string, n: number) {
|
|
1009
|
+
public async decrBy(key: string, n: number) {
|
|
1010
|
+
return this.incrBy(key, -n);
|
|
1011
|
+
}
|
|
1001
1012
|
}
|
|
1002
1013
|
|
|
1003
1014
|
const fakeClient = new FakeRedisClient();
|
|
@@ -1021,7 +1032,7 @@ The in-package vitest config runs `silent: true`, but if you're outside the pack
|
|
|
1021
1032
|
```ts
|
|
1022
1033
|
cache.setCacheConfigurations({
|
|
1023
1034
|
default: "memory",
|
|
1024
|
-
logging: false,
|
|
1035
|
+
logging: false, // <-- here
|
|
1025
1036
|
drivers: { memory: MemoryCacheDriver },
|
|
1026
1037
|
options: { memory: {} },
|
|
1027
1038
|
});
|
|
@@ -1040,7 +1051,7 @@ cache.on("hit", hits);
|
|
|
1040
1051
|
await service.getProfile("1");
|
|
1041
1052
|
await service.getProfile("1");
|
|
1042
1053
|
|
|
1043
|
-
expect(hits).toHaveBeenCalledTimes(1);
|
|
1054
|
+
expect(hits).toHaveBeenCalledTimes(1); // second call was a hit
|
|
1044
1055
|
```
|
|
1045
1056
|
|
|
1046
1057
|
Listeners registered via `cache.on(...)` automatically attach to any driver loaded later, so order of `on()` vs `init()` doesn't matter.
|
|
@@ -1053,9 +1064,7 @@ The chain-serialization guarantee is worth testing when your code fans out concu
|
|
|
1053
1064
|
await cache.set("counter", 0);
|
|
1054
1065
|
|
|
1055
1066
|
await Promise.all(
|
|
1056
|
-
Array.from({ length: 10 }, () =>
|
|
1057
|
-
cache.update<number>("counter", (c) => (c ?? 0) + 1),
|
|
1058
|
-
),
|
|
1067
|
+
Array.from({ length: 10 }, () => cache.update<number>("counter", (c) => (c ?? 0) + 1)),
|
|
1059
1068
|
);
|
|
1060
1069
|
|
|
1061
1070
|
await expect(cache.get("counter")).resolves.toBe(10);
|
|
@@ -1080,7 +1089,7 @@ beforeEach(async () => {
|
|
|
1080
1089
|
|
|
1081
1090
|
it("returns the most similar doc above threshold", async () => {
|
|
1082
1091
|
await cache.set("a", { text: "alpha" }, { vector: [1, 0, 0] });
|
|
1083
|
-
await cache.set("b", { text: "beta" },
|
|
1092
|
+
await cache.set("b", { text: "beta" }, { vector: [0, 1, 0] });
|
|
1084
1093
|
|
|
1085
1094
|
const hits = await cache.similar([1, 0, 0], { topK: 1, threshold: 0.5 });
|
|
1086
1095
|
|
|
@@ -1098,7 +1107,7 @@ The `pg` driver accepts any object satisfying `PgClientLike` (`{ query(text, val
|
|
|
1098
1107
|
|
|
1099
1108
|
---
|
|
1100
1109
|
name: use-cache-atomic
|
|
1101
|
-
description: 'Atomic counters via cache.increment(key, by=1) / cache.decrement(key, by=1) — returns the new number, throws on non-numeric values. Triggers: `cache.increment`, `cache.decrement`, "view counter", "page views", "atomic counter", "decrement stock", "rate-limit counter", "INCRBY"; typical import `import { cache } from "@warlock.js/cache"`. Skip: read-modify-write of objects — the `use-cache-update-merge` topic; named-lock coordination — the `use-cache-lock` topic; competing libs `ioredis` `INCR`, native counters in a `Map`.'
|
|
1110
|
+
description: 'Atomic counters via cache.increment(key, by=1, { ttl }?) / cache.decrement(key, by=1) — returns the new number, throws on non-numeric values. Triggers: `cache.increment`, `cache.decrement`, "view counter", "page views", "atomic counter", "decrement stock", "rate-limit counter", "INCRBY"; typical import `import { cache } from "@warlock.js/cache"`. Skip: read-modify-write of objects — the `use-cache-update-merge` topic; named-lock coordination — the `use-cache-lock` topic; competing libs `ioredis` `INCR`, native counters in a `Map`.'
|
|
1102
1111
|
---
|
|
1103
1112
|
|
|
1104
1113
|
# Atomic counters — `cache.increment` / `cache.decrement`
|
|
@@ -1109,9 +1118,9 @@ code. Both return the **new** value after the operation.
|
|
|
1109
1118
|
```ts
|
|
1110
1119
|
import { cache } from "@warlock.js/cache";
|
|
1111
1120
|
|
|
1112
|
-
const views = await cache.increment(`post.${id}.views`);
|
|
1113
|
-
const bulk = await cache.increment(`post.${id}.views`, 10);
|
|
1114
|
-
const left = await cache.decrement(`stock.${sku}`, 3);
|
|
1121
|
+
const views = await cache.increment(`post.${id}.views`); // +1 → 1, 2, 3…
|
|
1122
|
+
const bulk = await cache.increment(`post.${id}.views`, 10); // +10
|
|
1123
|
+
const left = await cache.decrement(`stock.${sku}`, 3); // -3
|
|
1115
1124
|
```
|
|
1116
1125
|
|
|
1117
1126
|
- A missing key is treated as `0`, so the first `increment` returns `by` (default `1`).
|
|
@@ -1123,18 +1132,24 @@ const left = await cache.decrement(`stock.${sku}`, 3); // -3
|
|
|
1123
1132
|
|
|
1124
1133
|
`increment`, `decrement`, `pull` and `update` are atomic, but only some drivers are atomic **across servers**:
|
|
1125
1134
|
|
|
1126
|
-
| Driver
|
|
1127
|
-
|
|
1128
|
-
| `redis`
|
|
1129
|
-
| `pg`
|
|
1130
|
-
| `memory` / `memoryExtended` / `lru` / `mock` | one process
|
|
1131
|
-
| `file`
|
|
1135
|
+
| Driver | `increment` / `decrement` | `pull` | `update` / `merge` |
|
|
1136
|
+
| -------------------------------------------- | ------------------------- | ----------------------------------- | ------------------------------------------- |
|
|
1137
|
+
| `redis` | cross-server (`INCRBY`) | cross-server (`GETDEL`) | cross-server (Lua compare-and-set, retries) |
|
|
1138
|
+
| `pg` | cross-server (one upsert) | cross-server (`DELETE … RETURNING`) | cross-server (compare-and-set, retries) |
|
|
1139
|
+
| `memory` / `memoryExtended` / `lru` / `mock` | one process | one process | one process |
|
|
1140
|
+
| `file` | one process | one process | not supported (throws) |
|
|
1132
1141
|
|
|
1133
1142
|
Use `redis` or `pg` for anything several servers touch (rate limits, login throttles, one-time tokens); memory is per-process. On redis/pg the `update` callback may run more than once under contention: keep it side-effect free.
|
|
1134
1143
|
|
|
1135
1144
|
## TTL is kept
|
|
1136
1145
|
|
|
1137
|
-
|
|
1146
|
+
Pass the TTL on **every increment** of an expiring counter:
|
|
1147
|
+
|
|
1148
|
+
```ts
|
|
1149
|
+
await cache.increment("login.failures", 1, { ttl: "1m" });
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
A missing or expired key starts at the amount with that TTL (seconds or duration string); an existing deadline is preserved. An explicit TTL also adds expiry to a live permanent counter, repairing counters left by an earlier seed/increment race. Without `options.ttl`, a fresh key uses the driver's default TTL. Redis applies the count and expiry in one Lua call; Postgres uses one upsert. This closes the race where a separate `set(key, 0, { ttl, onConflict: "create" })` seed expires just before the increment recreates the key. Tagged calls forward the option; namespaces use their default TTL unless overridden per call. Decrements keep existing TTLs too. Use `cache.update` (see the `use-cache-update-merge` topic) for objects.
|
|
1138
1153
|
|
|
1139
1154
|
## Common shapes
|
|
1140
1155
|
|
|
@@ -1191,11 +1206,14 @@ const missingIds = ids.filter((_, index) => users[index] === null);
|
|
|
1191
1206
|
## Write many — `setMany(items, ttl?)`
|
|
1192
1207
|
|
|
1193
1208
|
```ts
|
|
1194
|
-
await cache.setMany(
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1209
|
+
await cache.setMany(
|
|
1210
|
+
{
|
|
1211
|
+
"user.1": alice,
|
|
1212
|
+
"user.2": bob,
|
|
1213
|
+
"user.3": carol,
|
|
1214
|
+
},
|
|
1215
|
+
3600,
|
|
1216
|
+
); // optional TTL (seconds) applied to every entry
|
|
1199
1217
|
```
|
|
1200
1218
|
|
|
1201
1219
|
- Keys are the object keys; values are the object values.
|
|
@@ -1234,14 +1252,14 @@ Dedicated accessor for ordered collections — queues, recent-N buffers, sliding
|
|
|
1234
1252
|
```ts
|
|
1235
1253
|
const recent = cache.list<Event>("recent-events");
|
|
1236
1254
|
|
|
1237
|
-
await recent.push(event);
|
|
1238
|
-
await recent.unshift(priorityEvent);
|
|
1239
|
-
const tail = await recent.pop();
|
|
1240
|
-
const head = await recent.shift();
|
|
1241
|
-
const first10 = await recent.slice(0, 10);
|
|
1255
|
+
await recent.push(event); // append to tail — returns new length
|
|
1256
|
+
await recent.unshift(priorityEvent); // prepend to head — returns new length
|
|
1257
|
+
const tail = await recent.pop(); // remove + return tail
|
|
1258
|
+
const head = await recent.shift(); // remove + return head
|
|
1259
|
+
const first10 = await recent.slice(0, 10); // view — does not mutate
|
|
1242
1260
|
const all = await recent.all();
|
|
1243
1261
|
const count = await recent.length();
|
|
1244
|
-
await recent.trim(0, 99);
|
|
1262
|
+
await recent.trim(0, 99); // keep only indices 0..99 inclusive
|
|
1245
1263
|
await recent.clear();
|
|
1246
1264
|
```
|
|
1247
1265
|
|
|
@@ -1253,8 +1271,8 @@ The generic flows through every method. Pass the element type at the accessor ca
|
|
|
1253
1271
|
type Event = { type: string; at: number };
|
|
1254
1272
|
const queue = cache.list<Event>("jobs:queue");
|
|
1255
1273
|
|
|
1256
|
-
await queue.push({ type: "import", at: Date.now() });
|
|
1257
|
-
await queue.push("not an event" as never);
|
|
1274
|
+
await queue.push({ type: "import", at: Date.now() }); // ✓
|
|
1275
|
+
await queue.push("not an event" as never); // ✗ at the caller
|
|
1258
1276
|
```
|
|
1259
1277
|
|
|
1260
1278
|
## Current performance characteristics
|
|
@@ -1276,7 +1294,7 @@ When a list becomes empty (e.g. after successive `pop()` / `shift()` / `trim(0,
|
|
|
1276
1294
|
```ts
|
|
1277
1295
|
await recent.push("a");
|
|
1278
1296
|
await recent.pop();
|
|
1279
|
-
await cache.get("recent-events");
|
|
1297
|
+
await cache.get("recent-events"); // null, not []
|
|
1280
1298
|
```
|
|
1281
1299
|
|
|
1282
1300
|
## Typical recipes
|
|
@@ -1284,18 +1302,18 @@ await cache.get("recent-events"); // null, not []
|
|
|
1284
1302
|
```ts
|
|
1285
1303
|
// Recent-N audit log
|
|
1286
1304
|
const audit = cache.list<AuditEntry>("audit:recent");
|
|
1287
|
-
await audit.unshift(entry);
|
|
1288
|
-
await audit.trim(0, 999);
|
|
1305
|
+
await audit.unshift(entry); // newest at head
|
|
1306
|
+
await audit.trim(0, 999); // keep most-recent 1000
|
|
1289
1307
|
|
|
1290
1308
|
// Lightweight job queue (single-node)
|
|
1291
1309
|
const queue = cache.list<Job>("jobs:pending");
|
|
1292
1310
|
await queue.push(job);
|
|
1293
|
-
const next = await queue.shift();
|
|
1311
|
+
const next = await queue.shift(); // FIFO
|
|
1294
1312
|
|
|
1295
1313
|
// Stack
|
|
1296
1314
|
const stack = cache.list<Frame>("stack");
|
|
1297
1315
|
await stack.push(frame);
|
|
1298
|
-
const top = await stack.pop();
|
|
1316
|
+
const top = await stack.pop(); // LIFO
|
|
1299
1317
|
```
|
|
1300
1318
|
|
|
1301
1319
|
## What lists are NOT for
|
|
@@ -1349,9 +1367,7 @@ TTL rules: a number or a bare numeric string (`"90"`) is **seconds**; `"500ms"`
|
|
|
1349
1367
|
## Return shape — discriminated union
|
|
1350
1368
|
|
|
1351
1369
|
```ts
|
|
1352
|
-
type LockOutcome<T> =
|
|
1353
|
-
| { acquired: true; value: T }
|
|
1354
|
-
| { acquired: false };
|
|
1370
|
+
type LockOutcome<T> = { acquired: true; value: T } | { acquired: false };
|
|
1355
1371
|
```
|
|
1356
1372
|
|
|
1357
1373
|
Unambiguous even when `fn` returns `undefined`. Narrow with TS:
|
|
@@ -1360,7 +1376,7 @@ Unambiguous even when `fn` returns `undefined`. Narrow with TS:
|
|
|
1360
1376
|
const outcome = await cache.lock("lock.x", "1m", async () => compute());
|
|
1361
1377
|
|
|
1362
1378
|
if (outcome.acquired) {
|
|
1363
|
-
console.log(outcome.value);
|
|
1379
|
+
console.log(outcome.value); // typed
|
|
1364
1380
|
} else {
|
|
1365
1381
|
console.log("someone else is running");
|
|
1366
1382
|
}
|
|
@@ -1371,19 +1387,15 @@ if (outcome.acquired) {
|
|
|
1371
1387
|
### Cron on only one server
|
|
1372
1388
|
|
|
1373
1389
|
```ts
|
|
1374
|
-
cron.daily("3am", () =>
|
|
1375
|
-
cache.lock("lock.cleanup", "30m", () => db.cleanup()),
|
|
1376
|
-
);
|
|
1390
|
+
cron.daily("3am", () => cache.lock("lock.cleanup", "30m", () => db.cleanup()));
|
|
1377
1391
|
```
|
|
1378
1392
|
|
|
1379
1393
|
### Idempotent webhook
|
|
1380
1394
|
|
|
1381
1395
|
```ts
|
|
1382
1396
|
app.post("/webhooks/stripe", async (req, res) => {
|
|
1383
|
-
const outcome = await cache.lock(
|
|
1384
|
-
|
|
1385
|
-
"24h",
|
|
1386
|
-
() => processStripeEvent(req.body),
|
|
1397
|
+
const outcome = await cache.lock(`webhook.stripe.${req.body.id}`, "24h", () =>
|
|
1398
|
+
processStripeEvent(req.body),
|
|
1387
1399
|
);
|
|
1388
1400
|
|
|
1389
1401
|
if (!outcome.acquired) {
|
|
@@ -1408,13 +1420,13 @@ await cache.lock(
|
|
|
1408
1420
|
|
|
1409
1421
|
## Driver behavior
|
|
1410
1422
|
|
|
1411
|
-
| Driver
|
|
1412
|
-
|
|
1413
|
-
| `redis`
|
|
1414
|
-
| `pg`
|
|
1423
|
+
| Driver | Cross-process safe? |
|
|
1424
|
+
| ----------------------------------- | :-----------------------------------------------: |
|
|
1425
|
+
| `redis` | ✅ Native `SET … NX EX`, Lua release |
|
|
1426
|
+
| `pg` | ✅ `INSERT … ON CONFLICT`, one-statement release |
|
|
1415
1427
|
| `memory` / `memoryExtended` / `lru` | ❌ In-process only (exclusive within the process) |
|
|
1416
|
-
| `file`
|
|
1417
|
-
| `null`
|
|
1428
|
+
| `file` | ⚠️ Single-host only (exclusive create) |
|
|
1429
|
+
| `null` | n/a — always "acquires" |
|
|
1418
1430
|
|
|
1419
1431
|
## Gotchas
|
|
1420
1432
|
|
|
@@ -1440,10 +1452,10 @@ When you'll touch the same prefix more than a couple of times, grab a scoped han
|
|
|
1440
1452
|
```ts
|
|
1441
1453
|
const chat = cache.namespace(`chats.${id}`, { ttl: "30d", tags: [`user.${userId}`] });
|
|
1442
1454
|
|
|
1443
|
-
await chat.set("messages.10", msg);
|
|
1444
|
-
await chat.set("draft", d, { ttl: "1h" });
|
|
1445
|
-
await chat.tags(["unread"]).set("ping", p);
|
|
1446
|
-
await chat.clear();
|
|
1455
|
+
await chat.set("messages.10", msg); // chats.<id>.messages.10, 30d, user.<userId>
|
|
1456
|
+
await chat.set("draft", d, { ttl: "1h" }); // per-call ttl wins
|
|
1457
|
+
await chat.tags(["unread"]).set("ping", p); // tags merge: user.<userId> + unread
|
|
1458
|
+
await chat.clear(); // sugar for removeNamespace
|
|
1447
1459
|
```
|
|
1448
1460
|
|
|
1449
1461
|
Scopes are pure views — same connection, same driver, no extra state. Per-call options always win over scope defaults; tags merge additively.
|
|
@@ -1452,9 +1464,9 @@ Scopes are pure views — same connection, same driver, no extra state. Per-call
|
|
|
1452
1464
|
|
|
1453
1465
|
```ts
|
|
1454
1466
|
const chat = cache.namespace(`chats.${id}`, { ttl: "30d" });
|
|
1455
|
-
const typing = chat.namespace("typing", { ttl: "5s" });
|
|
1467
|
+
const typing = chat.namespace("typing", { ttl: "5s" }); // overrides parent ttl
|
|
1456
1468
|
|
|
1457
|
-
await typing.set("user.42", true);
|
|
1469
|
+
await typing.set("user.42", true); // chats.<id>.typing.user.42, 5s
|
|
1458
1470
|
```
|
|
1459
1471
|
|
|
1460
1472
|
Nested scopes inherit defaults and can override. Tags accumulate.
|
|
@@ -1469,14 +1481,14 @@ Inline prefixes are still fine for one-off writes.
|
|
|
1469
1481
|
|
|
1470
1482
|
## Plain `removeNamespace` when you already have the prefix
|
|
1471
1483
|
|
|
1472
|
-
When you
|
|
1484
|
+
When you _do_ know the prefix string and don't need a scoped handle for repeated reads/writes:
|
|
1473
1485
|
|
|
1474
1486
|
```ts
|
|
1475
1487
|
await cache.set("user:1:profile", profile);
|
|
1476
|
-
await cache.set("user:1:prefs",
|
|
1488
|
+
await cache.set("user:1:prefs", prefs);
|
|
1477
1489
|
await cache.set("user:2:profile", otherProfile);
|
|
1478
1490
|
|
|
1479
|
-
await cache.removeNamespace("user.1");
|
|
1491
|
+
await cache.removeNamespace("user.1"); // drops both user:1 entries, keeps user:2
|
|
1480
1492
|
```
|
|
1481
1493
|
|
|
1482
1494
|
Cheaper than tags (no reverse index to maintain). Every real driver supports it — memory family and `lru` by prefix-scan, `file` by directory (including dotted sibling keys like `ns.a`), `redis` by non-blocking `SCAN` (not `KEYS`), `pg` by key/`LIKE` prefix; `null` no-ops. Namespace strings are treated as data, not glob/path input — see the `pick-cache-driver` topic for the file-path-containment and redis-glob-escaping details.
|
|
@@ -1499,10 +1511,8 @@ options: {
|
|
|
1499
1511
|
```ts
|
|
1500
1512
|
const feed = cache.namespace(`feed.${userId}`, { tags: [`user.${userId}`] });
|
|
1501
1513
|
|
|
1502
|
-
await feed.swr(
|
|
1503
|
-
|
|
1504
|
-
{ freshTtl: "30s", staleTtl: "10m", tags: ["computed"] },
|
|
1505
|
-
() => buildHomeFeed(userId),
|
|
1514
|
+
await feed.swr("home", { freshTtl: "30s", staleTtl: "10m", tags: ["computed"] }, () =>
|
|
1515
|
+
buildHomeFeed(userId),
|
|
1506
1516
|
);
|
|
1507
1517
|
// stored at feed.<userId>.home, tagged [user.<userId>, computed]
|
|
1508
1518
|
```
|
|
@@ -1544,21 +1554,21 @@ const hits = await cache.similar<T>(queryVec, {
|
|
|
1544
1554
|
|
|
1545
1555
|
## Capability matrix
|
|
1546
1556
|
|
|
1547
|
-
| Driver
|
|
1548
|
-
|
|
1557
|
+
| Driver | `similar()` |
|
|
1558
|
+
| ----------------------------------- | -------------------------------------------------- |
|
|
1549
1559
|
| `memory` / `memoryExtended` / `lru` | ✅ Brute force (O(N)) — dev only past ~10k entries |
|
|
1550
|
-
| `pg`
|
|
1551
|
-
| `pg`
|
|
1552
|
-
| `redis`
|
|
1553
|
-
| `file`
|
|
1554
|
-
| `null`
|
|
1560
|
+
| `pg` _with_ `options.pg.vector` | ✅ pgvector + HNSW/IVFFlat index |
|
|
1561
|
+
| `pg` _without_ `options.pg.vector` | ❌ Throws `CacheUnsupportedError` |
|
|
1562
|
+
| `redis` | ❌ Throws (RediSearch on backlog) |
|
|
1563
|
+
| `file` | ❌ Throws |
|
|
1564
|
+
| `null` | Returns `[]` |
|
|
1555
1565
|
|
|
1556
1566
|
## Always-true facts
|
|
1557
1567
|
|
|
1558
1568
|
1. **Cache is embedding-agnostic.** Caller computes vectors. The cache stores and ranks; it doesn't call out to an embedder.
|
|
1559
1569
|
2. **Only entries written with `set({ vector })` show up.** A plain `set` adds the entry as KV — invisible to `similar()`.
|
|
1560
1570
|
3. **Score = cosine similarity** in `[0, 1]` for typical embedding spaces. The `pg` driver computes `1 - (embedding <=> $1::vector)` so the score matches the memory drivers.
|
|
1561
|
-
4. **Tag filter narrows the candidate pool
|
|
1571
|
+
4. **Tag filter narrows the candidate pool _before_ ranking** — union semantics (entry must carry at least one of the listed tags).
|
|
1562
1572
|
5. **Dimension mismatch throws `CacheConfigurationError`** at both `set({ vector })` and `similar()` time. Don't switch embedders without re-indexing.
|
|
1563
1573
|
6. **TTL + LRU eviction also drop the vector** — expired or evicted entries are invisible to `similar()`.
|
|
1564
1574
|
|
|
@@ -1571,7 +1581,7 @@ const queryVec = await embed(prompt);
|
|
|
1571
1581
|
const hits = await cache.similar<Answer>(queryVec, { topK: 1, threshold: 0.92 });
|
|
1572
1582
|
|
|
1573
1583
|
if (hits.length > 0) {
|
|
1574
|
-
return hits[0].value;
|
|
1584
|
+
return hits[0].value; // skip the LLM call
|
|
1575
1585
|
}
|
|
1576
1586
|
|
|
1577
1587
|
const answer = await llm.complete(prompt);
|
|
@@ -1630,7 +1640,7 @@ Tags let you invalidate by a label rather than by enumerating every key. Use the
|
|
|
1630
1640
|
```ts
|
|
1631
1641
|
// Inline — terser when you know the tags up front
|
|
1632
1642
|
await cache.set("user:1:profile", profile, { tags: ["users", "tenant-42"] });
|
|
1633
|
-
await cache.set("user:1:prefs",
|
|
1643
|
+
await cache.set("user:1:prefs", prefs, { tags: ["users", "tenant-42"] });
|
|
1634
1644
|
|
|
1635
1645
|
// Fluent — useful when you already have a tagged handle
|
|
1636
1646
|
const users = cache.tags(["users"]);
|
|
@@ -1652,11 +1662,11 @@ Multi-tag is **union** semantics: an entry is invalidated if it carries **at lea
|
|
|
1652
1662
|
|
|
1653
1663
|
## When to reach for tags vs namespaces
|
|
1654
1664
|
|
|
1655
|
-
| Use case
|
|
1656
|
-
|
|
|
1657
|
-
| The keys share a known prefix
|
|
1658
|
-
| The keys are spread across prefixes, tied by entity | Tags
|
|
1659
|
-
| Both apply
|
|
1665
|
+
| Use case | Reach for |
|
|
1666
|
+
| --------------------------------------------------- | ------------------------------------------------------------------- |
|
|
1667
|
+
| The keys share a known prefix | `cache.removeNamespace("prefix")` (the `use-cache-namespace` topic) |
|
|
1668
|
+
| The keys are spread across prefixes, tied by entity | Tags |
|
|
1669
|
+
| Both apply | Tags — more flexible; cheap on most drivers |
|
|
1660
1670
|
|
|
1661
1671
|
Namespaces are cheaper (no reverse index). Tags are more powerful (any key can carry any tag).
|
|
1662
1672
|
|
|
@@ -1666,13 +1676,13 @@ Subsequent `set("user:1", ...)` with **no** `tags` leaves previous associations
|
|
|
1666
1676
|
|
|
1667
1677
|
## Driver behavior
|
|
1668
1678
|
|
|
1669
|
-
| Driver
|
|
1670
|
-
|
|
|
1671
|
-
| `null`
|
|
1679
|
+
| Driver | Tag invalidation |
|
|
1680
|
+
| ----------------------------------- | :----------------------------------------------------------: |
|
|
1681
|
+
| `null` | noop |
|
|
1672
1682
|
| `memory` / `memoryExtended` / `lru` | ✓ (separate index store; eviction and expiry never touch it) |
|
|
1673
|
-
| `file`
|
|
1674
|
-
| `redis`
|
|
1675
|
-
| `pg`
|
|
1683
|
+
| `file` | ✓ (serialized index) |
|
|
1684
|
+
| `redis` | ✓ (native SET inside the prefix) |
|
|
1685
|
+
| `pg` | ✓ (JSON-array merge on the index row) |
|
|
1676
1686
|
|
|
1677
1687
|
The index uses driver primitives (`tagAdd` / `tagMembers` / `tagRemove`), so concurrent tagged writes never drop members. It stores un-prefixed keys, `invalidate()` deletes exactly the members it read, writes prune up to 20 dead members, and `remove()` / `pull()` detach the key. Tags are global across scopes. Across servers, use `redis` or `pg` so every server shares one index.
|
|
1678
1688
|
|
|
@@ -1694,7 +1704,7 @@ Tags re-apply on every successful refresh — see the `use-swr` topic.
|
|
|
1694
1704
|
const getUser = cached(fn, { key: (id) => `user.${id}`, ttl: "1h", tags: ["users"] });
|
|
1695
1705
|
const getPosts = cached(fn, { key: (u) => `posts.by.${u}`, ttl: "30m", tags: ["users", "posts"] });
|
|
1696
1706
|
|
|
1697
|
-
await cache.tags(["users"]).invalidate();
|
|
1707
|
+
await cache.tags(["users"]).invalidate(); // drops both wrappers' caches
|
|
1698
1708
|
```
|
|
1699
1709
|
|
|
1700
1710
|
See the `use-cached-hof` topic.
|
|
@@ -1734,7 +1744,7 @@ await cache.update<UserState>("user:1:state", (current) => ({
|
|
|
1734
1744
|
// Conditional update — return null to remove
|
|
1735
1745
|
await cache.update<Session>("session:abc", (current) => {
|
|
1736
1746
|
if (!current || current.expired) {
|
|
1737
|
-
return null;
|
|
1747
|
+
return null; // removes the key
|
|
1738
1748
|
}
|
|
1739
1749
|
return { ...current, extendedAt: Date.now() };
|
|
1740
1750
|
});
|
|
@@ -1777,21 +1787,21 @@ await Promise.all(
|
|
|
1777
1787
|
),
|
|
1778
1788
|
);
|
|
1779
1789
|
|
|
1780
|
-
await cache.get("counter");
|
|
1790
|
+
await cache.get("counter"); // 10 — not a lost-update
|
|
1781
1791
|
```
|
|
1782
1792
|
|
|
1783
1793
|
The per-key lock map lives on the driver instance and is cleared after each chain link finishes. No leak.
|
|
1784
1794
|
|
|
1785
1795
|
## When to reach for what
|
|
1786
1796
|
|
|
1787
|
-
| Task
|
|
1788
|
-
|
|
|
1789
|
-
| "Add 1 to this counter"
|
|
1790
|
-
| "Change one field on a cached object"
|
|
1791
|
-
| "Read-decide-maybe-write, possibly remove" | `update()`
|
|
1792
|
-
| "Only set if missing"
|
|
1793
|
-
| "Only set if already exists"
|
|
1794
|
-
| "Read-then-delete atomically"
|
|
1797
|
+
| Task | Use |
|
|
1798
|
+
| ------------------------------------------ | --------------------------------------------------------------- |
|
|
1799
|
+
| "Add 1 to this counter" | `increment()` (Redis-atomic via `INCRBY`; in-process elsewhere) |
|
|
1800
|
+
| "Change one field on a cached object" | `merge()` |
|
|
1801
|
+
| "Read-decide-maybe-write, possibly remove" | `update()` |
|
|
1802
|
+
| "Only set if missing" | `set(k, v, { onConflict: "create" })` |
|
|
1803
|
+
| "Only set if already exists" | `set(k, v, { onConflict: "update" })` |
|
|
1804
|
+
| "Read-then-delete atomically" | `pull()` |
|
|
1795
1805
|
|
|
1796
1806
|
|
|
1797
1807
|
## use-cache-utils `@warlock.js/cache/use-cache-utils/SKILL.md`
|
|
@@ -1813,17 +1823,17 @@ driver or do cache-adjacent work outside the manager.
|
|
|
1813
1823
|
```ts
|
|
1814
1824
|
import { parseTtl, expiresAtToTtl, resolveTtl } from "@warlock.js/cache";
|
|
1815
1825
|
|
|
1816
|
-
parseTtl(3600);
|
|
1817
|
-
parseTtl("1h");
|
|
1818
|
-
parseTtl(Infinity);
|
|
1819
|
-
parseTtl(-5);
|
|
1826
|
+
parseTtl(3600); // 3600
|
|
1827
|
+
parseTtl("1h"); // 3600 (duration string via `ms`)
|
|
1828
|
+
parseTtl(Infinity); // Infinity (no expiry)
|
|
1829
|
+
parseTtl(-5); // throws CacheConfigurationError
|
|
1820
1830
|
|
|
1821
1831
|
expiresAtToTtl(new Date(Date.now() + 60_000)); // ~60 (absolute → relative seconds)
|
|
1822
|
-
expiresAtToTtl(Date.now() - 1000);
|
|
1832
|
+
expiresAtToTtl(Date.now() - 1000); // throws — deadline in the past
|
|
1823
1833
|
|
|
1824
1834
|
// caller ttl > expiresAt > fallback; ttl+expiresAt together throws
|
|
1825
|
-
resolveTtl("1h", undefined, Infinity);
|
|
1826
|
-
resolveTtl(undefined, undefined, 1800);
|
|
1835
|
+
resolveTtl("1h", undefined, Infinity); // 3600
|
|
1836
|
+
resolveTtl(undefined, undefined, 1800); // 1800 (fallback)
|
|
1827
1837
|
```
|
|
1828
1838
|
|
|
1829
1839
|
## Option normalizers
|
|
@@ -1834,10 +1844,10 @@ Coerce the polymorphic 2nd/3rd argument of `set`/`remember` into a uniform shape
|
|
|
1834
1844
|
```ts
|
|
1835
1845
|
import { normalizeToOptions, normalizeToRememberOptions } from "@warlock.js/cache";
|
|
1836
1846
|
|
|
1837
|
-
normalizeToOptions(60);
|
|
1838
|
-
normalizeToOptions("1h");
|
|
1839
|
-
normalizeToOptions({ tags: ["x"] });
|
|
1840
|
-
normalizeToRememberOptions("1h");
|
|
1847
|
+
normalizeToOptions(60); // { ttl: 60 }
|
|
1848
|
+
normalizeToOptions("1h"); // { ttl: "1h" }
|
|
1849
|
+
normalizeToOptions({ tags: ["x"] }); // returned as-is
|
|
1850
|
+
normalizeToRememberOptions("1h"); // { ttl: "1h" } (no expiresAt/onConflict)
|
|
1841
1851
|
```
|
|
1842
1852
|
|
|
1843
1853
|
## Key + tag helpers
|
|
@@ -1845,14 +1855,14 @@ normalizeToRememberOptions("1h"); // { ttl: "1h" } (no expiresAt/onConfl
|
|
|
1845
1855
|
```ts
|
|
1846
1856
|
import { parseCacheKey, mergeTagSets, injectTags } from "@warlock.js/cache";
|
|
1847
1857
|
|
|
1848
|
-
parseCacheKey("users:1");
|
|
1849
|
-
parseCacheKey({ page: 1, q: "John" });
|
|
1858
|
+
parseCacheKey("users:1"); // "users.1"
|
|
1859
|
+
parseCacheKey({ page: 1, q: "John" }); // "page.1.q.John"
|
|
1850
1860
|
parseCacheKey("user:1", { globalPrefix: "app" }); // "app.user.1"
|
|
1851
1861
|
|
|
1852
|
-
mergeTagSets(["a", "b"], ["b", "c"]);
|
|
1853
|
-
mergeTagSets(undefined, undefined);
|
|
1862
|
+
mergeTagSets(["a", "b"], ["b", "c"]); // ["a","b","c"] (deduped union)
|
|
1863
|
+
mergeTagSets(undefined, undefined); // undefined
|
|
1854
1864
|
|
|
1855
|
-
injectTags({ ttl: "1h" }, ["unread"]);
|
|
1865
|
+
injectTags({ ttl: "1h" }, ["unread"]); // { ttl: "1h", tags: ["unread"] } (pure, no mutation)
|
|
1856
1866
|
```
|
|
1857
1867
|
|
|
1858
1868
|
## Vector scoring
|
|
@@ -1862,7 +1872,7 @@ import { cosineSimilarity } from "@warlock.js/cache";
|
|
|
1862
1872
|
|
|
1863
1873
|
cosineSimilarity([1, 0, 0], [1, 0, 0]); // 1
|
|
1864
1874
|
cosineSimilarity([1, 0, 0], [0, 1, 0]); // 0
|
|
1865
|
-
cosineSimilarity([1, 2, 3], [1, 2]);
|
|
1875
|
+
cosineSimilarity([1, 2, 3], [1, 2]); // throws — dimension mismatch
|
|
1866
1876
|
```
|
|
1867
1877
|
|
|
1868
1878
|
Powers the brute-force `cache.similar()` on the memory drivers — reach for it
|
|
@@ -1901,6 +1911,7 @@ description: 'Wrap an async function with cached(fn, options) — declare the ca
|
|
|
1901
1911
|
## When to use
|
|
1902
1912
|
|
|
1903
1913
|
Reach for `cached()` instead of `cache.remember()` when:
|
|
1914
|
+
|
|
1904
1915
|
- You have a function you'll call from many places.
|
|
1905
1916
|
- You want the caching strategy declared once, not repeated at every call site.
|
|
1906
1917
|
- You want `.invalidate()` available without manually deriving keys.
|
|
@@ -1929,11 +1940,11 @@ cached(fn, {
|
|
|
1929
1940
|
|
|
1930
1941
|
## Auto-key rules (shorthand only)
|
|
1931
1942
|
|
|
1932
|
-
| Args
|
|
1933
|
-
|
|
1934
|
-
| None
|
|
1935
|
-
| Any args
|
|
1936
|
-
| `Map` / `Set` / function / symbol / class instance / circular | throws `CacheConfigurationError`
|
|
1943
|
+
| Args | Key |
|
|
1944
|
+
| ------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
1945
|
+
| None | `prefix` |
|
|
1946
|
+
| Any args | `prefix.<16 hex>` — sha1 of a stable JSON encoding of the args, truncated |
|
|
1947
|
+
| `Map` / `Set` / function / symbol / class instance / circular | throws `CacheConfigurationError` |
|
|
1937
1948
|
|
|
1938
1949
|
The hash keeps distinct args on distinct keys (`("1.private", "")` ≠ `("1", "private")`). Object keys are sorted; `Date` → ISO string; order of args matters. Never build keys by hand: call `.invalidate(...args)`, which honours `config.driver`. When auto-key fails, use the options form with a custom `key` fn.
|
|
1939
1950
|
|
|
@@ -1967,7 +1978,7 @@ await getUser.invalidate(42);
|
|
|
1967
1978
|
const getUser = cached(fn, { key: (id) => `user.${id}`, ttl: "1h", tags: ["users"] });
|
|
1968
1979
|
const getPosts = cached(fn, { key: (u) => `posts.by.${u}`, ttl: "30m", tags: ["users", "posts"] });
|
|
1969
1980
|
|
|
1970
|
-
await cache.tags(["users"]).invalidate();
|
|
1981
|
+
await cache.tags(["users"]).invalidate(); // drops both wrappers' caches
|
|
1971
1982
|
```
|
|
1972
1983
|
|
|
1973
1984
|
See the `use-cache-tags` topic.
|
|
@@ -1977,7 +1988,7 @@ See the `use-cache-tags` topic.
|
|
|
1977
1988
|
```ts
|
|
1978
1989
|
const getCategoryMeta = cached(
|
|
1979
1990
|
(filters: Filters) => db.categories.meta(filters.category),
|
|
1980
|
-
{ key: (f) => `category.meta.${f.category}`, ttl: "1h" },
|
|
1991
|
+
{ key: (f) => `category.meta.${f.category}`, ttl: "1h" }, // ignores `sort`, `page`
|
|
1981
1992
|
);
|
|
1982
1993
|
```
|
|
1983
1994
|
|
|
@@ -2022,11 +2033,11 @@ Use `cache.remember()` when freshness is non-negotiable — auth, balances, bill
|
|
|
2022
2033
|
│ no upstream call │ bg refresh │ like a miss
|
|
2023
2034
|
```
|
|
2024
2035
|
|
|
2025
|
-
| Window
|
|
2026
|
-
|
|
2027
|
-
| `now < freshTtl`
|
|
2036
|
+
| Window | Behavior |
|
|
2037
|
+
| --------------------------- | ---------------------------------------------------------------------------------------- |
|
|
2038
|
+
| `now < freshTtl` | Return cached. No upstream call. |
|
|
2028
2039
|
| `freshTtl ≤ now < staleTtl` | Return cached immediately. Run `fn()` in background; next read sees the refreshed value. |
|
|
2029
|
-
| `now ≥ staleTtl`
|
|
2040
|
+
| `now ≥ staleTtl` | Block on `fn()`. Same as `remember()`. |
|
|
2030
2041
|
|
|
2031
2042
|
## API
|
|
2032
2043
|
|
|
@@ -2055,12 +2066,12 @@ await cache.swr(
|
|
|
2055
2066
|
|
|
2056
2067
|
## Driver support
|
|
2057
2068
|
|
|
2058
|
-
| Driver
|
|
2059
|
-
|
|
2060
|
-
| memory / memoryExtended / lru / file / mock | ✅ Full
|
|
2061
|
-
| redis
|
|
2062
|
-
| pg
|
|
2063
|
-
| null
|
|
2069
|
+
| Driver | Background refresh |
|
|
2070
|
+
| ------------------------------------------- | ------------------------------------------------------------------------- |
|
|
2071
|
+
| memory / memoryExtended / lru / file / mock | ✅ Full |
|
|
2072
|
+
| redis | ✅ Full (sidecar key for staleAt — backwards-compatible) |
|
|
2073
|
+
| pg | ✅ Full (`stale_at TIMESTAMPTZ` column — provision via `driver.schema()`) |
|
|
2074
|
+
| null | ❌ Always-fetch (null caches nothing) |
|
|
2064
2075
|
|
|
2065
2076
|
## Common shapes
|
|
2066
2077
|
|
|
@@ -2076,9 +2087,7 @@ await cache.swr(`dashboard.${tenantId}`, { freshTtl: "5m", staleTtl: "1h" }, ()
|
|
|
2076
2087
|
);
|
|
2077
2088
|
|
|
2078
2089
|
// Third-party API — degrade gracefully when upstream is down
|
|
2079
|
-
await cache.swr("exchange.rates", { freshTtl: "10m", staleTtl: "24h" }, () =>
|
|
2080
|
-
fetchFromForexAPI(),
|
|
2081
|
-
);
|
|
2090
|
+
await cache.swr("exchange.rates", { freshTtl: "10m", staleTtl: "24h" }, () => fetchFromForexAPI());
|
|
2082
2091
|
```
|
|
2083
2092
|
|
|
2084
2093
|
## Through scoped caches
|
|
@@ -2086,10 +2095,8 @@ await cache.swr("exchange.rates", { freshTtl: "10m", staleTtl: "24h" }, () =>
|
|
|
2086
2095
|
```ts
|
|
2087
2096
|
const feed = cache.namespace(`feed.${userId}`, { tags: [`user.${userId}`] });
|
|
2088
2097
|
|
|
2089
|
-
await feed.swr(
|
|
2090
|
-
|
|
2091
|
-
{ freshTtl: "30s", staleTtl: "10m", tags: ["computed"] },
|
|
2092
|
-
() => buildHomeFeed(userId),
|
|
2098
|
+
await feed.swr("home", { freshTtl: "30s", staleTtl: "10m", tags: ["computed"] }, () =>
|
|
2099
|
+
buildHomeFeed(userId),
|
|
2093
2100
|
);
|
|
2094
2101
|
// stored at feed.<userId>.home, tagged [user.<userId>, computed]
|
|
2095
2102
|
```
|
|
@@ -2097,7 +2104,7 @@ await feed.swr(
|
|
|
2097
2104
|
## Things NOT to do
|
|
2098
2105
|
|
|
2099
2106
|
- Don't use SWR when the user must see the latest data (auth, billing). Use `remember()` instead — block-until-fresh is the right semantic there.
|
|
2100
|
-
- Don't pick `freshTtl` to be the
|
|
2107
|
+
- Don't pick `freshTtl` to be the _same_ as `staleTtl` thinking it disables the stale window — that throws. Pick a tight `freshTtl` and wider `staleTtl` that reflects how stale your product can tolerate being.
|
|
2101
2108
|
- Don't ignore `error` events. A persistent stream of refresh failures means upstream is broken and the cache is masking it.
|
|
2102
2109
|
- Don't reach for SWR on the null driver — it caches nothing, so SWR always blocks.
|
|
2103
2110
|
|