@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.
- package/CHANGELOG.md +55 -0
- package/cjs/index.cjs +1439 -331
- package/cjs/index.cjs.map +1 -1
- package/esm/cache-manager.d.mts +4 -0
- package/esm/cache-manager.d.mts.map +1 -1
- package/esm/cache-manager.mjs +23 -5
- package/esm/cache-manager.mjs.map +1 -1
- package/esm/cached/auto-key.d.mts +14 -14
- package/esm/cached/auto-key.d.mts.map +1 -1
- package/esm/cached/auto-key.mjs +50 -34
- package/esm/cached/auto-key.mjs.map +1 -1
- package/esm/cached/cached.mjs +4 -0
- package/esm/cached/cached.mjs.map +1 -1
- package/esm/drivers/base-cache-driver.d.mts +84 -8
- package/esm/drivers/base-cache-driver.d.mts.map +1 -1
- package/esm/drivers/base-cache-driver.mjs +152 -42
- package/esm/drivers/base-cache-driver.mjs.map +1 -1
- package/esm/drivers/file-cache-driver.d.mts +15 -0
- package/esm/drivers/file-cache-driver.d.mts.map +1 -1
- package/esm/drivers/file-cache-driver.mjs +68 -18
- package/esm/drivers/file-cache-driver.mjs.map +1 -1
- package/esm/drivers/in-memory-tag-index.d.mts +44 -0
- package/esm/drivers/in-memory-tag-index.d.mts.map +1 -0
- package/esm/drivers/in-memory-tag-index.mjs +73 -0
- package/esm/drivers/in-memory-tag-index.mjs.map +1 -0
- package/esm/drivers/index.d.mts +1 -1
- package/esm/drivers/index.mjs +1 -1
- package/esm/drivers/lru-memory-cache-driver.d.mts +43 -0
- package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/lru-memory-cache-driver.mjs +96 -0
- package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-cache-driver.d.mts +87 -30
- package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-cache-driver.mjs +214 -96
- package/esm/drivers/memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.d.mts +9 -5
- package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.mjs +10 -23
- package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -1
- package/esm/drivers/mock-cache-driver.d.mts +41 -0
- package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
- package/esm/drivers/mock-cache-driver.mjs +105 -8
- package/esm/drivers/mock-cache-driver.mjs.map +1 -1
- package/esm/drivers/null-cache-driver.d.mts +8 -2
- package/esm/drivers/null-cache-driver.d.mts.map +1 -1
- package/esm/drivers/null-cache-driver.mjs +10 -2
- package/esm/drivers/null-cache-driver.mjs.map +1 -1
- package/esm/drivers/pg-cache-driver.d.mts +77 -3
- package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
- package/esm/drivers/pg-cache-driver.mjs +229 -7
- package/esm/drivers/pg-cache-driver.mjs.map +1 -1
- package/esm/drivers/redis-cache-driver.d.mts +86 -1
- package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
- package/esm/drivers/redis-cache-driver.mjs +252 -16
- 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 +11 -8
- package/esm/list/memory-cache-list.d.mts.map +1 -1
- package/esm/list/memory-cache-list.mjs +39 -37
- package/esm/list/memory-cache-list.mjs.map +1 -1
- package/esm/scoped-cache.d.mts +9 -3
- package/esm/scoped-cache.d.mts.map +1 -1
- package/esm/scoped-cache.mjs +19 -7
- package/esm/scoped-cache.mjs.map +1 -1
- package/esm/tagged-cache.d.mts +20 -0
- package/esm/tagged-cache.d.mts.map +1 -1
- package/esm/tagged-cache.mjs +62 -24
- package/esm/tagged-cache.mjs.map +1 -1
- package/esm/tagged-scoped-cache.d.mts +7 -3
- package/esm/tagged-scoped-cache.d.mts.map +1 -1
- package/esm/tagged-scoped-cache.mjs +19 -7
- package/esm/tagged-scoped-cache.mjs.map +1 -1
- package/esm/types.d.mts +41 -3
- package/esm/types.d.mts.map +1 -1
- package/esm/types.mjs.map +1 -1
- package/esm/utils.d.mts +9 -3
- package/esm/utils.d.mts.map +1 -1
- package/esm/utils.mjs +16 -6
- package/esm/utils.mjs.map +1 -1
- package/llms-full.txt +1685 -1666
- package/llms.txt +18 -18
- package/package.json +4 -4
- package/skills/apply-cache-patterns/SKILL.md +98 -97
- package/skills/configure-pg-cache/SKILL.md +6 -5
- package/skills/configure-set-options/SKILL.md +102 -96
- package/skills/handle-cache-errors/SKILL.md +92 -92
- package/skills/index.md +20 -0
- package/skills/observe-cache/SKILL.md +103 -103
- package/skills/pick-cache-driver/SKILL.md +129 -123
- package/skills/test-cache-code/SKILL.md +219 -219
- package/skills/use-cache-atomic/SKILL.md +57 -65
- 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 +113 -104
- package/skills/use-cache-namespace/SKILL.md +88 -88
- package/skills/use-cache-similarity/SKILL.md +94 -94
- package/skills/use-cache-tags/SKILL.md +7 -4
- 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 +100 -99
- package/skills/use-swr/SKILL.md +104 -104
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: use-cache-bulk
|
|
3
|
-
description:
|
|
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:
|
|
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:
|
|
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.**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```ts
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
###
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.
|