@warlock.js/cache 5.19.1 → 5.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/cjs/index.cjs +1439 -331
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/cache-manager.d.mts +4 -0
  5. package/esm/cache-manager.d.mts.map +1 -1
  6. package/esm/cache-manager.mjs +23 -5
  7. package/esm/cache-manager.mjs.map +1 -1
  8. package/esm/cached/auto-key.d.mts +14 -14
  9. package/esm/cached/auto-key.d.mts.map +1 -1
  10. package/esm/cached/auto-key.mjs +50 -34
  11. package/esm/cached/auto-key.mjs.map +1 -1
  12. package/esm/cached/cached.mjs +4 -0
  13. package/esm/cached/cached.mjs.map +1 -1
  14. package/esm/drivers/base-cache-driver.d.mts +84 -8
  15. package/esm/drivers/base-cache-driver.d.mts.map +1 -1
  16. package/esm/drivers/base-cache-driver.mjs +152 -42
  17. package/esm/drivers/base-cache-driver.mjs.map +1 -1
  18. package/esm/drivers/file-cache-driver.d.mts +15 -0
  19. package/esm/drivers/file-cache-driver.d.mts.map +1 -1
  20. package/esm/drivers/file-cache-driver.mjs +68 -18
  21. package/esm/drivers/file-cache-driver.mjs.map +1 -1
  22. package/esm/drivers/in-memory-tag-index.d.mts +44 -0
  23. package/esm/drivers/in-memory-tag-index.d.mts.map +1 -0
  24. package/esm/drivers/in-memory-tag-index.mjs +73 -0
  25. package/esm/drivers/in-memory-tag-index.mjs.map +1 -0
  26. package/esm/drivers/index.d.mts +1 -1
  27. package/esm/drivers/index.mjs +1 -1
  28. package/esm/drivers/lru-memory-cache-driver.d.mts +43 -0
  29. package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
  30. package/esm/drivers/lru-memory-cache-driver.mjs +96 -0
  31. package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
  32. package/esm/drivers/memory-cache-driver.d.mts +87 -30
  33. package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
  34. package/esm/drivers/memory-cache-driver.mjs +214 -96
  35. package/esm/drivers/memory-cache-driver.mjs.map +1 -1
  36. package/esm/drivers/memory-extended-cache-driver.d.mts +9 -5
  37. package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -1
  38. package/esm/drivers/memory-extended-cache-driver.mjs +10 -23
  39. package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -1
  40. package/esm/drivers/mock-cache-driver.d.mts +41 -0
  41. package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
  42. package/esm/drivers/mock-cache-driver.mjs +105 -8
  43. package/esm/drivers/mock-cache-driver.mjs.map +1 -1
  44. package/esm/drivers/null-cache-driver.d.mts +8 -2
  45. package/esm/drivers/null-cache-driver.d.mts.map +1 -1
  46. package/esm/drivers/null-cache-driver.mjs +10 -2
  47. package/esm/drivers/null-cache-driver.mjs.map +1 -1
  48. package/esm/drivers/pg-cache-driver.d.mts +77 -3
  49. package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
  50. package/esm/drivers/pg-cache-driver.mjs +229 -7
  51. package/esm/drivers/pg-cache-driver.mjs.map +1 -1
  52. package/esm/drivers/redis-cache-driver.d.mts +86 -1
  53. package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
  54. package/esm/drivers/redis-cache-driver.mjs +252 -16
  55. package/esm/drivers/redis-cache-driver.mjs.map +1 -1
  56. package/esm/index.d.mts +2 -2
  57. package/esm/index.mjs +2 -2
  58. package/esm/list/memory-cache-list.d.mts +11 -8
  59. package/esm/list/memory-cache-list.d.mts.map +1 -1
  60. package/esm/list/memory-cache-list.mjs +39 -37
  61. package/esm/list/memory-cache-list.mjs.map +1 -1
  62. package/esm/scoped-cache.d.mts +9 -3
  63. package/esm/scoped-cache.d.mts.map +1 -1
  64. package/esm/scoped-cache.mjs +19 -7
  65. package/esm/scoped-cache.mjs.map +1 -1
  66. package/esm/tagged-cache.d.mts +20 -0
  67. package/esm/tagged-cache.d.mts.map +1 -1
  68. package/esm/tagged-cache.mjs +62 -24
  69. package/esm/tagged-cache.mjs.map +1 -1
  70. package/esm/tagged-scoped-cache.d.mts +7 -3
  71. package/esm/tagged-scoped-cache.d.mts.map +1 -1
  72. package/esm/tagged-scoped-cache.mjs +19 -7
  73. package/esm/tagged-scoped-cache.mjs.map +1 -1
  74. package/esm/types.d.mts +41 -3
  75. package/esm/types.d.mts.map +1 -1
  76. package/esm/types.mjs.map +1 -1
  77. package/esm/utils.d.mts +9 -3
  78. package/esm/utils.d.mts.map +1 -1
  79. package/esm/utils.mjs +16 -6
  80. package/esm/utils.mjs.map +1 -1
  81. package/llms-full.txt +1685 -1666
  82. package/llms.txt +18 -18
  83. package/package.json +4 -4
  84. package/skills/apply-cache-patterns/SKILL.md +98 -97
  85. package/skills/configure-pg-cache/SKILL.md +6 -5
  86. package/skills/configure-set-options/SKILL.md +102 -96
  87. package/skills/handle-cache-errors/SKILL.md +92 -92
  88. package/skills/index.md +20 -0
  89. package/skills/observe-cache/SKILL.md +103 -103
  90. package/skills/pick-cache-driver/SKILL.md +129 -123
  91. package/skills/test-cache-code/SKILL.md +219 -219
  92. package/skills/use-cache-atomic/SKILL.md +57 -65
  93. package/skills/use-cache-bulk/SKILL.md +57 -57
  94. package/skills/use-cache-list/SKILL.md +85 -85
  95. package/skills/use-cache-lock/SKILL.md +113 -104
  96. package/skills/use-cache-namespace/SKILL.md +88 -88
  97. package/skills/use-cache-similarity/SKILL.md +94 -94
  98. package/skills/use-cache-tags/SKILL.md +7 -4
  99. package/skills/use-cache-update-merge/SKILL.md +84 -84
  100. package/skills/use-cache-utils/SKILL.md +89 -89
  101. package/skills/use-cached-hof/SKILL.md +100 -99
  102. package/skills/use-swr/SKILL.md +104 -104
@@ -1,88 +1,88 @@
1
- ---
2
- name: use-cache-namespace
3
- description: "Scoped caches — cache.namespace(prefix, options?) in @warlock.js/cache; use when you need to use cache namespace."
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 (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 [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) for the file-path-containment and redis-glob-escaping details.
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.
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 (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 [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) for the file-path-containment and redis-glob-escaping details.
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.
@@ -1,94 +1,94 @@
1
- ---
2
- name: use-cache-similarity
3
- description: "cache.similar() — vector-based retrieval in @warlock.js/cache; use when you need to use cache similarity."
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.
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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: use-cache-tags
3
- description: "Tag-based invalidation in @warlock.js/cache; use when you need to 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
4
  ---
5
5
 
6
6
  # Tag-based invalidation
@@ -51,9 +51,12 @@ Subsequent `set("user:1", ...)` with **no** `tags` leaves previous associations
51
51
  | Driver | Tag invalidation |
52
52
  | --- | :-: |
53
53
  | `null` | noop |
54
- | `memory` / `memoryExtended` / `lru` / `file` | ✓ (reverse index in driver state) |
55
- | `redis` | ✓ |
56
- | `pg` | ✓ native via `GIN(tags)` index |
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) |
58
+
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.
57
60
 
58
61
  ## SWR with tags
59
62
 
@@ -1,84 +1,84 @@
1
- ---
2
- name: use-cache-update-merge
3
- description: "update and merge — atomic read-modify-write in @warlock.js/cache; use when you need to use cache update merge."
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()` |
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()` |