@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,91 @@
1
+ ---
2
+ name: handle-cache-errors
3
+ description: 'Cache error classes — CacheError base, CacheConfigurationError, CacheConnectionError, CacheDriverNotInitializedError, CacheUnsupportedError, CacheConcurrencyError. Triggers: `CacheError`, `CacheConfigurationError`, `CacheConnectionError`, `CacheDriverNotInitializedError`, `CacheUnsupportedError`, `CacheConcurrencyError`; "catch cache errors at the boundary", "degrade when update or merge throws", "what does CacheUnsupportedError mean", "fall back when redis is down"; typical import `import { CacheError, CacheConfigurationError, CacheUnsupportedError } from "@warlock.js/cache"`. Skip: choosing a supported driver — `@warlock.js/cache/pick-cache-driver/SKILL.md`; observing errors via events — `@warlock.js/cache/observe-cache/SKILL.md`; competing libs ignore — generic `Error` patterns.'
4
+ ---
5
+
6
+ # Error classes
7
+
8
+ All cache errors extend `CacheError` which extends `Error`. Use `instanceof` to react selectively.
9
+
10
+ ```ts
11
+ import {
12
+ CacheError,
13
+ CacheConfigurationError,
14
+ CacheConnectionError,
15
+ CacheDriverNotInitializedError,
16
+ CacheUnsupportedError,
17
+ CacheConcurrencyError,
18
+ } from "@warlock.js/cache";
19
+ ```
20
+
21
+ | Class | When it's thrown | How to react |
22
+ | --- | --- | --- |
23
+ | `CacheError` | Abstract base — don't throw directly, match against it to catch any cache error | `catch (e) { if (e instanceof CacheError) … }` |
24
+ | `CacheConfigurationError` | Bad TTL string, `ttl` + `expiresAt` together, `expiresAt` in the past, missing required driver option (e.g. redis without url/host, file without directory), attempting to use an unregistered driver name | Fix the config / call site. Never catch at runtime — this is a programmer error, not a user-facing one. |
25
+ | `CacheConnectionError` | Declared for driver connection failures. Not thrown by any built-in driver today (Redis currently logs the error and emits an `"error"` event instead of throwing on failed connect). | Reserved for future use. |
26
+ | `CacheDriverNotInitializedError` | Any data op called before `cache.init()` / `cache.use()` | Call `cache.init()` at app startup. Tests often forget this — add a `beforeEach`. |
27
+ | `CacheUnsupportedError` | Driver doesn't implement the requested op. Today: `update` / `merge` on the file driver; `set({ vector })` and `similar()` on file / redis / pg-without-`vector`-config. | Switch driver (memory family for dev similarity, `pg` with `vector` config for production), or queue the op. |
28
+ | `CacheConcurrencyError` | Declared for future optimistic-concurrency exhaustion on Redis `update()` | Not thrown today. Reserved for the v2.1 `WATCH`/`MULTI` implementation. |
29
+
30
+ ## Special case — `setNX` unsupported
31
+
32
+ Calling `cache.setNX(...)` on a driver that doesn't implement it throws a plain `Error`, not a `CacheUnsupportedError`:
33
+
34
+ ```ts
35
+ // Error: "setNX is not supported by the current cache driver: memory"
36
+ ```
37
+
38
+ This is legacy. The v2-preferred way is `cache.set(k, v, { onConflict: "create" })` which works on every driver (Redis native, others emulated). See [`@warlock.js/cache/configure-set-options/SKILL.md`](@warlock.js/cache/configure-set-options/SKILL.md).
39
+
40
+ ## Patterns
41
+
42
+ ### Catch-all at the boundary
43
+
44
+ ```ts
45
+ try {
46
+ await doCachedWork();
47
+ } catch (error) {
48
+ if (error instanceof CacheError) {
49
+ logger.warn("cache unavailable, degrading", error);
50
+ await doWorkWithoutCache();
51
+ return;
52
+ }
53
+ throw error;
54
+ }
55
+ ```
56
+
57
+ ### Selective — configuration vs runtime
58
+
59
+ ```ts
60
+ try {
61
+ await cache.set("k", v, userSuppliedOptions);
62
+ } catch (error) {
63
+ if (error instanceof CacheConfigurationError) {
64
+ return res.status(400).json({ error: "invalid TTL options" });
65
+ }
66
+ throw error;
67
+ }
68
+ ```
69
+
70
+ ### Driver-missing fallback
71
+
72
+ ```ts
73
+ try {
74
+ await cache.merge("user:1", { lastSeen: Date.now() });
75
+ } catch (error) {
76
+ if (error instanceof CacheUnsupportedError) {
77
+ // File driver in dev — degrade gracefully
78
+ const current = await cache.get("user:1");
79
+ await cache.set("user:1", { ...current, lastSeen: Date.now() });
80
+ return;
81
+ }
82
+ throw error;
83
+ }
84
+ ```
85
+
86
+ ## Things the driver does NOT throw
87
+
88
+ - **Missing keys** — `get()` returns `null`, never throws. Tests checking "key not in cache" should assert `resolves.toBeNull()`, not `rejects.toThrow`.
89
+ - **Expired entries** — `get()` returns `null` and emits `"miss"` + `"expired"` events. No throw.
90
+ - **Flush on empty** — `flush()` succeeds silently when there's nothing to flush.
91
+ - **Concurrent writes clobbering each other** — last-write-wins by default. Use `update()` or `onConflict: "create"` if you need protection.
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: observe-cache
3
+ description: 'Cache observability — cache.metrics() for aggregate hit rate / latency p50/p95/p99 + event bus (cache.on(''hit'' / ''miss'' / ''set'' / ''removed'' / ''flushed'' / ''expired'' / ''error'', ...)). Triggers: `cache.metrics`, `cache.resetMetrics`, `cache.on`, `hit`, `miss`, `removed`, `flushed`, `error`, `hitRate`, `latencyMs`; "show cache hit rate", "page on cache errors", "is my cache being hit", "export metrics to prometheus"; typical import `import { cache } from "@warlock.js/cache"`. Skip: error classes — `@warlock.js/cache/handle-cache-errors/SKILL.md`; competing libs `prom-client`, `statsd-client`.'
4
+ ---
5
+
6
+ # Cache observability — `cache.metrics()` and the event bus
7
+
8
+ Two layers, different jobs.
9
+
10
+ ## Layer 1 — `cache.metrics()` for aggregate health
11
+
12
+ Built-in collector subscribed to the manager's event bus. Returns a snapshot whenever you ask:
13
+
14
+ ```ts
15
+ const m = cache.metrics();
16
+ // {
17
+ // hits, misses, sets, removed, errors,
18
+ // hitRate,
19
+ // latencyMs: { p50, p95, p99, samples },
20
+ // byDriver: { memory: {...}, redis: {...} },
21
+ // startedAt,
22
+ // }
23
+ ```
24
+
25
+ **Lazy** — the collector attaches on the first `cache.metrics()` / `cache.resetMetrics()` call. Apps that never read metrics pay zero cost. Earlier events are not retroactively counted, so if you want metrics on every op including the first, call `cache.metrics()` once during startup right after `cache.init()`.
26
+
27
+ **Survives `cache.use()` switches** — listens at the manager level, re-attaches to every loaded driver.
28
+
29
+ **Latency** is sampled by the manager around `get` / `set` / `remove` into a circular buffer (default 1000 samples per driver). Percentiles are computed at snapshot time. Older samples age out, so percentiles reflect the recent ~1000 ops.
30
+
31
+ `cache.resetMetrics()` zeroes counters + drops the buffer + bumps `startedAt`.
32
+
33
+ ## Layer 2 — Raw events for per-event reactions
34
+
35
+ When you need to react to specific events (alerting, audit logs, debugging), subscribe to the event bus:
36
+
37
+ ```ts
38
+ cache.on("error", ({ key, error }) => {
39
+ pagerDuty.trigger(`Cache error on ${key}`, error);
40
+ });
41
+
42
+ cache.on("miss", ({ key, driver }) => {
43
+ if (key.startsWith("hot.")) auditLog.miss(key, driver);
44
+ });
45
+ ```
46
+
47
+ Available events: `hit`, `miss`, `set`, `removed`, `flushed`, `expired`, `connected`, `disconnected`, `error`.
48
+
49
+ Listeners attached via `cache.on(...)` survive driver switches the same way the metrics collector does.
50
+
51
+ ## Which one to reach for
52
+
53
+ | Goal | Use |
54
+ |---|---|
55
+ | Show hit rate / latency in a dashboard | `cache.metrics()` |
56
+ | Page on cache errors | `cache.on("error", ...)` |
57
+ | Periodic export to Prometheus / StatsD | `cache.metrics()` + `setInterval` + `resetMetrics()` |
58
+ | Audit log of every removal | `cache.on("removed", ...)` |
59
+ | Detect a specific anti-pattern (e.g. always-miss key) | `cache.on("miss", ...)` |
60
+ | Debug "is the cache being hit at all?" in dev | `cache.metrics()` once at the end of a flow |
61
+
62
+ Both layers can coexist — events fire whether the metrics collector is attached or not.
63
+
64
+ ## Common shapes
65
+
66
+ ### Periodic export, then reset
67
+
68
+ ```ts
69
+ setInterval(() => {
70
+ const snapshot = cache.metrics();
71
+ exporter.send(snapshot);
72
+ cache.resetMetrics();
73
+ }, 60_000);
74
+ ```
75
+
76
+ The snapshot now reflects the last minute of traffic, not the lifetime.
77
+
78
+ ### Boundary measurement
79
+
80
+ ```ts
81
+ cache.resetMetrics();
82
+ await runTrafficBurst();
83
+ console.log(cache.metrics());
84
+ ```
85
+
86
+ Useful for benchmarks, soak tests, "did the cache help?" before/after comparisons.
87
+
88
+ ### Per-driver isolation
89
+
90
+ ```ts
91
+ const m = cache.metrics();
92
+ console.log(`memory hit rate: ${m.byDriver.memory?.hitRate ?? 0}`);
93
+ console.log(`redis p95: ${m.byDriver.redis?.latencyMs.p95 ?? 0}ms`);
94
+ ```
95
+
96
+ Drivers that never fire events stay absent from `byDriver` — guard with `?.` and `?? 0`.
97
+
98
+ ## Things NOT to do
99
+
100
+ - Don't subscribe to events to count things and ignore the built-in collector — that's exactly what it's built to do.
101
+ - Don't call `cache.metrics()` on every request expecting per-request data — it returns a running aggregate. Use the event bus for per-call observability.
102
+ - Don't expect lifetime percentiles. The buffer is bounded — for a 24h p95, sample-and-aggregate at your exporter, don't ask cache to remember every op forever.
103
+ - Don't forget to attach early. If startup metrics matter, call `cache.metrics()` right after `cache.init()`.
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: overview
3
+ description: 'Front-door orientation for `@warlock.js/cache` — multi-driver caching (memory / memoryExtended / lru / file / redis / pg / null / mock) with a single `cache` API: get/set/has/pull/remember, TTL shapes, tag-based invalidation, key namespaces, distributed locks, stale-while-revalidate, atomic update/merge, cache lists, vector similarity, metrics + events, and the `cached()` HOF. TRIGGER when: code imports from `@warlock.js/cache` (`cache`, `cached`, `setCacheConfigurations`, a `*CacheDriver`); user asks "what does @warlock.js/cache do", "which cache driver", "cache TTL / tags / invalidation", "distributed lock", "stale-while-revalidate", "semantic / vector cache", "compare with node-cache / keyv / cache-manager"; package.json adds `@warlock.js/cache`. Skip: specific task already known — load the matching task skill directly (`cache-basics`, `pick-cache-driver`, `configure-set-options`, `use-cache-tags`, `use-cache-namespace`, `use-cache-list`, `use-cache-lock`, `use-swr`, `use-cached-hof`, `use-cache-similarity`, `use-cache-update-merge`, `use-cache-atomic`, `use-cache-bulk`, `use-cache-utils`, `apply-cache-patterns`, `observe-cache`, `handle-cache-errors`, `configure-pg-cache`, `test-cache-code`).'
4
+ ---
5
+
6
+ # `@warlock.js/cache` — overview
7
+
8
+ One cache API over many drivers. Pick a driver (memory, memoryExtended, LRU, file, Redis, Postgres, null, mock), wire it once, and call `cache.get` / `cache.set` / `cache.remember` everywhere. On top of the key-value basics it adds tag invalidation, key namespaces, distributed locks, stale-while-revalidate, atomic update/merge, ordered lists, vector similarity, and built-in metrics + events.
9
+
10
+ ## When to reach for it
11
+
12
+ - You need a cache abstraction that swaps drivers per environment (memory in dev, Redis in prod) without changing call sites.
13
+ - You want more than get/set — tag-based bulk invalidation, locks for stampede safety, SWR for slow upstreams, or a semantic cache over vectors.
14
+ - You're inside a Warlock app (the framework wires the driver from config) — or standalone, calling `setCacheConfigurations` + `cache.init` yourself.
15
+
16
+ Skip if a plain `Map` covers your needs and you'll never need a second driver, TTLs, or invalidation.
17
+
18
+ ## The mental model in one paragraph
19
+
20
+ A single `cache` singleton fronts a configured driver. `cache.set(key, value, options?)` writes (TTL via `ttl`/`expiresAt`, inline `tags`, `onConflict`, a per-call `driver` override, or a `vector` for similarity); `cache.get` / `has` / `pull` / `remove` / `many` / `remember` read. Tags let you invalidate sets of keys you can't enumerate ahead of time; namespaces auto-prefix keys with shared TTL/tag defaults; locks serialize work across processes; SWR serves stale-but-instant while refreshing in the background; `update`/`merge` do atomic read-modify-write; `list<T>(key)` gives ordered collections; `similar(vector, …)` does nearest-neighbor retrieval. `cache.metrics()` and `cache.on(event, …)` make it observable.
21
+
22
+ ## Skills index
23
+
24
+ Nineteen task skills. Most apps start with `cache-basics` + `pick-cache-driver` + `configure-set-options`.
25
+
26
+ ### Foundations
27
+
28
+ - [`cache-basics`](@warlock.js/cache/cache-basics/SKILL.md) — the `cache` singleton, primary ops (`set`/`get`/`has`/`pull`/`remove`/`many`/`forever`/`increment`/`decrement`/`remember`), TTL shapes, init flow. **Start here.**
29
+ - [`pick-cache-driver`](@warlock.js/cache/pick-cache-driver/SKILL.md) — choose + configure a driver: `null` / `memory` / `memoryExtended` / `lru` / `file` / `redis` / `pg` / `mock`; `globalPrefix` for multi-tenant scoping.
30
+ - [`configure-set-options`](@warlock.js/cache/configure-set-options/SKILL.md) — `cache.set`'s third argument: `ttl`, `expiresAt`, `tags`, `onConflict` (create/update/upsert), `driver`, `vector`.
31
+ - [`configure-pg-cache`](@warlock.js/cache/configure-pg-cache/SKILL.md) — Postgres driver: KV-only (default) or pgvector mode; caller owns the `pg.Pool`, `driver.schema()` emits the DDL.
32
+
33
+ ### Invalidation + scoping
34
+
35
+ - [`use-cache-tags`](@warlock.js/cache/use-cache-tags/SKILL.md) — tag on write, `cache.tags([...]).invalidate()` drops every bound key.
36
+ - [`use-cache-namespace`](@warlock.js/cache/use-cache-namespace/SKILL.md) — `cache.namespace(prefix, options?)` auto-prefixes keys with scope-level TTL/tag defaults and nested scopes.
37
+
38
+ ### Patterns
39
+
40
+ - [`use-cached-hof`](@warlock.js/cache/use-cached-hof/SKILL.md) — `cached(fn, options)` wraps an async function; one declaration, many call sites, a bound `.invalidate(...args)`.
41
+ - [`apply-cache-patterns`](@warlock.js/cache/apply-cache-patterns/SKILL.md) — `remember()` memoization, distributed locks via `onConflict: "create"`, negative caching, counters, per-tenant prefix, `CACHE_FOR.*` TTL constants.
42
+ - [`use-cache-lock`](@warlock.js/cache/use-cache-lock/SKILL.md) — `cache.lock(key, ttl, fn)`: acquire → run → auto-release. For cron/imports/migrations and idempotent webhook/payment processing.
43
+ - [`use-swr`](@warlock.js/cache/use-swr/SKILL.md) — `cache.swr(key, { freshTtl, staleTtl }, fn)`: instant when fresh, instant + background refresh when stale, blocks only when fully expired.
44
+ - [`use-cache-update-merge`](@warlock.js/cache/use-cache-update-merge/SKILL.md) — atomic read-modify-write via `cache.update(key, fn)` / `cache.merge(key, partial)`, serialized per key, TTL-preserving.
45
+ - [`use-cache-atomic`](@warlock.js/cache/use-cache-atomic/SKILL.md) — `cache.increment` / `cache.decrement` counters; per-driver atomicity + TTL behavior.
46
+ - [`use-cache-bulk`](@warlock.js/cache/use-cache-bulk/SKILL.md) — `cache.many(keys)` / `cache.setMany(record, ttl?)` for batch reads/writes.
47
+ - [`use-cache-list`](@warlock.js/cache/use-cache-list/SKILL.md) — `cache.list<T>(key)`: `push`/`unshift`/`pop`/`shift`/`slice`/`trim`/`clear` for queues, recent-N buffers, sliding windows.
48
+ - [`use-cache-similarity`](@warlock.js/cache/use-cache-similarity/SKILL.md) — `cache.similar(vector, { topK, threshold?, tags? })` for semantic caches, RAG retrieval, nearest-neighbor lookup.
49
+
50
+ ### Operations
51
+
52
+ - [`observe-cache`](@warlock.js/cache/observe-cache/SKILL.md) — `cache.metrics()` (hit rate, latency p50/p95/p99) + the event bus (`cache.on("hit" | "miss" | "set" | "removed" | "flushed" | "expired" | "connected" | "disconnected" | "error", …)`).
53
+ - [`handle-cache-errors`](@warlock.js/cache/handle-cache-errors/SKILL.md) — the error classes: `CacheError`, `CacheConfigurationError`, `CacheConnectionError`, `CacheDriverNotInitializedError`, `CacheUnsupportedError`, `CacheConcurrencyError`.
54
+ - [`test-cache-code`](@warlock.js/cache/test-cache-code/SKILL.md) — `MockCacheDriver` (behavioral assertions), `MemoryCacheDriver` (full-stack), `NullCacheDriver` (graceful degradation).
55
+
56
+ ### Utilities
57
+
58
+ - [`use-cache-utils`](@warlock.js/cache/use-cache-utils/SKILL.md) — low-level re-exports: `parseTtl`, `parseCacheKey`, `resolveTtl`, `expiresAtToTtl`, `mergeTagSets`, `injectTags`, `cosineSimilarity`, and the `CACHE_FOR` TTL enum.
59
+
60
+ ## What this package deliberately doesn't do
61
+
62
+ - **Be a database.** It's a cache — entries expire, drivers may evict. Don't store anything you can't recompute.
63
+ - **Guarantee cross-driver feature parity.** Vector similarity needs a memory-family driver (`memory` / `memoryExtended` / `lru`, brute force) or `pg` with pgvector; `redis` and `file` raise `CacheUnsupportedError`. Locks/tags behave per driver. Unsupported ops raise `CacheUnsupportedError` rather than silently degrading.
64
+ - **Own your Postgres pool.** The `pg` driver takes the `pg.Pool`/`Client` you already built and never closes it — connection lifecycle stays yours. (Redis is the opposite: you pass `url`/`host` options and the driver builds and owns the client, calling `quit()` on `disconnect()`.)
65
+
66
+ ## See also
67
+
68
+ - [`@warlock.js/core/overview/SKILL.md`](@warlock.js/core/overview/SKILL.md) — wires the cache driver from app config and exposes the singleton.
69
+ - `mongez-agent-kit-authoring-skills` (load via agent-kit sync) — how this becomes `.claude/skills/warlock-js-cache-overview/`.
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: pick-cache-driver
3
+ description: 'Pick a cache driver — null / memory / memoryExtended / lru / file / redis / pg / mock — and configure it. Triggers: `cache.setCacheConfigurations`, `BaseCacheDriver`, `cache.use`, `cache.load`, `cache.driver`, `globalPrefix`; "which cache driver should I use", "configure redis driver", "register custom cache driver", "multi-tenant scoping"; typical import `import { cache, BaseCacheDriver } from "@warlock.js/cache"`. Skip: cache CRUD — `@warlock.js/cache/cache-basics/SKILL.md`; pg setup — `@warlock.js/cache/configure-pg-cache/SKILL.md`; competing libs `lru-cache`, `node-cache`, `keyv`, `ioredis`; native `Map`.'
4
+ ---
5
+
6
+ # Cache drivers — pick the right one
7
+
8
+ Seven production drivers + a mock driver ship in-box. Pick by durability, scope, and workload.
9
+
10
+ | Driver | Process scope | Persists on restart | Good for | Avoid when |
11
+ | --- | --- | --- | --- | --- |
12
+ | `null` | — | — | Disabling cache in tests; feature-flagging off | You actually want caching |
13
+ | `memory` | Single process | No | Hot in-process data with default TTL; smallest latency | Multi-process / multi-node |
14
+ | `memoryExtended` | Single process | No | Sliding-window TTL (TTL resets on every read) | Any multi-process deploy |
15
+ | `lru` | Single process | No | Bounded in-memory caches (capacity-based eviction) | Need cross-process sharing |
16
+ | `file` | Single host | Yes | Build artefacts, local dev persistence across restarts | Concurrency (no locks); multi-host |
17
+ | `redis` | Shared | Yes (Redis-managed) | Anything shared across processes / nodes | Single-process-only workload — overkill |
18
+ | `pg` | Shared | Yes (Postgres-managed) | You already run Postgres; semantic caching / RAG via pgvector | High-throughput hot reads (Redis is faster) |
19
+
20
+ ## Capability matrix
21
+
22
+ | Capability | null | memory | memoryExt | lru | file | redis | pg |
23
+ | --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
24
+ | `set` / `get` / `remove` / `flush` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
25
+ | TTL (number or string) | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
26
+ | Sliding TTL on read | — | — | ✓ | — | — | — | — |
27
+ | `removeNamespace` | noop | ✓ | ✓ | ✓ (prefix-scan) | ✓ | ✓ | ✓ (LIKE prefix) |
28
+ | `onConflict: "create"` / `"update"` | noop | emulated | emulated | emulated | emulated | native `NX`/`XX` | native (INSERT ON CONFLICT) |
29
+ | Native increment / decrement | — | ✓ | ✓ | ✓ | ✓ | atomic `INCRBY`/`DECRBY` | ✓ |
30
+ | `update()` / `merge()` | ✓ | ✓ | ✓ | ✓ | ✗ throws | ✓ (single-process safety only today) | ✓ |
31
+ | List sub-API | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (O(n) JSON blob today; native LPUSH/LRANGE in v2.1) | ✓ |
32
+ | Tagged invalidation | noop | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (native GIN(tags)) |
33
+ | `similar()` / `set({ vector })` | returns `[]` / noop | ✓ brute force | ✓ brute force | ✓ brute force | ✗ throws | ✗ throws (Phase 2 backlog) | ✓ (with `vector` config — pgvector) |
34
+
35
+ ## Global config TTL — accepts number or string
36
+
37
+ ```ts
38
+ options: {
39
+ redis: { url: "...", ttl: "7d" }, // string OK
40
+ memory: { ttl: 3600 }, // number OK
41
+ lru: { capacity: 10_000 }, // LRU has no TTL option today
42
+ file: { directory: () => "/var/cache/myapp", ttl: "1h" },
43
+ pg: { client: pool, ttl: "1h" }, // KV-only
44
+ // pg with pgvector:
45
+ // pg: { client: pool, vector: { dimensions: 1536, index: "hnsw" } },
46
+ }
47
+ ```
48
+
49
+ ## Global prefix (multi-tenant scoping)
50
+
51
+ Every driver accepts `globalPrefix: string | (() => string)`. The function form runs per call — pair it with request-local async context to scope every cached key to the current tenant / user / client automatically:
52
+
53
+ ```ts
54
+ options: {
55
+ redis: {
56
+ url: "...",
57
+ globalPrefix: () => `tenant-${currentContext.tenantId}`,
58
+ },
59
+ }
60
+ ```
61
+
62
+ ## Registering a custom driver
63
+
64
+ ```ts
65
+ import { BaseCacheDriver, cache } from "@warlock.js/cache";
66
+
67
+ class MemcachedCacheDriver extends BaseCacheDriver<MyClient, MyOptions> {
68
+ public name = "memcached";
69
+ // … implement set / get / remove / flush / removeNamespace / connect
70
+ }
71
+
72
+ cache.setCacheConfigurations({
73
+ default: "memcached",
74
+ drivers: { memcached: MemcachedCacheDriver },
75
+ options: { memcached: { host: "localhost" } },
76
+ });
77
+ ```
78
+
79
+ Extending `BaseCacheDriver` gives you free: TTL parsing, key parsing, event emission, stampede-safe `remember`, deep-clone-on-read, default `update` / `merge` / `list` implementations.
80
+
81
+ ## Runtime driver options — `cache.use(name, options)`
82
+
83
+ Some driver options can only be built at runtime (`pg`'s `client: pg.Pool`, pre-wired clients). Pass them as the second arg to `cache.use` / `cache.load` / `cache.driver` — they merge over `setCacheConfigurations({ options })` per-key, runtime wins.
84
+
85
+ ```ts
86
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
87
+
88
+ cache.setCacheConfigurations({
89
+ default: "pg",
90
+ drivers: { pg: PgCacheDriver },
91
+ options: { pg: { table: "cache" } }, // static
92
+ });
93
+
94
+ await cache.use("pg", { client: pool }); // runtime — skip init() in this case
95
+ ```
96
+
97
+ Constraints:
98
+ - The driver name must be registered in `setCacheConfigurations({ drivers })` — runtime options don't bypass registration.
99
+ - Once a driver is loaded, calling `use`/`load`/`driver` again with **non-empty** new options throws `CacheConfigurationError`. Register a second driver name if you need a different config.
100
+ - Calling without options (or with `{}`) on an already-loaded driver returns the cached instance silently.
101
+
102
+ ## Per-call driver override
103
+
104
+ When most writes go to the default driver but one call needs a different one:
105
+
106
+ ```ts
107
+ await cache.set("audit:event", event, { driver: "redis" });
108
+ ```
109
+
110
+ The manager loads (and connects) the override driver lazily on first use, then routes that single operation through it without mutating `currentDriver`.
111
+
112
+ ## See also
113
+
114
+ - [`@warlock.js/cache/configure-pg-cache/SKILL.md`](@warlock.js/cache/configure-pg-cache/SKILL.md) — full pg setup (KV-only and pgvector mode)
115
+ - [`@warlock.js/cache/test-cache-code/SKILL.md`](@warlock.js/cache/test-cache-code/SKILL.md) — `MockCacheDriver` and `NullCacheDriver` for tests
@@ -0,0 +1,219 @@
1
+ ---
2
+ name: test-cache-code
3
+ description: 'Test code that touches cache — MockCacheDriver (behavioral assertions with wasCalled / callLog), MemoryCacheDriver (full-stack), NullCacheDriver (graceful-degradation). Triggers: `MockCacheDriver`, `MemoryCacheDriver`, `NullCacheDriver`, `wasCalled`, `callLog`, `getStored`, `reset`, `cache.on`; "assert cache was invalidated", "test code that uses cache.set", "mock cache in vitest", "test similarity without a real embedder", "stub the pg cache driver"; typical import `import { cache, MockCacheDriver, MemoryCacheDriver } from "@warlock.js/cache"`. Skip: real driver picks — `@warlock.js/cache/pick-cache-driver/SKILL.md`; competing libs `jest-mock`, `sinon`, `redis-mock`; native `vi.fn`.'
4
+ ---
5
+
6
+ # Testing code that touches cache
7
+
8
+ Three good strategies — pick based on what you're testing.
9
+
10
+ ## Strategy 1 — `MockCacheDriver` for behavioral assertions (preferred)
11
+
12
+ When you want to assert "did my service actually invalidate the cache after the update?" or "was `set` called with the right TTL?", reach for `MockCacheDriver`. It implements the full driver contract on a `Map` and adds three introspection helpers:
13
+
14
+ - `wasCalled(operation, key?)` — was a given op invoked? Optional key matched post-`parseKey`.
15
+ - `getStored(key)` — raw stored value, bypassing TTL handling and clone protection.
16
+ - `reset()` — wipe storage, tag index, and call log in one call.
17
+ - `callLog: CacheCall[]` — ordered record of every op (operation, parsed key, raw args, timestamp).
18
+
19
+ ```ts
20
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
21
+ import { cache, MockCacheDriver } from "@warlock.js/cache";
22
+
23
+ describe("UserService.update", () => {
24
+ let driver: MockCacheDriver;
25
+
26
+ beforeEach(async () => {
27
+ cache.setCacheConfigurations({
28
+ default: "mock",
29
+ logging: false,
30
+ drivers: { mock: MockCacheDriver },
31
+ options: { mock: {} },
32
+ });
33
+ await cache.init();
34
+
35
+ driver = cache.currentDriver as MockCacheDriver;
36
+ });
37
+
38
+ afterEach(async () => {
39
+ driver.reset();
40
+ await cache.disconnect();
41
+ });
42
+
43
+ it("invalidates the user cache after update", async () => {
44
+ await new UserService().update(42, { name: "Jane" });
45
+
46
+ expect(driver.wasCalled("remove", "users.42")).toBe(true);
47
+ });
48
+
49
+ it("caches with the right TTL on read-through", async () => {
50
+ await new UserService().getProfile(1);
51
+
52
+ const setCall = driver.callLog.find((call) => call.operation === "set");
53
+ expect(setCall?.args[1]).toBe("1h");
54
+ });
55
+ });
56
+ ```
57
+
58
+ `wasCalled` normalizes object keys, so `wasCalled("set", { id: 1 })` and `wasCalled("set", "id.1")` match the same call.
59
+
60
+ ## Strategy 2 — `MemoryCacheDriver` for full-stack integration tests
61
+
62
+ When you want the real read/write semantics (eviction, TTL expiry, similarity scoring) without the introspection ceremony, `MemoryCacheDriver` is the right pick. Same setup pattern as the mock — swap `MockCacheDriver` for `MemoryCacheDriver`.
63
+
64
+ `MockCacheDriver` does NOT implement `similar()` — vector writes are recorded into the call log but nearest-neighbor scoring is not available. Tests that call `cache.similar(...)` should use the memory driver.
65
+
66
+ ## Strategy 3 — `NullCacheDriver` when you need cache *off*
67
+
68
+ Use `NullCacheDriver` to disable caching entirely for code paths that should still work without a cache (graceful-degradation tests):
69
+
70
+ ```ts
71
+ cache.setCacheConfigurations({
72
+ default: "null",
73
+ drivers: { null: NullCacheDriver },
74
+ options: { null: {} },
75
+ });
76
+ await cache.init();
77
+
78
+ // All cache ops no-op; get() always returns null; set() silently discards.
79
+ ```
80
+
81
+ ## Mocking Redis (for driver-level tests, not app code)
82
+
83
+ For tests that specifically exercise `RedisCacheDriver`, use `vi.mock("redis")` with an in-memory fake. Example (condensed from `redis-cache-driver.spec.ts`):
84
+
85
+ ```ts
86
+ import { vi } from "vitest";
87
+
88
+ class FakeRedisClient {
89
+ public store = new Map<string, string>();
90
+ private expires = new Map<string, number>();
91
+ public on() { return this; }
92
+ public async connect() {}
93
+ public async quit() {}
94
+ public async set(key: string, value: string, opts?: { EX?: number; NX?: boolean; XX?: boolean }) {
95
+ if (opts?.NX && this.store.has(key)) return null;
96
+ if (opts?.XX && !this.store.has(key)) return null;
97
+ this.store.set(key, value);
98
+ if (opts?.EX) this.expires.set(key, Date.now() + opts.EX * 1000);
99
+ return "OK";
100
+ }
101
+ public async get(key: string) {
102
+ const ttl = this.expires.get(key);
103
+ if (ttl && ttl < Date.now()) {
104
+ this.store.delete(key);
105
+ return null;
106
+ }
107
+ return this.store.get(key) ?? null;
108
+ }
109
+ public async del(keys: string | string[]) {
110
+ const arr = Array.isArray(keys) ? keys : [keys];
111
+ let count = 0;
112
+ for (const k of arr) if (this.store.delete(k)) count++;
113
+ return count;
114
+ }
115
+ public async keys(pattern: string) {
116
+ const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
117
+ return [...this.store.keys()].filter((k) => regex.test(k));
118
+ }
119
+ public async flushAll() { this.store.clear(); this.expires.clear(); }
120
+ public async incrBy(key: string, n: number) {
121
+ const next = Number(this.store.get(key) ?? 0) + n;
122
+ this.store.set(key, String(next));
123
+ return next;
124
+ }
125
+ public async decrBy(key: string, n: number) { return this.incrBy(key, -n); }
126
+ }
127
+
128
+ const fakeClient = new FakeRedisClient();
129
+ vi.mock("redis", () => ({ createClient: vi.fn(() => fakeClient) }));
130
+ ```
131
+
132
+ **One-time gotcha:** `RedisCacheDriver` kicks off an async `loadRedis()` at module import that resolves its internal "redis is available" flag. Before running any test that calls `connect()`, wait a short tick after the first dynamic import so the flag flips. The in-tree spec uses:
133
+
134
+ ```ts
135
+ async function importDriver() {
136
+ const mod = await import("./redis-cache-driver");
137
+ await new Promise((resolve) => setTimeout(resolve, 250));
138
+ return mod.RedisCacheDriver;
139
+ }
140
+ ```
141
+
142
+ ## Silencing cache logs in tests
143
+
144
+ The in-package vitest config runs `silent: true`, but if you're outside the package, explicitly disable logging:
145
+
146
+ ```ts
147
+ cache.setCacheConfigurations({
148
+ default: "memory",
149
+ logging: false, // <-- here
150
+ drivers: { memory: MemoryCacheDriver },
151
+ options: { memory: {} },
152
+ });
153
+ ```
154
+
155
+ Or per-driver: `driver.setLoggingState(false)`.
156
+
157
+ ## Spying on events
158
+
159
+ Every driver emits `hit`, `miss`, `set`, `removed`, `flushed`, `expired`. Attach listeners to assert cache behavior without inspecting internal state:
160
+
161
+ ```ts
162
+ const hits = vi.fn();
163
+ cache.on("hit", hits);
164
+
165
+ await service.getProfile("1");
166
+ await service.getProfile("1");
167
+
168
+ expect(hits).toHaveBeenCalledTimes(1); // second call was a hit
169
+ ```
170
+
171
+ Listeners registered via `cache.on(...)` automatically attach to any driver loaded later, so order of `on()` vs `init()` doesn't matter.
172
+
173
+ ## Tests for `update` / `merge` concurrency
174
+
175
+ The chain-serialization guarantee is worth testing when your code fans out concurrent updates:
176
+
177
+ ```ts
178
+ await cache.set("counter", 0);
179
+
180
+ await Promise.all(
181
+ Array.from({ length: 10 }, () =>
182
+ cache.update<number>("counter", (c) => (c ?? 0) + 1),
183
+ ),
184
+ );
185
+
186
+ await expect(cache.get("counter")).resolves.toBe(10);
187
+ ```
188
+
189
+ Runs in-process only — for cross-process safety see [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md).
190
+
191
+ ## Testing similarity code paths
192
+
193
+ `MemoryCacheDriver` runs `similar()` brute-force in-process — perfect for tests of code that calls `cache.similar(...)`. Use stable, hand-written vectors (don't call a real embedder in tests):
194
+
195
+ ```ts
196
+ beforeEach(async () => {
197
+ cache.setCacheConfigurations({
198
+ default: "memory",
199
+ logging: false,
200
+ drivers: { memory: MemoryCacheDriver },
201
+ options: { memory: {} },
202
+ });
203
+ await cache.init();
204
+ });
205
+
206
+ it("returns the most similar doc above threshold", async () => {
207
+ await cache.set("a", { text: "alpha" }, { vector: [1, 0, 0] });
208
+ await cache.set("b", { text: "beta" }, { vector: [0, 1, 0] });
209
+
210
+ const hits = await cache.similar([1, 0, 0], { topK: 1, threshold: 0.5 });
211
+
212
+ expect(hits).toHaveLength(1);
213
+ expect(hits[0].key).toBe("a");
214
+ });
215
+ ```
216
+
217
+ ## Testing the `pg` driver without a real Postgres
218
+
219
+ The `pg` driver accepts any object satisfying `PgClientLike` (`{ query(text, values) }`). For unit tests, hand-roll a minimal Map-backed fake — see `src/drivers/pg-cache-driver.spec.ts` for a 60-line `FakePool` that recognizes the exact SQL shapes the driver issues. For integration coverage, gate a real-PG suite on a `POSTGRES_URL` env var and skip cleanly when unset.