@warlock.js/cache 5.24.0 → 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 +6 -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
package/llms.txt
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
- [use-cache-lock](@warlock.js/cache/use-cache-lock/SKILL.md): (no description)
|
|
22
22
|
- [use-cache-namespace](@warlock.js/cache/use-cache-namespace/SKILL.md): (no description)
|
|
23
23
|
- [use-cache-similarity](@warlock.js/cache/use-cache-similarity/SKILL.md): (no description)
|
|
24
|
-
- [use-cache-tags](@warlock.js/cache/use-cache-tags/SKILL.md):
|
|
24
|
+
- [use-cache-tags](@warlock.js/cache/use-cache-tags/SKILL.md): (no description)
|
|
25
25
|
- [use-cache-update-merge](@warlock.js/cache/use-cache-update-merge/SKILL.md): (no description)
|
|
26
26
|
- [use-cache-utils](@warlock.js/cache/use-cache-utils/SKILL.md): (no description)
|
|
27
27
|
- [use-cached-hof](@warlock.js/cache/use-cached-hof/SKILL.md): (no description)
|
package/package.json
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
"environment": "server"
|
|
6
6
|
},
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@warlock.js/fs": "5.
|
|
8
|
+
"@warlock.js/fs": "5.25.0",
|
|
9
9
|
"@mongez/reinforcements": "^4.0.2",
|
|
10
|
-
"@warlock.js/logger": "5.
|
|
10
|
+
"@warlock.js/logger": "5.25.0",
|
|
11
11
|
"ms": "^2.1.3"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
],
|
|
47
47
|
"author": "hassanzohdy",
|
|
48
48
|
"license": "MIT",
|
|
49
|
-
"version": "5.
|
|
49
|
+
"version": "5.25.0",
|
|
50
50
|
"main": "./cjs/index.cjs",
|
|
51
51
|
"module": "./esm/index.mjs",
|
|
52
52
|
"types": "./esm/index.d.mts",
|
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: apply-cache-patterns
|
|
3
|
-
description: 'Compose cache primitives into real-world patterns — remember() memoization, cross-node stampede protection via a distributed lock (onConflict: ''create''), negative caching, and per-tenant scoping. Triggers: `cache.remember`, `cache.set` with `onConflict: "create"`, `globalPrefix`; "memoize this function", "prevent cache stampede across nodes", "cache not-found results", "per-tenant cache scoping"; typical import `import { cache } from "@warlock.js/cache"`. Skip: counters — `@warlock.js/cache/use-cache-atomic/SKILL.md`; bulk get/set — `@warlock.js/cache/use-cache-bulk/SKILL.md`; TTL constants/utilities — `@warlock.js/cache/use-cache-utils/SKILL.md`; named lock wrapper — `@warlock.js/cache/use-cache-lock/SKILL.md`; SWR — `@warlock.js/cache/use-swr/SKILL.md`; competing libs `lru-cache`, `node-cache`, `keyv`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Real-world caching patterns
|
|
7
|
-
|
|
8
|
-
Common shapes — the "general patterns" file. Specialized topics have dedicated skills: [`use-cache-tags`](@warlock.js/cache/use-cache-tags/SKILL.md), [`use-cache-namespace`](@warlock.js/cache/use-cache-namespace/SKILL.md), [`use-swr`](@warlock.js/cache/use-swr/SKILL.md), [`use-cache-lock`](@warlock.js/cache/use-cache-lock/SKILL.md), [`use-cache-list`](@warlock.js/cache/use-cache-list/SKILL.md).
|
|
9
|
-
|
|
10
|
-
## Memoize an expensive function — `remember`
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
const user = await cache.remember(`user:${id}`, "1h", async () => {
|
|
14
|
-
return db.users.find(id); // runs only on cache miss
|
|
15
|
-
});
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
- The callback runs once per miss.
|
|
19
|
-
- Concurrent callers for the same key share the in-flight promise (stampede protection) — within one Node process.
|
|
1
|
+
---
|
|
2
|
+
name: apply-cache-patterns
|
|
3
|
+
description: 'Compose cache primitives into real-world patterns — remember() memoization, cross-node stampede protection via a distributed lock (onConflict: ''create''), negative caching, and per-tenant scoping. Triggers: `cache.remember`, `cache.set` with `onConflict: "create"`, `globalPrefix`; "memoize this function", "prevent cache stampede across nodes", "cache not-found results", "per-tenant cache scoping"; typical import `import { cache } from "@warlock.js/cache"`. Skip: counters — `@warlock.js/cache/use-cache-atomic/SKILL.md`; bulk get/set — `@warlock.js/cache/use-cache-bulk/SKILL.md`; TTL constants/utilities — `@warlock.js/cache/use-cache-utils/SKILL.md`; named lock wrapper — `@warlock.js/cache/use-cache-lock/SKILL.md`; SWR — `@warlock.js/cache/use-swr/SKILL.md`; competing libs `lru-cache`, `node-cache`, `keyv`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Real-world caching patterns
|
|
7
|
+
|
|
8
|
+
Common shapes — the "general patterns" file. Specialized topics have dedicated skills: [`use-cache-tags`](@warlock.js/cache/use-cache-tags/SKILL.md), [`use-cache-namespace`](@warlock.js/cache/use-cache-namespace/SKILL.md), [`use-swr`](@warlock.js/cache/use-swr/SKILL.md), [`use-cache-lock`](@warlock.js/cache/use-cache-lock/SKILL.md), [`use-cache-list`](@warlock.js/cache/use-cache-list/SKILL.md).
|
|
9
|
+
|
|
10
|
+
## Memoize an expensive function — `remember`
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
const user = await cache.remember(`user:${id}`, "1h", async () => {
|
|
14
|
+
return db.users.find(id); // runs only on cache miss
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- The callback runs once per miss.
|
|
19
|
+
- Concurrent callers for the same key share the in-flight promise (stampede protection) — within one Node process.
|
|
20
20
|
- `null` is the only miss. `0`, `false` and `""` are cached like any other value, so `remember` doesn't recompute them. A callback returning `null` is not cached, so the callback re-runs on the next call: to cache a "not found," store a sentinel (see negative caching below).
|
|
21
21
|
- Stampede protection is per process, even on redis. To run something once across servers, wrap it in `cache.lock()` on `redis` or `pg`.
|
|
22
|
-
|
|
23
|
-
## Cross-process stampede protection — distributed lock via `onConflict`
|
|
24
|
-
|
|
25
|
-
`remember`'s lock is per-process. For cross-node safety, acquire a short-lived distributed lock before doing expensive work:
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
const lockKey = `lock:build-report:${reportId}`;
|
|
29
|
-
const acquired = await cache.set(lockKey, process.pid, {
|
|
30
|
-
onConflict: "create",
|
|
31
|
-
ttl: "2m",
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
if (!acquired.wasSet) {
|
|
35
|
-
// another node is already building — wait or skip
|
|
36
|
-
return cache.get(`report:${reportId}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const report = await buildExpensiveReport(reportId);
|
|
41
|
-
await cache.set(`report:${reportId}`, report, "1h");
|
|
42
|
-
return report;
|
|
43
|
-
} finally {
|
|
44
|
-
await cache.remove(lockKey);
|
|
45
|
-
}
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
This requires a driver with atomic `SET NX` — Redis is native, memory/LRU/file emulate (single-process only). For a higher-level wrapper that does the lock-and-release for you, see [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md).
|
|
49
|
-
|
|
50
|
-
## Negative caching
|
|
51
|
-
|
|
52
|
-
Cache "not found" results with a shorter TTL to avoid hammering the origin:
|
|
53
|
-
|
|
54
|
-
```ts
|
|
55
|
-
const user = await cache.remember(`user:${id}`, "5m", async () => {
|
|
56
|
-
const found = await db.users.find(id);
|
|
57
|
-
return found ?? { __miss: true };
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
if (user?.__miss) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
22
|
+
|
|
23
|
+
## Cross-process stampede protection — distributed lock via `onConflict`
|
|
24
|
+
|
|
25
|
+
`remember`'s lock is per-process. For cross-node safety, acquire a short-lived distributed lock before doing expensive work:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const lockKey = `lock:build-report:${reportId}`;
|
|
29
|
+
const acquired = await cache.set(lockKey, process.pid, {
|
|
30
|
+
onConflict: "create",
|
|
31
|
+
ttl: "2m",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!acquired.wasSet) {
|
|
35
|
+
// another node is already building — wait or skip
|
|
36
|
+
return cache.get(`report:${reportId}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const report = await buildExpensiveReport(reportId);
|
|
41
|
+
await cache.set(`report:${reportId}`, report, "1h");
|
|
42
|
+
return report;
|
|
43
|
+
} finally {
|
|
44
|
+
await cache.remove(lockKey);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This requires a driver with atomic `SET NX` — Redis is native, memory/LRU/file emulate (single-process only). For a higher-level wrapper that does the lock-and-release for you, see [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md).
|
|
49
|
+
|
|
50
|
+
## Negative caching
|
|
51
|
+
|
|
52
|
+
Cache "not found" results with a shorter TTL to avoid hammering the origin:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const user = await cache.remember(`user:${id}`, "5m", async () => {
|
|
56
|
+
const found = await db.users.find(id);
|
|
57
|
+
return found ?? { __miss: true };
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (user?.__miss) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
65
|
Don't return raw `null` inside `remember` to "cache the miss": `null` is the miss sentinel, so the callback re-runs every time and the origin still gets hammered. The `{ __miss: true }` sentinel is what skips the next call.
|
|
66
|
-
|
|
67
|
-
## Per-tenant caching
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
// In cache config:
|
|
71
|
-
options: {
|
|
72
|
-
redis: {
|
|
73
|
-
url: "...",
|
|
74
|
-
globalPrefix: () => `tenant-${currentContext.tenantId}`,
|
|
75
|
-
},
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// At the call site — no tenancy awareness needed:
|
|
79
|
-
await cache.set("user:1", user, "1h");
|
|
80
|
-
// Actual key: "tenant-42.user.1"
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Clear a tenant out:
|
|
84
|
-
```ts
|
|
85
|
-
await cache.removeNamespace(""); // when globalPrefix is set, flush scopes to it
|
|
86
|
-
// or
|
|
87
|
-
await cache.tags([`tenant-${tenantId}`]).invalidate();
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## See also
|
|
91
|
-
|
|
92
|
-
- [`@warlock.js/cache/use-cache-atomic/SKILL.md`](@warlock.js/cache/use-cache-atomic/SKILL.md) — `increment` / `decrement` counters
|
|
93
|
-
- [`@warlock.js/cache/use-cache-bulk/SKILL.md`](@warlock.js/cache/use-cache-bulk/SKILL.md) — `many` / `setMany`
|
|
94
|
-
- [`@warlock.js/cache/use-cache-utils/SKILL.md`](@warlock.js/cache/use-cache-utils/SKILL.md) — `CACHE_FOR` constants and TTL/key helpers
|
|
95
|
-
- [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md) — tag-based invalidation
|
|
96
|
-
- [`@warlock.js/cache/use-cache-namespace/SKILL.md`](@warlock.js/cache/use-cache-namespace/SKILL.md) — scoped handles and `removeNamespace`
|
|
97
|
-
- [`@warlock.js/cache/use-swr/SKILL.md`](@warlock.js/cache/use-swr/SKILL.md) — stale-while-revalidate for slow upstreams
|
|
98
|
-
- [`@warlock.js/cache/use-cached-hof/SKILL.md`](@warlock.js/cache/use-cached-hof/SKILL.md) — `cached()` HOF for declarative memoization
|
|
66
|
+
|
|
67
|
+
## Per-tenant caching
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// In cache config:
|
|
71
|
+
options: {
|
|
72
|
+
redis: {
|
|
73
|
+
url: "...",
|
|
74
|
+
globalPrefix: () => `tenant-${currentContext.tenantId}`,
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// At the call site — no tenancy awareness needed:
|
|
79
|
+
await cache.set("user:1", user, "1h");
|
|
80
|
+
// Actual key: "tenant-42.user.1"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Clear a tenant out:
|
|
84
|
+
```ts
|
|
85
|
+
await cache.removeNamespace(""); // when globalPrefix is set, flush scopes to it
|
|
86
|
+
// or
|
|
87
|
+
await cache.tags([`tenant-${tenantId}`]).invalidate();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## See also
|
|
91
|
+
|
|
92
|
+
- [`@warlock.js/cache/use-cache-atomic/SKILL.md`](@warlock.js/cache/use-cache-atomic/SKILL.md) — `increment` / `decrement` counters
|
|
93
|
+
- [`@warlock.js/cache/use-cache-bulk/SKILL.md`](@warlock.js/cache/use-cache-bulk/SKILL.md) — `many` / `setMany`
|
|
94
|
+
- [`@warlock.js/cache/use-cache-utils/SKILL.md`](@warlock.js/cache/use-cache-utils/SKILL.md) — `CACHE_FOR` constants and TTL/key helpers
|
|
95
|
+
- [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md) — tag-based invalidation
|
|
96
|
+
- [`@warlock.js/cache/use-cache-namespace/SKILL.md`](@warlock.js/cache/use-cache-namespace/SKILL.md) — scoped handles and `removeNamespace`
|
|
97
|
+
- [`@warlock.js/cache/use-swr/SKILL.md`](@warlock.js/cache/use-swr/SKILL.md) — stale-while-revalidate for slow upstreams
|
|
98
|
+
- [`@warlock.js/cache/use-cached-hof/SKILL.md`](@warlock.js/cache/use-cached-hof/SKILL.md) — `cached()` HOF for declarative memoization
|
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: configure-set-options
|
|
3
|
-
description: 'Configure cache.set''s third argument — ttl, expiresAt, tags, onConflict (create / update / upsert), driver, vector. Triggers: `cache.set`, `ttl`, `expiresAt`, `tags`, `onConflict`, `driver`, `vector`, `CacheSetResult`, `wasSet`; "set a key only if missing", "set with absolute deadline", "attach tags inline", "route one cache call to redis"; typical import `import { cache } from "@warlock.js/cache"`. Skip: tag fluent API — `@warlock.js/cache/use-cache-tags/SKILL.md`; vector queries — `@warlock.js/cache/use-cache-similarity/SKILL.md`; competing libs `keyv`, `ioredis`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# The `set` options object
|
|
7
|
-
|
|
8
|
-
`cache.set(key, value, ttlOrOptions?)` — the 3rd argument accepts three shapes.
|
|
9
|
-
|
|
10
|
-
## The three shapes
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
// 1. Number — seconds
|
|
14
|
-
await cache.set("name", "Jane", 600);
|
|
15
|
-
|
|
16
|
-
// 2. String — human-readable duration, parsed via `ms`
|
|
17
|
-
await cache.set("name", "Jane", "10m"); // "1s", "30m", "1h", "7d", "2 weeks"
|
|
18
|
-
|
|
19
|
-
// 3. Options object
|
|
20
|
-
await cache.set("user:1", user, {
|
|
21
|
-
ttl: "1h",
|
|
22
|
-
expiresAt: new Date("2026-12-31"),
|
|
23
|
-
tags: ["users"],
|
|
24
|
-
onConflict: "create",
|
|
25
|
-
driver: "redis",
|
|
26
|
-
});
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Option keys
|
|
30
|
-
|
|
31
|
-
| Key | Type | Notes |
|
|
32
|
-
| --- | --- | --- |
|
|
33
|
-
| `ttl` | `number \| string` | Relative expiry. Mutually exclusive with `expiresAt`. |
|
|
34
|
-
| `expiresAt` | `number \| Date` | Absolute deadline (epoch ms or Date). Must be in the future. Mutually exclusive with `ttl`. |
|
|
35
|
-
| `tags` | `string[]` | Inline equivalent of `cache.tags([...]).set(...)`. See [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md). |
|
|
36
|
-
| `onConflict` | `"create" \| "update" \| "upsert"` | See below. Default `"upsert"`. |
|
|
37
|
-
| `driver` | `string` | Per-call driver override by registered name. |
|
|
38
|
-
| `vector` | `number[]` | Embedding indexed alongside the entry for [`cache.similar()`](@warlock.js/cache/use-cache-similarity/SKILL.md). Drivers without similarity support throw `CacheUnsupportedError`. |
|
|
39
|
-
|
|
1
|
+
---
|
|
2
|
+
name: configure-set-options
|
|
3
|
+
description: 'Configure cache.set''s third argument — ttl, expiresAt, tags, onConflict (create / update / upsert), driver, vector. Triggers: `cache.set`, `ttl`, `expiresAt`, `tags`, `onConflict`, `driver`, `vector`, `CacheSetResult`, `wasSet`; "set a key only if missing", "set with absolute deadline", "attach tags inline", "route one cache call to redis"; typical import `import { cache } from "@warlock.js/cache"`. Skip: tag fluent API — `@warlock.js/cache/use-cache-tags/SKILL.md`; vector queries — `@warlock.js/cache/use-cache-similarity/SKILL.md`; competing libs `keyv`, `ioredis`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# The `set` options object
|
|
7
|
+
|
|
8
|
+
`cache.set(key, value, ttlOrOptions?)` — the 3rd argument accepts three shapes.
|
|
9
|
+
|
|
10
|
+
## The three shapes
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// 1. Number — seconds
|
|
14
|
+
await cache.set("name", "Jane", 600);
|
|
15
|
+
|
|
16
|
+
// 2. String — human-readable duration, parsed via `ms`
|
|
17
|
+
await cache.set("name", "Jane", "10m"); // "1s", "30m", "1h", "7d", "2 weeks"
|
|
18
|
+
|
|
19
|
+
// 3. Options object
|
|
20
|
+
await cache.set("user:1", user, {
|
|
21
|
+
ttl: "1h",
|
|
22
|
+
expiresAt: new Date("2026-12-31"),
|
|
23
|
+
tags: ["users"],
|
|
24
|
+
onConflict: "create",
|
|
25
|
+
driver: "redis",
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Option keys
|
|
30
|
+
|
|
31
|
+
| Key | Type | Notes |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| `ttl` | `number \| string` | Relative expiry. Mutually exclusive with `expiresAt`. |
|
|
34
|
+
| `expiresAt` | `number \| Date` | Absolute deadline (epoch ms or Date). Must be in the future. Mutually exclusive with `ttl`. |
|
|
35
|
+
| `tags` | `string[]` | Inline equivalent of `cache.tags([...]).set(...)`. See [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md). |
|
|
36
|
+
| `onConflict` | `"create" \| "update" \| "upsert"` | See below. Default `"upsert"`. |
|
|
37
|
+
| `driver` | `string` | Per-call driver override by registered name. |
|
|
38
|
+
| `vector` | `number[]` | Embedding indexed alongside the entry for [`cache.similar()`](@warlock.js/cache/use-cache-similarity/SKILL.md). Drivers without similarity support throw `CacheUnsupportedError`. |
|
|
39
|
+
|
|
40
40
|
## TTL parsing
|
|
41
41
|
|
|
42
42
|
- Number or bare numeric string (`"3600"`) = **seconds**. Fractions and sub-second values (`0.5`, `"500ms"`) round **up** to whole seconds, never 0.
|
|
43
43
|
- `0` / `Infinity` = no expiry. `NaN`, negative and unparseable values throw `CacheConfigurationError`.
|
|
44
44
|
- `cache.lock()` refuses any TTL that never expires.
|
|
45
45
|
|
|
46
|
-
## `onConflict` policies
|
|
47
|
-
|
|
48
|
-
Self-documenting enum; Redis maps these to `NX` / `XX` natively, others emulate.
|
|
49
|
-
|
|
50
|
-
```ts
|
|
51
|
-
// create — set only if key is missing
|
|
52
|
-
const result = await cache.set("lock:jobs:import", workerId, {
|
|
53
|
-
onConflict: "create",
|
|
54
|
-
ttl: "5m",
|
|
55
|
-
});
|
|
56
|
-
// result: { wasSet: true, existing: null } on acquire
|
|
57
|
-
// { wasSet: false, existing: <prior workerId> } on conflict — someone else holds the lock
|
|
58
|
-
|
|
59
|
-
if (!result.wasSet) {
|
|
60
|
-
// another worker is already running; abort.
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// update — set only if key exists (don't resurrect expired sessions)
|
|
64
|
-
await cache.set("session:abc", session, { onConflict: "update" });
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Conditional writes (`"create"` / `"update"`) return a `CacheSetResult`; unconditional `"upsert"` returns the value or driver instance as before.
|
|
68
|
-
|
|
69
|
-
## Mutually-exclusive validations
|
|
70
|
-
|
|
71
|
-
Both of these throw `CacheConfigurationError`:
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
await cache.set("k", v, { ttl: "1h", expiresAt: Date.now() + 1000 }); // both set
|
|
75
|
-
await cache.set("k", v, { expiresAt: Date.now() - 1000 }); // past deadline
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
## Inline tags vs `cache.tags([...]).set(...)`
|
|
79
|
-
|
|
80
|
-
Both work; inline is terser when you're writing one value under known tags:
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
// Inline
|
|
84
|
-
await cache.set("user:1", user, { tags: ["users", "tenant-42"] });
|
|
85
|
-
|
|
86
|
-
// Fluent (useful when you already have a tagged instance)
|
|
87
|
-
const users = cache.tags(["users"]);
|
|
88
|
-
await users.set("user:1", user);
|
|
89
|
-
await users.invalidate(); // drops every key tagged "users"
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Inline tag semantics are **additive**, never replace. A subsequent `set("user:1", ...)` with no `tags` leaves previous associations intact (the tag index still points to the key), and a `set(..., { tags: [...] })` only appends the key to those tags' index entries — it never removes the key from tags it was bound to earlier. To drop stale bindings, invalidate the old tag explicitly via `cache.tags([...]).invalidate()`.
|
|
93
|
-
|
|
94
|
-
## Back-compat note
|
|
95
|
-
|
|
96
|
-
Every call site using the old positional-TTL shape keeps working:
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
await cache.set("k", v); // no TTL — driver default
|
|
100
|
-
await cache.set("k", v, 3600); // seconds
|
|
101
|
-
await cache.set("k", v, undefined); // same as no TTL
|
|
102
|
-
```
|
|
46
|
+
## `onConflict` policies
|
|
47
|
+
|
|
48
|
+
Self-documenting enum; Redis maps these to `NX` / `XX` natively, others emulate.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// create — set only if key is missing
|
|
52
|
+
const result = await cache.set("lock:jobs:import", workerId, {
|
|
53
|
+
onConflict: "create",
|
|
54
|
+
ttl: "5m",
|
|
55
|
+
});
|
|
56
|
+
// result: { wasSet: true, existing: null } on acquire
|
|
57
|
+
// { wasSet: false, existing: <prior workerId> } on conflict — someone else holds the lock
|
|
58
|
+
|
|
59
|
+
if (!result.wasSet) {
|
|
60
|
+
// another worker is already running; abort.
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// update — set only if key exists (don't resurrect expired sessions)
|
|
64
|
+
await cache.set("session:abc", session, { onConflict: "update" });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Conditional writes (`"create"` / `"update"`) return a `CacheSetResult`; unconditional `"upsert"` returns the value or driver instance as before.
|
|
68
|
+
|
|
69
|
+
## Mutually-exclusive validations
|
|
70
|
+
|
|
71
|
+
Both of these throw `CacheConfigurationError`:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
await cache.set("k", v, { ttl: "1h", expiresAt: Date.now() + 1000 }); // both set
|
|
75
|
+
await cache.set("k", v, { expiresAt: Date.now() - 1000 }); // past deadline
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Inline tags vs `cache.tags([...]).set(...)`
|
|
79
|
+
|
|
80
|
+
Both work; inline is terser when you're writing one value under known tags:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// Inline
|
|
84
|
+
await cache.set("user:1", user, { tags: ["users", "tenant-42"] });
|
|
85
|
+
|
|
86
|
+
// Fluent (useful when you already have a tagged instance)
|
|
87
|
+
const users = cache.tags(["users"]);
|
|
88
|
+
await users.set("user:1", user);
|
|
89
|
+
await users.invalidate(); // drops every key tagged "users"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Inline tag semantics are **additive**, never replace. A subsequent `set("user:1", ...)` with no `tags` leaves previous associations intact (the tag index still points to the key), and a `set(..., { tags: [...] })` only appends the key to those tags' index entries — it never removes the key from tags it was bound to earlier. To drop stale bindings, invalidate the old tag explicitly via `cache.tags([...]).invalidate()`.
|
|
93
|
+
|
|
94
|
+
## Back-compat note
|
|
95
|
+
|
|
96
|
+
Every call site using the old positional-TTL shape keeps working:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
await cache.set("k", v); // no TTL — driver default
|
|
100
|
+
await cache.set("k", v, 3600); // seconds
|
|
101
|
+
await cache.set("k", v, undefined); // same as no TTL
|
|
102
|
+
```
|
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: handle-cache-errors
|
|
3
|
-
description: 'Cache error classes — CacheError base, CacheConfigurationError, CacheConnectionError, CacheDriverNotInitializedError, CacheUnsupportedError, CacheConcurrencyError. Triggers: `CacheError`, `CacheConfigurationError`, `CacheConnectionError`, `CacheDriverNotInitializedError`, `CacheUnsupportedError`, `CacheConcurrencyError`; "catch cache errors at the boundary", "degrade when update or merge throws", "what does CacheUnsupportedError mean", "fall back when redis is down"; typical import `import { CacheError, CacheConfigurationError, CacheUnsupportedError } from "@warlock.js/cache"`. Skip: choosing a supported driver — `@warlock.js/cache/pick-cache-driver/SKILL.md`; observing errors via events — `@warlock.js/cache/observe-cache/SKILL.md`; competing libs ignore — generic `Error` patterns.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Error classes
|
|
7
|
-
|
|
8
|
-
All cache errors extend `CacheError` which extends `Error`. Use `instanceof` to react selectively.
|
|
9
|
-
|
|
10
|
-
```ts
|
|
11
|
-
import {
|
|
12
|
-
CacheError,
|
|
13
|
-
CacheConfigurationError,
|
|
14
|
-
CacheConnectionError,
|
|
15
|
-
CacheDriverNotInitializedError,
|
|
16
|
-
CacheUnsupportedError,
|
|
17
|
-
CacheConcurrencyError,
|
|
18
|
-
} from "@warlock.js/cache";
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
| Class | When it's thrown | How to react |
|
|
22
|
-
| --- | --- | --- |
|
|
23
|
-
| `CacheError` | Abstract base — don't throw directly, match against it to catch any cache error | `catch (e) { if (e instanceof CacheError) … }` |
|
|
24
|
-
| `CacheConfigurationError` | Bad TTL string, `ttl` + `expiresAt` together, `expiresAt` in the past, missing required driver option (e.g. redis without url/host, file without directory), attempting to use an unregistered driver name | Fix the config / call site. Never catch at runtime — this is a programmer error, not a user-facing one. |
|
|
25
|
-
| `CacheConnectionError` | Declared for driver connection failures. Not thrown by any built-in driver today (Redis currently logs the error and emits an `"error"` event instead of throwing on failed connect). | Reserved for future use. |
|
|
26
|
-
| `CacheDriverNotInitializedError` | Any data op called before `cache.init()` / `cache.use()` | Call `cache.init()` at app startup. Tests often forget this — add a `beforeEach`. |
|
|
27
|
-
| `CacheUnsupportedError` | Driver doesn't implement the requested op. Today: `update` / `merge` on the file driver; `set({ vector })` and `similar()` on file / redis / pg-without-`vector`-config. | Switch driver (memory family for dev similarity, `pg` with `vector` config for production), or queue the op. |
|
|
28
|
-
| `CacheConcurrencyError` | Declared for future optimistic-concurrency exhaustion on Redis `update()` | Not thrown today. Reserved for the v2.1 `WATCH`/`MULTI` implementation. |
|
|
29
|
-
| `CacheError` (file driver, path containment) | A key/namespace, after percent-encoding, still resolves outside the cache root — `"Cache key resolves outside the cache directory: ..."`. Last line of defense against path traversal on top of the encoding step; should not happen in normal use. | Treat as a programmer/attacker-input error — don't retry; investigate the key source. See [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) for the encoding contract. |
|
|
30
|
-
|
|
31
|
-
## Special case — `setNX` unsupported
|
|
32
|
-
|
|
33
|
-
Calling `cache.setNX(...)` on a driver that doesn't implement it throws a plain `Error`, not a `CacheUnsupportedError`:
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
// Error: "setNX is not supported by the current cache driver: memory"
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
This is legacy. The v2-preferred way is `cache.set(k, v, { onConflict: "create" })` which works on every driver (Redis native, others emulated). See [`@warlock.js/cache/configure-set-options/SKILL.md`](@warlock.js/cache/configure-set-options/SKILL.md).
|
|
40
|
-
|
|
41
|
-
## Patterns
|
|
42
|
-
|
|
43
|
-
### Catch-all at the boundary
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
try {
|
|
47
|
-
await doCachedWork();
|
|
48
|
-
} catch (error) {
|
|
49
|
-
if (error instanceof CacheError) {
|
|
50
|
-
logger.warn("cache unavailable, degrading", error);
|
|
51
|
-
await doWorkWithoutCache();
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Selective — configuration vs runtime
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
try {
|
|
62
|
-
await cache.set("k", v, userSuppliedOptions);
|
|
63
|
-
} catch (error) {
|
|
64
|
-
if (error instanceof CacheConfigurationError) {
|
|
65
|
-
return res.status(400).json({ error: "invalid TTL options" });
|
|
66
|
-
}
|
|
67
|
-
throw error;
|
|
68
|
-
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Driver-missing fallback
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
try {
|
|
75
|
-
await cache.merge("user:1", { lastSeen: Date.now() });
|
|
76
|
-
} catch (error) {
|
|
77
|
-
if (error instanceof CacheUnsupportedError) {
|
|
78
|
-
// File driver in dev — degrade gracefully
|
|
79
|
-
const current = await cache.get("user:1");
|
|
80
|
-
await cache.set("user:1", { ...current, lastSeen: Date.now() });
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## Things the driver does NOT throw
|
|
88
|
-
|
|
89
|
-
- **Missing keys** — `get()` returns `null`, never throws. Tests checking "key not in cache" should assert `resolves.toBeNull()`, not `rejects.toThrow`.
|
|
90
|
-
- **Expired entries** — `get()` returns `null` and emits `"miss"` + `"expired"` events. No throw.
|
|
91
|
-
- **Flush on empty** — `flush()` succeeds silently when there's nothing to flush.
|
|
92
|
-
- **Concurrent writes clobbering each other** — last-write-wins by default. Use `update()` or `onConflict: "create"` if you need protection.
|
|
1
|
+
---
|
|
2
|
+
name: handle-cache-errors
|
|
3
|
+
description: 'Cache error classes — CacheError base, CacheConfigurationError, CacheConnectionError, CacheDriverNotInitializedError, CacheUnsupportedError, CacheConcurrencyError. Triggers: `CacheError`, `CacheConfigurationError`, `CacheConnectionError`, `CacheDriverNotInitializedError`, `CacheUnsupportedError`, `CacheConcurrencyError`; "catch cache errors at the boundary", "degrade when update or merge throws", "what does CacheUnsupportedError mean", "fall back when redis is down"; typical import `import { CacheError, CacheConfigurationError, CacheUnsupportedError } from "@warlock.js/cache"`. Skip: choosing a supported driver — `@warlock.js/cache/pick-cache-driver/SKILL.md`; observing errors via events — `@warlock.js/cache/observe-cache/SKILL.md`; competing libs ignore — generic `Error` patterns.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Error classes
|
|
7
|
+
|
|
8
|
+
All cache errors extend `CacheError` which extends `Error`. Use `instanceof` to react selectively.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import {
|
|
12
|
+
CacheError,
|
|
13
|
+
CacheConfigurationError,
|
|
14
|
+
CacheConnectionError,
|
|
15
|
+
CacheDriverNotInitializedError,
|
|
16
|
+
CacheUnsupportedError,
|
|
17
|
+
CacheConcurrencyError,
|
|
18
|
+
} from "@warlock.js/cache";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| Class | When it's thrown | How to react |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `CacheError` | Abstract base — don't throw directly, match against it to catch any cache error | `catch (e) { if (e instanceof CacheError) … }` |
|
|
24
|
+
| `CacheConfigurationError` | Bad TTL string, `ttl` + `expiresAt` together, `expiresAt` in the past, missing required driver option (e.g. redis without url/host, file without directory), attempting to use an unregistered driver name | Fix the config / call site. Never catch at runtime — this is a programmer error, not a user-facing one. |
|
|
25
|
+
| `CacheConnectionError` | Declared for driver connection failures. Not thrown by any built-in driver today (Redis currently logs the error and emits an `"error"` event instead of throwing on failed connect). | Reserved for future use. |
|
|
26
|
+
| `CacheDriverNotInitializedError` | Any data op called before `cache.init()` / `cache.use()` | Call `cache.init()` at app startup. Tests often forget this — add a `beforeEach`. |
|
|
27
|
+
| `CacheUnsupportedError` | Driver doesn't implement the requested op. Today: `update` / `merge` on the file driver; `set({ vector })` and `similar()` on file / redis / pg-without-`vector`-config. | Switch driver (memory family for dev similarity, `pg` with `vector` config for production), or queue the op. |
|
|
28
|
+
| `CacheConcurrencyError` | Declared for future optimistic-concurrency exhaustion on Redis `update()` | Not thrown today. Reserved for the v2.1 `WATCH`/`MULTI` implementation. |
|
|
29
|
+
| `CacheError` (file driver, path containment) | A key/namespace, after percent-encoding, still resolves outside the cache root — `"Cache key resolves outside the cache directory: ..."`. Last line of defense against path traversal on top of the encoding step; should not happen in normal use. | Treat as a programmer/attacker-input error — don't retry; investigate the key source. See [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) for the encoding contract. |
|
|
30
|
+
|
|
31
|
+
## Special case — `setNX` unsupported
|
|
32
|
+
|
|
33
|
+
Calling `cache.setNX(...)` on a driver that doesn't implement it throws a plain `Error`, not a `CacheUnsupportedError`:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// Error: "setNX is not supported by the current cache driver: memory"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This is legacy. The v2-preferred way is `cache.set(k, v, { onConflict: "create" })` which works on every driver (Redis native, others emulated). See [`@warlock.js/cache/configure-set-options/SKILL.md`](@warlock.js/cache/configure-set-options/SKILL.md).
|
|
40
|
+
|
|
41
|
+
## Patterns
|
|
42
|
+
|
|
43
|
+
### Catch-all at the boundary
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
try {
|
|
47
|
+
await doCachedWork();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error instanceof CacheError) {
|
|
50
|
+
logger.warn("cache unavailable, degrading", error);
|
|
51
|
+
await doWorkWithoutCache();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Selective — configuration vs runtime
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
try {
|
|
62
|
+
await cache.set("k", v, userSuppliedOptions);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error instanceof CacheConfigurationError) {
|
|
65
|
+
return res.status(400).json({ error: "invalid TTL options" });
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Driver-missing fallback
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
try {
|
|
75
|
+
await cache.merge("user:1", { lastSeen: Date.now() });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof CacheUnsupportedError) {
|
|
78
|
+
// File driver in dev — degrade gracefully
|
|
79
|
+
const current = await cache.get("user:1");
|
|
80
|
+
await cache.set("user:1", { ...current, lastSeen: Date.now() });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Things the driver does NOT throw
|
|
88
|
+
|
|
89
|
+
- **Missing keys** — `get()` returns `null`, never throws. Tests checking "key not in cache" should assert `resolves.toBeNull()`, not `rejects.toThrow`.
|
|
90
|
+
- **Expired entries** — `get()` returns `null` and emits `"miss"` + `"expired"` events. No throw.
|
|
91
|
+
- **Flush on empty** — `flush()` succeeds silently when there's nothing to flush.
|
|
92
|
+
- **Concurrent writes clobbering each other** — last-write-wins by default. Use `update()` or `onConflict: "create"` if you need protection.
|