@warlock.js/cache 5.19.0 → 5.20.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 +55 -0
- package/cjs/index.cjs +1439 -331
- package/cjs/index.cjs.map +1 -1
- package/esm/cache-manager.d.mts +4 -0
- package/esm/cache-manager.d.mts.map +1 -1
- package/esm/cache-manager.mjs +23 -5
- package/esm/cache-manager.mjs.map +1 -1
- package/esm/cached/auto-key.d.mts +14 -14
- package/esm/cached/auto-key.d.mts.map +1 -1
- package/esm/cached/auto-key.mjs +50 -34
- package/esm/cached/auto-key.mjs.map +1 -1
- package/esm/cached/cached.mjs +4 -0
- package/esm/cached/cached.mjs.map +1 -1
- package/esm/drivers/base-cache-driver.d.mts +84 -8
- package/esm/drivers/base-cache-driver.d.mts.map +1 -1
- package/esm/drivers/base-cache-driver.mjs +152 -42
- package/esm/drivers/base-cache-driver.mjs.map +1 -1
- package/esm/drivers/file-cache-driver.d.mts +15 -0
- package/esm/drivers/file-cache-driver.d.mts.map +1 -1
- package/esm/drivers/file-cache-driver.mjs +68 -18
- package/esm/drivers/file-cache-driver.mjs.map +1 -1
- package/esm/drivers/in-memory-tag-index.d.mts +44 -0
- package/esm/drivers/in-memory-tag-index.d.mts.map +1 -0
- package/esm/drivers/in-memory-tag-index.mjs +73 -0
- package/esm/drivers/in-memory-tag-index.mjs.map +1 -0
- package/esm/drivers/index.d.mts +1 -1
- package/esm/drivers/index.mjs +1 -1
- package/esm/drivers/lru-memory-cache-driver.d.mts +43 -0
- package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/lru-memory-cache-driver.mjs +96 -0
- package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-cache-driver.d.mts +87 -30
- package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-cache-driver.mjs +214 -96
- package/esm/drivers/memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.d.mts +9 -5
- package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.mjs +10 -23
- package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -1
- package/esm/drivers/mock-cache-driver.d.mts +41 -0
- package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
- package/esm/drivers/mock-cache-driver.mjs +105 -8
- package/esm/drivers/mock-cache-driver.mjs.map +1 -1
- package/esm/drivers/null-cache-driver.d.mts +8 -2
- package/esm/drivers/null-cache-driver.d.mts.map +1 -1
- package/esm/drivers/null-cache-driver.mjs +10 -2
- package/esm/drivers/null-cache-driver.mjs.map +1 -1
- package/esm/drivers/pg-cache-driver.d.mts +77 -3
- package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
- package/esm/drivers/pg-cache-driver.mjs +229 -7
- package/esm/drivers/pg-cache-driver.mjs.map +1 -1
- package/esm/drivers/redis-cache-driver.d.mts +86 -1
- package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
- package/esm/drivers/redis-cache-driver.mjs +252 -16
- package/esm/drivers/redis-cache-driver.mjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/index.mjs +2 -2
- package/esm/list/memory-cache-list.d.mts +11 -8
- package/esm/list/memory-cache-list.d.mts.map +1 -1
- package/esm/list/memory-cache-list.mjs +39 -37
- package/esm/list/memory-cache-list.mjs.map +1 -1
- package/esm/scoped-cache.d.mts +9 -3
- package/esm/scoped-cache.d.mts.map +1 -1
- package/esm/scoped-cache.mjs +19 -7
- package/esm/scoped-cache.mjs.map +1 -1
- package/esm/tagged-cache.d.mts +20 -0
- package/esm/tagged-cache.d.mts.map +1 -1
- package/esm/tagged-cache.mjs +62 -24
- package/esm/tagged-cache.mjs.map +1 -1
- package/esm/tagged-scoped-cache.d.mts +7 -3
- package/esm/tagged-scoped-cache.d.mts.map +1 -1
- package/esm/tagged-scoped-cache.mjs +19 -7
- package/esm/tagged-scoped-cache.mjs.map +1 -1
- package/esm/types.d.mts +41 -3
- package/esm/types.d.mts.map +1 -1
- package/esm/types.mjs.map +1 -1
- package/esm/utils.d.mts +9 -3
- package/esm/utils.d.mts.map +1 -1
- package/esm/utils.mjs +16 -6
- package/esm/utils.mjs.map +1 -1
- package/llms-full.txt +1685 -1666
- package/llms.txt +18 -18
- package/package.json +4 -4
- package/skills/apply-cache-patterns/SKILL.md +98 -97
- package/skills/configure-pg-cache/SKILL.md +6 -5
- package/skills/configure-set-options/SKILL.md +102 -96
- package/skills/handle-cache-errors/SKILL.md +92 -92
- package/skills/index.md +20 -0
- package/skills/observe-cache/SKILL.md +103 -103
- package/skills/pick-cache-driver/SKILL.md +129 -123
- package/skills/test-cache-code/SKILL.md +219 -219
- package/skills/use-cache-atomic/SKILL.md +57 -65
- 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 +113 -104
- package/skills/use-cache-namespace/SKILL.md +88 -88
- package/skills/use-cache-similarity/SKILL.md +94 -94
- package/skills/use-cache-tags/SKILL.md +7 -4
- 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 +100 -99
- package/skills/use-swr/SKILL.md +104 -104
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: handle-cache-errors
|
|
3
|
-
description:
|
|
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.
|
package/skills/index.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Cache layer for Warlock apps with pluggable drivers (memory, LRU, file, Redis, Postgres). Exports `cache`, `CacheManager`, `cached`, `TaggedCache`, `ScopedCache`, `RedisCacheDriver`, `MemoryCacheDriver`, `MockCacheDriver`, `PgCacheDriver`. Use for: \"cache this query for 5 minutes\", \"invalidate every key for tenant 42\", \"stale-while-revalidate\", \"take a lock so only one worker runs this\", \"tag keys and flush the tag\", \"increment a counter in cache\". Not this package: HTTP response/page caching belongs to @warlock.js/core or web; persistent data is a @warlock.js/cascade model."
|
|
3
|
+
---
|
|
4
|
+
# @warlock.js/cache
|
|
5
|
+
|
|
6
|
+
The `cache` singleton (a `CacheManager`) fronts one active driver. Everything, from `get`/`set` through `remember`, `swr`, `tags`, `namespace`, `lock` and `list`, works the same on any driver, so pick a driver once and write app code against `cache`.
|
|
7
|
+
|
|
8
|
+
## The 80% path
|
|
9
|
+
1. Orient: `overview.md`, `cache-basics.md`.
|
|
10
|
+
2. Choose and configure a driver: `pick-cache-driver.md` (Postgres: `configure-pg-cache.md`; TTL and options: `configure-set-options.md`).
|
|
11
|
+
3. Cache expensive reads with `remember` or the `cached` wrapper: `use-cached-hof.md`, `apply-cache-patterns.md`.
|
|
12
|
+
4. Invalidate in groups: `use-cache-tags.md` or `use-cache-namespace.md`.
|
|
13
|
+
5. Stampede protection and freshness: `use-swr.md`, `use-cache-lock.md`.
|
|
14
|
+
6. Test with the mock driver: `test-cache-code.md`; errors and metrics: `handle-cache-errors.md`, `observe-cache.md`.
|
|
15
|
+
|
|
16
|
+
## Conventions and pitfalls
|
|
17
|
+
- Tags versus namespaces: tags are cross-cutting groups you flush together; a namespace is a key prefix you remove wholesale. Choose per invalidation need.
|
|
18
|
+
- Check `configure-set-options.md` for TTL units, and avoid `forever` for data that can go stale.
|
|
19
|
+
- Atomic counters, bulk ops, lists, merge and similarity have their own topics (`use-cache-atomic.md`, `use-cache-bulk.md`, `use-cache-list.md`, `use-cache-update-merge.md`, `use-cache-similarity.md`); do not hand-roll read-modify-write.
|
|
20
|
+
- Not every driver supports every feature; check the driver topic first.
|
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: observe-cache
|
|
3
|
-
description:
|
|
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
|
-
| Detect a specific anti-pattern (e.g. always-miss key) | `cache.on("miss", ...)` |
|
|
60
|
-
| Debug "is the cache being hit at all?" in dev | `cache.metrics()` once at the end of a flow |
|
|
61
|
-
|
|
62
|
-
Both layers can coexist — events fire whether the metrics collector is attached or not.
|
|
63
|
-
|
|
64
|
-
## Common shapes
|
|
65
|
-
|
|
66
|
-
### Periodic export, then reset
|
|
67
|
-
|
|
68
|
-
```ts
|
|
69
|
-
setInterval(() => {
|
|
70
|
-
const snapshot = cache.metrics();
|
|
71
|
-
exporter.send(snapshot);
|
|
72
|
-
cache.resetMetrics();
|
|
73
|
-
}, 60_000);
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
The snapshot now reflects the last minute of traffic, not the lifetime.
|
|
77
|
-
|
|
78
|
-
### Boundary measurement
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
cache.resetMetrics();
|
|
82
|
-
await runTrafficBurst();
|
|
83
|
-
console.log(cache.metrics());
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Useful for benchmarks, soak tests, "did the cache help?" before/after comparisons.
|
|
87
|
-
|
|
88
|
-
### Per-driver isolation
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
const m = cache.metrics();
|
|
92
|
-
console.log(`memory hit rate: ${m.byDriver.memory?.hitRate ?? 0}`);
|
|
93
|
-
console.log(`redis p95: ${m.byDriver.redis?.latencyMs.p95 ?? 0}ms`);
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Drivers that never fire events stay absent from `byDriver` — guard with `?.` and `?? 0`.
|
|
97
|
-
|
|
98
|
-
## Things NOT to do
|
|
99
|
-
|
|
100
|
-
- Don't subscribe to events to count things and ignore the built-in collector — that's exactly what it's built to do.
|
|
101
|
-
- 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.
|
|
102
|
-
- 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.
|
|
103
|
-
- Don't forget to attach early. If startup metrics matter, call `cache.metrics()` right after `cache.init()`.
|
|
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
|
+
| Detect a specific anti-pattern (e.g. always-miss key) | `cache.on("miss", ...)` |
|
|
60
|
+
| Debug "is the cache being hit at all?" in dev | `cache.metrics()` once at the end of a flow |
|
|
61
|
+
|
|
62
|
+
Both layers can coexist — events fire whether the metrics collector is attached or not.
|
|
63
|
+
|
|
64
|
+
## Common shapes
|
|
65
|
+
|
|
66
|
+
### Periodic export, then reset
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
setInterval(() => {
|
|
70
|
+
const snapshot = cache.metrics();
|
|
71
|
+
exporter.send(snapshot);
|
|
72
|
+
cache.resetMetrics();
|
|
73
|
+
}, 60_000);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The snapshot now reflects the last minute of traffic, not the lifetime.
|
|
77
|
+
|
|
78
|
+
### Boundary measurement
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
cache.resetMetrics();
|
|
82
|
+
await runTrafficBurst();
|
|
83
|
+
console.log(cache.metrics());
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Useful for benchmarks, soak tests, "did the cache help?" before/after comparisons.
|
|
87
|
+
|
|
88
|
+
### Per-driver isolation
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const m = cache.metrics();
|
|
92
|
+
console.log(`memory hit rate: ${m.byDriver.memory?.hitRate ?? 0}`);
|
|
93
|
+
console.log(`redis p95: ${m.byDriver.redis?.latencyMs.p95 ?? 0}ms`);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Drivers that never fire events stay absent from `byDriver` — guard with `?.` and `?? 0`.
|
|
97
|
+
|
|
98
|
+
## Things NOT to do
|
|
99
|
+
|
|
100
|
+
- Don't subscribe to events to count things and ignore the built-in collector — that's exactly what it's built to do.
|
|
101
|
+
- 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.
|
|
102
|
+
- 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.
|
|
103
|
+
- Don't forget to attach early. If startup metrics matter, call `cache.metrics()` right after `cache.init()`.
|