@warlock.js/cache 5.30.0 → 5.32.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 +26 -8
- package/README.md +12 -21
- package/cjs/index.cjs +50 -31
- package/cjs/index.cjs.map +1 -1
- package/esm/cache-manager.d.mts +5 -5
- package/esm/cache-manager.d.mts.map +1 -1
- package/esm/cache-manager.mjs +2 -2
- package/esm/cache-manager.mjs.map +1 -1
- package/esm/cached/auto-key.d.mts +1 -2
- package/esm/cached/auto-key.d.mts.map +1 -1
- package/esm/cached/cached.d.mts +4 -6
- package/esm/cached/cached.d.mts.map +1 -1
- package/esm/cached/index.d.mts +2 -1
- package/esm/cached/index.mjs +1 -1
- package/esm/cached/normalize-args.d.mts +3 -5
- package/esm/cached/normalize-args.d.mts.map +1 -1
- package/esm/drivers/base-cache-driver.d.mts +5 -5
- package/esm/drivers/base-cache-driver.d.mts.map +1 -1
- package/esm/drivers/base-cache-driver.mjs +4 -3
- package/esm/drivers/base-cache-driver.mjs.map +1 -1
- package/esm/drivers/file-cache-driver.d.mts +1 -3
- package/esm/drivers/file-cache-driver.d.mts.map +1 -1
- package/esm/drivers/file-cache-driver.mjs.map +1 -1
- package/esm/drivers/in-memory-tag-index.d.mts +1 -2
- package/esm/drivers/in-memory-tag-index.d.mts.map +1 -1
- package/esm/drivers/index.d.mts +2 -1
- package/esm/drivers/index.mjs +1 -1
- package/esm/drivers/lru-memory-cache-driver.d.mts +5 -4
- package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/lru-memory-cache-driver.mjs +6 -4
- package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-cache-driver.d.mts +4 -4
- package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-cache-driver.mjs +9 -3
- package/esm/drivers/memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.d.mts +1 -3
- package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -1
- package/esm/drivers/mock-cache-driver.d.mts +4 -4
- package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
- package/esm/drivers/mock-cache-driver.mjs +11 -4
- package/esm/drivers/mock-cache-driver.mjs.map +1 -1
- package/esm/drivers/null-cache-driver.d.mts +1 -3
- package/esm/drivers/null-cache-driver.d.mts.map +1 -1
- package/esm/drivers/pg-cache-driver.d.mts +4 -4
- package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
- package/esm/drivers/pg-cache-driver.mjs +6 -3
- package/esm/drivers/pg-cache-driver.mjs.map +1 -1
- package/esm/drivers/redis-cache-driver.d.mts +9 -9
- package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
- package/esm/drivers/redis-cache-driver.mjs +4 -3
- package/esm/drivers/redis-cache-driver.mjs.map +1 -1
- package/esm/index.d.mts +3 -0
- package/esm/list/index.d.mts +2 -1
- package/esm/list/memory-cache-list.d.mts +1 -3
- package/esm/list/memory-cache-list.d.mts.map +1 -1
- package/esm/list/memory-cache-list.mjs.map +1 -1
- package/esm/metrics.d.mts +4 -4
- package/esm/metrics.d.mts.map +1 -1
- package/esm/metrics.mjs +2 -2
- package/esm/metrics.mjs.map +1 -1
- package/esm/scoped-cache.d.mts +4 -4
- package/esm/scoped-cache.d.mts.map +1 -1
- package/esm/scoped-cache.mjs +2 -2
- package/esm/scoped-cache.mjs.map +1 -1
- package/esm/tagged-cache.d.mts +4 -4
- package/esm/tagged-cache.d.mts.map +1 -1
- package/esm/tagged-cache.mjs +3 -3
- package/esm/tagged-cache.mjs.map +1 -1
- package/esm/tagged-scoped-cache.d.mts +4 -4
- package/esm/tagged-scoped-cache.d.mts.map +1 -1
- package/esm/tagged-scoped-cache.mjs +2 -2
- package/esm/tagged-scoped-cache.mjs.map +1 -1
- package/esm/types.d.mts +86 -62
- package/esm/types.d.mts.map +1 -1
- package/esm/types.mjs.map +1 -1
- package/esm/utils.d.mts +12 -14
- package/esm/utils.d.mts.map +1 -1
- package/esm/utils.mjs.map +1 -1
- package/llms-full.txt +229 -224
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/skills/apply-cache-patterns/SKILL.md +3 -2
- package/skills/cache-basics/SKILL.md +24 -24
- package/skills/configure-pg-cache/SKILL.md +2 -2
- package/skills/configure-set-options/SKILL.md +14 -14
- package/skills/handle-cache-errors/SKILL.md +8 -8
- package/skills/index.md +4 -1
- package/skills/observe-cache/SKILL.md +8 -8
- package/skills/pick-cache-driver/SKILL.md +24 -23
- package/skills/test-cache-code/SKILL.md +15 -10
- package/skills/use-cache-atomic/SKILL.md +17 -11
- package/skills/use-cache-bulk/SKILL.md +8 -5
- package/skills/use-cache-list/SKILL.md +13 -13
- package/skills/use-cache-lock/SKILL.md +11 -17
- package/skills/use-cache-namespace/SKILL.md +11 -13
- package/skills/use-cache-similarity/SKILL.md +9 -9
- package/skills/use-cache-tags/SKILL.md +13 -13
- package/skills/use-cache-update-merge/SKILL.md +10 -10
- package/skills/use-cache-utils/SKILL.md +17 -17
- package/skills/use-cached-hof/SKILL.md +8 -7
- package/skills/use-swr/SKILL.md +14 -18
|
@@ -38,9 +38,7 @@ TTL rules: a number or a bare numeric string (`"90"`) is **seconds**; `"500ms"`
|
|
|
38
38
|
## Return shape — discriminated union
|
|
39
39
|
|
|
40
40
|
```ts
|
|
41
|
-
type LockOutcome<T> =
|
|
42
|
-
| { acquired: true; value: T }
|
|
43
|
-
| { acquired: false };
|
|
41
|
+
type LockOutcome<T> = { acquired: true; value: T } | { acquired: false };
|
|
44
42
|
```
|
|
45
43
|
|
|
46
44
|
Unambiguous even when `fn` returns `undefined`. Narrow with TS:
|
|
@@ -49,7 +47,7 @@ Unambiguous even when `fn` returns `undefined`. Narrow with TS:
|
|
|
49
47
|
const outcome = await cache.lock("lock.x", "1m", async () => compute());
|
|
50
48
|
|
|
51
49
|
if (outcome.acquired) {
|
|
52
|
-
console.log(outcome.value);
|
|
50
|
+
console.log(outcome.value); // typed
|
|
53
51
|
} else {
|
|
54
52
|
console.log("someone else is running");
|
|
55
53
|
}
|
|
@@ -60,19 +58,15 @@ if (outcome.acquired) {
|
|
|
60
58
|
### Cron on only one server
|
|
61
59
|
|
|
62
60
|
```ts
|
|
63
|
-
cron.daily("3am", () =>
|
|
64
|
-
cache.lock("lock.cleanup", "30m", () => db.cleanup()),
|
|
65
|
-
);
|
|
61
|
+
cron.daily("3am", () => cache.lock("lock.cleanup", "30m", () => db.cleanup()));
|
|
66
62
|
```
|
|
67
63
|
|
|
68
64
|
### Idempotent webhook
|
|
69
65
|
|
|
70
66
|
```ts
|
|
71
67
|
app.post("/webhooks/stripe", async (req, res) => {
|
|
72
|
-
const outcome = await cache.lock(
|
|
73
|
-
|
|
74
|
-
"24h",
|
|
75
|
-
() => processStripeEvent(req.body),
|
|
68
|
+
const outcome = await cache.lock(`webhook.stripe.${req.body.id}`, "24h", () =>
|
|
69
|
+
processStripeEvent(req.body),
|
|
76
70
|
);
|
|
77
71
|
|
|
78
72
|
if (!outcome.acquired) {
|
|
@@ -97,13 +91,13 @@ await cache.lock(
|
|
|
97
91
|
|
|
98
92
|
## Driver behavior
|
|
99
93
|
|
|
100
|
-
| Driver
|
|
101
|
-
|
|
102
|
-
| `redis`
|
|
103
|
-
| `pg`
|
|
94
|
+
| Driver | Cross-process safe? |
|
|
95
|
+
| ----------------------------------- | :-----------------------------------------------: |
|
|
96
|
+
| `redis` | ✅ Native `SET … NX EX`, Lua release |
|
|
97
|
+
| `pg` | ✅ `INSERT … ON CONFLICT`, one-statement release |
|
|
104
98
|
| `memory` / `memoryExtended` / `lru` | ❌ In-process only (exclusive within the process) |
|
|
105
|
-
| `file`
|
|
106
|
-
| `null`
|
|
99
|
+
| `file` | ⚠️ Single-host only (exclusive create) |
|
|
100
|
+
| `null` | n/a — always "acquires" |
|
|
107
101
|
|
|
108
102
|
## Gotchas
|
|
109
103
|
|
|
@@ -12,10 +12,10 @@ When you'll touch the same prefix more than a couple of times, grab a scoped han
|
|
|
12
12
|
```ts
|
|
13
13
|
const chat = cache.namespace(`chats.${id}`, { ttl: "30d", tags: [`user.${userId}`] });
|
|
14
14
|
|
|
15
|
-
await chat.set("messages.10", msg);
|
|
16
|
-
await chat.set("draft", d, { ttl: "1h" });
|
|
17
|
-
await chat.tags(["unread"]).set("ping", p);
|
|
18
|
-
await chat.clear();
|
|
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
19
|
```
|
|
20
20
|
|
|
21
21
|
Scopes are pure views — same connection, same driver, no extra state. Per-call options always win over scope defaults; tags merge additively.
|
|
@@ -24,9 +24,9 @@ Scopes are pure views — same connection, same driver, no extra state. Per-call
|
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
26
|
const chat = cache.namespace(`chats.${id}`, { ttl: "30d" });
|
|
27
|
-
const typing = chat.namespace("typing", { ttl: "5s" });
|
|
27
|
+
const typing = chat.namespace("typing", { ttl: "5s" }); // overrides parent ttl
|
|
28
28
|
|
|
29
|
-
await typing.set("user.42", true);
|
|
29
|
+
await typing.set("user.42", true); // chats.<id>.typing.user.42, 5s
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Nested scopes inherit defaults and can override. Tags accumulate.
|
|
@@ -41,14 +41,14 @@ Inline prefixes are still fine for one-off writes.
|
|
|
41
41
|
|
|
42
42
|
## Plain `removeNamespace` when you already have the prefix
|
|
43
43
|
|
|
44
|
-
When you
|
|
44
|
+
When you _do_ know the prefix string and don't need a scoped handle for repeated reads/writes:
|
|
45
45
|
|
|
46
46
|
```ts
|
|
47
47
|
await cache.set("user:1:profile", profile);
|
|
48
|
-
await cache.set("user:1:prefs",
|
|
48
|
+
await cache.set("user:1:prefs", prefs);
|
|
49
49
|
await cache.set("user:2:profile", otherProfile);
|
|
50
50
|
|
|
51
|
-
await cache.removeNamespace("user.1");
|
|
51
|
+
await cache.removeNamespace("user.1"); // drops both user:1 entries, keeps user:2
|
|
52
52
|
```
|
|
53
53
|
|
|
54
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 the `pick-cache-driver` topic for the file-path-containment and redis-glob-escaping details.
|
|
@@ -71,10 +71,8 @@ options: {
|
|
|
71
71
|
```ts
|
|
72
72
|
const feed = cache.namespace(`feed.${userId}`, { tags: [`user.${userId}`] });
|
|
73
73
|
|
|
74
|
-
await feed.swr(
|
|
75
|
-
|
|
76
|
-
{ freshTtl: "30s", staleTtl: "10m", tags: ["computed"] },
|
|
77
|
-
() => buildHomeFeed(userId),
|
|
74
|
+
await feed.swr("home", { freshTtl: "30s", staleTtl: "10m", tags: ["computed"] }, () =>
|
|
75
|
+
buildHomeFeed(userId),
|
|
78
76
|
);
|
|
79
77
|
// stored at feed.<userId>.home, tagged [user.<userId>, computed]
|
|
80
78
|
```
|
|
@@ -24,21 +24,21 @@ const hits = await cache.similar<T>(queryVec, {
|
|
|
24
24
|
|
|
25
25
|
## Capability matrix
|
|
26
26
|
|
|
27
|
-
| Driver
|
|
28
|
-
|
|
27
|
+
| Driver | `similar()` |
|
|
28
|
+
| ----------------------------------- | -------------------------------------------------- |
|
|
29
29
|
| `memory` / `memoryExtended` / `lru` | ✅ Brute force (O(N)) — dev only past ~10k entries |
|
|
30
|
-
| `pg`
|
|
31
|
-
| `pg`
|
|
32
|
-
| `redis`
|
|
33
|
-
| `file`
|
|
34
|
-
| `null`
|
|
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
35
|
|
|
36
36
|
## Always-true facts
|
|
37
37
|
|
|
38
38
|
1. **Cache is embedding-agnostic.** Caller computes vectors. The cache stores and ranks; it doesn't call out to an embedder.
|
|
39
39
|
2. **Only entries written with `set({ vector })` show up.** A plain `set` adds the entry as KV — invisible to `similar()`.
|
|
40
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
|
|
41
|
+
4. **Tag filter narrows the candidate pool _before_ ranking** — union semantics (entry must carry at least one of the listed tags).
|
|
42
42
|
5. **Dimension mismatch throws `CacheConfigurationError`** at both `set({ vector })` and `similar()` time. Don't switch embedders without re-indexing.
|
|
43
43
|
6. **TTL + LRU eviction also drop the vector** — expired or evicted entries are invisible to `similar()`.
|
|
44
44
|
|
|
@@ -51,7 +51,7 @@ const queryVec = await embed(prompt);
|
|
|
51
51
|
const hits = await cache.similar<Answer>(queryVec, { topK: 1, threshold: 0.92 });
|
|
52
52
|
|
|
53
53
|
if (hits.length > 0) {
|
|
54
|
-
return hits[0].value;
|
|
54
|
+
return hits[0].value; // skip the LLM call
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
const answer = await llm.complete(prompt);
|
|
@@ -12,7 +12,7 @@ Tags let you invalidate by a label rather than by enumerating every key. Use the
|
|
|
12
12
|
```ts
|
|
13
13
|
// Inline — terser when you know the tags up front
|
|
14
14
|
await cache.set("user:1:profile", profile, { tags: ["users", "tenant-42"] });
|
|
15
|
-
await cache.set("user:1:prefs",
|
|
15
|
+
await cache.set("user:1:prefs", prefs, { tags: ["users", "tenant-42"] });
|
|
16
16
|
|
|
17
17
|
// Fluent — useful when you already have a tagged handle
|
|
18
18
|
const users = cache.tags(["users"]);
|
|
@@ -34,11 +34,11 @@ Multi-tag is **union** semantics: an entry is invalidated if it carries **at lea
|
|
|
34
34
|
|
|
35
35
|
## When to reach for tags vs namespaces
|
|
36
36
|
|
|
37
|
-
| Use case
|
|
38
|
-
|
|
|
39
|
-
| The keys share a known prefix
|
|
40
|
-
| The keys are spread across prefixes, tied by entity | Tags
|
|
41
|
-
| Both apply
|
|
37
|
+
| Use case | Reach for |
|
|
38
|
+
| --------------------------------------------------- | ------------------------------------------------------------------- |
|
|
39
|
+
| The keys share a known prefix | `cache.removeNamespace("prefix")` (the `use-cache-namespace` topic) |
|
|
40
|
+
| The keys are spread across prefixes, tied by entity | Tags |
|
|
41
|
+
| Both apply | Tags — more flexible; cheap on most drivers |
|
|
42
42
|
|
|
43
43
|
Namespaces are cheaper (no reverse index). Tags are more powerful (any key can carry any tag).
|
|
44
44
|
|
|
@@ -48,13 +48,13 @@ Subsequent `set("user:1", ...)` with **no** `tags` leaves previous associations
|
|
|
48
48
|
|
|
49
49
|
## Driver behavior
|
|
50
50
|
|
|
51
|
-
| Driver
|
|
52
|
-
|
|
|
53
|
-
| `null`
|
|
51
|
+
| Driver | Tag invalidation |
|
|
52
|
+
| ----------------------------------- | :----------------------------------------------------------: |
|
|
53
|
+
| `null` | noop |
|
|
54
54
|
| `memory` / `memoryExtended` / `lru` | ✓ (separate index store; eviction and expiry never touch it) |
|
|
55
|
-
| `file`
|
|
56
|
-
| `redis`
|
|
57
|
-
| `pg`
|
|
55
|
+
| `file` | ✓ (serialized index) |
|
|
56
|
+
| `redis` | ✓ (native SET inside the prefix) |
|
|
57
|
+
| `pg` | ✓ (JSON-array merge on the index row) |
|
|
58
58
|
|
|
59
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.
|
|
60
60
|
|
|
@@ -76,7 +76,7 @@ Tags re-apply on every successful refresh — see the `use-swr` topic.
|
|
|
76
76
|
const getUser = cached(fn, { key: (id) => `user.${id}`, ttl: "1h", tags: ["users"] });
|
|
77
77
|
const getPosts = cached(fn, { key: (u) => `posts.by.${u}`, ttl: "30m", tags: ["users", "posts"] });
|
|
78
78
|
|
|
79
|
-
await cache.tags(["users"]).invalidate();
|
|
79
|
+
await cache.tags(["users"]).invalidate(); // drops both wrappers' caches
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
See the `use-cached-hof` topic.
|
|
@@ -24,7 +24,7 @@ await cache.update<UserState>("user:1:state", (current) => ({
|
|
|
24
24
|
// Conditional update — return null to remove
|
|
25
25
|
await cache.update<Session>("session:abc", (current) => {
|
|
26
26
|
if (!current || current.expired) {
|
|
27
|
-
return null;
|
|
27
|
+
return null; // removes the key
|
|
28
28
|
}
|
|
29
29
|
return { ...current, extendedAt: Date.now() };
|
|
30
30
|
});
|
|
@@ -67,18 +67,18 @@ await Promise.all(
|
|
|
67
67
|
),
|
|
68
68
|
);
|
|
69
69
|
|
|
70
|
-
await cache.get("counter");
|
|
70
|
+
await cache.get("counter"); // 10 — not a lost-update
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
The per-key lock map lives on the driver instance and is cleared after each chain link finishes. No leak.
|
|
74
74
|
|
|
75
75
|
## When to reach for what
|
|
76
76
|
|
|
77
|
-
| Task
|
|
78
|
-
|
|
|
79
|
-
| "Add 1 to this counter"
|
|
80
|
-
| "Change one field on a cached object"
|
|
81
|
-
| "Read-decide-maybe-write, possibly remove" | `update()`
|
|
82
|
-
| "Only set if missing"
|
|
83
|
-
| "Only set if already exists"
|
|
84
|
-
| "Read-then-delete atomically"
|
|
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()` |
|
|
@@ -15,17 +15,17 @@ driver or do cache-adjacent work outside the manager.
|
|
|
15
15
|
```ts
|
|
16
16
|
import { parseTtl, expiresAtToTtl, resolveTtl } from "@warlock.js/cache";
|
|
17
17
|
|
|
18
|
-
parseTtl(3600);
|
|
19
|
-
parseTtl("1h");
|
|
20
|
-
parseTtl(Infinity);
|
|
21
|
-
parseTtl(-5);
|
|
18
|
+
parseTtl(3600); // 3600
|
|
19
|
+
parseTtl("1h"); // 3600 (duration string via `ms`)
|
|
20
|
+
parseTtl(Infinity); // Infinity (no expiry)
|
|
21
|
+
parseTtl(-5); // throws CacheConfigurationError
|
|
22
22
|
|
|
23
23
|
expiresAtToTtl(new Date(Date.now() + 60_000)); // ~60 (absolute → relative seconds)
|
|
24
|
-
expiresAtToTtl(Date.now() - 1000);
|
|
24
|
+
expiresAtToTtl(Date.now() - 1000); // throws — deadline in the past
|
|
25
25
|
|
|
26
26
|
// caller ttl > expiresAt > fallback; ttl+expiresAt together throws
|
|
27
|
-
resolveTtl("1h", undefined, Infinity);
|
|
28
|
-
resolveTtl(undefined, undefined, 1800);
|
|
27
|
+
resolveTtl("1h", undefined, Infinity); // 3600
|
|
28
|
+
resolveTtl(undefined, undefined, 1800); // 1800 (fallback)
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## Option normalizers
|
|
@@ -36,10 +36,10 @@ Coerce the polymorphic 2nd/3rd argument of `set`/`remember` into a uniform shape
|
|
|
36
36
|
```ts
|
|
37
37
|
import { normalizeToOptions, normalizeToRememberOptions } from "@warlock.js/cache";
|
|
38
38
|
|
|
39
|
-
normalizeToOptions(60);
|
|
40
|
-
normalizeToOptions("1h");
|
|
41
|
-
normalizeToOptions({ tags: ["x"] });
|
|
42
|
-
normalizeToRememberOptions("1h");
|
|
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
43
|
```
|
|
44
44
|
|
|
45
45
|
## Key + tag helpers
|
|
@@ -47,14 +47,14 @@ normalizeToRememberOptions("1h"); // { ttl: "1h" } (no expiresAt/onConfl
|
|
|
47
47
|
```ts
|
|
48
48
|
import { parseCacheKey, mergeTagSets, injectTags } from "@warlock.js/cache";
|
|
49
49
|
|
|
50
|
-
parseCacheKey("users:1");
|
|
51
|
-
parseCacheKey({ page: 1, q: "John" });
|
|
50
|
+
parseCacheKey("users:1"); // "users.1"
|
|
51
|
+
parseCacheKey({ page: 1, q: "John" }); // "page.1.q.John"
|
|
52
52
|
parseCacheKey("user:1", { globalPrefix: "app" }); // "app.user.1"
|
|
53
53
|
|
|
54
|
-
mergeTagSets(["a", "b"], ["b", "c"]);
|
|
55
|
-
mergeTagSets(undefined, undefined);
|
|
54
|
+
mergeTagSets(["a", "b"], ["b", "c"]); // ["a","b","c"] (deduped union)
|
|
55
|
+
mergeTagSets(undefined, undefined); // undefined
|
|
56
56
|
|
|
57
|
-
injectTags({ ttl: "1h" }, ["unread"]);
|
|
57
|
+
injectTags({ ttl: "1h" }, ["unread"]); // { ttl: "1h", tags: ["unread"] } (pure, no mutation)
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
## Vector scoring
|
|
@@ -64,7 +64,7 @@ import { cosineSimilarity } from "@warlock.js/cache";
|
|
|
64
64
|
|
|
65
65
|
cosineSimilarity([1, 0, 0], [1, 0, 0]); // 1
|
|
66
66
|
cosineSimilarity([1, 0, 0], [0, 1, 0]); // 0
|
|
67
|
-
cosineSimilarity([1, 2, 3], [1, 2]);
|
|
67
|
+
cosineSimilarity([1, 2, 3], [1, 2]); // throws — dimension mismatch
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
Powers the brute-force `cache.similar()` on the memory drivers — reach for it
|
|
@@ -10,6 +10,7 @@ description: 'Wrap an async function with cached(fn, options) — declare the ca
|
|
|
10
10
|
## When to use
|
|
11
11
|
|
|
12
12
|
Reach for `cached()` instead of `cache.remember()` when:
|
|
13
|
+
|
|
13
14
|
- You have a function you'll call from many places.
|
|
14
15
|
- You want the caching strategy declared once, not repeated at every call site.
|
|
15
16
|
- You want `.invalidate()` available without manually deriving keys.
|
|
@@ -38,11 +39,11 @@ cached(fn, {
|
|
|
38
39
|
|
|
39
40
|
## Auto-key rules (shorthand only)
|
|
40
41
|
|
|
41
|
-
| Args
|
|
42
|
-
|
|
43
|
-
| None
|
|
44
|
-
| Any args
|
|
45
|
-
| `Map` / `Set` / function / symbol / class instance / circular | throws `CacheConfigurationError`
|
|
42
|
+
| Args | Key |
|
|
43
|
+
| ------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
44
|
+
| None | `prefix` |
|
|
45
|
+
| Any args | `prefix.<16 hex>` — sha1 of a stable JSON encoding of the args, truncated |
|
|
46
|
+
| `Map` / `Set` / function / symbol / class instance / circular | throws `CacheConfigurationError` |
|
|
46
47
|
|
|
47
48
|
The hash keeps distinct args on distinct keys (`("1.private", "")` ≠ `("1", "private")`). Object keys are sorted; `Date` → ISO string; order of args matters. Never build keys by hand: call `.invalidate(...args)`, which honours `config.driver`. When auto-key fails, use the options form with a custom `key` fn.
|
|
48
49
|
|
|
@@ -76,7 +77,7 @@ await getUser.invalidate(42);
|
|
|
76
77
|
const getUser = cached(fn, { key: (id) => `user.${id}`, ttl: "1h", tags: ["users"] });
|
|
77
78
|
const getPosts = cached(fn, { key: (u) => `posts.by.${u}`, ttl: "30m", tags: ["users", "posts"] });
|
|
78
79
|
|
|
79
|
-
await cache.tags(["users"]).invalidate();
|
|
80
|
+
await cache.tags(["users"]).invalidate(); // drops both wrappers' caches
|
|
80
81
|
```
|
|
81
82
|
|
|
82
83
|
See the `use-cache-tags` topic.
|
|
@@ -86,7 +87,7 @@ See the `use-cache-tags` topic.
|
|
|
86
87
|
```ts
|
|
87
88
|
const getCategoryMeta = cached(
|
|
88
89
|
(filters: Filters) => db.categories.meta(filters.category),
|
|
89
|
-
{ key: (f) => `category.meta.${f.category}`, ttl: "1h" },
|
|
90
|
+
{ key: (f) => `category.meta.${f.category}`, ttl: "1h" }, // ignores `sort`, `page`
|
|
90
91
|
);
|
|
91
92
|
```
|
|
92
93
|
|
package/skills/use-swr/SKILL.md
CHANGED
|
@@ -24,11 +24,11 @@ Use `cache.remember()` when freshness is non-negotiable — auth, balances, bill
|
|
|
24
24
|
│ no upstream call │ bg refresh │ like a miss
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
| Window
|
|
28
|
-
|
|
29
|
-
| `now < freshTtl`
|
|
27
|
+
| Window | Behavior |
|
|
28
|
+
| --------------------------- | ---------------------------------------------------------------------------------------- |
|
|
29
|
+
| `now < freshTtl` | Return cached. No upstream call. |
|
|
30
30
|
| `freshTtl ≤ now < staleTtl` | Return cached immediately. Run `fn()` in background; next read sees the refreshed value. |
|
|
31
|
-
| `now ≥ staleTtl`
|
|
31
|
+
| `now ≥ staleTtl` | Block on `fn()`. Same as `remember()`. |
|
|
32
32
|
|
|
33
33
|
## API
|
|
34
34
|
|
|
@@ -57,12 +57,12 @@ await cache.swr(
|
|
|
57
57
|
|
|
58
58
|
## Driver support
|
|
59
59
|
|
|
60
|
-
| Driver
|
|
61
|
-
|
|
62
|
-
| memory / memoryExtended / lru / file / mock | ✅ Full
|
|
63
|
-
| redis
|
|
64
|
-
| pg
|
|
65
|
-
| null
|
|
60
|
+
| Driver | Background refresh |
|
|
61
|
+
| ------------------------------------------- | ------------------------------------------------------------------------- |
|
|
62
|
+
| memory / memoryExtended / lru / file / mock | ✅ Full |
|
|
63
|
+
| redis | ✅ Full (sidecar key for staleAt — backwards-compatible) |
|
|
64
|
+
| pg | ✅ Full (`stale_at TIMESTAMPTZ` column — provision via `driver.schema()`) |
|
|
65
|
+
| null | ❌ Always-fetch (null caches nothing) |
|
|
66
66
|
|
|
67
67
|
## Common shapes
|
|
68
68
|
|
|
@@ -78,9 +78,7 @@ await cache.swr(`dashboard.${tenantId}`, { freshTtl: "5m", staleTtl: "1h" }, ()
|
|
|
78
78
|
);
|
|
79
79
|
|
|
80
80
|
// Third-party API — degrade gracefully when upstream is down
|
|
81
|
-
await cache.swr("exchange.rates", { freshTtl: "10m", staleTtl: "24h" }, () =>
|
|
82
|
-
fetchFromForexAPI(),
|
|
83
|
-
);
|
|
81
|
+
await cache.swr("exchange.rates", { freshTtl: "10m", staleTtl: "24h" }, () => fetchFromForexAPI());
|
|
84
82
|
```
|
|
85
83
|
|
|
86
84
|
## Through scoped caches
|
|
@@ -88,10 +86,8 @@ await cache.swr("exchange.rates", { freshTtl: "10m", staleTtl: "24h" }, () =>
|
|
|
88
86
|
```ts
|
|
89
87
|
const feed = cache.namespace(`feed.${userId}`, { tags: [`user.${userId}`] });
|
|
90
88
|
|
|
91
|
-
await feed.swr(
|
|
92
|
-
|
|
93
|
-
{ freshTtl: "30s", staleTtl: "10m", tags: ["computed"] },
|
|
94
|
-
() => buildHomeFeed(userId),
|
|
89
|
+
await feed.swr("home", { freshTtl: "30s", staleTtl: "10m", tags: ["computed"] }, () =>
|
|
90
|
+
buildHomeFeed(userId),
|
|
95
91
|
);
|
|
96
92
|
// stored at feed.<userId>.home, tagged [user.<userId>, computed]
|
|
97
93
|
```
|
|
@@ -99,6 +95,6 @@ await feed.swr(
|
|
|
99
95
|
## Things NOT to do
|
|
100
96
|
|
|
101
97
|
- Don't use SWR when the user must see the latest data (auth, billing). Use `remember()` instead — block-until-fresh is the right semantic there.
|
|
102
|
-
- Don't pick `freshTtl` to be the
|
|
98
|
+
- Don't pick `freshTtl` to be the _same_ as `staleTtl` thinking it disables the stale window — that throws. Pick a tight `freshTtl` and wider `staleTtl` that reflects how stale your product can tolerate being.
|
|
103
99
|
- Don't ignore `error` events. A persistent stream of refresh failures means upstream is broken and the cache is masking it.
|
|
104
100
|
- Don't reach for SWR on the null driver — it caches nothing, so SWR always blocks.
|