@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +616 -616
  3. package/cjs/index.cjs +21 -1
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/drivers/base-cache-driver.d.mts +12 -0
  6. package/esm/drivers/base-cache-driver.d.mts.map +1 -1
  7. package/esm/drivers/base-cache-driver.mjs +10 -0
  8. package/esm/drivers/base-cache-driver.mjs.map +1 -1
  9. package/esm/drivers/null-cache-driver.mjs.map +1 -1
  10. package/esm/tagged-cache.d.mts.map +1 -1
  11. package/esm/tagged-cache.mjs +11 -1
  12. package/esm/tagged-cache.mjs.map +1 -1
  13. package/esm/types.d.mts +5 -2
  14. package/esm/types.d.mts.map +1 -1
  15. package/esm/types.mjs.map +1 -1
  16. package/esm/utils.mjs.map +1 -1
  17. package/llms-full.txt +1632 -1631
  18. package/llms.txt +1 -1
  19. package/package.json +3 -3
  20. package/skills/apply-cache-patterns/SKILL.md +95 -95
  21. package/skills/configure-set-options/SKILL.md +96 -96
  22. package/skills/handle-cache-errors/SKILL.md +92 -92
  23. package/skills/observe-cache/SKILL.md +104 -103
  24. package/skills/pick-cache-driver/SKILL.md +121 -121
  25. package/skills/test-cache-code/SKILL.md +219 -219
  26. package/skills/use-cache-atomic/SKILL.md +42 -42
  27. package/skills/use-cache-bulk/SKILL.md +57 -57
  28. package/skills/use-cache-list/SKILL.md +85 -85
  29. package/skills/use-cache-lock/SKILL.md +101 -101
  30. package/skills/use-cache-namespace/SKILL.md +88 -88
  31. package/skills/use-cache-similarity/SKILL.md +94 -94
  32. package/skills/use-cache-update-merge/SKILL.md +84 -84
  33. package/skills/use-cache-utils/SKILL.md +89 -89
  34. package/skills/use-cached-hof/SKILL.md +93 -93
  35. package/skills/use-swr/SKILL.md +104 -104
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): Tag-based invalidation — attach tags on write, then cache.tags([...]).invalidate() drops every key bound to any of those tags. Triggers: `cache.tags`, `invalidate`, `cache.set` with `tags`; "invalidate every key tagged users", "drop everything for tenant 42", "bulk cache invalidation without knowing keys", "tag a cached value"; typical import `import { cache } from "@warlock.js/cache"`. Skip: prefix-based drop — `@warlock.js/cache/use-cache-namespace/SKILL.md`; HOF memoization with tags — `@warlock.js/cache/use-cached-hof/SKILL.md`; SWR — `@warlock.js/cache/use-swr/SKILL.md`; competing libs `cache-manager` tags, Next.js `revalidateTag`.
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.24.0",
8
+ "@warlock.js/fs": "5.25.0",
9
9
  "@mongez/reinforcements": "^4.0.2",
10
- "@warlock.js/logger": "5.24.0",
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.24.0",
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.