@warlock.js/cache 5.24.0 → 5.26.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 (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +616 -616
  3. package/cjs/index.cjs +21 -1
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/drivers/base-cache-driver.d.mts +12 -0
  6. package/esm/drivers/base-cache-driver.d.mts.map +1 -1
  7. package/esm/drivers/base-cache-driver.mjs +10 -0
  8. package/esm/drivers/base-cache-driver.mjs.map +1 -1
  9. package/esm/drivers/null-cache-driver.mjs.map +1 -1
  10. package/esm/tagged-cache.d.mts.map +1 -1
  11. package/esm/tagged-cache.mjs +11 -1
  12. package/esm/tagged-cache.mjs.map +1 -1
  13. package/esm/types.d.mts +5 -2
  14. package/esm/types.d.mts.map +1 -1
  15. package/esm/types.mjs.map +1 -1
  16. package/esm/utils.mjs.map +1 -1
  17. package/llms-full.txt +1632 -1631
  18. package/llms.txt +1 -1
  19. package/package.json +3 -3
  20. package/skills/apply-cache-patterns/SKILL.md +95 -95
  21. package/skills/configure-set-options/SKILL.md +96 -96
  22. package/skills/handle-cache-errors/SKILL.md +92 -92
  23. package/skills/observe-cache/SKILL.md +104 -103
  24. package/skills/pick-cache-driver/SKILL.md +121 -121
  25. package/skills/test-cache-code/SKILL.md +219 -219
  26. package/skills/use-cache-atomic/SKILL.md +42 -42
  27. package/skills/use-cache-bulk/SKILL.md +57 -57
  28. package/skills/use-cache-list/SKILL.md +85 -85
  29. package/skills/use-cache-lock/SKILL.md +101 -101
  30. package/skills/use-cache-namespace/SKILL.md +88 -88
  31. package/skills/use-cache-similarity/SKILL.md +94 -94
  32. package/skills/use-cache-update-merge/SKILL.md +84 -84
  33. package/skills/use-cache-utils/SKILL.md +89 -89
  34. package/skills/use-cached-hof/SKILL.md +93 -93
  35. package/skills/use-swr/SKILL.md +104 -104
@@ -1,94 +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.
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,84 +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()` |
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()` |
@@ -1,89 +1,89 @@
1
- ---
2
- name: use-cache-utils
3
- description: 'Low-level cache utilities re-exported from @warlock.js/cache — parseTtl, expiresAtToTtl, resolveTtl, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, mergeTagSets, injectTags, cosineSimilarity, and the CACHE_FOR TTL enum. Triggers: `parseTtl`, `parseCacheKey`, `resolveTtl`, `expiresAtToTtl`, `cosineSimilarity`, `mergeTagSets`, `injectTags`, `CACHE_FOR`; "parse a duration to seconds", "build a cache key from an object", "score two vectors", "common TTL constant"; typical import `import { parseTtl, CACHE_FOR } from "@warlock.js/cache"`. Skip: building a whole custom driver — `@warlock.js/cache/pick-cache-driver/SKILL.md`; the high-level set API — `@warlock.js/cache/configure-set-options/SKILL.md`.'
4
- ---
5
-
6
- # Cache utilities
7
-
8
- The helpers the drivers are built from, all re-exported from
9
- `@warlock.js/cache`. You rarely need them at the call site — `cache.set("k", v,
10
- "1h")` parses the duration for you. They earn their keep when you write a custom
11
- driver or do cache-adjacent work outside the manager.
12
-
13
- ## TTL helpers
14
-
15
- ```ts
16
- import { parseTtl, expiresAtToTtl, resolveTtl } from "@warlock.js/cache";
17
-
18
- parseTtl(3600); // 3600
19
- parseTtl("1h"); // 3600 (duration string via `ms`)
20
- parseTtl(Infinity); // Infinity (no expiry)
21
- parseTtl(-5); // throws CacheConfigurationError
22
-
23
- expiresAtToTtl(new Date(Date.now() + 60_000)); // ~60 (absolute → relative seconds)
24
- expiresAtToTtl(Date.now() - 1000); // throws — deadline in the past
25
-
26
- // caller ttl > expiresAt > fallback; ttl+expiresAt together throws
27
- resolveTtl("1h", undefined, Infinity); // 3600
28
- resolveTtl(undefined, undefined, 1800); // 1800 (fallback)
29
- ```
30
-
31
- ## Option normalizers
32
-
33
- Coerce the polymorphic 2nd/3rd argument of `set`/`remember` into a uniform shape
34
- — what `BaseCacheDriver` uses internally:
35
-
36
- ```ts
37
- import { normalizeToOptions, normalizeToRememberOptions } from "@warlock.js/cache";
38
-
39
- normalizeToOptions(60); // { ttl: 60 }
40
- normalizeToOptions("1h"); // { ttl: "1h" }
41
- normalizeToOptions({ tags: ["x"] }); // returned as-is
42
- normalizeToRememberOptions("1h"); // { ttl: "1h" } (no expiresAt/onConflict)
43
- ```
44
-
45
- ## Key + tag helpers
46
-
47
- ```ts
48
- import { parseCacheKey, mergeTagSets, injectTags } from "@warlock.js/cache";
49
-
50
- parseCacheKey("users:1"); // "users.1"
51
- parseCacheKey({ page: 1, q: "John" }); // "page.1.q.John"
52
- parseCacheKey("user:1", { globalPrefix: "app" }); // "app.user.1"
53
-
54
- mergeTagSets(["a", "b"], ["b", "c"]); // ["a","b","c"] (deduped union)
55
- mergeTagSets(undefined, undefined); // undefined
56
-
57
- injectTags({ ttl: "1h" }, ["unread"]); // { ttl: "1h", tags: ["unread"] } (pure, no mutation)
58
- ```
59
-
60
- ## Vector scoring
61
-
62
- ```ts
63
- import { cosineSimilarity } from "@warlock.js/cache";
64
-
65
- cosineSimilarity([1, 0, 0], [1, 0, 0]); // 1
66
- cosineSimilarity([1, 0, 0], [0, 1, 0]); // 0
67
- cosineSimilarity([1, 2, 3], [1, 2]); // throws — dimension mismatch
68
- ```
69
-
70
- Powers the brute-force `cache.similar()` on the memory drivers — reach for it
71
- directly only when scoring vectors outside the cache.
72
-
73
- ## TTL constants — `CACHE_FOR`
74
-
75
- ```ts
76
- import { cache, CACHE_FOR } from "@warlock.js/cache";
77
-
78
- await cache.set("report", data, CACHE_FOR.ONE_WEEK);
79
- ```
80
-
81
- Members: `HALF_HOUR`, `ONE_HOUR`, `HALF_DAY`, `ONE_DAY`, `ONE_WEEK`,
82
- `HALF_MONTH`, `ONE_MONTH`, `TWO_MONTHS`, `SIX_MONTHS`, `ONE_YEAR` (all seconds).
83
- For most call sites the duration string (`"1h"`, `"7d"`) reads better.
84
-
85
- ## See also
86
-
87
- - [`@warlock.js/cache/configure-set-options/SKILL.md`](@warlock.js/cache/configure-set-options/SKILL.md) — the high-level `set` options these normalize
88
- - [`@warlock.js/cache/use-cache-similarity/SKILL.md`](@warlock.js/cache/use-cache-similarity/SKILL.md) — `cache.similar()`, which uses `cosineSimilarity`
89
- - [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) — building a custom driver where these help
1
+ ---
2
+ name: use-cache-utils
3
+ description: 'Low-level cache utilities re-exported from @warlock.js/cache — parseTtl, expiresAtToTtl, resolveTtl, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, mergeTagSets, injectTags, cosineSimilarity, and the CACHE_FOR TTL enum. Triggers: `parseTtl`, `parseCacheKey`, `resolveTtl`, `expiresAtToTtl`, `cosineSimilarity`, `mergeTagSets`, `injectTags`, `CACHE_FOR`; "parse a duration to seconds", "build a cache key from an object", "score two vectors", "common TTL constant"; typical import `import { parseTtl, CACHE_FOR } from "@warlock.js/cache"`. Skip: building a whole custom driver — `@warlock.js/cache/pick-cache-driver/SKILL.md`; the high-level set API — `@warlock.js/cache/configure-set-options/SKILL.md`.'
4
+ ---
5
+
6
+ # Cache utilities
7
+
8
+ The helpers the drivers are built from, all re-exported from
9
+ `@warlock.js/cache`. You rarely need them at the call site — `cache.set("k", v,
10
+ "1h")` parses the duration for you. They earn their keep when you write a custom
11
+ driver or do cache-adjacent work outside the manager.
12
+
13
+ ## TTL helpers
14
+
15
+ ```ts
16
+ import { parseTtl, expiresAtToTtl, resolveTtl } from "@warlock.js/cache";
17
+
18
+ parseTtl(3600); // 3600
19
+ parseTtl("1h"); // 3600 (duration string via `ms`)
20
+ parseTtl(Infinity); // Infinity (no expiry)
21
+ parseTtl(-5); // throws CacheConfigurationError
22
+
23
+ expiresAtToTtl(new Date(Date.now() + 60_000)); // ~60 (absolute → relative seconds)
24
+ expiresAtToTtl(Date.now() - 1000); // throws — deadline in the past
25
+
26
+ // caller ttl > expiresAt > fallback; ttl+expiresAt together throws
27
+ resolveTtl("1h", undefined, Infinity); // 3600
28
+ resolveTtl(undefined, undefined, 1800); // 1800 (fallback)
29
+ ```
30
+
31
+ ## Option normalizers
32
+
33
+ Coerce the polymorphic 2nd/3rd argument of `set`/`remember` into a uniform shape
34
+ — what `BaseCacheDriver` uses internally:
35
+
36
+ ```ts
37
+ import { normalizeToOptions, normalizeToRememberOptions } from "@warlock.js/cache";
38
+
39
+ normalizeToOptions(60); // { ttl: 60 }
40
+ normalizeToOptions("1h"); // { ttl: "1h" }
41
+ normalizeToOptions({ tags: ["x"] }); // returned as-is
42
+ normalizeToRememberOptions("1h"); // { ttl: "1h" } (no expiresAt/onConflict)
43
+ ```
44
+
45
+ ## Key + tag helpers
46
+
47
+ ```ts
48
+ import { parseCacheKey, mergeTagSets, injectTags } from "@warlock.js/cache";
49
+
50
+ parseCacheKey("users:1"); // "users.1"
51
+ parseCacheKey({ page: 1, q: "John" }); // "page.1.q.John"
52
+ parseCacheKey("user:1", { globalPrefix: "app" }); // "app.user.1"
53
+
54
+ mergeTagSets(["a", "b"], ["b", "c"]); // ["a","b","c"] (deduped union)
55
+ mergeTagSets(undefined, undefined); // undefined
56
+
57
+ injectTags({ ttl: "1h" }, ["unread"]); // { ttl: "1h", tags: ["unread"] } (pure, no mutation)
58
+ ```
59
+
60
+ ## Vector scoring
61
+
62
+ ```ts
63
+ import { cosineSimilarity } from "@warlock.js/cache";
64
+
65
+ cosineSimilarity([1, 0, 0], [1, 0, 0]); // 1
66
+ cosineSimilarity([1, 0, 0], [0, 1, 0]); // 0
67
+ cosineSimilarity([1, 2, 3], [1, 2]); // throws — dimension mismatch
68
+ ```
69
+
70
+ Powers the brute-force `cache.similar()` on the memory drivers — reach for it
71
+ directly only when scoring vectors outside the cache.
72
+
73
+ ## TTL constants — `CACHE_FOR`
74
+
75
+ ```ts
76
+ import { cache, CACHE_FOR } from "@warlock.js/cache";
77
+
78
+ await cache.set("report", data, CACHE_FOR.ONE_WEEK);
79
+ ```
80
+
81
+ Members: `HALF_HOUR`, `ONE_HOUR`, `HALF_DAY`, `ONE_DAY`, `ONE_WEEK`,
82
+ `HALF_MONTH`, `ONE_MONTH`, `TWO_MONTHS`, `SIX_MONTHS`, `ONE_YEAR` (all seconds).
83
+ For most call sites the duration string (`"1h"`, `"7d"`) reads better.
84
+
85
+ ## See also
86
+
87
+ - [`@warlock.js/cache/configure-set-options/SKILL.md`](@warlock.js/cache/configure-set-options/SKILL.md) — the high-level `set` options these normalize
88
+ - [`@warlock.js/cache/use-cache-similarity/SKILL.md`](@warlock.js/cache/use-cache-similarity/SKILL.md) — `cache.similar()`, which uses `cosineSimilarity`
89
+ - [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) — building a custom driver where these help