@warlock.js/cache 5.19.0 → 5.20.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 (102) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/cjs/index.cjs +1439 -331
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/cache-manager.d.mts +4 -0
  5. package/esm/cache-manager.d.mts.map +1 -1
  6. package/esm/cache-manager.mjs +23 -5
  7. package/esm/cache-manager.mjs.map +1 -1
  8. package/esm/cached/auto-key.d.mts +14 -14
  9. package/esm/cached/auto-key.d.mts.map +1 -1
  10. package/esm/cached/auto-key.mjs +50 -34
  11. package/esm/cached/auto-key.mjs.map +1 -1
  12. package/esm/cached/cached.mjs +4 -0
  13. package/esm/cached/cached.mjs.map +1 -1
  14. package/esm/drivers/base-cache-driver.d.mts +84 -8
  15. package/esm/drivers/base-cache-driver.d.mts.map +1 -1
  16. package/esm/drivers/base-cache-driver.mjs +152 -42
  17. package/esm/drivers/base-cache-driver.mjs.map +1 -1
  18. package/esm/drivers/file-cache-driver.d.mts +15 -0
  19. package/esm/drivers/file-cache-driver.d.mts.map +1 -1
  20. package/esm/drivers/file-cache-driver.mjs +68 -18
  21. package/esm/drivers/file-cache-driver.mjs.map +1 -1
  22. package/esm/drivers/in-memory-tag-index.d.mts +44 -0
  23. package/esm/drivers/in-memory-tag-index.d.mts.map +1 -0
  24. package/esm/drivers/in-memory-tag-index.mjs +73 -0
  25. package/esm/drivers/in-memory-tag-index.mjs.map +1 -0
  26. package/esm/drivers/index.d.mts +1 -1
  27. package/esm/drivers/index.mjs +1 -1
  28. package/esm/drivers/lru-memory-cache-driver.d.mts +43 -0
  29. package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
  30. package/esm/drivers/lru-memory-cache-driver.mjs +96 -0
  31. package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
  32. package/esm/drivers/memory-cache-driver.d.mts +87 -30
  33. package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
  34. package/esm/drivers/memory-cache-driver.mjs +214 -96
  35. package/esm/drivers/memory-cache-driver.mjs.map +1 -1
  36. package/esm/drivers/memory-extended-cache-driver.d.mts +9 -5
  37. package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -1
  38. package/esm/drivers/memory-extended-cache-driver.mjs +10 -23
  39. package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -1
  40. package/esm/drivers/mock-cache-driver.d.mts +41 -0
  41. package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
  42. package/esm/drivers/mock-cache-driver.mjs +105 -8
  43. package/esm/drivers/mock-cache-driver.mjs.map +1 -1
  44. package/esm/drivers/null-cache-driver.d.mts +8 -2
  45. package/esm/drivers/null-cache-driver.d.mts.map +1 -1
  46. package/esm/drivers/null-cache-driver.mjs +10 -2
  47. package/esm/drivers/null-cache-driver.mjs.map +1 -1
  48. package/esm/drivers/pg-cache-driver.d.mts +77 -3
  49. package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
  50. package/esm/drivers/pg-cache-driver.mjs +229 -7
  51. package/esm/drivers/pg-cache-driver.mjs.map +1 -1
  52. package/esm/drivers/redis-cache-driver.d.mts +86 -1
  53. package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
  54. package/esm/drivers/redis-cache-driver.mjs +252 -16
  55. package/esm/drivers/redis-cache-driver.mjs.map +1 -1
  56. package/esm/index.d.mts +2 -2
  57. package/esm/index.mjs +2 -2
  58. package/esm/list/memory-cache-list.d.mts +11 -8
  59. package/esm/list/memory-cache-list.d.mts.map +1 -1
  60. package/esm/list/memory-cache-list.mjs +39 -37
  61. package/esm/list/memory-cache-list.mjs.map +1 -1
  62. package/esm/scoped-cache.d.mts +9 -3
  63. package/esm/scoped-cache.d.mts.map +1 -1
  64. package/esm/scoped-cache.mjs +19 -7
  65. package/esm/scoped-cache.mjs.map +1 -1
  66. package/esm/tagged-cache.d.mts +20 -0
  67. package/esm/tagged-cache.d.mts.map +1 -1
  68. package/esm/tagged-cache.mjs +62 -24
  69. package/esm/tagged-cache.mjs.map +1 -1
  70. package/esm/tagged-scoped-cache.d.mts +7 -3
  71. package/esm/tagged-scoped-cache.d.mts.map +1 -1
  72. package/esm/tagged-scoped-cache.mjs +19 -7
  73. package/esm/tagged-scoped-cache.mjs.map +1 -1
  74. package/esm/types.d.mts +41 -3
  75. package/esm/types.d.mts.map +1 -1
  76. package/esm/types.mjs.map +1 -1
  77. package/esm/utils.d.mts +9 -3
  78. package/esm/utils.d.mts.map +1 -1
  79. package/esm/utils.mjs +16 -6
  80. package/esm/utils.mjs.map +1 -1
  81. package/llms-full.txt +1685 -1666
  82. package/llms.txt +18 -18
  83. package/package.json +4 -4
  84. package/skills/apply-cache-patterns/SKILL.md +98 -97
  85. package/skills/configure-pg-cache/SKILL.md +6 -5
  86. package/skills/configure-set-options/SKILL.md +102 -96
  87. package/skills/handle-cache-errors/SKILL.md +92 -92
  88. package/skills/index.md +20 -0
  89. package/skills/observe-cache/SKILL.md +103 -103
  90. package/skills/pick-cache-driver/SKILL.md +129 -123
  91. package/skills/test-cache-code/SKILL.md +219 -219
  92. package/skills/use-cache-atomic/SKILL.md +57 -65
  93. package/skills/use-cache-bulk/SKILL.md +57 -57
  94. package/skills/use-cache-list/SKILL.md +85 -85
  95. package/skills/use-cache-lock/SKILL.md +113 -104
  96. package/skills/use-cache-namespace/SKILL.md +88 -88
  97. package/skills/use-cache-similarity/SKILL.md +94 -94
  98. package/skills/use-cache-tags/SKILL.md +7 -4
  99. package/skills/use-cache-update-merge/SKILL.md +84 -84
  100. package/skills/use-cache-utils/SKILL.md +89 -89
  101. package/skills/use-cached-hof/SKILL.md +100 -99
  102. package/skills/use-swr/SKILL.md +104 -104
@@ -1,57 +1,57 @@
1
- ---
2
- name: use-cache-bulk
3
- description: "Bulk operations — cache.many / cache.setMany in @warlock.js/cache; use when you need to use cache bulk."
4
- ---
5
-
6
- # Bulk operations — `cache.many` / `cache.setMany`
7
-
8
- Read or write a batch of keys in one call instead of awaiting them one at a time.
9
-
10
- ## Read many — `many(keys)`
11
-
12
- ```ts
13
- import { cache } from "@warlock.js/cache";
14
-
15
- const [alice, bob, carol] = await cache.many(["user.1", "user.2", "user.3"]);
16
- ```
17
-
18
- - Returns an array **positionally aligned** with `keys`.
19
- - Missing keys come back as `null` (same as `get`), so the result length always
20
- equals the input length — zip them back together by index.
21
-
22
- ```ts
23
- const ids = [1, 2, 3];
24
- const users = await cache.many(ids.map((id) => `user.${id}`));
25
-
26
- const missingIds = ids.filter((_, index) => users[index] === null);
27
- // fetch only the misses from the origin…
28
- ```
29
-
30
- ## Write many — `setMany(items, ttl?)`
31
-
32
- ```ts
33
- await cache.setMany({
34
- "user.1": alice,
35
- "user.2": bob,
36
- "user.3": carol,
37
- }, 3600); // optional TTL (seconds) applied to every entry
38
- ```
39
-
40
- - Keys are the object keys; values are the object values.
41
- - The optional second arg is a single TTL (seconds) applied to **all** entries —
42
- there's no per-entry TTL or `tags` knob here. When you need tags or mixed TTLs,
43
- loop with [`cache.set`](@warlock.js/cache/configure-set-options/SKILL.md) and
44
- the rich options object instead.
45
-
46
- ## Performance note
47
-
48
- On every driver these run their underlying `get`/`set` calls **concurrently**
49
- (`Promise.all`) on the shared connection; on the memory family it's effectively
50
- instant. There's no partial-failure handling — if one write rejects, the
51
- returned promise rejects.
52
-
53
- ## See also
54
-
55
- - [`@warlock.js/cache/cache-basics/SKILL.md`](@warlock.js/cache/cache-basics/SKILL.md) — single-key `get` / `set` / `remember`
56
- - [`@warlock.js/cache/configure-set-options/SKILL.md`](@warlock.js/cache/configure-set-options/SKILL.md) — per-entry TTL, tags, conflict policy
57
- - [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md) — invalidate a batch by tag
1
+ ---
2
+ name: use-cache-bulk
3
+ description: 'Bulk reads/writes via cache.many(keys[]) → values[] (nulls for misses, order preserved) and cache.setMany(record, ttl?) → void. Triggers: `cache.many`, `cache.setMany`, "get multiple keys at once", "batch read cache", "warm the cache", "preload many keys", "mget", "mset"; typical import `import { cache } from "@warlock.js/cache"`. Skip: tag-based bulk invalidation — `@warlock.js/cache/use-cache-tags/SKILL.md`; single-key ops — `@warlock.js/cache/cache-basics/SKILL.md`; competing libs `ioredis` `MGET`/`MSET`.'
4
+ ---
5
+
6
+ # Bulk operations — `cache.many` / `cache.setMany`
7
+
8
+ Read or write a batch of keys in one call instead of awaiting them one at a time.
9
+
10
+ ## Read many — `many(keys)`
11
+
12
+ ```ts
13
+ import { cache } from "@warlock.js/cache";
14
+
15
+ const [alice, bob, carol] = await cache.many(["user.1", "user.2", "user.3"]);
16
+ ```
17
+
18
+ - Returns an array **positionally aligned** with `keys`.
19
+ - Missing keys come back as `null` (same as `get`), so the result length always
20
+ equals the input length — zip them back together by index.
21
+
22
+ ```ts
23
+ const ids = [1, 2, 3];
24
+ const users = await cache.many(ids.map((id) => `user.${id}`));
25
+
26
+ const missingIds = ids.filter((_, index) => users[index] === null);
27
+ // fetch only the misses from the origin…
28
+ ```
29
+
30
+ ## Write many — `setMany(items, ttl?)`
31
+
32
+ ```ts
33
+ await cache.setMany({
34
+ "user.1": alice,
35
+ "user.2": bob,
36
+ "user.3": carol,
37
+ }, 3600); // optional TTL (seconds) applied to every entry
38
+ ```
39
+
40
+ - Keys are the object keys; values are the object values.
41
+ - The optional second arg is a single TTL (seconds) applied to **all** entries —
42
+ there's no per-entry TTL or `tags` knob here. When you need tags or mixed TTLs,
43
+ loop with [`cache.set`](@warlock.js/cache/configure-set-options/SKILL.md) and
44
+ the rich options object instead.
45
+
46
+ ## Performance note
47
+
48
+ On every driver these run their underlying `get`/`set` calls **concurrently**
49
+ (`Promise.all`) on the shared connection; on the memory family it's effectively
50
+ instant. There's no partial-failure handling — if one write rejects, the
51
+ returned promise rejects.
52
+
53
+ ## See also
54
+
55
+ - [`@warlock.js/cache/cache-basics/SKILL.md`](@warlock.js/cache/cache-basics/SKILL.md) — single-key `get` / `set` / `remember`
56
+ - [`@warlock.js/cache/configure-set-options/SKILL.md`](@warlock.js/cache/configure-set-options/SKILL.md) — per-entry TTL, tags, conflict policy
57
+ - [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md) — invalidate a batch by tag
@@ -1,85 +1,85 @@
1
- ---
2
- name: use-cache-list
3
- description: "Lists — the cache.list<T>(key) sub-API in @warlock.js/cache; use when you need to use cache list."
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,104 +1,113 @@
1
- ---
2
- name: use-cache-lock
3
- description: "cache.lock() — distributed locks with auto-release in @warlock.js/cache; use when you need to use cache lock."
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
- **TTL is required.** Forgotten locks stay forever if the process crashes; the TTL is your safety net.
29
-
30
- ## Return shape — discriminated union
31
-
32
- ```ts
33
- type LockOutcome<T> =
34
- | { acquired: true; value: T }
35
- | { acquired: false };
36
- ```
37
-
38
- Unambiguous even when `fn` returns `undefined`. Narrow with TS:
39
-
40
- ```ts
41
- const outcome = await cache.lock("lock.x", "1m", async () => compute());
42
-
43
- if (outcome.acquired) {
44
- console.log(outcome.value); // typed
45
- } else {
46
- console.log("someone else is running");
47
- }
48
- ```
49
-
50
- ## Recipes
51
-
52
- ### Cron on only one server
53
-
54
- ```ts
55
- cron.daily("3am", () =>
56
- cache.lock("lock.cleanup", "30m", () => db.cleanup()),
57
- );
58
- ```
59
-
60
- ### Idempotent webhook
61
-
62
- ```ts
63
- app.post("/webhooks/stripe", async (req, res) => {
64
- const outcome = await cache.lock(
65
- `webhook.stripe.${req.body.id}`,
66
- "24h",
67
- () => processStripeEvent(req.body),
68
- );
69
-
70
- if (!outcome.acquired) {
71
- return res.status(200).json({ status: "already-processed" });
72
- }
73
-
74
- res.status(200).json({ status: "processed" });
75
- });
76
- ```
77
-
78
- ### Batch job with debug-friendly owner
79
-
80
- ```ts
81
- await cache.lock(
82
- `lock.report.${date}`,
83
- { ttl: "1h", owner: `worker.${process.env.HOSTNAME}` },
84
- () => generateReport(date),
85
- );
86
- ```
87
-
88
- `await cache.get("lock.report.2026-04-24")` reveals which worker holds the lock.
89
-
90
- ## Driver behavior
91
-
92
- | Driver | Cross-process safe? |
93
- |--------|:-:|
94
- | `redis` | ✅ Native `SET … NX EX` |
95
- | `memory` / `memoryExtended` / `lru` | ❌ In-process only |
96
- | `file` | ⚠️ Single-host only (races across hosts) |
97
- | `null` | n/a — always "acquires" |
98
-
99
- ## Gotchas
100
-
101
- - **Non-re-entrant in v1.** A recursive call for the same key gets `{ acquired: false }`.
102
- - **Don't release inside `fn`.** `lock()` handles release in `finally`. Manual `cache.remove(lockKey)` inside `fn` would let another process jump in mid-work.
103
- - **TTL shorter than `fn` runtime = race.** Pick a TTL with generous margin.
104
- - **Cross-server requires Redis.** Memory / LRU drivers don't coordinate across processes.
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
+ **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
+
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`.
31
+
32
+ ## Lock value and release
33
+
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
+ - 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
+ |--------|:-:|
102
+ | `redis` | ✅ Native `SET … NX EX`, Lua release |
103
+ | `pg` | ✅ `INSERT … ON CONFLICT`, one-statement release |
104
+ | `memory` / `memoryExtended` / `lru` | ❌ In-process only (exclusive within the process) |
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.