@warlock.js/cache 5.23.2 → 5.25.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 +12 -0
- package/README.md +616 -616
- package/cjs/index.cjs +21 -1
- package/cjs/index.cjs.map +1 -1
- package/esm/drivers/base-cache-driver.d.mts +12 -0
- package/esm/drivers/base-cache-driver.d.mts.map +1 -1
- package/esm/drivers/base-cache-driver.mjs +10 -0
- package/esm/drivers/base-cache-driver.mjs.map +1 -1
- package/esm/drivers/null-cache-driver.mjs.map +1 -1
- package/esm/tagged-cache.d.mts.map +1 -1
- package/esm/tagged-cache.mjs +11 -1
- package/esm/tagged-cache.mjs.map +1 -1
- package/esm/types.d.mts +5 -2
- package/esm/types.d.mts.map +1 -1
- package/esm/types.mjs.map +1 -1
- package/esm/utils.mjs.map +1 -1
- package/llms-full.txt +1632 -1631
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/skills/apply-cache-patterns/SKILL.md +95 -95
- package/skills/configure-set-options/SKILL.md +96 -96
- package/skills/handle-cache-errors/SKILL.md +92 -92
- package/skills/observe-cache/SKILL.md +104 -103
- package/skills/pick-cache-driver/SKILL.md +121 -121
- package/skills/test-cache-code/SKILL.md +219 -219
- package/skills/use-cache-atomic/SKILL.md +42 -42
- package/skills/use-cache-bulk/SKILL.md +57 -57
- package/skills/use-cache-list/SKILL.md +85 -85
- package/skills/use-cache-lock/SKILL.md +101 -101
- package/skills/use-cache-namespace/SKILL.md +88 -88
- package/skills/use-cache-similarity/SKILL.md +94 -94
- package/skills/use-cache-update-merge/SKILL.md +84 -84
- package/skills/use-cache-utils/SKILL.md +89 -89
- package/skills/use-cached-hof/SKILL.md +93 -93
- package/skills/use-swr/SKILL.md +104 -104
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: use-cached-hof
|
|
3
|
-
description: 'Wrap an async function with cached(fn, options) — declare the caching strategy once, call from many sites, get a bound .invalidate(...args) helper. Triggers: `cached`, `invalidate`, `key`, `ttl`, `tags`, `driver`; "wrap a DB lookup with caching", "memoize a function and invalidate by args", "one declaration many call sites", "auto-derive cache key from args"; typical import `import { cached } from "@warlock.js/cache"`. Skip: one-shot memoization — `@warlock.js/cache/apply-cache-patterns/SKILL.md` (`cache.remember`); tag bulk drop — `@warlock.js/cache/use-cache-tags/SKILL.md`; competing libs `p-memoize`, `mem`, `lodash.memoize`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# `cached()` — function-memoization wrapper
|
|
7
|
-
|
|
8
|
-
`cached()` turns any async function into a memoized version. One declaration, many call sites, with a bound `.invalidate()` helper.
|
|
9
|
-
|
|
10
|
-
## When to use
|
|
11
|
-
|
|
12
|
-
Reach for `cached()` instead of `cache.remember()` when:
|
|
13
|
-
- You have a function you'll call from many places.
|
|
14
|
-
- You want the caching strategy declared once, not repeated at every call site.
|
|
15
|
-
- You want `.invalidate()` available without manually deriving keys.
|
|
16
|
-
|
|
17
|
-
Stick with `cache.remember()` for one-shot "get-or-compute" calls.
|
|
18
|
-
|
|
19
|
-
## The three shapes — `fn` always first
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import { cached } from "@warlock.js/cache";
|
|
23
|
-
|
|
24
|
-
// 1. Prefix shorthand — driver default TTL; key auto-derived from args
|
|
25
|
-
cached(fn, "user");
|
|
26
|
-
|
|
27
|
-
// 2. Prefix + TTL
|
|
28
|
-
cached(fn, "user", "1h");
|
|
29
|
-
|
|
30
|
-
// 3. Options form — custom key fn, tags, per-call driver
|
|
31
|
-
cached(fn, {
|
|
32
|
-
key: (id: number) => `user.${id}`,
|
|
33
|
-
ttl: "1h",
|
|
34
|
-
tags: ["users"],
|
|
35
|
-
driver: "redis",
|
|
36
|
-
});
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Auto-key rules (shorthand only)
|
|
40
|
-
|
|
1
|
+
---
|
|
2
|
+
name: use-cached-hof
|
|
3
|
+
description: 'Wrap an async function with cached(fn, options) — declare the caching strategy once, call from many sites, get a bound .invalidate(...args) helper. Triggers: `cached`, `invalidate`, `key`, `ttl`, `tags`, `driver`; "wrap a DB lookup with caching", "memoize a function and invalidate by args", "one declaration many call sites", "auto-derive cache key from args"; typical import `import { cached } from "@warlock.js/cache"`. Skip: one-shot memoization — `@warlock.js/cache/apply-cache-patterns/SKILL.md` (`cache.remember`); tag bulk drop — `@warlock.js/cache/use-cache-tags/SKILL.md`; competing libs `p-memoize`, `mem`, `lodash.memoize`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# `cached()` — function-memoization wrapper
|
|
7
|
+
|
|
8
|
+
`cached()` turns any async function into a memoized version. One declaration, many call sites, with a bound `.invalidate()` helper.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
Reach for `cached()` instead of `cache.remember()` when:
|
|
13
|
+
- You have a function you'll call from many places.
|
|
14
|
+
- You want the caching strategy declared once, not repeated at every call site.
|
|
15
|
+
- You want `.invalidate()` available without manually deriving keys.
|
|
16
|
+
|
|
17
|
+
Stick with `cache.remember()` for one-shot "get-or-compute" calls.
|
|
18
|
+
|
|
19
|
+
## The three shapes — `fn` always first
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { cached } from "@warlock.js/cache";
|
|
23
|
+
|
|
24
|
+
// 1. Prefix shorthand — driver default TTL; key auto-derived from args
|
|
25
|
+
cached(fn, "user");
|
|
26
|
+
|
|
27
|
+
// 2. Prefix + TTL
|
|
28
|
+
cached(fn, "user", "1h");
|
|
29
|
+
|
|
30
|
+
// 3. Options form — custom key fn, tags, per-call driver
|
|
31
|
+
cached(fn, {
|
|
32
|
+
key: (id: number) => `user.${id}`,
|
|
33
|
+
ttl: "1h",
|
|
34
|
+
tags: ["users"],
|
|
35
|
+
driver: "redis",
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Auto-key rules (shorthand only)
|
|
40
|
+
|
|
41
41
|
| Args | Key |
|
|
42
42
|
|------|-----|
|
|
43
43
|
| None | `prefix` |
|
|
@@ -48,56 +48,56 @@ The hash keeps distinct args on distinct keys (`("1.private", "")` ≠ `("1", "p
|
|
|
48
48
|
|
|
49
49
|
**Upgrade (5.20):** the key format changed from dot-joined args, so old entries are never read and every call misses once.
|
|
50
50
|
|
|
51
|
-
## Return shape
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
type CachedFn<Args, R> = ((...args: Args) => Promise<R>) & {
|
|
55
|
-
invalidate(...args: Args): Promise<void>;
|
|
56
|
-
};
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
`.refresh()` and `.peek()` are deferred to v2.1 — file demand in `backlog.md` if you need them.
|
|
60
|
-
|
|
61
|
-
## Recipes
|
|
62
|
-
|
|
63
|
-
### Cached DB lookup with write-side invalidation
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
const getUser = cached((id: number) => db.users.find(id), "user", "1h");
|
|
67
|
-
|
|
68
|
-
// On update
|
|
69
|
-
await db.users.update(42, patch);
|
|
70
|
-
await getUser.invalidate(42);
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Tag-based bulk invalidation across wrappers
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
const getUser = cached(fn, { key: (id) => `user.${id}`, ttl: "1h", tags: ["users"] });
|
|
77
|
-
const getPosts = cached(fn, { key: (u) => `posts.by.${u}`, ttl: "30m", tags: ["users", "posts"] });
|
|
78
|
-
|
|
79
|
-
await cache.tags(["users"]).invalidate(); // drops both wrappers' caches
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
See [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md).
|
|
83
|
-
|
|
84
|
-
### Project a subset of args into the key
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
const getCategoryMeta = cached(
|
|
88
|
-
(filters: Filters) => db.categories.meta(filters.category),
|
|
89
|
-
{ key: (f) => `category.meta.${f.category}`, ttl: "1h" }, // ignores `sort`, `page`
|
|
90
|
-
);
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Interaction with the rest of the API
|
|
94
|
-
|
|
95
|
-
- Uses `cache.remember()` internally → inherits stampede protection within a single Node process.
|
|
96
|
-
- Forwards `tags` and `driver` through the (extended) `RememberOptions` shape on `remember`.
|
|
97
|
-
- `.invalidate()` calls `cache.remove()` — no side effects beyond the single entry.
|
|
98
|
-
|
|
99
|
-
## Things NOT to do
|
|
100
|
-
|
|
101
|
-
- Don't wrap a function whose args include `Map`, `Set` or class instances with the shorthand form (it throws). Use the options form and project a stable subset into the key.
|
|
102
|
-
- Don't rely on cross-process stampede safety. `cached` inherits `remember`'s in-process lock; cross-process needs a distributed lock via `onConflict: "create"`.
|
|
103
|
-
- Don't include secrets in args — they'd land in cache keys. Project only the identifying fields into the key.
|
|
51
|
+
## Return shape
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
type CachedFn<Args, R> = ((...args: Args) => Promise<R>) & {
|
|
55
|
+
invalidate(...args: Args): Promise<void>;
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`.refresh()` and `.peek()` are deferred to v2.1 — file demand in `backlog.md` if you need them.
|
|
60
|
+
|
|
61
|
+
## Recipes
|
|
62
|
+
|
|
63
|
+
### Cached DB lookup with write-side invalidation
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const getUser = cached((id: number) => db.users.find(id), "user", "1h");
|
|
67
|
+
|
|
68
|
+
// On update
|
|
69
|
+
await db.users.update(42, patch);
|
|
70
|
+
await getUser.invalidate(42);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Tag-based bulk invalidation across wrappers
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const getUser = cached(fn, { key: (id) => `user.${id}`, ttl: "1h", tags: ["users"] });
|
|
77
|
+
const getPosts = cached(fn, { key: (u) => `posts.by.${u}`, ttl: "30m", tags: ["users", "posts"] });
|
|
78
|
+
|
|
79
|
+
await cache.tags(["users"]).invalidate(); // drops both wrappers' caches
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
See [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md).
|
|
83
|
+
|
|
84
|
+
### Project a subset of args into the key
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const getCategoryMeta = cached(
|
|
88
|
+
(filters: Filters) => db.categories.meta(filters.category),
|
|
89
|
+
{ key: (f) => `category.meta.${f.category}`, ttl: "1h" }, // ignores `sort`, `page`
|
|
90
|
+
);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Interaction with the rest of the API
|
|
94
|
+
|
|
95
|
+
- Uses `cache.remember()` internally → inherits stampede protection within a single Node process.
|
|
96
|
+
- Forwards `tags` and `driver` through the (extended) `RememberOptions` shape on `remember`.
|
|
97
|
+
- `.invalidate()` calls `cache.remove()` — no side effects beyond the single entry.
|
|
98
|
+
|
|
99
|
+
## Things NOT to do
|
|
100
|
+
|
|
101
|
+
- Don't wrap a function whose args include `Map`, `Set` or class instances with the shorthand form (it throws). Use the options form and project a stable subset into the key.
|
|
102
|
+
- Don't rely on cross-process stampede safety. `cached` inherits `remember`'s in-process lock; cross-process needs a distributed lock via `onConflict: "create"`.
|
|
103
|
+
- Don't include secrets in args — they'd land in cache keys. Project only the identifying fields into the key.
|
package/skills/use-swr/SKILL.md
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: use-swr
|
|
3
|
-
description: 'Stale-while-revalidate via cache.swr(key, {freshTtl, staleTtl}, fn) — returns cached instantly when fresh, returns cached + background refresh when stale, blocks only when fully expired. Triggers: `cache.swr`, `freshTtl`, `staleTtl`, `tags`, `driver`, `stale_at`; "serve stale while refreshing", "degrade when upstream is down", "never block on cache miss"; typical import `import { cache } from "@warlock.js/cache"`. Skip: block-until-fresh memoization — `@warlock.js/cache/apply-cache-patterns/SKILL.md`; observing refresh failures — `@warlock.js/cache/observe-cache/SKILL.md`; competing libs `swr` (React, client-side).'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Stale-while-revalidate — `cache.swr(key, options, fn)`
|
|
7
|
-
|
|
8
|
-
Returns the cached value immediately when it can; refreshes in the background when the value is getting old; only blocks when the entry is fully expired. The single biggest production-reliability win in the package — every cache miss past `freshTtl` becomes invisible to callers.
|
|
9
|
-
|
|
10
|
-
## When to reach for it
|
|
11
|
-
|
|
12
|
-
Use `cache.swr()` when **slightly-stale data is acceptable** and **the upstream is slow / occasionally fails**. That's most product-detail pages, dashboards, third-party API responses, expensive aggregations.
|
|
13
|
-
|
|
14
|
-
Use `cache.remember()` when freshness is non-negotiable — auth, balances, billing, anything where the user must see the latest. Remember blocks every miss; SWR doesn't. See [`@warlock.js/cache/apply-cache-patterns/SKILL.md`](@warlock.js/cache/apply-cache-patterns/SKILL.md).
|
|
15
|
-
|
|
16
|
-
## Three windows
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
write freshTtl staleTtl
|
|
20
|
-
│ │ │
|
|
21
|
-
▼ ▼ ▼
|
|
22
|
-
─────┬──── fresh ─────────┬──── stale ─────────┬──── expired ──→
|
|
23
|
-
│ return cached │ return cached + │ block, refetch
|
|
24
|
-
│ no upstream call │ bg refresh │ like a miss
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
| Window | Behavior |
|
|
28
|
-
|---|---|
|
|
29
|
-
| `now < freshTtl` | Return cached. No upstream call. |
|
|
30
|
-
| `freshTtl ≤ now < staleTtl` | Return cached immediately. Run `fn()` in background; next read sees the refreshed value. |
|
|
31
|
-
| `now ≥ staleTtl` | Block on `fn()`. Same as `remember()`. |
|
|
32
|
-
|
|
33
|
-
## API
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
await cache.swr(
|
|
37
|
-
"product.42",
|
|
38
|
-
{
|
|
39
|
-
freshTtl: "1m", // CacheTtl — within this, no upstream call
|
|
40
|
-
staleTtl: "1h", // CacheTtl — past this, block-and-refetch
|
|
41
|
-
tags?: string[], // applied on first miss + every successful refresh
|
|
42
|
-
driver?: string, // per-call driver override, like remember()
|
|
43
|
-
},
|
|
44
|
-
() => db.products.find(42),
|
|
45
|
-
);
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
`staleTtl` MUST be greater than `freshTtl` — otherwise throws.
|
|
49
|
-
|
|
50
|
-
## Key invariants
|
|
51
|
-
|
|
52
|
-
1. **Concurrent stale-window callers share one refresh.** Per-key dedupe via the driver's existing locks map — no thundering herd on background refresh.
|
|
53
|
-
2. **Failed background refreshes preserve the stale entry.** No retry storm; the next stale-window read tries again. Failures emit `error` events for observability.
|
|
54
|
-
3. **The caller never sees a refresh failure.** If you returned the stale value, you got your data — failures only show up via `cache.on("error", ...)`. See [`@warlock.js/cache/observe-cache/SKILL.md`](@warlock.js/cache/observe-cache/SKILL.md).
|
|
55
|
-
4. **Tags compose.** Per-call tags + scope tags (when via `cache.namespace().swr(...)`) merge additively.
|
|
56
|
-
5. **Scope `ttl` defaults are NOT applied to SWR.** `freshTtl` / `staleTtl` always come from the call site.
|
|
57
|
-
|
|
58
|
-
## Driver support
|
|
59
|
-
|
|
60
|
-
| Driver | Background refresh |
|
|
61
|
-
|---|---|
|
|
62
|
-
| memory / memoryExtended / lru / file / mock | ✅ Full |
|
|
63
|
-
| redis | ✅ Full (sidecar key for staleAt — backwards-compatible) |
|
|
64
|
-
| pg | ✅ Full (`stale_at TIMESTAMPTZ` column — provision via `driver.schema()`) |
|
|
65
|
-
| null | ❌ Always-fetch (null caches nothing) |
|
|
66
|
-
|
|
67
|
-
## Common shapes
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
// Product detail — slightly stale OK, never want to block on DB
|
|
71
|
-
await cache.swr(`product.${id}`, { freshTtl: "1m", staleTtl: "1h" }, () =>
|
|
72
|
-
db.products.findById(id),
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
// Dashboard — expensive aggregation, OK to be 5min stale
|
|
76
|
-
await cache.swr(`dashboard.${tenantId}`, { freshTtl: "5m", staleTtl: "1h" }, () =>
|
|
77
|
-
computeKPIs(tenantId),
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
// Third-party API — degrade gracefully when upstream is down
|
|
81
|
-
await cache.swr("exchange.rates", { freshTtl: "10m", staleTtl: "24h" }, () =>
|
|
82
|
-
fetchFromForexAPI(),
|
|
83
|
-
);
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## Through scoped caches
|
|
87
|
-
|
|
88
|
-
```ts
|
|
89
|
-
const feed = cache.namespace(`feed.${userId}`, { tags: [`user.${userId}`] });
|
|
90
|
-
|
|
91
|
-
await feed.swr(
|
|
92
|
-
"home",
|
|
93
|
-
{ freshTtl: "30s", staleTtl: "10m", tags: ["computed"] },
|
|
94
|
-
() => buildHomeFeed(userId),
|
|
95
|
-
);
|
|
96
|
-
// stored at feed.<userId>.home, tagged [user.<userId>, computed]
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Things NOT to do
|
|
100
|
-
|
|
101
|
-
- 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.
|
|
102
|
-
- 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.
|
|
103
|
-
- Don't ignore `error` events. A persistent stream of refresh failures means upstream is broken and the cache is masking it.
|
|
104
|
-
- Don't reach for SWR on the null driver — it caches nothing, so SWR always blocks.
|
|
1
|
+
---
|
|
2
|
+
name: use-swr
|
|
3
|
+
description: 'Stale-while-revalidate via cache.swr(key, {freshTtl, staleTtl}, fn) — returns cached instantly when fresh, returns cached + background refresh when stale, blocks only when fully expired. Triggers: `cache.swr`, `freshTtl`, `staleTtl`, `tags`, `driver`, `stale_at`; "serve stale while refreshing", "degrade when upstream is down", "never block on cache miss"; typical import `import { cache } from "@warlock.js/cache"`. Skip: block-until-fresh memoization — `@warlock.js/cache/apply-cache-patterns/SKILL.md`; observing refresh failures — `@warlock.js/cache/observe-cache/SKILL.md`; competing libs `swr` (React, client-side).'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Stale-while-revalidate — `cache.swr(key, options, fn)`
|
|
7
|
+
|
|
8
|
+
Returns the cached value immediately when it can; refreshes in the background when the value is getting old; only blocks when the entry is fully expired. The single biggest production-reliability win in the package — every cache miss past `freshTtl` becomes invisible to callers.
|
|
9
|
+
|
|
10
|
+
## When to reach for it
|
|
11
|
+
|
|
12
|
+
Use `cache.swr()` when **slightly-stale data is acceptable** and **the upstream is slow / occasionally fails**. That's most product-detail pages, dashboards, third-party API responses, expensive aggregations.
|
|
13
|
+
|
|
14
|
+
Use `cache.remember()` when freshness is non-negotiable — auth, balances, billing, anything where the user must see the latest. Remember blocks every miss; SWR doesn't. See [`@warlock.js/cache/apply-cache-patterns/SKILL.md`](@warlock.js/cache/apply-cache-patterns/SKILL.md).
|
|
15
|
+
|
|
16
|
+
## Three windows
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
write freshTtl staleTtl
|
|
20
|
+
│ │ │
|
|
21
|
+
▼ ▼ ▼
|
|
22
|
+
─────┬──── fresh ─────────┬──── stale ─────────┬──── expired ──→
|
|
23
|
+
│ return cached │ return cached + │ block, refetch
|
|
24
|
+
│ no upstream call │ bg refresh │ like a miss
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| Window | Behavior |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `now < freshTtl` | Return cached. No upstream call. |
|
|
30
|
+
| `freshTtl ≤ now < staleTtl` | Return cached immediately. Run `fn()` in background; next read sees the refreshed value. |
|
|
31
|
+
| `now ≥ staleTtl` | Block on `fn()`. Same as `remember()`. |
|
|
32
|
+
|
|
33
|
+
## API
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
await cache.swr(
|
|
37
|
+
"product.42",
|
|
38
|
+
{
|
|
39
|
+
freshTtl: "1m", // CacheTtl — within this, no upstream call
|
|
40
|
+
staleTtl: "1h", // CacheTtl — past this, block-and-refetch
|
|
41
|
+
tags?: string[], // applied on first miss + every successful refresh
|
|
42
|
+
driver?: string, // per-call driver override, like remember()
|
|
43
|
+
},
|
|
44
|
+
() => db.products.find(42),
|
|
45
|
+
);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`staleTtl` MUST be greater than `freshTtl` — otherwise throws.
|
|
49
|
+
|
|
50
|
+
## Key invariants
|
|
51
|
+
|
|
52
|
+
1. **Concurrent stale-window callers share one refresh.** Per-key dedupe via the driver's existing locks map — no thundering herd on background refresh.
|
|
53
|
+
2. **Failed background refreshes preserve the stale entry.** No retry storm; the next stale-window read tries again. Failures emit `error` events for observability.
|
|
54
|
+
3. **The caller never sees a refresh failure.** If you returned the stale value, you got your data — failures only show up via `cache.on("error", ...)`. See [`@warlock.js/cache/observe-cache/SKILL.md`](@warlock.js/cache/observe-cache/SKILL.md).
|
|
55
|
+
4. **Tags compose.** Per-call tags + scope tags (when via `cache.namespace().swr(...)`) merge additively.
|
|
56
|
+
5. **Scope `ttl` defaults are NOT applied to SWR.** `freshTtl` / `staleTtl` always come from the call site.
|
|
57
|
+
|
|
58
|
+
## Driver support
|
|
59
|
+
|
|
60
|
+
| Driver | Background refresh |
|
|
61
|
+
|---|---|
|
|
62
|
+
| memory / memoryExtended / lru / file / mock | ✅ Full |
|
|
63
|
+
| redis | ✅ Full (sidecar key for staleAt — backwards-compatible) |
|
|
64
|
+
| pg | ✅ Full (`stale_at TIMESTAMPTZ` column — provision via `driver.schema()`) |
|
|
65
|
+
| null | ❌ Always-fetch (null caches nothing) |
|
|
66
|
+
|
|
67
|
+
## Common shapes
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// Product detail — slightly stale OK, never want to block on DB
|
|
71
|
+
await cache.swr(`product.${id}`, { freshTtl: "1m", staleTtl: "1h" }, () =>
|
|
72
|
+
db.products.findById(id),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Dashboard — expensive aggregation, OK to be 5min stale
|
|
76
|
+
await cache.swr(`dashboard.${tenantId}`, { freshTtl: "5m", staleTtl: "1h" }, () =>
|
|
77
|
+
computeKPIs(tenantId),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Third-party API — degrade gracefully when upstream is down
|
|
81
|
+
await cache.swr("exchange.rates", { freshTtl: "10m", staleTtl: "24h" }, () =>
|
|
82
|
+
fetchFromForexAPI(),
|
|
83
|
+
);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Through scoped caches
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const feed = cache.namespace(`feed.${userId}`, { tags: [`user.${userId}`] });
|
|
90
|
+
|
|
91
|
+
await feed.swr(
|
|
92
|
+
"home",
|
|
93
|
+
{ freshTtl: "30s", staleTtl: "10m", tags: ["computed"] },
|
|
94
|
+
() => buildHomeFeed(userId),
|
|
95
|
+
);
|
|
96
|
+
// stored at feed.<userId>.home, tagged [user.<userId>, computed]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Things NOT to do
|
|
100
|
+
|
|
101
|
+
- 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.
|
|
102
|
+
- 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.
|
|
103
|
+
- Don't ignore `error` events. A persistent stream of refresh failures means upstream is broken and the cache is masking it.
|
|
104
|
+
- Don't reach for SWR on the null driver — it caches nothing, so SWR always blocks.
|