@ultimat3/cache 2.0.0 → 3.0.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/CLAUDE.md +10 -0
- package/package.json +2 -2
- package/src/invalidate.ts +8 -13
- package/src/tier-failures.ts +24 -3
package/CLAUDE.md
CHANGED
|
@@ -40,6 +40,16 @@ Tier 1. Tagged caching + THE invalidation graph.
|
|
|
40
40
|
`TierName` plus `'query-read'` — closed, and deliberately NOT a widening of `TierName`: a name
|
|
41
41
|
missing from `TIER_ORDER` sorts to `-1`, ahead of the request memo. A label is a log facet; a
|
|
42
42
|
`TierName` is a position on the ladder.
|
|
43
|
+
- **A refusal is rendered with `renderThrowable()`, never `error.message`** — the four sites that
|
|
44
|
+
absorb one (`bestEffort`'s log entry, and `fanOut`'s tier, ISR and broadcast catch blocks). A
|
|
45
|
+
tier, a revalidator and a broadcast are all app-supplied, so the value they reject with is too:
|
|
46
|
+
`instanceof` runs a `Proxy`'s `getPrototypeOf` trap and `String()` runs `Symbol.toPrimitive`, so
|
|
47
|
+
building the log line used to raise INSTEAD of absorbing the refusal — on the business write that
|
|
48
|
+
triggered the bust, which is the one caller both contracts promise to protect. The code field
|
|
49
|
+
keeps its own total probe (`ultimateCode` in `tier-failures.ts`) rather than core's `stringField`:
|
|
50
|
+
a driver error's `code` is a SQLSTATE and must never be reported as an `X_*` one. Consequence to
|
|
51
|
+
know: a recorded `message` carries the throwable's NAME (`Error: nats is down`, `"just a string"`),
|
|
52
|
+
which is what `renderThrowable` renders and what the tests here now pin.
|
|
43
53
|
- Tier failures go into `report.errors`. A cache tier may never fail a business read or write.
|
|
44
54
|
`createCacheStack` routes every `get`/`set`/`del` through `bestEffort()` for that reason — a
|
|
45
55
|
refusal becomes "that tier did not answer" and lands in `recentTierFailures()`, the read side's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cache",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Tagged caching: request memo, LRU, Redis, CDN — one invalidation graph",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,6 +31,6 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ultimat3/core": "
|
|
34
|
+
"@ultimat3/core": "3.0.0"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/src/invalidate.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// the returned report is what the `/_x` cache panel renders, so "did it actually clear?" is
|
|
5
5
|
// answerable without a log dive.
|
|
6
6
|
|
|
7
|
-
import { currentSpan, logger, systemClock, withSpan } from '@ultimat3/core';
|
|
7
|
+
import { currentSpan, logger, renderThrowable, systemClock, withSpan } from '@ultimat3/core';
|
|
8
8
|
import { markInvalidated } from './fence';
|
|
9
9
|
import { dependentsOfKind } from './graph';
|
|
10
10
|
import type { CacheTag } from './tags';
|
|
@@ -222,10 +222,11 @@ function fanOut(tags: readonly CacheTag[], options: FanOutOptions): Promise<Inva
|
|
|
222
222
|
try {
|
|
223
223
|
tiers.push(await tier.invalidateTags(tags));
|
|
224
224
|
} catch (error) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
225
|
+
// `renderThrowable`, never `error.message`: a tier is app-supplied, so the value it
|
|
226
|
+
// rejects with is too, and both `instanceof` and `String()` RUN app code on it. A render
|
|
227
|
+
// that throws here rejects the whole fan-out — the failure the line above promises not to
|
|
228
|
+
// let reach the write that triggered the bust.
|
|
229
|
+
errors.push({ tier: tier.name, message: renderThrowable(error) });
|
|
229
230
|
}
|
|
230
231
|
}
|
|
231
232
|
// The report is read order, not clear order: it is what the `/_x` panel renders, and a ladder
|
|
@@ -240,10 +241,7 @@ function fanOut(tags: readonly CacheTag[], options: FanOutOptions): Promise<Inva
|
|
|
240
241
|
try {
|
|
241
242
|
await revalidator?.(path);
|
|
242
243
|
} catch (error) {
|
|
243
|
-
errors.push({
|
|
244
|
-
tier: 'isr',
|
|
245
|
-
message: error instanceof Error ? error.message : String(error),
|
|
246
|
-
});
|
|
244
|
+
errors.push({ tier: 'isr', message: renderThrowable(error) });
|
|
247
245
|
}
|
|
248
246
|
}
|
|
249
247
|
|
|
@@ -255,10 +253,7 @@ function fanOut(tags: readonly CacheTag[], options: FanOutOptions): Promise<Inva
|
|
|
255
253
|
try {
|
|
256
254
|
await broadcast(wire);
|
|
257
255
|
} catch (error) {
|
|
258
|
-
errors.push({
|
|
259
|
-
tier: 'broadcast',
|
|
260
|
-
message: error instanceof Error ? error.message : String(error),
|
|
261
|
-
});
|
|
256
|
+
errors.push({ tier: 'broadcast', message: renderThrowable(error) });
|
|
262
257
|
}
|
|
263
258
|
}
|
|
264
259
|
|
package/src/tier-failures.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// to return, so every swallowed refusal lands in one bounded log plus one `warn` — a stack
|
|
4
4
|
// running degraded stays answerable instead of merely looking slow.
|
|
5
5
|
|
|
6
|
-
import { logger, systemClock, UltimateError } from '@ultimat3/core';
|
|
6
|
+
import { logger, renderThrowable, systemClock, UltimateError } from '@ultimat3/core';
|
|
7
7
|
import type { TierLabel } from './tiers';
|
|
8
8
|
|
|
9
9
|
/** The three tier calls a stack makes on the value path. `invalidateTags` reports its own. */
|
|
@@ -77,14 +77,35 @@ export async function bestEffort<T>(
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* The `X_*` code when the tier threw an `UltimateError`, and `undefined` for every other answer —
|
|
82
|
+
* "the probe itself threw" included. `instanceof` RUNS a `Proxy`'s `getPrototypeOf` trap and the
|
|
83
|
+
* read past it is a getter call, both on a value this package did not build; the one place the
|
|
84
|
+
* question is asked is the catch block absorbing a refusal, which has nothing left to answer with
|
|
85
|
+
* if asking it raises. Core's `isThrownError` is this guard for `Error` and `stringField` is it for
|
|
86
|
+
* a loose field — neither fits here, because a driver error's `code` is a SQLSTATE and must never
|
|
87
|
+
* be reported as an `X_*` one.
|
|
88
|
+
*/
|
|
89
|
+
function ultimateCode(error: unknown): string | undefined {
|
|
90
|
+
try {
|
|
91
|
+
if (!(error instanceof UltimateError)) return undefined;
|
|
92
|
+
return typeof error.code === 'string' ? error.code : undefined;
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
function record(tier: TierLabel, op: TierOperation, key: string, error: unknown): void {
|
|
99
|
+
const code = ultimateCode(error);
|
|
81
100
|
const failure: TierFailure = {
|
|
82
101
|
at: systemClock.now().toISOString(),
|
|
83
102
|
tier,
|
|
84
103
|
op,
|
|
85
104
|
key,
|
|
86
|
-
...(
|
|
87
|
-
|
|
105
|
+
...(code === undefined ? {} : { code }),
|
|
106
|
+
// Never `error.message`: a rendering that throws replaces the absorbed refusal with a
|
|
107
|
+
// `TypeError` on the business read this function exists to keep alive.
|
|
108
|
+
message: renderThrowable(error),
|
|
88
109
|
};
|
|
89
110
|
failureLog.unshift(failure);
|
|
90
111
|
failureLog.length = Math.min(failureLog.length, MAX_TIER_FAILURES);
|