@warlock.js/cache 5.30.0 → 5.32.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 (101) hide show
  1. package/CHANGELOG.md +26 -8
  2. package/README.md +12 -21
  3. package/cjs/index.cjs +50 -31
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/cache-manager.d.mts +5 -5
  6. package/esm/cache-manager.d.mts.map +1 -1
  7. package/esm/cache-manager.mjs +2 -2
  8. package/esm/cache-manager.mjs.map +1 -1
  9. package/esm/cached/auto-key.d.mts +1 -2
  10. package/esm/cached/auto-key.d.mts.map +1 -1
  11. package/esm/cached/cached.d.mts +4 -6
  12. package/esm/cached/cached.d.mts.map +1 -1
  13. package/esm/cached/index.d.mts +2 -1
  14. package/esm/cached/index.mjs +1 -1
  15. package/esm/cached/normalize-args.d.mts +3 -5
  16. package/esm/cached/normalize-args.d.mts.map +1 -1
  17. package/esm/drivers/base-cache-driver.d.mts +5 -5
  18. package/esm/drivers/base-cache-driver.d.mts.map +1 -1
  19. package/esm/drivers/base-cache-driver.mjs +4 -3
  20. package/esm/drivers/base-cache-driver.mjs.map +1 -1
  21. package/esm/drivers/file-cache-driver.d.mts +1 -3
  22. package/esm/drivers/file-cache-driver.d.mts.map +1 -1
  23. package/esm/drivers/file-cache-driver.mjs.map +1 -1
  24. package/esm/drivers/in-memory-tag-index.d.mts +1 -2
  25. package/esm/drivers/in-memory-tag-index.d.mts.map +1 -1
  26. package/esm/drivers/index.d.mts +2 -1
  27. package/esm/drivers/index.mjs +1 -1
  28. package/esm/drivers/lru-memory-cache-driver.d.mts +5 -4
  29. package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
  30. package/esm/drivers/lru-memory-cache-driver.mjs +6 -4
  31. package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
  32. package/esm/drivers/memory-cache-driver.d.mts +4 -4
  33. package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
  34. package/esm/drivers/memory-cache-driver.mjs +9 -3
  35. package/esm/drivers/memory-cache-driver.mjs.map +1 -1
  36. package/esm/drivers/memory-extended-cache-driver.d.mts +1 -3
  37. package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -1
  38. package/esm/drivers/mock-cache-driver.d.mts +4 -4
  39. package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
  40. package/esm/drivers/mock-cache-driver.mjs +11 -4
  41. package/esm/drivers/mock-cache-driver.mjs.map +1 -1
  42. package/esm/drivers/null-cache-driver.d.mts +1 -3
  43. package/esm/drivers/null-cache-driver.d.mts.map +1 -1
  44. package/esm/drivers/pg-cache-driver.d.mts +4 -4
  45. package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
  46. package/esm/drivers/pg-cache-driver.mjs +6 -3
  47. package/esm/drivers/pg-cache-driver.mjs.map +1 -1
  48. package/esm/drivers/redis-cache-driver.d.mts +9 -9
  49. package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
  50. package/esm/drivers/redis-cache-driver.mjs +4 -3
  51. package/esm/drivers/redis-cache-driver.mjs.map +1 -1
  52. package/esm/index.d.mts +3 -0
  53. package/esm/list/index.d.mts +2 -1
  54. package/esm/list/memory-cache-list.d.mts +1 -3
  55. package/esm/list/memory-cache-list.d.mts.map +1 -1
  56. package/esm/list/memory-cache-list.mjs.map +1 -1
  57. package/esm/metrics.d.mts +4 -4
  58. package/esm/metrics.d.mts.map +1 -1
  59. package/esm/metrics.mjs +2 -2
  60. package/esm/metrics.mjs.map +1 -1
  61. package/esm/scoped-cache.d.mts +4 -4
  62. package/esm/scoped-cache.d.mts.map +1 -1
  63. package/esm/scoped-cache.mjs +2 -2
  64. package/esm/scoped-cache.mjs.map +1 -1
  65. package/esm/tagged-cache.d.mts +4 -4
  66. package/esm/tagged-cache.d.mts.map +1 -1
  67. package/esm/tagged-cache.mjs +3 -3
  68. package/esm/tagged-cache.mjs.map +1 -1
  69. package/esm/tagged-scoped-cache.d.mts +4 -4
  70. package/esm/tagged-scoped-cache.d.mts.map +1 -1
  71. package/esm/tagged-scoped-cache.mjs +2 -2
  72. package/esm/tagged-scoped-cache.mjs.map +1 -1
  73. package/esm/types.d.mts +86 -62
  74. package/esm/types.d.mts.map +1 -1
  75. package/esm/types.mjs.map +1 -1
  76. package/esm/utils.d.mts +12 -14
  77. package/esm/utils.d.mts.map +1 -1
  78. package/esm/utils.mjs.map +1 -1
  79. package/llms-full.txt +229 -224
  80. package/llms.txt +1 -1
  81. package/package.json +3 -3
  82. package/skills/apply-cache-patterns/SKILL.md +3 -2
  83. package/skills/cache-basics/SKILL.md +24 -24
  84. package/skills/configure-pg-cache/SKILL.md +2 -2
  85. package/skills/configure-set-options/SKILL.md +14 -14
  86. package/skills/handle-cache-errors/SKILL.md +8 -8
  87. package/skills/index.md +4 -1
  88. package/skills/observe-cache/SKILL.md +8 -8
  89. package/skills/pick-cache-driver/SKILL.md +24 -23
  90. package/skills/test-cache-code/SKILL.md +15 -10
  91. package/skills/use-cache-atomic/SKILL.md +17 -11
  92. package/skills/use-cache-bulk/SKILL.md +8 -5
  93. package/skills/use-cache-list/SKILL.md +13 -13
  94. package/skills/use-cache-lock/SKILL.md +11 -17
  95. package/skills/use-cache-namespace/SKILL.md +11 -13
  96. package/skills/use-cache-similarity/SKILL.md +9 -9
  97. package/skills/use-cache-tags/SKILL.md +13 -13
  98. package/skills/use-cache-update-merge/SKILL.md +10 -10
  99. package/skills/use-cache-utils/SKILL.md +17 -17
  100. package/skills/use-cached-hof/SKILL.md +8 -7
  101. package/skills/use-swr/SKILL.md +14 -18
@@ -38,9 +38,7 @@ TTL rules: a number or a bare numeric string (`"90"`) is **seconds**; `"500ms"`
38
38
  ## Return shape — discriminated union
39
39
 
40
40
  ```ts
41
- type LockOutcome<T> =
42
- | { acquired: true; value: T }
43
- | { acquired: false };
41
+ type LockOutcome<T> = { acquired: true; value: T } | { acquired: false };
44
42
  ```
45
43
 
46
44
  Unambiguous even when `fn` returns `undefined`. Narrow with TS:
@@ -49,7 +47,7 @@ Unambiguous even when `fn` returns `undefined`. Narrow with TS:
49
47
  const outcome = await cache.lock("lock.x", "1m", async () => compute());
50
48
 
51
49
  if (outcome.acquired) {
52
- console.log(outcome.value); // typed
50
+ console.log(outcome.value); // typed
53
51
  } else {
54
52
  console.log("someone else is running");
55
53
  }
@@ -60,19 +58,15 @@ if (outcome.acquired) {
60
58
  ### Cron on only one server
61
59
 
62
60
  ```ts
63
- cron.daily("3am", () =>
64
- cache.lock("lock.cleanup", "30m", () => db.cleanup()),
65
- );
61
+ cron.daily("3am", () => cache.lock("lock.cleanup", "30m", () => db.cleanup()));
66
62
  ```
67
63
 
68
64
  ### Idempotent webhook
69
65
 
70
66
  ```ts
71
67
  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),
68
+ const outcome = await cache.lock(`webhook.stripe.${req.body.id}`, "24h", () =>
69
+ processStripeEvent(req.body),
76
70
  );
77
71
 
78
72
  if (!outcome.acquired) {
@@ -97,13 +91,13 @@ await cache.lock(
97
91
 
98
92
  ## Driver behavior
99
93
 
100
- | Driver | Cross-process safe? |
101
- |--------|:-:|
102
- | `redis` | ✅ Native `SET … NX EX`, Lua release |
103
- | `pg` | ✅ `INSERT … ON CONFLICT`, one-statement release |
94
+ | Driver | Cross-process safe? |
95
+ | ----------------------------------- | :-----------------------------------------------: |
96
+ | `redis` | ✅ Native `SET … NX EX`, Lua release |
97
+ | `pg` | ✅ `INSERT … ON CONFLICT`, one-statement release |
104
98
  | `memory` / `memoryExtended` / `lru` | ❌ In-process only (exclusive within the process) |
105
- | `file` | ⚠️ Single-host only (exclusive create) |
106
- | `null` | n/a — always "acquires" |
99
+ | `file` | ⚠️ Single-host only (exclusive create) |
100
+ | `null` | n/a — always "acquires" |
107
101
 
108
102
  ## Gotchas
109
103
 
@@ -12,10 +12,10 @@ When you'll touch the same prefix more than a couple of times, grab a scoped han
12
12
  ```ts
13
13
  const chat = cache.namespace(`chats.${id}`, { ttl: "30d", tags: [`user.${userId}`] });
14
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
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
19
  ```
20
20
 
21
21
  Scopes are pure views — same connection, same driver, no extra state. Per-call options always win over scope defaults; tags merge additively.
@@ -24,9 +24,9 @@ Scopes are pure views — same connection, same driver, no extra state. Per-call
24
24
 
25
25
  ```ts
26
26
  const chat = cache.namespace(`chats.${id}`, { ttl: "30d" });
27
- const typing = chat.namespace("typing", { ttl: "5s" }); // overrides parent ttl
27
+ const typing = chat.namespace("typing", { ttl: "5s" }); // overrides parent ttl
28
28
 
29
- await typing.set("user.42", true); // chats.<id>.typing.user.42, 5s
29
+ await typing.set("user.42", true); // chats.<id>.typing.user.42, 5s
30
30
  ```
31
31
 
32
32
  Nested scopes inherit defaults and can override. Tags accumulate.
@@ -41,14 +41,14 @@ Inline prefixes are still fine for one-off writes.
41
41
 
42
42
  ## Plain `removeNamespace` when you already have the prefix
43
43
 
44
- When you *do* know the prefix string and don't need a scoped handle for repeated reads/writes:
44
+ When you _do_ know the prefix string and don't need a scoped handle for repeated reads/writes:
45
45
 
46
46
  ```ts
47
47
  await cache.set("user:1:profile", profile);
48
- await cache.set("user:1:prefs", prefs);
48
+ await cache.set("user:1:prefs", prefs);
49
49
  await cache.set("user:2:profile", otherProfile);
50
50
 
51
- await cache.removeNamespace("user.1"); // drops both user:1 entries, keeps user:2
51
+ await cache.removeNamespace("user.1"); // drops both user:1 entries, keeps user:2
52
52
  ```
53
53
 
54
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 the `pick-cache-driver` topic for the file-path-containment and redis-glob-escaping details.
@@ -71,10 +71,8 @@ options: {
71
71
  ```ts
72
72
  const feed = cache.namespace(`feed.${userId}`, { tags: [`user.${userId}`] });
73
73
 
74
- await feed.swr(
75
- "home",
76
- { freshTtl: "30s", staleTtl: "10m", tags: ["computed"] },
77
- () => buildHomeFeed(userId),
74
+ await feed.swr("home", { freshTtl: "30s", staleTtl: "10m", tags: ["computed"] }, () =>
75
+ buildHomeFeed(userId),
78
76
  );
79
77
  // stored at feed.<userId>.home, tagged [user.<userId>, computed]
80
78
  ```
@@ -24,21 +24,21 @@ const hits = await cache.similar<T>(queryVec, {
24
24
 
25
25
  ## Capability matrix
26
26
 
27
- | Driver | `similar()` |
28
- |---|---|
27
+ | Driver | `similar()` |
28
+ | ----------------------------------- | -------------------------------------------------- |
29
29
  | `memory` / `memoryExtended` / `lru` | ✅ Brute force (O(N)) — dev only past ~10k entries |
30
- | `pg` *with* `options.pg.vector` | ✅ pgvector + HNSW/IVFFlat index |
31
- | `pg` *without* `options.pg.vector` | ❌ Throws `CacheUnsupportedError` |
32
- | `redis` | ❌ Throws (RediSearch on backlog) |
33
- | `file` | ❌ Throws |
34
- | `null` | Returns `[]` |
30
+ | `pg` _with_ `options.pg.vector` | ✅ pgvector + HNSW/IVFFlat index |
31
+ | `pg` _without_ `options.pg.vector` | ❌ Throws `CacheUnsupportedError` |
32
+ | `redis` | ❌ Throws (RediSearch on backlog) |
33
+ | `file` | ❌ Throws |
34
+ | `null` | Returns `[]` |
35
35
 
36
36
  ## Always-true facts
37
37
 
38
38
  1. **Cache is embedding-agnostic.** Caller computes vectors. The cache stores and ranks; it doesn't call out to an embedder.
39
39
  2. **Only entries written with `set({ vector })` show up.** A plain `set` adds the entry as KV — invisible to `similar()`.
40
40
  3. **Score = cosine similarity** in `[0, 1]` for typical embedding spaces. The `pg` driver computes `1 - (embedding <=> $1::vector)` so the score matches the memory drivers.
41
- 4. **Tag filter narrows the candidate pool *before* ranking** — union semantics (entry must carry at least one of the listed tags).
41
+ 4. **Tag filter narrows the candidate pool _before_ ranking** — union semantics (entry must carry at least one of the listed tags).
42
42
  5. **Dimension mismatch throws `CacheConfigurationError`** at both `set({ vector })` and `similar()` time. Don't switch embedders without re-indexing.
43
43
  6. **TTL + LRU eviction also drop the vector** — expired or evicted entries are invisible to `similar()`.
44
44
 
@@ -51,7 +51,7 @@ const queryVec = await embed(prompt);
51
51
  const hits = await cache.similar<Answer>(queryVec, { topK: 1, threshold: 0.92 });
52
52
 
53
53
  if (hits.length > 0) {
54
- return hits[0].value; // skip the LLM call
54
+ return hits[0].value; // skip the LLM call
55
55
  }
56
56
 
57
57
  const answer = await llm.complete(prompt);
@@ -12,7 +12,7 @@ Tags let you invalidate by a label rather than by enumerating every key. Use the
12
12
  ```ts
13
13
  // Inline — terser when you know the tags up front
14
14
  await cache.set("user:1:profile", profile, { tags: ["users", "tenant-42"] });
15
- await cache.set("user:1:prefs", prefs, { tags: ["users", "tenant-42"] });
15
+ await cache.set("user:1:prefs", prefs, { tags: ["users", "tenant-42"] });
16
16
 
17
17
  // Fluent — useful when you already have a tagged handle
18
18
  const users = cache.tags(["users"]);
@@ -34,11 +34,11 @@ Multi-tag is **union** semantics: an entry is invalidated if it carries **at lea
34
34
 
35
35
  ## When to reach for tags vs namespaces
36
36
 
37
- | Use case | Reach for |
38
- | --- | --- |
39
- | The keys share a known prefix | `cache.removeNamespace("prefix")` (the `use-cache-namespace` topic) |
40
- | The keys are spread across prefixes, tied by entity | Tags |
41
- | Both apply | Tags — more flexible; cheap on most drivers |
37
+ | Use case | Reach for |
38
+ | --------------------------------------------------- | ------------------------------------------------------------------- |
39
+ | The keys share a known prefix | `cache.removeNamespace("prefix")` (the `use-cache-namespace` topic) |
40
+ | The keys are spread across prefixes, tied by entity | Tags |
41
+ | Both apply | Tags — more flexible; cheap on most drivers |
42
42
 
43
43
  Namespaces are cheaper (no reverse index). Tags are more powerful (any key can carry any tag).
44
44
 
@@ -48,13 +48,13 @@ Subsequent `set("user:1", ...)` with **no** `tags` leaves previous associations
48
48
 
49
49
  ## Driver behavior
50
50
 
51
- | Driver | Tag invalidation |
52
- | --- | :-: |
53
- | `null` | noop |
51
+ | Driver | Tag invalidation |
52
+ | ----------------------------------- | :----------------------------------------------------------: |
53
+ | `null` | noop |
54
54
  | `memory` / `memoryExtended` / `lru` | ✓ (separate index store; eviction and expiry never touch it) |
55
- | `file` | ✓ (serialized index) |
56
- | `redis` | ✓ (native SET inside the prefix) |
57
- | `pg` | ✓ (JSON-array merge on the index row) |
55
+ | `file` | ✓ (serialized index) |
56
+ | `redis` | ✓ (native SET inside the prefix) |
57
+ | `pg` | ✓ (JSON-array merge on the index row) |
58
58
 
59
59
  The index uses driver primitives (`tagAdd` / `tagMembers` / `tagRemove`), so concurrent tagged writes never drop members. It stores un-prefixed keys, `invalidate()` deletes exactly the members it read, writes prune up to 20 dead members, and `remove()` / `pull()` detach the key. Tags are global across scopes. Across servers, use `redis` or `pg` so every server shares one index.
60
60
 
@@ -76,7 +76,7 @@ Tags re-apply on every successful refresh — see the `use-swr` topic.
76
76
  const getUser = cached(fn, { key: (id) => `user.${id}`, ttl: "1h", tags: ["users"] });
77
77
  const getPosts = cached(fn, { key: (u) => `posts.by.${u}`, ttl: "30m", tags: ["users", "posts"] });
78
78
 
79
- await cache.tags(["users"]).invalidate(); // drops both wrappers' caches
79
+ await cache.tags(["users"]).invalidate(); // drops both wrappers' caches
80
80
  ```
81
81
 
82
82
  See the `use-cached-hof` topic.
@@ -24,7 +24,7 @@ await cache.update<UserState>("user:1:state", (current) => ({
24
24
  // Conditional update — return null to remove
25
25
  await cache.update<Session>("session:abc", (current) => {
26
26
  if (!current || current.expired) {
27
- return null; // removes the key
27
+ return null; // removes the key
28
28
  }
29
29
  return { ...current, extendedAt: Date.now() };
30
30
  });
@@ -67,18 +67,18 @@ await Promise.all(
67
67
  ),
68
68
  );
69
69
 
70
- await cache.get("counter"); // 10 — not a lost-update
70
+ await cache.get("counter"); // 10 — not a lost-update
71
71
  ```
72
72
 
73
73
  The per-key lock map lives on the driver instance and is cleared after each chain link finishes. No leak.
74
74
 
75
75
  ## When to reach for what
76
76
 
77
- | Task | Use |
78
- | --- | --- |
79
- | "Add 1 to this counter" | `increment()` (Redis-atomic via `INCRBY`; in-process elsewhere) |
80
- | "Change one field on a cached object" | `merge()` |
81
- | "Read-decide-maybe-write, possibly remove" | `update()` |
82
- | "Only set if missing" | `set(k, v, { onConflict: "create" })` |
83
- | "Only set if already exists" | `set(k, v, { onConflict: "update" })` |
84
- | "Read-then-delete atomically" | `pull()` |
77
+ | Task | Use |
78
+ | ------------------------------------------ | --------------------------------------------------------------- |
79
+ | "Add 1 to this counter" | `increment()` (Redis-atomic via `INCRBY`; in-process elsewhere) |
80
+ | "Change one field on a cached object" | `merge()` |
81
+ | "Read-decide-maybe-write, possibly remove" | `update()` |
82
+ | "Only set if missing" | `set(k, v, { onConflict: "create" })` |
83
+ | "Only set if already exists" | `set(k, v, { onConflict: "update" })` |
84
+ | "Read-then-delete atomically" | `pull()` |
@@ -15,17 +15,17 @@ driver or do cache-adjacent work outside the manager.
15
15
  ```ts
16
16
  import { parseTtl, expiresAtToTtl, resolveTtl } from "@warlock.js/cache";
17
17
 
18
- parseTtl(3600); // 3600
19
- parseTtl("1h"); // 3600 (duration string via `ms`)
20
- parseTtl(Infinity); // Infinity (no expiry)
21
- parseTtl(-5); // throws CacheConfigurationError
18
+ parseTtl(3600); // 3600
19
+ parseTtl("1h"); // 3600 (duration string via `ms`)
20
+ parseTtl(Infinity); // Infinity (no expiry)
21
+ parseTtl(-5); // throws CacheConfigurationError
22
22
 
23
23
  expiresAtToTtl(new Date(Date.now() + 60_000)); // ~60 (absolute → relative seconds)
24
- expiresAtToTtl(Date.now() - 1000); // throws — deadline in the past
24
+ expiresAtToTtl(Date.now() - 1000); // throws — deadline in the past
25
25
 
26
26
  // caller ttl > expiresAt > fallback; ttl+expiresAt together throws
27
- resolveTtl("1h", undefined, Infinity); // 3600
28
- resolveTtl(undefined, undefined, 1800); // 1800 (fallback)
27
+ resolveTtl("1h", undefined, Infinity); // 3600
28
+ resolveTtl(undefined, undefined, 1800); // 1800 (fallback)
29
29
  ```
30
30
 
31
31
  ## Option normalizers
@@ -36,10 +36,10 @@ Coerce the polymorphic 2nd/3rd argument of `set`/`remember` into a uniform shape
36
36
  ```ts
37
37
  import { normalizeToOptions, normalizeToRememberOptions } from "@warlock.js/cache";
38
38
 
39
- normalizeToOptions(60); // { ttl: 60 }
40
- normalizeToOptions("1h"); // { ttl: "1h" }
41
- normalizeToOptions({ tags: ["x"] }); // returned as-is
42
- normalizeToRememberOptions("1h"); // { ttl: "1h" } (no expiresAt/onConflict)
39
+ normalizeToOptions(60); // { ttl: 60 }
40
+ normalizeToOptions("1h"); // { ttl: "1h" }
41
+ normalizeToOptions({ tags: ["x"] }); // returned as-is
42
+ normalizeToRememberOptions("1h"); // { ttl: "1h" } (no expiresAt/onConflict)
43
43
  ```
44
44
 
45
45
  ## Key + tag helpers
@@ -47,14 +47,14 @@ normalizeToRememberOptions("1h"); // { ttl: "1h" } (no expiresAt/onConfl
47
47
  ```ts
48
48
  import { parseCacheKey, mergeTagSets, injectTags } from "@warlock.js/cache";
49
49
 
50
- parseCacheKey("users:1"); // "users.1"
51
- parseCacheKey({ page: 1, q: "John" }); // "page.1.q.John"
50
+ parseCacheKey("users:1"); // "users.1"
51
+ parseCacheKey({ page: 1, q: "John" }); // "page.1.q.John"
52
52
  parseCacheKey("user:1", { globalPrefix: "app" }); // "app.user.1"
53
53
 
54
- mergeTagSets(["a", "b"], ["b", "c"]); // ["a","b","c"] (deduped union)
55
- mergeTagSets(undefined, undefined); // undefined
54
+ mergeTagSets(["a", "b"], ["b", "c"]); // ["a","b","c"] (deduped union)
55
+ mergeTagSets(undefined, undefined); // undefined
56
56
 
57
- injectTags({ ttl: "1h" }, ["unread"]); // { ttl: "1h", tags: ["unread"] } (pure, no mutation)
57
+ injectTags({ ttl: "1h" }, ["unread"]); // { ttl: "1h", tags: ["unread"] } (pure, no mutation)
58
58
  ```
59
59
 
60
60
  ## Vector scoring
@@ -64,7 +64,7 @@ import { cosineSimilarity } from "@warlock.js/cache";
64
64
 
65
65
  cosineSimilarity([1, 0, 0], [1, 0, 0]); // 1
66
66
  cosineSimilarity([1, 0, 0], [0, 1, 0]); // 0
67
- cosineSimilarity([1, 2, 3], [1, 2]); // throws — dimension mismatch
67
+ cosineSimilarity([1, 2, 3], [1, 2]); // throws — dimension mismatch
68
68
  ```
69
69
 
70
70
  Powers the brute-force `cache.similar()` on the memory drivers — reach for it
@@ -10,6 +10,7 @@ description: 'Wrap an async function with cached(fn, options) — declare the ca
10
10
  ## When to use
11
11
 
12
12
  Reach for `cached()` instead of `cache.remember()` when:
13
+
13
14
  - You have a function you'll call from many places.
14
15
  - You want the caching strategy declared once, not repeated at every call site.
15
16
  - You want `.invalidate()` available without manually deriving keys.
@@ -38,11 +39,11 @@ cached(fn, {
38
39
 
39
40
  ## Auto-key rules (shorthand only)
40
41
 
41
- | Args | Key |
42
- |------|-----|
43
- | None | `prefix` |
44
- | Any args | `prefix.<16 hex>` — sha1 of a stable JSON encoding of the args, truncated |
45
- | `Map` / `Set` / function / symbol / class instance / circular | throws `CacheConfigurationError` |
42
+ | Args | Key |
43
+ | ------------------------------------------------------------- | ------------------------------------------------------------------------- |
44
+ | None | `prefix` |
45
+ | Any args | `prefix.<16 hex>` — sha1 of a stable JSON encoding of the args, truncated |
46
+ | `Map` / `Set` / function / symbol / class instance / circular | throws `CacheConfigurationError` |
46
47
 
47
48
  The hash keeps distinct args on distinct keys (`("1.private", "")` ≠ `("1", "private")`). Object keys are sorted; `Date` → ISO string; order of args matters. Never build keys by hand: call `.invalidate(...args)`, which honours `config.driver`. When auto-key fails, use the options form with a custom `key` fn.
48
49
 
@@ -76,7 +77,7 @@ await getUser.invalidate(42);
76
77
  const getUser = cached(fn, { key: (id) => `user.${id}`, ttl: "1h", tags: ["users"] });
77
78
  const getPosts = cached(fn, { key: (u) => `posts.by.${u}`, ttl: "30m", tags: ["users", "posts"] });
78
79
 
79
- await cache.tags(["users"]).invalidate(); // drops both wrappers' caches
80
+ await cache.tags(["users"]).invalidate(); // drops both wrappers' caches
80
81
  ```
81
82
 
82
83
  See the `use-cache-tags` topic.
@@ -86,7 +87,7 @@ See the `use-cache-tags` topic.
86
87
  ```ts
87
88
  const getCategoryMeta = cached(
88
89
  (filters: Filters) => db.categories.meta(filters.category),
89
- { key: (f) => `category.meta.${f.category}`, ttl: "1h" }, // ignores `sort`, `page`
90
+ { key: (f) => `category.meta.${f.category}`, ttl: "1h" }, // ignores `sort`, `page`
90
91
  );
91
92
  ```
92
93
 
@@ -24,11 +24,11 @@ Use `cache.remember()` when freshness is non-negotiable — auth, balances, bill
24
24
  │ no upstream call │ bg refresh │ like a miss
25
25
  ```
26
26
 
27
- | Window | Behavior |
28
- |---|---|
29
- | `now < freshTtl` | Return cached. No upstream call. |
27
+ | Window | Behavior |
28
+ | --------------------------- | ---------------------------------------------------------------------------------------- |
29
+ | `now < freshTtl` | Return cached. No upstream call. |
30
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()`. |
31
+ | `now ≥ staleTtl` | Block on `fn()`. Same as `remember()`. |
32
32
 
33
33
  ## API
34
34
 
@@ -57,12 +57,12 @@ await cache.swr(
57
57
 
58
58
  ## Driver support
59
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) |
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
66
 
67
67
  ## Common shapes
68
68
 
@@ -78,9 +78,7 @@ await cache.swr(`dashboard.${tenantId}`, { freshTtl: "5m", staleTtl: "1h" }, ()
78
78
  );
79
79
 
80
80
  // Third-party API — degrade gracefully when upstream is down
81
- await cache.swr("exchange.rates", { freshTtl: "10m", staleTtl: "24h" }, () =>
82
- fetchFromForexAPI(),
83
- );
81
+ await cache.swr("exchange.rates", { freshTtl: "10m", staleTtl: "24h" }, () => fetchFromForexAPI());
84
82
  ```
85
83
 
86
84
  ## Through scoped caches
@@ -88,10 +86,8 @@ await cache.swr("exchange.rates", { freshTtl: "10m", staleTtl: "24h" }, () =>
88
86
  ```ts
89
87
  const feed = cache.namespace(`feed.${userId}`, { tags: [`user.${userId}`] });
90
88
 
91
- await feed.swr(
92
- "home",
93
- { freshTtl: "30s", staleTtl: "10m", tags: ["computed"] },
94
- () => buildHomeFeed(userId),
89
+ await feed.swr("home", { freshTtl: "30s", staleTtl: "10m", tags: ["computed"] }, () =>
90
+ buildHomeFeed(userId),
95
91
  );
96
92
  // stored at feed.<userId>.home, tagged [user.<userId>, computed]
97
93
  ```
@@ -99,6 +95,6 @@ await feed.swr(
99
95
  ## Things NOT to do
100
96
 
101
97
  - 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.
98
+ - 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
99
  - Don't ignore `error` events. A persistent stream of refresh failures means upstream is broken and the cache is masking it.
104
100
  - Don't reach for SWR on the null driver — it caches nothing, so SWR always blocks.