@warlock.js/cache 5.23.2 → 5.25.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.
- package/CHANGELOG.md +12 -0
- package/README.md +616 -616
- package/cjs/index.cjs +21 -1
- package/cjs/index.cjs.map +1 -1
- package/esm/drivers/base-cache-driver.d.mts +12 -0
- package/esm/drivers/base-cache-driver.d.mts.map +1 -1
- package/esm/drivers/base-cache-driver.mjs +10 -0
- package/esm/drivers/base-cache-driver.mjs.map +1 -1
- package/esm/drivers/null-cache-driver.mjs.map +1 -1
- package/esm/tagged-cache.d.mts.map +1 -1
- package/esm/tagged-cache.mjs +11 -1
- package/esm/tagged-cache.mjs.map +1 -1
- package/esm/types.d.mts +5 -2
- package/esm/types.d.mts.map +1 -1
- package/esm/types.mjs.map +1 -1
- package/esm/utils.mjs.map +1 -1
- package/llms-full.txt +1632 -1631
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/skills/apply-cache-patterns/SKILL.md +95 -95
- package/skills/configure-set-options/SKILL.md +96 -96
- package/skills/handle-cache-errors/SKILL.md +92 -92
- package/skills/observe-cache/SKILL.md +104 -103
- package/skills/pick-cache-driver/SKILL.md +121 -121
- package/skills/test-cache-code/SKILL.md +219 -219
- package/skills/use-cache-atomic/SKILL.md +42 -42
- package/skills/use-cache-bulk/SKILL.md +57 -57
- package/skills/use-cache-list/SKILL.md +85 -85
- package/skills/use-cache-lock/SKILL.md +101 -101
- package/skills/use-cache-namespace/SKILL.md +88 -88
- package/skills/use-cache-similarity/SKILL.md +94 -94
- package/skills/use-cache-update-merge/SKILL.md +84 -84
- package/skills/use-cache-utils/SKILL.md +89 -89
- package/skills/use-cached-hof/SKILL.md +93 -93
- 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
|