@warlock.js/cache 5.24.0 → 5.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +616 -616
- package/cjs/index.cjs +21 -1
- package/cjs/index.cjs.map +1 -1
- package/esm/drivers/base-cache-driver.d.mts +12 -0
- package/esm/drivers/base-cache-driver.d.mts.map +1 -1
- package/esm/drivers/base-cache-driver.mjs +10 -0
- package/esm/drivers/base-cache-driver.mjs.map +1 -1
- package/esm/drivers/null-cache-driver.mjs.map +1 -1
- package/esm/tagged-cache.d.mts.map +1 -1
- package/esm/tagged-cache.mjs +11 -1
- package/esm/tagged-cache.mjs.map +1 -1
- package/esm/types.d.mts +5 -2
- package/esm/types.d.mts.map +1 -1
- package/esm/types.mjs.map +1 -1
- package/esm/utils.mjs.map +1 -1
- package/llms-full.txt +1632 -1631
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/skills/apply-cache-patterns/SKILL.md +95 -95
- package/skills/configure-set-options/SKILL.md +96 -96
- package/skills/handle-cache-errors/SKILL.md +92 -92
- package/skills/observe-cache/SKILL.md +104 -103
- package/skills/pick-cache-driver/SKILL.md +121 -121
- package/skills/test-cache-code/SKILL.md +219 -219
- package/skills/use-cache-atomic/SKILL.md +42 -42
- package/skills/use-cache-bulk/SKILL.md +57 -57
- package/skills/use-cache-list/SKILL.md +85 -85
- package/skills/use-cache-lock/SKILL.md +101 -101
- package/skills/use-cache-namespace/SKILL.md +88 -88
- package/skills/use-cache-similarity/SKILL.md +94 -94
- package/skills/use-cache-update-merge/SKILL.md +84 -84
- package/skills/use-cache-utils/SKILL.md +89 -89
- package/skills/use-cached-hof/SKILL.md +93 -93
- package/skills/use-swr/SKILL.md +104 -104
|
@@ -1,103 +1,104 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: observe-cache
|
|
3
|
-
description: 'Cache observability — cache.metrics() for aggregate hit rate / latency p50/p95/p99 + event bus (cache.on(''hit'' / ''miss'' / ''set'' / ''removed'' / ''flushed'' / ''expired'' / ''error'', ...)). Triggers: `cache.metrics`, `cache.resetMetrics`, `cache.on`, `hit`, `miss`, `removed`, `flushed`, `error`, `hitRate`, `latencyMs`; "show cache hit rate", "page on cache errors", "is my cache being hit", "export metrics to prometheus"; typical import `import { cache } from "@warlock.js/cache"`. Skip: error classes — `@warlock.js/cache/handle-cache-errors/SKILL.md`; competing libs `prom-client`, `statsd-client`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Cache observability — `cache.metrics()` and the event bus
|
|
7
|
-
|
|
8
|
-
Two layers, different jobs.
|
|
9
|
-
|
|
10
|
-
## Layer 1 — `cache.metrics()` for aggregate health
|
|
11
|
-
|
|
12
|
-
Built-in collector subscribed to the manager's event bus. Returns a snapshot whenever you ask:
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
const m = cache.metrics();
|
|
16
|
-
// {
|
|
17
|
-
// hits, misses, sets, removed, errors,
|
|
18
|
-
// hitRate,
|
|
19
|
-
// latencyMs: { p50, p95, p99, samples },
|
|
20
|
-
// byDriver: { memory: {...}, redis: {...} },
|
|
21
|
-
// startedAt,
|
|
22
|
-
// }
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
**Lazy** — the collector attaches on the first `cache.metrics()` / `cache.resetMetrics()` call. Apps that never read metrics pay zero cost. Earlier events are not retroactively counted, so if you want metrics on every op including the first, call `cache.metrics()` once during startup right after `cache.init()`.
|
|
26
|
-
|
|
27
|
-
**Survives `cache.use()` switches** — listens at the manager level, re-attaches to every loaded driver.
|
|
28
|
-
|
|
29
|
-
**Latency** is sampled by the manager around `get` / `set` / `remove` into a circular buffer (default 1000 samples per driver). Percentiles are computed at snapshot time. Older samples age out, so percentiles reflect the recent ~1000 ops.
|
|
30
|
-
|
|
31
|
-
`cache.resetMetrics()` zeroes counters + drops the buffer + bumps `startedAt`.
|
|
32
|
-
|
|
33
|
-
## Layer 2 — Raw events for per-event reactions
|
|
34
|
-
|
|
35
|
-
When you need to react to specific events (alerting, audit logs, debugging), subscribe to the event bus:
|
|
36
|
-
|
|
37
|
-
```ts
|
|
38
|
-
cache.on("error", ({ key, error }) => {
|
|
39
|
-
pagerDuty.trigger(`Cache error on ${key}`, error);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
cache.on("miss", ({ key, driver }) => {
|
|
43
|
-
if (key.startsWith("hot.")) auditLog.miss(key, driver);
|
|
44
|
-
});
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Available events: `hit`, `miss`, `set`, `removed`, `flushed`, `expired`, `connected`, `disconnected`, `error`.
|
|
48
|
-
|
|
49
|
-
Listeners attached via `cache.on(...)` survive driver switches the same way the metrics collector does.
|
|
50
|
-
|
|
51
|
-
## Which one to reach for
|
|
52
|
-
|
|
53
|
-
| Goal | Use |
|
|
54
|
-
|---|---|
|
|
55
|
-
| Show hit rate / latency in a dashboard | `cache.metrics()` |
|
|
56
|
-
| Page on cache errors | `cache.on("error", ...)` |
|
|
57
|
-
| Periodic export to Prometheus / StatsD | `cache.metrics()` + `setInterval` + `resetMetrics()` |
|
|
58
|
-
| Audit log of every removal | `cache.on("removed", ...)` |
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
console.log(`
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
- Don't
|
|
102
|
-
- Don't
|
|
103
|
-
- Don't
|
|
1
|
+
---
|
|
2
|
+
name: observe-cache
|
|
3
|
+
description: 'Cache observability — cache.metrics() for aggregate hit rate / latency p50/p95/p99 + event bus (cache.on(''hit'' / ''miss'' / ''set'' / ''removed'' / ''flushed'' / ''expired'' / ''error'', ...)). Triggers: `cache.metrics`, `cache.resetMetrics`, `cache.on`, `hit`, `miss`, `removed`, `flushed`, `error`, `hitRate`, `latencyMs`; "show cache hit rate", "page on cache errors", "is my cache being hit", "export metrics to prometheus"; typical import `import { cache } from "@warlock.js/cache"`. Skip: error classes — `@warlock.js/cache/handle-cache-errors/SKILL.md`; competing libs `prom-client`, `statsd-client`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Cache observability — `cache.metrics()` and the event bus
|
|
7
|
+
|
|
8
|
+
Two layers, different jobs.
|
|
9
|
+
|
|
10
|
+
## Layer 1 — `cache.metrics()` for aggregate health
|
|
11
|
+
|
|
12
|
+
Built-in collector subscribed to the manager's event bus. Returns a snapshot whenever you ask:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
const m = cache.metrics();
|
|
16
|
+
// {
|
|
17
|
+
// hits, misses, sets, removed, errors,
|
|
18
|
+
// hitRate,
|
|
19
|
+
// latencyMs: { p50, p95, p99, samples },
|
|
20
|
+
// byDriver: { memory: {...}, redis: {...} },
|
|
21
|
+
// startedAt,
|
|
22
|
+
// }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Lazy** — the collector attaches on the first `cache.metrics()` / `cache.resetMetrics()` call. Apps that never read metrics pay zero cost. Earlier events are not retroactively counted, so if you want metrics on every op including the first, call `cache.metrics()` once during startup right after `cache.init()`.
|
|
26
|
+
|
|
27
|
+
**Survives `cache.use()` switches** — listens at the manager level, re-attaches to every loaded driver.
|
|
28
|
+
|
|
29
|
+
**Latency** is sampled by the manager around `get` / `set` / `remove` into a circular buffer (default 1000 samples per driver). Percentiles are computed at snapshot time. Older samples age out, so percentiles reflect the recent ~1000 ops.
|
|
30
|
+
|
|
31
|
+
`cache.resetMetrics()` zeroes counters + drops the buffer + bumps `startedAt`.
|
|
32
|
+
|
|
33
|
+
## Layer 2 — Raw events for per-event reactions
|
|
34
|
+
|
|
35
|
+
When you need to react to specific events (alerting, audit logs, debugging), subscribe to the event bus:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
cache.on("error", ({ key, error }) => {
|
|
39
|
+
pagerDuty.trigger(`Cache error on ${key}`, error);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
cache.on("miss", ({ key, driver }) => {
|
|
43
|
+
if (key.startsWith("hot.")) auditLog.miss(key, driver);
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Available events: `hit`, `miss`, `set`, `removed`, `invalidated`, `flushed`, `expired`, `connected`, `disconnected`, `error`.
|
|
48
|
+
|
|
49
|
+
Listeners attached via `cache.on(...)` survive driver switches the same way the metrics collector does.
|
|
50
|
+
|
|
51
|
+
## Which one to reach for
|
|
52
|
+
|
|
53
|
+
| Goal | Use |
|
|
54
|
+
|---|---|
|
|
55
|
+
| Show hit rate / latency in a dashboard | `cache.metrics()` |
|
|
56
|
+
| Page on cache errors | `cache.on("error", ...)` |
|
|
57
|
+
| Periodic export to Prometheus / StatsD | `cache.metrics()` + `setInterval` + `resetMetrics()` |
|
|
58
|
+
| Audit log of every removal | `cache.on("removed", ...)` |
|
|
59
|
+
| See which tag invalidations ran, what they removed and how long they took | `cache.on("invalidated", ({ tags, keys, durationMs }) => ...)` — once per `tags([...]).invalidate()` |
|
|
60
|
+
| Detect a specific anti-pattern (e.g. always-miss key) | `cache.on("miss", ...)` |
|
|
61
|
+
| Debug "is the cache being hit at all?" in dev | `cache.metrics()` once at the end of a flow |
|
|
62
|
+
|
|
63
|
+
Both layers can coexist — events fire whether the metrics collector is attached or not.
|
|
64
|
+
|
|
65
|
+
## Common shapes
|
|
66
|
+
|
|
67
|
+
### Periodic export, then reset
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
setInterval(() => {
|
|
71
|
+
const snapshot = cache.metrics();
|
|
72
|
+
exporter.send(snapshot);
|
|
73
|
+
cache.resetMetrics();
|
|
74
|
+
}, 60_000);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The snapshot now reflects the last minute of traffic, not the lifetime.
|
|
78
|
+
|
|
79
|
+
### Boundary measurement
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
cache.resetMetrics();
|
|
83
|
+
await runTrafficBurst();
|
|
84
|
+
console.log(cache.metrics());
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Useful for benchmarks, soak tests, "did the cache help?" before/after comparisons.
|
|
88
|
+
|
|
89
|
+
### Per-driver isolation
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const m = cache.metrics();
|
|
93
|
+
console.log(`memory hit rate: ${m.byDriver.memory?.hitRate ?? 0}`);
|
|
94
|
+
console.log(`redis p95: ${m.byDriver.redis?.latencyMs.p95 ?? 0}ms`);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Drivers that never fire events stay absent from `byDriver` — guard with `?.` and `?? 0`.
|
|
98
|
+
|
|
99
|
+
## Things NOT to do
|
|
100
|
+
|
|
101
|
+
- Don't subscribe to events to count things and ignore the built-in collector — that's exactly what it's built to do.
|
|
102
|
+
- Don't call `cache.metrics()` on every request expecting per-request data — it returns a running aggregate. Use the event bus for per-call observability.
|
|
103
|
+
- Don't expect lifetime percentiles. The buffer is bounded — for a 24h p95, sample-and-aggregate at your exporter, don't ask cache to remember every op forever.
|
|
104
|
+
- Don't forget to attach early. If startup metrics matter, call `cache.metrics()` right after `cache.init()`.
|
|
@@ -1,129 +1,129 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pick-cache-driver
|
|
3
|
-
description: 'Pick a cache driver — null / memory / memoryExtended / lru / file / redis / pg / mock — and configure it. Triggers: `cache.setCacheConfigurations`, `BaseCacheDriver`, `cache.use`, `cache.load`, `cache.driver`, `globalPrefix`; "which cache driver should I use", "configure redis driver", "register custom cache driver", "multi-tenant scoping"; typical import `import { cache, BaseCacheDriver } from "@warlock.js/cache"`. Skip: cache CRUD — `@warlock.js/cache/cache-basics/SKILL.md`; pg setup — `@warlock.js/cache/configure-pg-cache/SKILL.md`; competing libs `lru-cache`, `node-cache`, `keyv`, `ioredis`; native `Map`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Cache drivers — pick the right one
|
|
7
|
-
|
|
8
|
-
Seven production drivers + a mock driver ship in-box. Pick by durability, scope, and workload.
|
|
9
|
-
|
|
10
|
-
| Driver | Process scope | Persists on restart | Good for | Avoid when |
|
|
11
|
-
| --- | --- | --- | --- | --- |
|
|
12
|
-
| `null` | — | — | Disabling cache in tests; feature-flagging off | You actually want caching |
|
|
13
|
-
| `memory` | Single process | No | Hot in-process data with default TTL; smallest latency | Multi-process / multi-node |
|
|
14
|
-
| `memoryExtended` | Single process | No | Sliding-window TTL (TTL resets on every read) | Any multi-process deploy |
|
|
15
|
-
| `lru` | Single process | No | Bounded in-memory caches (capacity-based eviction) | Need cross-process sharing |
|
|
16
|
-
| `file` | Single host | Yes | Build artefacts, local dev persistence across restarts | Concurrency (no locks); multi-host |
|
|
17
|
-
| `redis` | Shared | Yes (Redis-managed) | Anything shared across processes / nodes | Single-process-only workload — overkill |
|
|
18
|
-
| `pg` | Shared | Yes (Postgres-managed) | You already run Postgres; semantic caching / RAG via pgvector | High-throughput hot reads (Redis is faster) |
|
|
19
|
-
|
|
20
|
-
## Capability matrix
|
|
21
|
-
|
|
22
|
-
| Capability | null | memory | memoryExt | lru | file | redis | pg |
|
|
23
|
-
| --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
|
|
24
|
-
| `set` / `get` / `remove` / `flush` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
25
|
-
| TTL (number or string; numeric strings = seconds) | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
26
|
-
| Sliding TTL on read | — | — | ✓ | — | — | — | — |
|
|
27
|
-
| `removeNamespace` | noop | ✓ | ✓ | ✓ (prefix-scan) | ✓ | ✓ | ✓ (LIKE prefix) |
|
|
28
|
-
| `onConflict: "create"` / `"update"` | noop | emulated | emulated | emulated | emulated | native `NX`/`XX` | native (INSERT ON CONFLICT) |
|
|
1
|
+
---
|
|
2
|
+
name: pick-cache-driver
|
|
3
|
+
description: 'Pick a cache driver — null / memory / memoryExtended / lru / file / redis / pg / mock — and configure it. Triggers: `cache.setCacheConfigurations`, `BaseCacheDriver`, `cache.use`, `cache.load`, `cache.driver`, `globalPrefix`; "which cache driver should I use", "configure redis driver", "register custom cache driver", "multi-tenant scoping"; typical import `import { cache, BaseCacheDriver } from "@warlock.js/cache"`. Skip: cache CRUD — `@warlock.js/cache/cache-basics/SKILL.md`; pg setup — `@warlock.js/cache/configure-pg-cache/SKILL.md`; competing libs `lru-cache`, `node-cache`, `keyv`, `ioredis`; native `Map`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Cache drivers — pick the right one
|
|
7
|
+
|
|
8
|
+
Seven production drivers + a mock driver ship in-box. Pick by durability, scope, and workload.
|
|
9
|
+
|
|
10
|
+
| Driver | Process scope | Persists on restart | Good for | Avoid when |
|
|
11
|
+
| --- | --- | --- | --- | --- |
|
|
12
|
+
| `null` | — | — | Disabling cache in tests; feature-flagging off | You actually want caching |
|
|
13
|
+
| `memory` | Single process | No | Hot in-process data with default TTL; smallest latency | Multi-process / multi-node |
|
|
14
|
+
| `memoryExtended` | Single process | No | Sliding-window TTL (TTL resets on every read) | Any multi-process deploy |
|
|
15
|
+
| `lru` | Single process | No | Bounded in-memory caches (capacity-based eviction) | Need cross-process sharing |
|
|
16
|
+
| `file` | Single host | Yes | Build artefacts, local dev persistence across restarts | Concurrency (no locks); multi-host |
|
|
17
|
+
| `redis` | Shared | Yes (Redis-managed) | Anything shared across processes / nodes | Single-process-only workload — overkill |
|
|
18
|
+
| `pg` | Shared | Yes (Postgres-managed) | You already run Postgres; semantic caching / RAG via pgvector | High-throughput hot reads (Redis is faster) |
|
|
19
|
+
|
|
20
|
+
## Capability matrix
|
|
21
|
+
|
|
22
|
+
| Capability | null | memory | memoryExt | lru | file | redis | pg |
|
|
23
|
+
| --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
|
|
24
|
+
| `set` / `get` / `remove` / `flush` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
25
|
+
| TTL (number or string; numeric strings = seconds) | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
26
|
+
| Sliding TTL on read | — | — | ✓ | — | — | — | — |
|
|
27
|
+
| `removeNamespace` | noop | ✓ | ✓ | ✓ (prefix-scan) | ✓ | ✓ | ✓ (LIKE prefix) |
|
|
28
|
+
| `onConflict: "create"` / `"update"` | noop | emulated | emulated | emulated | emulated | native `NX`/`XX` | native (INSERT ON CONFLICT) |
|
|
29
29
|
| `increment` / `decrement` / `pull` | — | one process | one process | one process | one process | **cross-server** | **cross-server** |
|
|
30
30
|
| `update()` / `merge()` | ✓ | one process | one process | one process | ✗ throws | **cross-server** (compare-and-set) | **cross-server** (compare-and-set) |
|
|
31
|
-
| List sub-API | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (O(n) JSON blob today; native LPUSH/LRANGE in v2.1) | ✓ |
|
|
32
|
-
| Tagged invalidation | noop | ✓ | ✓ | ✓ | ✓ (serialized index) | ✓ (native SET) | ✓ (JSON-array merge) |
|
|
33
|
-
| `similar()` / `set({ vector })` | returns `[]` / noop | ✓ brute force | ✓ brute force | ✓ brute force | ✗ throws | ✗ throws (Phase 2 backlog) | ✓ (with `vector` config — pgvector) |
|
|
34
|
-
|
|
31
|
+
| List sub-API | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (O(n) JSON blob today; native LPUSH/LRANGE in v2.1) | ✓ |
|
|
32
|
+
| Tagged invalidation | noop | ✓ | ✓ | ✓ | ✓ (serialized index) | ✓ (native SET) | ✓ (JSON-array merge) |
|
|
33
|
+
| `similar()` / `set({ vector })` | returns `[]` / noop | ✓ brute force | ✓ brute force | ✓ brute force | ✗ throws | ✗ throws (Phase 2 backlog) | ✓ (with `vector` config — pgvector) |
|
|
34
|
+
|
|
35
35
|
## Multi-server: use redis or pg
|
|
36
36
|
|
|
37
37
|
Memory, memoryExtended, lru and file caches are **per-process** (file: per host). With more than one server, use `redis` or `pg` for counters, throttles, one-time tokens (`pull`), shared `update`, `lock()` and tag invalidation; on memory each server counts and invalidates alone. `remember()`/`swr()`/`cached()` stampede protection is per process even on redis. Redis Cluster is unsupported.
|
|
38
38
|
|
|
39
39
|
Driver storage notes: `memory` stores flat keys (`get("users")` after `set("users.1")` is a miss) and `maxSize` counts entries in LRU order; redis `flush()` without a `globalPrefix` runs `FLUSHDB` (whole database), so set a prefix.
|
|
40
40
|
|
|
41
|
-
## Global config TTL — accepts number or string
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
options: {
|
|
45
|
-
redis: { url: "...", ttl: "7d" }, // string OK
|
|
46
|
-
memory: { ttl: 3600 }, // number OK
|
|
47
|
-
lru: { capacity: 10_000 }, // LRU has no TTL option today
|
|
48
|
-
file: { directory: () => "/var/cache/myapp", ttl: "1h" },
|
|
49
|
-
pg: { client: pool, ttl: "1h" }, // KV-only
|
|
50
|
-
// pg with pgvector:
|
|
51
|
-
// pg: { client: pool, vector: { dimensions: 1536, index: "hnsw" } },
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## Global prefix (multi-tenant scoping)
|
|
56
|
-
|
|
57
|
-
Every driver accepts `globalPrefix: string | (() => string)`. The function form runs per call — pair it with request-local async context to scope every cached key to the current tenant / user / client automatically:
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
options: {
|
|
61
|
-
redis: {
|
|
62
|
-
url: "...",
|
|
63
|
-
globalPrefix: () => `tenant-${currentContext.tenantId}`,
|
|
64
|
-
},
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Key & namespace safety (file / redis)
|
|
69
|
-
|
|
70
|
-
Cache keys can carry untrusted input (e.g. a `cached()` auto-key derived from a request param), so every built-in driver treats a key as data, never as a path or a query fragment:
|
|
71
|
-
|
|
72
|
-
- **`file`** — a key/namespace maps to exactly one on-disk directory component: `%`, `/`, and `\` are percent-encoded before the path is built, so `../../etc` becomes an inert directory name instead of a traversal. The resolved path is also asserted to stay inside the configured cache root (throws `CacheError` otherwise) — a second, encoding-independent check. `removeNamespace` clears both the namespace's own directory and every dotted child (`ns`, `ns.*`), honoring `globalPrefix`.
|
|
73
|
-
- **`redis`** — `removeNamespace` escapes glob metacharacters (`*`, `?`, `[`, `\`) in the namespace before building its match pattern, and walks matches with a `SCAN` cursor (`scanIterator`) instead of the blocking `KEYS` command, so clearing a namespace on a large keyspace doesn't stall the event loop for other tenants.
|
|
74
|
-
- **Errors that echo a connection string** (Redis `connect()` failures, any driver's failed op) are logged through a `safeErrorInfo()` helper that redacts `scheme://user:pass@` credentials to `scheme://[REDACTED]@` and never prints the raw `Error` object — see [`@warlock.js/cache/handle-cache-errors/SKILL.md`](@warlock.js/cache/handle-cache-errors/SKILL.md).
|
|
75
|
-
|
|
76
|
-
## Registering a custom driver
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
import { BaseCacheDriver, cache } from "@warlock.js/cache";
|
|
80
|
-
|
|
81
|
-
class MemcachedCacheDriver extends BaseCacheDriver<MyClient, MyOptions> {
|
|
82
|
-
public name = "memcached";
|
|
83
|
-
// … implement set / get / remove / flush / removeNamespace / connect
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
cache.setCacheConfigurations({
|
|
87
|
-
default: "memcached",
|
|
88
|
-
drivers: { memcached: MemcachedCacheDriver },
|
|
89
|
-
options: { memcached: { host: "localhost" } },
|
|
90
|
-
});
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Extending `BaseCacheDriver` gives you free: TTL parsing, key parsing, event emission, stampede-safe `remember`, deep-clone-on-read, default `update` / `merge` / `list` implementations.
|
|
94
|
-
|
|
95
|
-
## Runtime driver options — `cache.use(name, options)`
|
|
96
|
-
|
|
97
|
-
Some driver options can only be built at runtime (`pg`'s `client: pg.Pool`, pre-wired clients). Pass them as the second arg to `cache.use` / `cache.load` / `cache.driver` — they merge over `setCacheConfigurations({ options })` per-key, runtime wins.
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
101
|
-
|
|
102
|
-
cache.setCacheConfigurations({
|
|
103
|
-
default: "pg",
|
|
104
|
-
drivers: { pg: PgCacheDriver },
|
|
105
|
-
options: { pg: { table: "cache" } }, // static
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
await cache.use("pg", { client: pool }); // runtime — skip init() in this case
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Constraints:
|
|
112
|
-
- The driver name must be registered in `setCacheConfigurations({ drivers })` — runtime options don't bypass registration.
|
|
113
|
-
- Once a driver is loaded, calling `use`/`load`/`driver` again with **non-empty** new options throws `CacheConfigurationError`. Register a second driver name if you need a different config.
|
|
114
|
-
- Calling without options (or with `{}`) on an already-loaded driver returns the cached instance silently.
|
|
115
|
-
|
|
116
|
-
## Per-call driver override
|
|
117
|
-
|
|
118
|
-
When most writes go to the default driver but one call needs a different one:
|
|
119
|
-
|
|
120
|
-
```ts
|
|
121
|
-
await cache.set("audit:event", event, { driver: "redis" });
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
The manager loads (and connects) the override driver lazily on first use, then routes that single operation through it without mutating `currentDriver`.
|
|
125
|
-
|
|
126
|
-
## See also
|
|
127
|
-
|
|
128
|
-
- [`@warlock.js/cache/configure-pg-cache/SKILL.md`](@warlock.js/cache/configure-pg-cache/SKILL.md) — full pg setup (KV-only and pgvector mode)
|
|
129
|
-
- [`@warlock.js/cache/test-cache-code/SKILL.md`](@warlock.js/cache/test-cache-code/SKILL.md) — `MockCacheDriver` and `NullCacheDriver` for tests
|
|
41
|
+
## Global config TTL — accepts number or string
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
options: {
|
|
45
|
+
redis: { url: "...", ttl: "7d" }, // string OK
|
|
46
|
+
memory: { ttl: 3600 }, // number OK
|
|
47
|
+
lru: { capacity: 10_000 }, // LRU has no TTL option today
|
|
48
|
+
file: { directory: () => "/var/cache/myapp", ttl: "1h" },
|
|
49
|
+
pg: { client: pool, ttl: "1h" }, // KV-only
|
|
50
|
+
// pg with pgvector:
|
|
51
|
+
// pg: { client: pool, vector: { dimensions: 1536, index: "hnsw" } },
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Global prefix (multi-tenant scoping)
|
|
56
|
+
|
|
57
|
+
Every driver accepts `globalPrefix: string | (() => string)`. The function form runs per call — pair it with request-local async context to scope every cached key to the current tenant / user / client automatically:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
options: {
|
|
61
|
+
redis: {
|
|
62
|
+
url: "...",
|
|
63
|
+
globalPrefix: () => `tenant-${currentContext.tenantId}`,
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Key & namespace safety (file / redis)
|
|
69
|
+
|
|
70
|
+
Cache keys can carry untrusted input (e.g. a `cached()` auto-key derived from a request param), so every built-in driver treats a key as data, never as a path or a query fragment:
|
|
71
|
+
|
|
72
|
+
- **`file`** — a key/namespace maps to exactly one on-disk directory component: `%`, `/`, and `\` are percent-encoded before the path is built, so `../../etc` becomes an inert directory name instead of a traversal. The resolved path is also asserted to stay inside the configured cache root (throws `CacheError` otherwise) — a second, encoding-independent check. `removeNamespace` clears both the namespace's own directory and every dotted child (`ns`, `ns.*`), honoring `globalPrefix`.
|
|
73
|
+
- **`redis`** — `removeNamespace` escapes glob metacharacters (`*`, `?`, `[`, `\`) in the namespace before building its match pattern, and walks matches with a `SCAN` cursor (`scanIterator`) instead of the blocking `KEYS` command, so clearing a namespace on a large keyspace doesn't stall the event loop for other tenants.
|
|
74
|
+
- **Errors that echo a connection string** (Redis `connect()` failures, any driver's failed op) are logged through a `safeErrorInfo()` helper that redacts `scheme://user:pass@` credentials to `scheme://[REDACTED]@` and never prints the raw `Error` object — see [`@warlock.js/cache/handle-cache-errors/SKILL.md`](@warlock.js/cache/handle-cache-errors/SKILL.md).
|
|
75
|
+
|
|
76
|
+
## Registering a custom driver
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { BaseCacheDriver, cache } from "@warlock.js/cache";
|
|
80
|
+
|
|
81
|
+
class MemcachedCacheDriver extends BaseCacheDriver<MyClient, MyOptions> {
|
|
82
|
+
public name = "memcached";
|
|
83
|
+
// … implement set / get / remove / flush / removeNamespace / connect
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
cache.setCacheConfigurations({
|
|
87
|
+
default: "memcached",
|
|
88
|
+
drivers: { memcached: MemcachedCacheDriver },
|
|
89
|
+
options: { memcached: { host: "localhost" } },
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Extending `BaseCacheDriver` gives you free: TTL parsing, key parsing, event emission, stampede-safe `remember`, deep-clone-on-read, default `update` / `merge` / `list` implementations.
|
|
94
|
+
|
|
95
|
+
## Runtime driver options — `cache.use(name, options)`
|
|
96
|
+
|
|
97
|
+
Some driver options can only be built at runtime (`pg`'s `client: pg.Pool`, pre-wired clients). Pass them as the second arg to `cache.use` / `cache.load` / `cache.driver` — they merge over `setCacheConfigurations({ options })` per-key, runtime wins.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
101
|
+
|
|
102
|
+
cache.setCacheConfigurations({
|
|
103
|
+
default: "pg",
|
|
104
|
+
drivers: { pg: PgCacheDriver },
|
|
105
|
+
options: { pg: { table: "cache" } }, // static
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await cache.use("pg", { client: pool }); // runtime — skip init() in this case
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Constraints:
|
|
112
|
+
- The driver name must be registered in `setCacheConfigurations({ drivers })` — runtime options don't bypass registration.
|
|
113
|
+
- Once a driver is loaded, calling `use`/`load`/`driver` again with **non-empty** new options throws `CacheConfigurationError`. Register a second driver name if you need a different config.
|
|
114
|
+
- Calling without options (or with `{}`) on an already-loaded driver returns the cached instance silently.
|
|
115
|
+
|
|
116
|
+
## Per-call driver override
|
|
117
|
+
|
|
118
|
+
When most writes go to the default driver but one call needs a different one:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
await cache.set("audit:event", event, { driver: "redis" });
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The manager loads (and connects) the override driver lazily on first use, then routes that single operation through it without mutating `currentDriver`.
|
|
125
|
+
|
|
126
|
+
## See also
|
|
127
|
+
|
|
128
|
+
- [`@warlock.js/cache/configure-pg-cache/SKILL.md`](@warlock.js/cache/configure-pg-cache/SKILL.md) — full pg setup (KV-only and pgvector mode)
|
|
129
|
+
- [`@warlock.js/cache/test-cache-code/SKILL.md`](@warlock.js/cache/test-cache-code/SKILL.md) — `MockCacheDriver` and `NullCacheDriver` for tests
|