@warlock.js/cache 5.24.0 → 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.
Files changed (35) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +616 -616
  3. package/cjs/index.cjs +21 -1
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/drivers/base-cache-driver.d.mts +12 -0
  6. package/esm/drivers/base-cache-driver.d.mts.map +1 -1
  7. package/esm/drivers/base-cache-driver.mjs +10 -0
  8. package/esm/drivers/base-cache-driver.mjs.map +1 -1
  9. package/esm/drivers/null-cache-driver.mjs.map +1 -1
  10. package/esm/tagged-cache.d.mts.map +1 -1
  11. package/esm/tagged-cache.mjs +11 -1
  12. package/esm/tagged-cache.mjs.map +1 -1
  13. package/esm/types.d.mts +5 -2
  14. package/esm/types.d.mts.map +1 -1
  15. package/esm/types.mjs.map +1 -1
  16. package/esm/utils.mjs.map +1 -1
  17. package/llms-full.txt +1632 -1631
  18. package/llms.txt +1 -1
  19. package/package.json +3 -3
  20. package/skills/apply-cache-patterns/SKILL.md +95 -95
  21. package/skills/configure-set-options/SKILL.md +96 -96
  22. package/skills/handle-cache-errors/SKILL.md +92 -92
  23. package/skills/observe-cache/SKILL.md +104 -103
  24. package/skills/pick-cache-driver/SKILL.md +121 -121
  25. package/skills/test-cache-code/SKILL.md +219 -219
  26. package/skills/use-cache-atomic/SKILL.md +42 -42
  27. package/skills/use-cache-bulk/SKILL.md +57 -57
  28. package/skills/use-cache-list/SKILL.md +85 -85
  29. package/skills/use-cache-lock/SKILL.md +101 -101
  30. package/skills/use-cache-namespace/SKILL.md +88 -88
  31. package/skills/use-cache-similarity/SKILL.md +94 -94
  32. package/skills/use-cache-update-merge/SKILL.md +84 -84
  33. package/skills/use-cache-utils/SKILL.md +89 -89
  34. package/skills/use-cached-hof/SKILL.md +93 -93
  35. package/skills/use-swr/SKILL.md +104 -104
@@ -1,85 +1,85 @@
1
- ---
2
- name: use-cache-list
3
- description: 'Ordered collections via cache.list<T>(key) — push / unshift / pop / shift / slice / all / length / trim / clear. Triggers: `cache.list`, `push`, `unshift`, `pop`, `shift`, `slice`, `trim`, `clear`; "job queue in cache", "keep most recent N events", "audit log buffer", "FIFO queue"; typical import `import { cache } from "@warlock.js/cache"`. Skip: locking around list writes — `@warlock.js/cache/use-cache-lock/SKILL.md`; competing libs `bullmq`, `bee-queue`, `bull`, `ioredis` `LPUSH`; native `Array.push`.'
4
- ---
5
-
6
- # Lists — the `cache.list<T>(key)` sub-API
7
-
8
- Dedicated accessor for ordered collections — queues, recent-N buffers, sliding windows. Keeps the flat `CacheDriver` contract lean while giving list-shaped data a typed, purpose-built surface.
9
-
10
- ## Shape
11
-
12
- ```ts
13
- const recent = cache.list<Event>("recent-events");
14
-
15
- await recent.push(event); // append to tail — returns new length
16
- await recent.unshift(priorityEvent); // prepend to head — returns new length
17
- const tail = await recent.pop(); // remove + return tail
18
- const head = await recent.shift(); // remove + return head
19
- const first10 = await recent.slice(0, 10); // view — does not mutate
20
- const all = await recent.all();
21
- const count = await recent.length();
22
- await recent.trim(0, 99); // keep only indices 0..99 inclusive
23
- await recent.clear();
24
- ```
25
-
26
- ## Type safety
27
-
28
- The generic flows through every method. Pass the element type at the accessor call, not on each method:
29
-
30
- ```ts
31
- type Event = { type: string; at: number };
32
- const queue = cache.list<Event>("jobs:queue");
33
-
34
- await queue.push({ type: "import", at: Date.now() }); // ✓
35
- await queue.push("not an event" as never); // ✗ at the caller
36
- ```
37
-
38
- ## Current performance characteristics
39
-
40
- The default implementation (used by memory, memoryExtended, LRU, file, **and redis today**) stores the entire list as a single cache entry and does read-mutate-write on every op. Correct for every driver, O(n) per op.
41
-
42
- Redis-native `LPUSH` / `RPUSH` / `LRANGE` / `LTRIM` is planned for v2.1 (see `domains/cache/backlog.md`). Until then, treat Redis list ops as O(n) and avoid very large lists on Redis.
43
-
44
- ## Concurrency warning
45
-
46
- List writes on memory / file / LRU drivers **race** when two callers push simultaneously — the default read-mutate-write loop has no lock. If you need safe concurrent list writes today, wrap pushes in a distributed-lock pattern (see [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md)) or use a single writer.
47
-
48
- Single-process memory with a single writer (typical test / script usage) is fine.
49
-
50
- ## Empty-list cleanup
51
-
52
- When a list becomes empty (e.g. after successive `pop()` / `shift()` / `trim(0, -1)`), the backing cache entry is **removed** — `cache.get(key)` returns `null`, not `[]`. This keeps the store from accumulating empty list entries.
53
-
54
- ```ts
55
- await recent.push("a");
56
- await recent.pop();
57
- await cache.get("recent-events"); // null, not []
58
- ```
59
-
60
- ## Typical recipes
61
-
62
- ```ts
63
- // Recent-N audit log
64
- const audit = cache.list<AuditEntry>("audit:recent");
65
- await audit.unshift(entry); // newest at head
66
- await audit.trim(0, 999); // keep most-recent 1000
67
-
68
- // Lightweight job queue (single-node)
69
- const queue = cache.list<Job>("jobs:pending");
70
- await queue.push(job);
71
- const next = await queue.shift(); // FIFO
72
-
73
- // Stack
74
- const stack = cache.list<Frame>("stack");
75
- await stack.push(frame);
76
- const top = await stack.pop(); // LIFO
77
- ```
78
-
79
- ## What lists are NOT for
80
-
81
- - Unordered uniqueness — no native set today; use a plain object/Map in memory, or roll your own via `cache.get/set`.
82
- - Hash / field maps — same; use individual keys with a shared prefix.
83
- - Ordered top-N with scoring — no sorted-set analog today.
84
-
85
- These are tracked as candidates for v3.
1
+ ---
2
+ name: use-cache-list
3
+ description: 'Ordered collections via cache.list<T>(key) — push / unshift / pop / shift / slice / all / length / trim / clear. Triggers: `cache.list`, `push`, `unshift`, `pop`, `shift`, `slice`, `trim`, `clear`; "job queue in cache", "keep most recent N events", "audit log buffer", "FIFO queue"; typical import `import { cache } from "@warlock.js/cache"`. Skip: locking around list writes — `@warlock.js/cache/use-cache-lock/SKILL.md`; competing libs `bullmq`, `bee-queue`, `bull`, `ioredis` `LPUSH`; native `Array.push`.'
4
+ ---
5
+
6
+ # Lists — the `cache.list<T>(key)` sub-API
7
+
8
+ Dedicated accessor for ordered collections — queues, recent-N buffers, sliding windows. Keeps the flat `CacheDriver` contract lean while giving list-shaped data a typed, purpose-built surface.
9
+
10
+ ## Shape
11
+
12
+ ```ts
13
+ const recent = cache.list<Event>("recent-events");
14
+
15
+ await recent.push(event); // append to tail — returns new length
16
+ await recent.unshift(priorityEvent); // prepend to head — returns new length
17
+ const tail = await recent.pop(); // remove + return tail
18
+ const head = await recent.shift(); // remove + return head
19
+ const first10 = await recent.slice(0, 10); // view — does not mutate
20
+ const all = await recent.all();
21
+ const count = await recent.length();
22
+ await recent.trim(0, 99); // keep only indices 0..99 inclusive
23
+ await recent.clear();
24
+ ```
25
+
26
+ ## Type safety
27
+
28
+ The generic flows through every method. Pass the element type at the accessor call, not on each method:
29
+
30
+ ```ts
31
+ type Event = { type: string; at: number };
32
+ const queue = cache.list<Event>("jobs:queue");
33
+
34
+ await queue.push({ type: "import", at: Date.now() }); // ✓
35
+ await queue.push("not an event" as never); // ✗ at the caller
36
+ ```
37
+
38
+ ## Current performance characteristics
39
+
40
+ The default implementation (used by memory, memoryExtended, LRU, file, **and redis today**) stores the entire list as a single cache entry and does read-mutate-write on every op. Correct for every driver, O(n) per op.
41
+
42
+ Redis-native `LPUSH` / `RPUSH` / `LRANGE` / `LTRIM` is planned for v2.1 (see `domains/cache/backlog.md`). Until then, treat Redis list ops as O(n) and avoid very large lists on Redis.
43
+
44
+ ## Concurrency warning
45
+
46
+ List writes on memory / file / LRU drivers **race** when two callers push simultaneously — the default read-mutate-write loop has no lock. If you need safe concurrent list writes today, wrap pushes in a distributed-lock pattern (see [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md)) or use a single writer.
47
+
48
+ Single-process memory with a single writer (typical test / script usage) is fine.
49
+
50
+ ## Empty-list cleanup
51
+
52
+ When a list becomes empty (e.g. after successive `pop()` / `shift()` / `trim(0, -1)`), the backing cache entry is **removed** — `cache.get(key)` returns `null`, not `[]`. This keeps the store from accumulating empty list entries.
53
+
54
+ ```ts
55
+ await recent.push("a");
56
+ await recent.pop();
57
+ await cache.get("recent-events"); // null, not []
58
+ ```
59
+
60
+ ## Typical recipes
61
+
62
+ ```ts
63
+ // Recent-N audit log
64
+ const audit = cache.list<AuditEntry>("audit:recent");
65
+ await audit.unshift(entry); // newest at head
66
+ await audit.trim(0, 999); // keep most-recent 1000
67
+
68
+ // Lightweight job queue (single-node)
69
+ const queue = cache.list<Job>("jobs:pending");
70
+ await queue.push(job);
71
+ const next = await queue.shift(); // FIFO
72
+
73
+ // Stack
74
+ const stack = cache.list<Frame>("stack");
75
+ await stack.push(frame);
76
+ const top = await stack.pop(); // LIFO
77
+ ```
78
+
79
+ ## What lists are NOT for
80
+
81
+ - Unordered uniqueness — no native set today; use a plain object/Map in memory, or roll your own via `cache.get/set`.
82
+ - Hash / field maps — same; use individual keys with a shared prefix.
83
+ - Ordered top-N with scoring — no sorted-set analog today.
84
+
85
+ These are tracked as candidates for v3.
@@ -1,30 +1,30 @@
1
- ---
2
- name: use-cache-lock
3
- description: 'Distributed lock via cache.lock(key, ttl, fn) — acquire, run fn, auto-release. Returns {acquired: true, value} or {acquired: false}. Triggers: `cache.lock`, `LockOutcome`, `acquired`, `owner`; "run cron on only one server", "idempotent webhook handler", "dedup payment processing", "lock a task across nodes"; typical import `import { cache } from "@warlock.js/cache"`. Skip: raw `onConflict: "create"` recipe — `@warlock.js/cache/apply-cache-patterns/SKILL.md`; memoization — `@warlock.js/cache/use-cached-hof/SKILL.md`; competing libs `redlock`, `async-mutex`, `proper-lockfile`.'
4
- ---
5
-
6
- # `cache.lock()` — distributed locks with auto-release
7
-
8
- `cache.lock(key, ttl, fn)` acquires a distributed lock, runs `fn`, and auto-releases (even on throw). Built on `set({ onConflict: "create" })` — Redis-native where available, emulated elsewhere.
9
-
10
- ## When to use
11
-
12
- - A task should run on only **one server at a time** (cron jobs, imports, migrations).
13
- - Idempotent webhook or payment processing — dedup across retries.
14
- - Any time you'd otherwise write `try { … } finally { cache.remove(lockKey); }`.
15
-
16
- **Not for memoization** — use [`cached()`](@warlock.js/cache/use-cached-hof/SKILL.md) or `cache.remember()`.
17
-
18
- ## Shape
19
-
20
- ```ts
21
- // Primary — positional TTL
22
- await cache.lock(key, ttl, fn);
23
-
24
- // With options — owner for debugging, per-call driver override
25
- await cache.lock(key, { ttl, owner?, driver? }, fn);
26
- ```
27
-
1
+ ---
2
+ name: use-cache-lock
3
+ description: 'Distributed lock via cache.lock(key, ttl, fn) — acquire, run fn, auto-release. Returns {acquired: true, value} or {acquired: false}. Triggers: `cache.lock`, `LockOutcome`, `acquired`, `owner`; "run cron on only one server", "idempotent webhook handler", "dedup payment processing", "lock a task across nodes"; typical import `import { cache } from "@warlock.js/cache"`. Skip: raw `onConflict: "create"` recipe — `@warlock.js/cache/apply-cache-patterns/SKILL.md`; memoization — `@warlock.js/cache/use-cached-hof/SKILL.md`; competing libs `redlock`, `async-mutex`, `proper-lockfile`.'
4
+ ---
5
+
6
+ # `cache.lock()` — distributed locks with auto-release
7
+
8
+ `cache.lock(key, ttl, fn)` acquires a distributed lock, runs `fn`, and auto-releases (even on throw). Built on `set({ onConflict: "create" })` — Redis-native where available, emulated elsewhere.
9
+
10
+ ## When to use
11
+
12
+ - A task should run on only **one server at a time** (cron jobs, imports, migrations).
13
+ - Idempotent webhook or payment processing — dedup across retries.
14
+ - Any time you'd otherwise write `try { … } finally { cache.remove(lockKey); }`.
15
+
16
+ **Not for memoization** — use [`cached()`](@warlock.js/cache/use-cached-hof/SKILL.md) or `cache.remember()`.
17
+
18
+ ## Shape
19
+
20
+ ```ts
21
+ // Primary — positional TTL
22
+ await cache.lock(key, ttl, fn);
23
+
24
+ // With options — owner for debugging, per-call driver override
25
+ await cache.lock(key, { ttl, owner?, driver? }, fn);
26
+ ```
27
+
28
28
  **TTL is required, finite and positive.** `0`, `Infinity`, negative, `NaN` and unparseable TTLs throw `CacheConfigurationError` before anything is written. A lock with no expiry blocks every server once its holder crashes.
29
29
 
30
30
  TTL rules: a number or a bare numeric string (`"90"`) is **seconds**; `"500ms"` and `0.5` round up to 1 s. There is no renewal or fencing, so the TTL must exceed the worst-case duration of `fn`.
@@ -33,81 +33,81 @@ TTL rules: a number or a bare numeric string (`"90"`) is **seconds**; `"500ms"`
33
33
 
34
34
  - The stored value is `<owner>#<token>` (owner defaults to `pid.<pid>`; the token is random per acquisition). Match on the `<owner>#` prefix, never the bare owner.
35
35
  - Release is a compare-and-delete (atomic on memory, redis, pg): if your TTL expired and a successor acquired the key, your release leaves their lock alone.
36
- - A release error is logged and never replaces `fn`'s result or error.
37
-
38
- ## Return shape — discriminated union
39
-
40
- ```ts
41
- type LockOutcome<T> =
42
- | { acquired: true; value: T }
43
- | { acquired: false };
44
- ```
45
-
46
- Unambiguous even when `fn` returns `undefined`. Narrow with TS:
47
-
48
- ```ts
49
- const outcome = await cache.lock("lock.x", "1m", async () => compute());
50
-
51
- if (outcome.acquired) {
52
- console.log(outcome.value); // typed
53
- } else {
54
- console.log("someone else is running");
55
- }
56
- ```
57
-
58
- ## Recipes
59
-
60
- ### Cron on only one server
61
-
62
- ```ts
63
- cron.daily("3am", () =>
64
- cache.lock("lock.cleanup", "30m", () => db.cleanup()),
65
- );
66
- ```
67
-
68
- ### Idempotent webhook
69
-
70
- ```ts
71
- app.post("/webhooks/stripe", async (req, res) => {
72
- const outcome = await cache.lock(
73
- `webhook.stripe.${req.body.id}`,
74
- "24h",
75
- () => processStripeEvent(req.body),
76
- );
77
-
78
- if (!outcome.acquired) {
79
- return res.status(200).json({ status: "already-processed" });
80
- }
81
-
82
- res.status(200).json({ status: "processed" });
83
- });
84
- ```
85
-
86
- ### Batch job with debug-friendly owner
87
-
88
- ```ts
89
- await cache.lock(
90
- `lock.report.${date}`,
91
- { ttl: "1h", owner: `worker.${process.env.HOSTNAME}` },
92
- () => generateReport(date),
93
- );
94
- ```
95
-
96
- `await cache.get("lock.report.2026-04-24")` reveals which worker holds the lock.
97
-
98
- ## Driver behavior
99
-
100
- | Driver | Cross-process safe? |
101
- |--------|:-:|
36
+ - A release error is logged and never replaces `fn`'s result or error.
37
+
38
+ ## Return shape — discriminated union
39
+
40
+ ```ts
41
+ type LockOutcome<T> =
42
+ | { acquired: true; value: T }
43
+ | { acquired: false };
44
+ ```
45
+
46
+ Unambiguous even when `fn` returns `undefined`. Narrow with TS:
47
+
48
+ ```ts
49
+ const outcome = await cache.lock("lock.x", "1m", async () => compute());
50
+
51
+ if (outcome.acquired) {
52
+ console.log(outcome.value); // typed
53
+ } else {
54
+ console.log("someone else is running");
55
+ }
56
+ ```
57
+
58
+ ## Recipes
59
+
60
+ ### Cron on only one server
61
+
62
+ ```ts
63
+ cron.daily("3am", () =>
64
+ cache.lock("lock.cleanup", "30m", () => db.cleanup()),
65
+ );
66
+ ```
67
+
68
+ ### Idempotent webhook
69
+
70
+ ```ts
71
+ app.post("/webhooks/stripe", async (req, res) => {
72
+ const outcome = await cache.lock(
73
+ `webhook.stripe.${req.body.id}`,
74
+ "24h",
75
+ () => processStripeEvent(req.body),
76
+ );
77
+
78
+ if (!outcome.acquired) {
79
+ return res.status(200).json({ status: "already-processed" });
80
+ }
81
+
82
+ res.status(200).json({ status: "processed" });
83
+ });
84
+ ```
85
+
86
+ ### Batch job with debug-friendly owner
87
+
88
+ ```ts
89
+ await cache.lock(
90
+ `lock.report.${date}`,
91
+ { ttl: "1h", owner: `worker.${process.env.HOSTNAME}` },
92
+ () => generateReport(date),
93
+ );
94
+ ```
95
+
96
+ `await cache.get("lock.report.2026-04-24")` reveals which worker holds the lock.
97
+
98
+ ## Driver behavior
99
+
100
+ | Driver | Cross-process safe? |
101
+ |--------|:-:|
102
102
  | `redis` | ✅ Native `SET … NX EX`, Lua release |
103
103
  | `pg` | ✅ `INSERT … ON CONFLICT`, one-statement release |
104
104
  | `memory` / `memoryExtended` / `lru` | ❌ In-process only (exclusive within the process) |
105
105
  | `file` | ⚠️ Single-host only (exclusive create) |
106
- | `null` | n/a — always "acquires" |
107
-
108
- ## Gotchas
109
-
110
- - **Non-re-entrant in v1.** A recursive call for the same key gets `{ acquired: false }`.
111
- - **Don't release inside `fn`.** `lock()` handles release in `finally`. Manual `cache.remove(lockKey)` inside `fn` would let another process jump in mid-work.
112
- - **TTL shorter than `fn` runtime = race.** Pick a TTL with generous margin; there is no renewal.
113
- - **Cross-server requires `redis` or `pg`.** Memory / LRU / file drivers don't coordinate across servers.
106
+ | `null` | n/a — always "acquires" |
107
+
108
+ ## Gotchas
109
+
110
+ - **Non-re-entrant in v1.** A recursive call for the same key gets `{ acquired: false }`.
111
+ - **Don't release inside `fn`.** `lock()` handles release in `finally`. Manual `cache.remove(lockKey)` inside `fn` would let another process jump in mid-work.
112
+ - **TTL shorter than `fn` runtime = race.** Pick a TTL with generous margin; there is no renewal.
113
+ - **Cross-server requires `redis` or `pg`.** Memory / LRU / file drivers don't coordinate across servers.
@@ -1,88 +1,88 @@
1
- ---
2
- name: use-cache-namespace
3
- description: 'Scope cache keys via cache.namespace(prefix, options?) — every key auto-prefixed, scope-level ttl / tags defaults, nested scopes, .clear() sugar. Triggers: `cache.namespace`, `cache.removeNamespace`, `clear`, `globalPrefix`; "scope cache keys under a prefix", "share TTL across a whole prefix", "drop every key under user.1", "nested cache scopes"; typical import `import { cache } from "@warlock.js/cache"`. Skip: tag-based bulk drop — `@warlock.js/cache/use-cache-tags/SKILL.md`; multi-tenant driver-level prefix — `@warlock.js/cache/pick-cache-driver/SKILL.md`; SWR — `@warlock.js/cache/use-swr/SKILL.md`; competing libs `keyv` namespaces.'
4
- ---
5
-
6
- # Scoped caches — `cache.namespace(prefix, options?)`
7
-
8
- When you'll touch the same prefix more than a couple of times, grab a scoped handle instead of repeating the prefix at every call site.
9
-
10
- ## Shape
11
-
12
- ```ts
13
- const chat = cache.namespace(`chats.${id}`, { ttl: "30d", tags: [`user.${userId}`] });
14
-
15
- await chat.set("messages.10", msg); // chats.<id>.messages.10, 30d, user.<userId>
16
- await chat.set("draft", d, { ttl: "1h" }); // per-call ttl wins
17
- await chat.tags(["unread"]).set("ping", p); // tags merge: user.<userId> + unread
18
- await chat.clear(); // sugar for removeNamespace
19
- ```
20
-
21
- Scopes are pure views — same connection, same driver, no extra state. Per-call options always win over scope defaults; tags merge additively.
22
-
23
- ## Nested scopes
24
-
25
- ```ts
26
- const chat = cache.namespace(`chats.${id}`, { ttl: "30d" });
27
- const typing = chat.namespace("typing", { ttl: "5s" }); // overrides parent ttl
28
-
29
- await typing.set("user.42", true); // chats.<id>.typing.user.42, 5s
30
- ```
31
-
32
- Nested scopes inherit defaults and can override. Tags accumulate.
33
-
34
- ## When to reach for it
35
-
36
- - The prefix repeats more than 2–3 times.
37
- - A whole prefix shares a TTL or tag policy.
38
- - You want `.clear()` to read like the intent ("clear this chat") instead of `removeNamespace(...)` boilerplate.
39
-
40
- Inline prefixes are still fine for one-off writes.
41
-
42
- ## Plain `removeNamespace` when you already have the prefix
43
-
44
- When you *do* know the prefix string and don't need a scoped handle for repeated reads/writes:
45
-
46
- ```ts
47
- await cache.set("user:1:profile", profile);
48
- await cache.set("user:1:prefs", prefs);
49
- await cache.set("user:2:profile", otherProfile);
50
-
51
- await cache.removeNamespace("user.1"); // drops both user:1 entries, keeps user:2
52
- ```
53
-
54
- 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 [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) for the file-path-containment and redis-glob-escaping details.
55
-
56
- ## Multi-tenant scoping at the driver level
57
-
58
- Instead of every call passing a tenant prefix, attach `globalPrefix` to the driver config — see [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md). Function form runs per call.
59
-
60
- ```ts
61
- options: {
62
- redis: {
63
- url: "...",
64
- globalPrefix: () => `tenant-${currentContext.tenantId}`,
65
- },
66
- }
67
- ```
68
-
69
- ## SWR + namespace
70
-
71
- ```ts
72
- const feed = cache.namespace(`feed.${userId}`, { tags: [`user.${userId}`] });
73
-
74
- await feed.swr(
75
- "home",
76
- { freshTtl: "30s", staleTtl: "10m", tags: ["computed"] },
77
- () => buildHomeFeed(userId),
78
- );
79
- // stored at feed.<userId>.home, tagged [user.<userId>, computed]
80
- ```
81
-
82
- Note: scope `ttl` defaults are NOT applied to SWR — `freshTtl` / `staleTtl` always come from the call site. See [`@warlock.js/cache/use-swr/SKILL.md`](@warlock.js/cache/use-swr/SKILL.md).
83
-
84
- ## Things NOT to do
85
-
86
- - Don't create a `cache.namespace(prefix)` for a single read/write — the boilerplate doesn't pay off until the prefix repeats. Inline `cache.set("prefix.foo", ...)` is fine.
87
- - Don't expect `cache.namespace(prefix).clear()` to do anything on the `null` driver — `removeNamespace` no-ops there (it caches nothing).
88
- - Don't mix prefix separators. The convention is `.` (dot) — pick one and stick with it across scopes so nested prefixes compose predictably.
1
+ ---
2
+ name: use-cache-namespace
3
+ description: 'Scope cache keys via cache.namespace(prefix, options?) — every key auto-prefixed, scope-level ttl / tags defaults, nested scopes, .clear() sugar. Triggers: `cache.namespace`, `cache.removeNamespace`, `clear`, `globalPrefix`; "scope cache keys under a prefix", "share TTL across a whole prefix", "drop every key under user.1", "nested cache scopes"; typical import `import { cache } from "@warlock.js/cache"`. Skip: tag-based bulk drop — `@warlock.js/cache/use-cache-tags/SKILL.md`; multi-tenant driver-level prefix — `@warlock.js/cache/pick-cache-driver/SKILL.md`; SWR — `@warlock.js/cache/use-swr/SKILL.md`; competing libs `keyv` namespaces.'
4
+ ---
5
+
6
+ # Scoped caches — `cache.namespace(prefix, options?)`
7
+
8
+ When you'll touch the same prefix more than a couple of times, grab a scoped handle instead of repeating the prefix at every call site.
9
+
10
+ ## Shape
11
+
12
+ ```ts
13
+ const chat = cache.namespace(`chats.${id}`, { ttl: "30d", tags: [`user.${userId}`] });
14
+
15
+ await chat.set("messages.10", msg); // chats.<id>.messages.10, 30d, user.<userId>
16
+ await chat.set("draft", d, { ttl: "1h" }); // per-call ttl wins
17
+ await chat.tags(["unread"]).set("ping", p); // tags merge: user.<userId> + unread
18
+ await chat.clear(); // sugar for removeNamespace
19
+ ```
20
+
21
+ Scopes are pure views — same connection, same driver, no extra state. Per-call options always win over scope defaults; tags merge additively.
22
+
23
+ ## Nested scopes
24
+
25
+ ```ts
26
+ const chat = cache.namespace(`chats.${id}`, { ttl: "30d" });
27
+ const typing = chat.namespace("typing", { ttl: "5s" }); // overrides parent ttl
28
+
29
+ await typing.set("user.42", true); // chats.<id>.typing.user.42, 5s
30
+ ```
31
+
32
+ Nested scopes inherit defaults and can override. Tags accumulate.
33
+
34
+ ## When to reach for it
35
+
36
+ - The prefix repeats more than 2–3 times.
37
+ - A whole prefix shares a TTL or tag policy.
38
+ - You want `.clear()` to read like the intent ("clear this chat") instead of `removeNamespace(...)` boilerplate.
39
+
40
+ Inline prefixes are still fine for one-off writes.
41
+
42
+ ## Plain `removeNamespace` when you already have the prefix
43
+
44
+ When you *do* know the prefix string and don't need a scoped handle for repeated reads/writes:
45
+
46
+ ```ts
47
+ await cache.set("user:1:profile", profile);
48
+ await cache.set("user:1:prefs", prefs);
49
+ await cache.set("user:2:profile", otherProfile);
50
+
51
+ await cache.removeNamespace("user.1"); // drops both user:1 entries, keeps user:2
52
+ ```
53
+
54
+ 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 [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) for the file-path-containment and redis-glob-escaping details.
55
+
56
+ ## Multi-tenant scoping at the driver level
57
+
58
+ Instead of every call passing a tenant prefix, attach `globalPrefix` to the driver config — see [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md). Function form runs per call.
59
+
60
+ ```ts
61
+ options: {
62
+ redis: {
63
+ url: "...",
64
+ globalPrefix: () => `tenant-${currentContext.tenantId}`,
65
+ },
66
+ }
67
+ ```
68
+
69
+ ## SWR + namespace
70
+
71
+ ```ts
72
+ const feed = cache.namespace(`feed.${userId}`, { tags: [`user.${userId}`] });
73
+
74
+ await feed.swr(
75
+ "home",
76
+ { freshTtl: "30s", staleTtl: "10m", tags: ["computed"] },
77
+ () => buildHomeFeed(userId),
78
+ );
79
+ // stored at feed.<userId>.home, tagged [user.<userId>, computed]
80
+ ```
81
+
82
+ Note: scope `ttl` defaults are NOT applied to SWR — `freshTtl` / `staleTtl` always come from the call site. See [`@warlock.js/cache/use-swr/SKILL.md`](@warlock.js/cache/use-swr/SKILL.md).
83
+
84
+ ## Things NOT to do
85
+
86
+ - Don't create a `cache.namespace(prefix)` for a single read/write — the boilerplate doesn't pay off until the prefix repeats. Inline `cache.set("prefix.foo", ...)` is fine.
87
+ - Don't expect `cache.namespace(prefix).clear()` to do anything on the `null` driver — `removeNamespace` no-ops there (it caches nothing).
88
+ - Don't mix prefix separators. The convention is `.` (dot) — pick one and stick with it across scopes so nested prefixes compose predictably.