@warlock.js/cache 4.0.171 → 4.1.1

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 (213) hide show
  1. package/README.md +85 -0
  2. package/cjs/index.cjs +4088 -0
  3. package/cjs/index.cjs.map +1 -0
  4. package/esm/cache-manager.d.mts +314 -0
  5. package/esm/cache-manager.d.mts.map +1 -0
  6. package/esm/cache-manager.mjs +486 -0
  7. package/esm/cache-manager.mjs.map +1 -0
  8. package/esm/cached/auto-key.d.mts +25 -0
  9. package/esm/cached/auto-key.d.mts.map +1 -0
  10. package/esm/cached/auto-key.mjs +55 -0
  11. package/esm/cached/auto-key.mjs.map +1 -0
  12. package/esm/cached/cached.d.mts +54 -0
  13. package/esm/cached/cached.d.mts.map +1 -0
  14. package/esm/cached/cached.mjs +25 -0
  15. package/esm/cached/cached.mjs.map +1 -0
  16. package/esm/cached/index.d.mts +3 -0
  17. package/esm/cached/index.mjs +5 -0
  18. package/esm/cached/normalize-args.d.mts +51 -0
  19. package/esm/cached/normalize-args.d.mts.map +1 -0
  20. package/esm/cached/normalize-args.mjs +26 -0
  21. package/esm/cached/normalize-args.mjs.map +1 -0
  22. package/esm/drivers/base-cache-driver.d.mts +322 -0
  23. package/esm/drivers/base-cache-driver.d.mts.map +1 -0
  24. package/esm/drivers/base-cache-driver.mjs +522 -0
  25. package/esm/drivers/base-cache-driver.mjs.map +1 -0
  26. package/esm/drivers/file-cache-driver.d.mts +68 -0
  27. package/esm/drivers/file-cache-driver.d.mts.map +1 -0
  28. package/esm/drivers/file-cache-driver.mjs +174 -0
  29. package/esm/drivers/file-cache-driver.mjs.map +1 -0
  30. package/esm/drivers/index.d.mts +9 -0
  31. package/esm/drivers/index.mjs +11 -0
  32. package/esm/drivers/lru-memory-cache-driver.d.mts +136 -0
  33. package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -0
  34. package/esm/drivers/lru-memory-cache-driver.mjs +317 -0
  35. package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -0
  36. package/esm/drivers/memory-cache-driver.d.mts +112 -0
  37. package/esm/drivers/memory-cache-driver.d.mts.map +1 -0
  38. package/esm/drivers/memory-cache-driver.mjs +241 -0
  39. package/esm/drivers/memory-cache-driver.mjs.map +1 -0
  40. package/esm/drivers/memory-extended-cache-driver.d.mts +17 -0
  41. package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -0
  42. package/esm/drivers/memory-extended-cache-driver.mjs +34 -0
  43. package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -0
  44. package/esm/drivers/mock-cache-driver.d.mts +137 -0
  45. package/esm/drivers/mock-cache-driver.d.mts.map +1 -0
  46. package/esm/drivers/mock-cache-driver.mjs +226 -0
  47. package/esm/drivers/mock-cache-driver.mjs.map +1 -0
  48. package/esm/drivers/null-cache-driver.d.mts +69 -0
  49. package/esm/drivers/null-cache-driver.d.mts.map +1 -0
  50. package/esm/drivers/null-cache-driver.mjs +92 -0
  51. package/esm/drivers/null-cache-driver.mjs.map +1 -0
  52. package/esm/drivers/pg-cache-driver.d.mts +148 -0
  53. package/esm/drivers/pg-cache-driver.d.mts.map +1 -0
  54. package/esm/drivers/pg-cache-driver.mjs +437 -0
  55. package/esm/drivers/pg-cache-driver.mjs.map +1 -0
  56. package/esm/drivers/redis-cache-driver.d.mts +86 -0
  57. package/esm/drivers/redis-cache-driver.d.mts.map +1 -0
  58. package/esm/drivers/redis-cache-driver.mjs +312 -0
  59. package/esm/drivers/redis-cache-driver.mjs.map +1 -0
  60. package/esm/index.d.mts +21 -0
  61. package/esm/index.mjs +24 -0
  62. package/esm/list/index.d.mts +1 -0
  63. package/esm/list/memory-cache-list.d.mts +77 -0
  64. package/esm/list/memory-cache-list.d.mts.map +1 -0
  65. package/esm/list/memory-cache-list.mjs +119 -0
  66. package/esm/list/memory-cache-list.mjs.map +1 -0
  67. package/esm/metrics.d.mts +118 -0
  68. package/esm/metrics.d.mts.map +1 -0
  69. package/esm/metrics.mjs +197 -0
  70. package/esm/metrics.mjs.map +1 -0
  71. package/esm/scoped-cache.d.mts +205 -0
  72. package/esm/scoped-cache.d.mts.map +1 -0
  73. package/esm/scoped-cache.mjs +274 -0
  74. package/esm/scoped-cache.mjs.map +1 -0
  75. package/esm/tagged-cache.d.mts +89 -0
  76. package/esm/tagged-cache.d.mts.map +1 -0
  77. package/esm/tagged-cache.mjs +147 -0
  78. package/esm/tagged-cache.mjs.map +1 -0
  79. package/esm/tagged-scoped-cache.d.mts +111 -0
  80. package/esm/tagged-scoped-cache.d.mts.map +1 -0
  81. package/esm/tagged-scoped-cache.mjs +142 -0
  82. package/esm/tagged-scoped-cache.mjs.map +1 -0
  83. package/esm/types.d.mts +1067 -0
  84. package/esm/types.d.mts.map +1 -0
  85. package/esm/types.mjs +62 -0
  86. package/esm/types.mjs.map +1 -0
  87. package/esm/utils.d.mts +161 -0
  88. package/esm/utils.d.mts.map +1 -0
  89. package/esm/utils.mjs +222 -0
  90. package/esm/utils.mjs.map +1 -0
  91. package/llms-full.txt +2071 -0
  92. package/llms.txt +28 -0
  93. package/package.json +53 -39
  94. package/skills/apply-cache-patterns/SKILL.md +97 -0
  95. package/skills/cache-basics/SKILL.md +121 -0
  96. package/skills/configure-pg-cache/SKILL.md +115 -0
  97. package/skills/configure-set-options/SKILL.md +96 -0
  98. package/skills/handle-cache-errors/SKILL.md +91 -0
  99. package/skills/observe-cache/SKILL.md +103 -0
  100. package/skills/overview/SKILL.md +69 -0
  101. package/skills/pick-cache-driver/SKILL.md +115 -0
  102. package/skills/test-cache-code/SKILL.md +219 -0
  103. package/skills/use-cache-atomic/SKILL.md +67 -0
  104. package/skills/use-cache-bulk/SKILL.md +57 -0
  105. package/skills/use-cache-list/SKILL.md +85 -0
  106. package/skills/use-cache-lock/SKILL.md +104 -0
  107. package/skills/use-cache-namespace/SKILL.md +88 -0
  108. package/skills/use-cache-similarity/SKILL.md +94 -0
  109. package/skills/use-cache-tags/SKILL.md +85 -0
  110. package/skills/use-cache-update-merge/SKILL.md +84 -0
  111. package/skills/use-cache-utils/SKILL.md +89 -0
  112. package/skills/use-cached-hof/SKILL.md +102 -0
  113. package/skills/use-swr/SKILL.md +104 -0
  114. package/cjs/cache-manager.d.ts +0 -163
  115. package/cjs/cache-manager.d.ts.map +0 -1
  116. package/cjs/cache-manager.js +0 -322
  117. package/cjs/cache-manager.js.map +0 -1
  118. package/cjs/drivers/base-cache-driver.d.ts +0 -152
  119. package/cjs/drivers/base-cache-driver.d.ts.map +0 -1
  120. package/cjs/drivers/base-cache-driver.js +0 -321
  121. package/cjs/drivers/base-cache-driver.js.map +0 -1
  122. package/cjs/drivers/file-cache-driver.d.ts +0 -45
  123. package/cjs/drivers/file-cache-driver.d.ts.map +0 -1
  124. package/cjs/drivers/file-cache-driver.js +0 -133
  125. package/cjs/drivers/file-cache-driver.js.map +0 -1
  126. package/cjs/drivers/index.d.ts +0 -8
  127. package/cjs/drivers/index.d.ts.map +0 -1
  128. package/cjs/drivers/lru-memory-cache-driver.d.ts +0 -98
  129. package/cjs/drivers/lru-memory-cache-driver.d.ts.map +0 -1
  130. package/cjs/drivers/lru-memory-cache-driver.js +0 -252
  131. package/cjs/drivers/lru-memory-cache-driver.js.map +0 -1
  132. package/cjs/drivers/memory-cache-driver.d.ts +0 -82
  133. package/cjs/drivers/memory-cache-driver.d.ts.map +0 -1
  134. package/cjs/drivers/memory-cache-driver.js +0 -218
  135. package/cjs/drivers/memory-cache-driver.js.map +0 -1
  136. package/cjs/drivers/memory-extended-cache-driver.d.ts +0 -13
  137. package/cjs/drivers/memory-extended-cache-driver.d.ts.map +0 -1
  138. package/cjs/drivers/memory-extended-cache-driver.js +0 -25
  139. package/cjs/drivers/memory-extended-cache-driver.js.map +0 -1
  140. package/cjs/drivers/null-cache-driver.d.ts +0 -58
  141. package/cjs/drivers/null-cache-driver.d.ts.map +0 -1
  142. package/cjs/drivers/null-cache-driver.js +0 -84
  143. package/cjs/drivers/null-cache-driver.js.map +0 -1
  144. package/cjs/drivers/redis-cache-driver.d.ts +0 -57
  145. package/cjs/drivers/redis-cache-driver.d.ts.map +0 -1
  146. package/cjs/drivers/redis-cache-driver.js +0 -263
  147. package/cjs/drivers/redis-cache-driver.js.map +0 -1
  148. package/cjs/index.d.ts +0 -6
  149. package/cjs/index.d.ts.map +0 -1
  150. package/cjs/index.js +0 -1
  151. package/cjs/index.js.map +0 -1
  152. package/cjs/tagged-cache.d.ts +0 -77
  153. package/cjs/tagged-cache.d.ts.map +0 -1
  154. package/cjs/tagged-cache.js +0 -160
  155. package/cjs/tagged-cache.js.map +0 -1
  156. package/cjs/types.d.ts +0 -391
  157. package/cjs/types.d.ts.map +0 -1
  158. package/cjs/types.js +0 -36
  159. package/cjs/types.js.map +0 -1
  160. package/cjs/utils.d.ts +0 -50
  161. package/cjs/utils.d.ts.map +0 -1
  162. package/cjs/utils.js +0 -55
  163. package/cjs/utils.js.map +0 -1
  164. package/esm/cache-manager.d.ts +0 -163
  165. package/esm/cache-manager.d.ts.map +0 -1
  166. package/esm/cache-manager.js +0 -322
  167. package/esm/cache-manager.js.map +0 -1
  168. package/esm/drivers/base-cache-driver.d.ts +0 -152
  169. package/esm/drivers/base-cache-driver.d.ts.map +0 -1
  170. package/esm/drivers/base-cache-driver.js +0 -321
  171. package/esm/drivers/base-cache-driver.js.map +0 -1
  172. package/esm/drivers/file-cache-driver.d.ts +0 -45
  173. package/esm/drivers/file-cache-driver.d.ts.map +0 -1
  174. package/esm/drivers/file-cache-driver.js +0 -133
  175. package/esm/drivers/file-cache-driver.js.map +0 -1
  176. package/esm/drivers/index.d.ts +0 -8
  177. package/esm/drivers/index.d.ts.map +0 -1
  178. package/esm/drivers/lru-memory-cache-driver.d.ts +0 -98
  179. package/esm/drivers/lru-memory-cache-driver.d.ts.map +0 -1
  180. package/esm/drivers/lru-memory-cache-driver.js +0 -252
  181. package/esm/drivers/lru-memory-cache-driver.js.map +0 -1
  182. package/esm/drivers/memory-cache-driver.d.ts +0 -82
  183. package/esm/drivers/memory-cache-driver.d.ts.map +0 -1
  184. package/esm/drivers/memory-cache-driver.js +0 -218
  185. package/esm/drivers/memory-cache-driver.js.map +0 -1
  186. package/esm/drivers/memory-extended-cache-driver.d.ts +0 -13
  187. package/esm/drivers/memory-extended-cache-driver.d.ts.map +0 -1
  188. package/esm/drivers/memory-extended-cache-driver.js +0 -25
  189. package/esm/drivers/memory-extended-cache-driver.js.map +0 -1
  190. package/esm/drivers/null-cache-driver.d.ts +0 -58
  191. package/esm/drivers/null-cache-driver.d.ts.map +0 -1
  192. package/esm/drivers/null-cache-driver.js +0 -84
  193. package/esm/drivers/null-cache-driver.js.map +0 -1
  194. package/esm/drivers/redis-cache-driver.d.ts +0 -57
  195. package/esm/drivers/redis-cache-driver.d.ts.map +0 -1
  196. package/esm/drivers/redis-cache-driver.js +0 -263
  197. package/esm/drivers/redis-cache-driver.js.map +0 -1
  198. package/esm/index.d.ts +0 -6
  199. package/esm/index.d.ts.map +0 -1
  200. package/esm/index.js +0 -1
  201. package/esm/index.js.map +0 -1
  202. package/esm/tagged-cache.d.ts +0 -77
  203. package/esm/tagged-cache.d.ts.map +0 -1
  204. package/esm/tagged-cache.js +0 -160
  205. package/esm/tagged-cache.js.map +0 -1
  206. package/esm/types.d.ts +0 -391
  207. package/esm/types.d.ts.map +0 -1
  208. package/esm/types.js +0 -36
  209. package/esm/types.js.map +0 -1
  210. package/esm/utils.d.ts +0 -50
  211. package/esm/utils.d.ts.map +0 -1
  212. package/esm/utils.js +0 -55
  213. package/esm/utils.js.map +0 -1
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: use-cache-atomic
3
+ description: 'Atomic counters via cache.increment(key, by=1) / cache.decrement(key, by=1) — returns the new number, throws on non-numeric values. Triggers: `cache.increment`, `cache.decrement`, "view counter", "page views", "atomic counter", "decrement stock", "rate-limit counter", "INCRBY"; typical import `import { cache } from "@warlock.js/cache"`. Skip: read-modify-write of objects — `@warlock.js/cache/use-cache-update-merge/SKILL.md`; named-lock coordination — `@warlock.js/cache/use-cache-lock/SKILL.md`; competing libs `ioredis` `INCR`, native counters in a `Map`.'
4
+ ---
5
+
6
+ # Atomic counters — `cache.increment` / `cache.decrement`
7
+
8
+ Numeric counters that go up and down without a read-then-write race in your own
9
+ code. Both return the **new** value after the operation.
10
+
11
+ ```ts
12
+ import { cache } from "@warlock.js/cache";
13
+
14
+ const views = await cache.increment(`post.${id}.views`); // +1 → 1, 2, 3…
15
+ const bulk = await cache.increment(`post.${id}.views`, 10); // +10
16
+ const left = await cache.decrement(`stock.${sku}`, 3); // -3
17
+ ```
18
+
19
+ - A missing key is treated as `0`, so the first `increment` returns `by` (default `1`).
20
+ - `decrement(key, n)` is exactly `increment(key, -n)`.
21
+ - The stored value must be numeric — incrementing a string/object throws:
22
+ `Error: Cannot increment non-numeric value for key: <key>`.
23
+
24
+ ## Atomicity is per-driver
25
+
26
+ | Driver | Guarantee |
27
+ |---|---|
28
+ | `redis` | Native `INCRBY` / `DECRBY` — atomic **across processes/nodes** |
29
+ | memory family / `file` / `pg` | Read-modify-write — atomic **within one process** only |
30
+
31
+ For a counter that multiple instances bump concurrently (a global rate limit, a
32
+ shared tally), use the [`redis`](@warlock.js/cache/pick-cache-driver/SKILL.md)
33
+ driver. In-memory counters are fine for single-node work.
34
+
35
+ ## TTL behavior differs too
36
+
37
+ This is the gotcha to remember:
38
+
39
+ - **Redis** `INCRBY` **preserves** the key's existing TTL.
40
+ - **Memory-family / pg** write the new value through `set()` with the driver's
41
+ **default** TTL — they do **not** carry over the previous entry's remaining TTL.
42
+
43
+ So if you need a counter that expires (a fixed window), set the TTL explicitly
44
+ when you create it and don't rely on `increment` to keep a window alive on the
45
+ in-memory drivers. For a value that should keep its TTL across edits, reach for
46
+ [`cache.update`](@warlock.js/cache/use-cache-update-merge/SKILL.md), which
47
+ preserves the remaining TTL.
48
+
49
+ ## Common shapes
50
+
51
+ ```ts
52
+ // View counter
53
+ await cache.increment(`post.${id}.views`);
54
+
55
+ // Decrement stock, guard against oversell
56
+ const remaining = await cache.decrement(`stock.${sku}`, qty);
57
+ if (remaining < 0) {
58
+ await cache.increment(`stock.${sku}`, qty); // roll back
59
+ throw new Error("Out of stock");
60
+ }
61
+ ```
62
+
63
+ ## See also
64
+
65
+ - [`@warlock.js/cache/use-cache-update-merge/SKILL.md`](@warlock.js/cache/use-cache-update-merge/SKILL.md) — atomic read-modify-write for objects, TTL-preserving
66
+ - [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md) — coordinate multi-step critical sections
67
+ - [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) — when you need cross-node atomicity
@@ -0,0 +1,57 @@
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
@@ -0,0 +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.
@@ -0,0 +1,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.** 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.
@@ -0,0 +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, `redis`/`pg` by key/`LIKE` prefix; `null` no-ops. See [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md).
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.
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: use-cache-similarity
3
+ description: 'Vector retrieval via cache.similar(vector, {topK, threshold?, tags?}) — index with set(k, v, {vector}), query nearest by cosine similarity. Triggers: `cache.similar`, `cache.set` with `vector`, `topK`, `threshold`, `tags`, `cosineSimilarity`; "build a semantic cache for an LLM", "RAG retrieval from cache", "nearest-neighbor over cached entries", "skip the LLM when a similar answer exists"; typical import `import { cache } from "@warlock.js/cache"`. Skip: pgvector setup specifics — `@warlock.js/cache/configure-pg-cache/SKILL.md`; competing libs `pinecone`, `weaviate`, `chromadb`, `lancedb`, `faiss-node`.'
4
+ ---
5
+
6
+ # `cache.similar()` — vector-based retrieval
7
+
8
+ `cache.similar(vector, { topK, threshold?, tags? })` returns stored entries closest to `vector` by cosine similarity, ordered descending by score. Same `set` / `get` model — different lookup function.
9
+
10
+ ## Shape
11
+
12
+ ```ts
13
+ // Index on the way in.
14
+ await cache.set(key, value, { vector: number[], tags?, ttl? });
15
+
16
+ // Query.
17
+ const hits = await cache.similar<T>(queryVec, {
18
+ topK: number, // required
19
+ threshold?: number, // [0, 1]; hits below are dropped
20
+ tags?: string[], // narrow candidate pool by tag (union)
21
+ });
22
+ // hits: { key: string; value: T; score: number }[] // score in [0, 1]
23
+ ```
24
+
25
+ ## Capability matrix
26
+
27
+ | Driver | `similar()` |
28
+ |---|---|
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 `[]` |
35
+
36
+ ## Always-true facts
37
+
38
+ 1. **Cache is embedding-agnostic.** Caller computes vectors. The cache stores and ranks; it doesn't call out to an embedder.
39
+ 2. **Only entries written with `set({ vector })` show up.** A plain `set` adds the entry as KV — invisible to `similar()`.
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).
42
+ 5. **Dimension mismatch throws `CacheConfigurationError`** at both `set({ vector })` and `similar()` time. Don't switch embedders without re-indexing.
43
+ 6. **TTL + LRU eviction also drop the vector** — expired or evicted entries are invisible to `similar()`.
44
+
45
+ ## Recipes
46
+
47
+ ### Semantic cache for an LLM
48
+
49
+ ```ts
50
+ const queryVec = await embed(prompt);
51
+ const hits = await cache.similar<Answer>(queryVec, { topK: 1, threshold: 0.92 });
52
+
53
+ if (hits.length > 0) {
54
+ return hits[0].value; // skip the LLM call
55
+ }
56
+
57
+ const answer = await llm.complete(prompt);
58
+ await cache.set(`q.${hash(prompt)}`, answer, {
59
+ vector: queryVec,
60
+ ttl: "30d",
61
+ tags: ["llm-cache"],
62
+ });
63
+ return answer;
64
+ ```
65
+
66
+ ### Tag-narrowed RAG
67
+
68
+ ```ts
69
+ const hits = await cache.similar<Doc>(await embed(question), {
70
+ topK: 5,
71
+ threshold: 0.7,
72
+ tags: ["docs", `tenant.${tenantId}`],
73
+ });
74
+ ```
75
+
76
+ ### Production swap — same code, different driver
77
+
78
+ ```ts
79
+ // Dev:
80
+ options: { memory: { ttl: "1h" } }
81
+
82
+ // Prod — same set/similar calls; index now lives in pgvector:
83
+ options: { pg: { client: pool, vector: { dimensions: 1536 } } }
84
+ ```
85
+
86
+ See [`@warlock.js/cache/configure-pg-cache/SKILL.md`](@warlock.js/cache/configure-pg-cache/SKILL.md).
87
+
88
+ ## Things NOT to do
89
+
90
+ - Don't use `cache.similar()` on a memory driver with 100k+ vectorized entries — it scales O(N) per query. Switch to `pg` with `vector` config.
91
+ - Don't pass an empty array as `vector` — `cosineSimilarity` throws `CacheConfigurationError`.
92
+ - Don't mix vector dimensions in the same driver — re-embed when models change.
93
+ - Don't expect `similar()` to surface a missing vector (`set` without the `vector` option). Plain KV entries stay out of the similarity index.
94
+ - Don't use `topK: 0` or negative — `pg` rejects with `CacheConfigurationError`; memory drivers return `[]` but it's a code smell.
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: use-cache-tags
3
+ description: 'Tag-based invalidation — attach tags on write, then cache.tags([...]).invalidate() drops every key bound to any of those tags. Triggers: `cache.tags`, `invalidate`, `cache.set` with `tags`; "invalidate every key tagged users", "drop everything for tenant 42", "bulk cache invalidation without knowing keys", "tag a cached value"; typical import `import { cache } from "@warlock.js/cache"`. Skip: prefix-based drop — `@warlock.js/cache/use-cache-namespace/SKILL.md`; HOF memoization with tags — `@warlock.js/cache/use-cached-hof/SKILL.md`; SWR — `@warlock.js/cache/use-swr/SKILL.md`; competing libs `cache-manager` tags, Next.js `revalidateTag`.'
4
+ ---
5
+
6
+ # Tag-based invalidation
7
+
8
+ Tags let you invalidate by a label rather than by enumerating every key. Use them when the set of keys to invalidate is not known ahead of time.
9
+
10
+ ## Attach tags on write
11
+
12
+ ```ts
13
+ // Inline — terser when you know the tags up front
14
+ await cache.set("user:1:profile", profile, { tags: ["users", "tenant-42"] });
15
+ await cache.set("user:1:prefs", prefs, { tags: ["users", "tenant-42"] });
16
+
17
+ // Fluent — useful when you already have a tagged handle
18
+ const users = cache.tags(["users"]);
19
+ await users.set("user:1", user);
20
+ await users.set("user:2", otherUser);
21
+ ```
22
+
23
+ ## Invalidate
24
+
25
+ ```ts
26
+ // Drop everything tagged "users"
27
+ await cache.tags(["users"]).invalidate();
28
+
29
+ // Multi-tag — matches either tag (union)
30
+ await cache.tags(["tenant-42"]).invalidate();
31
+ ```
32
+
33
+ Multi-tag is **union** semantics: an entry is invalidated if it carries **at least one** of the listed tags.
34
+
35
+ ## When to reach for tags vs namespaces
36
+
37
+ | Use case | Reach for |
38
+ | --- | --- |
39
+ | The keys share a known prefix | `cache.removeNamespace("prefix")` ([`use-cache-namespace`](@warlock.js/cache/use-cache-namespace/SKILL.md)) |
40
+ | The keys are spread across prefixes, tied by entity | Tags |
41
+ | Both apply | Tags — more flexible; cheap on most drivers |
42
+
43
+ Namespaces are cheaper (no reverse index). Tags are more powerful (any key can carry any tag).
44
+
45
+ ## Inline tag semantics
46
+
47
+ Subsequent `set("user:1", ...)` with **no** `tags` leaves previous associations intact (the tag index still points to the key), but a `set(..., { tags: [...] })` adds to whatever index entries already exist rather than removing old tag bindings. Tag associations are additive at write-time.
48
+
49
+ ## Driver behavior
50
+
51
+ | Driver | Tag invalidation |
52
+ | --- | :-: |
53
+ | `null` | noop |
54
+ | `memory` / `memoryExtended` / `lru` / `file` | ✓ (reverse index in driver state) |
55
+ | `redis` | ✓ |
56
+ | `pg` | ✓ native via `GIN(tags)` index |
57
+
58
+ ## SWR with tags
59
+
60
+ ```ts
61
+ await cache.swr(
62
+ `product.${id}`,
63
+ { freshTtl: "1m", staleTtl: "1h", tags: ["products", `tenant.${tenantId}`] },
64
+ () => db.products.find(id),
65
+ );
66
+ ```
67
+
68
+ Tags re-apply on every successful refresh — see [`@warlock.js/cache/use-swr/SKILL.md`](@warlock.js/cache/use-swr/SKILL.md).
69
+
70
+ ## `cached()` HOF with tags
71
+
72
+ ```ts
73
+ const getUser = cached(fn, { key: (id) => `user.${id}`, ttl: "1h", tags: ["users"] });
74
+ const getPosts = cached(fn, { key: (u) => `posts.by.${u}`, ttl: "30m", tags: ["users", "posts"] });
75
+
76
+ await cache.tags(["users"]).invalidate(); // drops both wrappers' caches
77
+ ```
78
+
79
+ See [`@warlock.js/cache/use-cached-hof/SKILL.md`](@warlock.js/cache/use-cached-hof/SKILL.md).
80
+
81
+ ## Things NOT to do
82
+
83
+ - Don't tag aggressively. A reverse index per tag is cheap but not free — pick tags that actually correspond to invalidation events.
84
+ - Don't expect tag invalidation to fire events for each affected key. It's a bulk op; the event bus emits one `flushed` or per-key `removed` per driver implementation. Test by reading back, not by counting events.
85
+ - Don't use tags as a query mechanism. Tags drop keys; they don't list them. If you need "give me every user", store an index list separately.
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: use-cache-update-merge
3
+ description: 'Atomic read-modify-write via cache.update(key, fn) (callback receives current) or cache.merge(key, partial) (shallow merge). Per-key chain lock serializes concurrent in-process callers. Triggers: `cache.update`, `cache.merge`, `cache.increment`, `cache.pull`; "atomically update a cached counter", "change one field on a cached object", "avoid get-spread-set race", "serialize concurrent cache writers"; typical import `import { cache } from "@warlock.js/cache"`. Skip: cross-process locking — `@warlock.js/cache/use-cache-lock/SKILL.md`; conditional create/update — `@warlock.js/cache/configure-set-options/SKILL.md`; competing libs `lodash.merge`, raw redis `WATCH`/`MULTI`.'
4
+ ---
5
+
6
+ # `update` and `merge` — atomic read-modify-write
7
+
8
+ Prefer these over the ad-hoc `get → spread → set` pattern. Each call takes a per-key chain lock so concurrent callers in the same process are serialized end-to-end.
9
+
10
+ ## `update(key, fn, options?)`
11
+
12
+ Read the current value, pass it to `fn`, write what `fn` returns.
13
+
14
+ ```ts
15
+ // Counter increment
16
+ await cache.update<number>("views", (current) => (current ?? 0) + 1);
17
+
18
+ // Update nested state with defaults
19
+ await cache.update<UserState>("user:1:state", (current) => ({
20
+ ...(current ?? defaultState),
21
+ lastSeenAt: Date.now(),
22
+ }));
23
+
24
+ // Conditional update — return null to remove
25
+ await cache.update<Session>("session:abc", (current) => {
26
+ if (!current || current.expired) {
27
+ return null; // removes the key
28
+ }
29
+ return { ...current, extendedAt: Date.now() };
30
+ });
31
+ ```
32
+
33
+ - `fn` receives `current: T | null`. Missing keys are `null`, not an exception.
34
+ - Returning `null` **removes** the entry.
35
+ - TTL is preserved by default. To reset, pass `{ ttl: "1h" }` as the 3rd arg.
36
+
37
+ ## `merge(key, partial, options?)`
38
+
39
+ Shallow-merge sugar for the common "update one field" shape:
40
+
41
+ ```ts
42
+ await cache.merge<User>("user:1", { name: "Jane" });
43
+ await cache.merge<User>("user:1", { lastSeenAt: Date.now() }, { ttl: "1h" });
44
+ ```
45
+
46
+ - **Shallow only.** Arrays are replaced wholesale. Nested objects overwrite.
47
+ - Missing key → treats current as `{}`, creates with the partial.
48
+ - Preserves existing TTL unless the options override is passed.
49
+
50
+ Deep merge is not built in by design — too many edge cases with arrays and nullish values. If you need deep, write a custom `update(key, deepMerge(current, partial))`.
51
+
52
+ ## What you can't do
53
+
54
+ - No JSONPath / dot-path partial updates (`update(key, "profile.name", "Jane")`). Use the callback form.
55
+ - No file-driver support — both methods throw `CacheUnsupportedError` there. Use memory or redis.
56
+ - No cross-process safety yet on Redis. The chain lock is in-process only. Cross-process safety requires `WATCH`/`MULTI` (tracked in `domains/cache/backlog.md` as a v2.1 follow-up). If two nodes run `update` on the same key simultaneously, last-write-wins.
57
+
58
+ ## Concurrent-in-process correctness
59
+
60
+ ```ts
61
+ await cache.set("counter", 0);
62
+
63
+ // 10 concurrent increments, all on the same key — all serialize
64
+ await Promise.all(
65
+ Array.from({ length: 10 }, () =>
66
+ cache.update<number>("counter", (current) => (current ?? 0) + 1),
67
+ ),
68
+ );
69
+
70
+ await cache.get("counter"); // 10 — not a lost-update
71
+ ```
72
+
73
+ The per-key lock map lives on the driver instance and is cleared after each chain link finishes. No leak.
74
+
75
+ ## When to reach for what
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()` |