actly 1.1.0 → 1.2.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/LICENSE +0 -0
- package/README.md +262 -232
- package/dist/core/act.cjs +505 -39
- package/dist/core/act.d.ts +81 -10
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +504 -40
- package/dist/core/act.js.map +1 -1
- package/dist/core/executor.cjs +21 -9
- package/dist/core/executor.d.ts +31 -6
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +21 -9
- package/dist/core/executor.js.map +1 -1
- package/dist/core/health.cjs +34 -0
- package/dist/core/health.d.ts +18 -0
- package/dist/core/health.d.ts.map +1 -0
- package/dist/core/health.js +28 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/shutdown.cjs +60 -0
- package/dist/core/shutdown.d.ts +8 -0
- package/dist/core/shutdown.d.ts.map +1 -0
- package/dist/core/shutdown.js +56 -0
- package/dist/core/shutdown.js.map +1 -0
- package/dist/core/tenant.cjs +71 -0
- package/dist/core/tenant.d.ts +29 -0
- package/dist/core/tenant.d.ts.map +1 -0
- package/dist/core/tenant.js +68 -0
- package/dist/core/tenant.js.map +1 -0
- package/dist/errors.cjs +182 -0
- package/dist/errors.d.ts +151 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +171 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.cjs +56 -6
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -4
- package/dist/index.js.map +1 -1
- package/dist/observability.cjs +45 -0
- package/dist/observability.d.ts +145 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +43 -0
- package/dist/observability.js.map +1 -0
- package/dist/policies/bulkhead.cjs +97 -0
- package/dist/policies/bulkhead.d.ts +3 -0
- package/dist/policies/bulkhead.d.ts.map +1 -0
- package/dist/policies/bulkhead.js +95 -0
- package/dist/policies/bulkhead.js.map +1 -0
- package/dist/policies/cache.cjs +223 -22
- package/dist/policies/cache.d.ts +42 -7
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +223 -22
- package/dist/policies/cache.js.map +1 -1
- package/dist/policies/circuitBreaker.cjs +85 -0
- package/dist/policies/circuitBreaker.d.ts +3 -0
- package/dist/policies/circuitBreaker.d.ts.map +1 -0
- package/dist/policies/circuitBreaker.js +83 -0
- package/dist/policies/circuitBreaker.js.map +1 -0
- package/dist/policies/dedupe.cjs +98 -21
- package/dist/policies/dedupe.d.ts +20 -13
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +98 -21
- package/dist/policies/dedupe.js.map +1 -1
- package/dist/policies/rateLimit.cjs +35 -0
- package/dist/policies/rateLimit.d.ts +3 -0
- package/dist/policies/rateLimit.d.ts.map +1 -0
- package/dist/policies/rateLimit.js +33 -0
- package/dist/policies/rateLimit.js.map +1 -0
- package/dist/policies/retry.cjs +103 -26
- package/dist/policies/retry.d.ts +25 -2
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +103 -26
- package/dist/policies/retry.js.map +1 -1
- package/dist/policies/timeout.cjs +78 -36
- package/dist/policies/timeout.d.ts +7 -18
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +84 -31
- package/dist/policies/timeout.js.map +1 -1
- package/dist/state/store.cjs +9 -2
- package/dist/state/store.d.ts +9 -0
- package/dist/state/store.d.ts.map +1 -1
- package/dist/state/store.js +9 -2
- package/dist/state/store.js.map +1 -1
- package/dist/stores/base.cjs +4 -4
- package/dist/stores/base.d.ts +32 -56
- package/dist/stores/base.d.ts.map +1 -1
- package/dist/stores/base.js +4 -4
- package/dist/stores/base.js.map +1 -1
- package/dist/stores/memory.cjs +161 -39
- package/dist/stores/memory.d.ts +64 -24
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +160 -39
- package/dist/stores/memory.js.map +1 -1
- package/dist/types/index.d.ts +315 -37
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/abort.cjs +212 -0
- package/dist/utils/abort.d.ts +79 -0
- package/dist/utils/abort.d.ts.map +1 -0
- package/dist/utils/abort.js +206 -0
- package/dist/utils/abort.js.map +1 -0
- package/dist/utils/abortPool.cjs +26 -0
- package/dist/utils/abortPool.d.ts +4 -0
- package/dist/utils/abortPool.d.ts.map +1 -0
- package/dist/utils/abortPool.js +22 -0
- package/dist/utils/abortPool.js.map +1 -0
- package/dist/utils/backoff.cjs +54 -0
- package/dist/utils/backoff.d.ts +14 -0
- package/dist/utils/backoff.d.ts.map +1 -0
- package/dist/utils/backoff.js +52 -0
- package/dist/utils/backoff.js.map +1 -0
- package/dist/utils/key.cjs +77 -0
- package/dist/utils/key.d.ts +29 -0
- package/dist/utils/key.d.ts.map +1 -0
- package/dist/utils/key.js +75 -0
- package/dist/utils/key.js.map +1 -0
- package/dist/utils/limits.cjs +40 -0
- package/dist/utils/limits.d.ts +39 -0
- package/dist/utils/limits.d.ts.map +1 -0
- package/dist/utils/limits.js +38 -0
- package/dist/utils/limits.js.map +1 -0
- package/dist/utils/sanitize.cjs +29 -0
- package/dist/utils/sanitize.d.ts +3 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +26 -0
- package/dist/utils/sanitize.js.map +1 -0
- package/dist/utils/validate.cjs +153 -0
- package/dist/utils/validate.d.ts +26 -0
- package/dist/utils/validate.d.ts.map +1 -0
- package/dist/utils/validate.js +141 -0
- package/dist/utils/validate.js.map +1 -0
- package/package.json +20 -37
package/dist/policies/cache.cjs
CHANGED
|
@@ -3,41 +3,242 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.cachePolicy = cachePolicy;
|
|
4
4
|
const base_js_1 = require("../stores/base.js");
|
|
5
5
|
const NS = 'cache:';
|
|
6
|
+
const INFLIGHT_NS = '__inflight:cache:';
|
|
7
|
+
let generationCounter = 0;
|
|
8
|
+
function nextGeneration() {
|
|
9
|
+
generationCounter = (generationCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
10
|
+
return generationCounter;
|
|
11
|
+
}
|
|
6
12
|
/**
|
|
7
|
-
* Short-
|
|
8
|
-
* On a miss,
|
|
13
|
+
* Short-circuit the entire downstream chain on a cache hit.
|
|
14
|
+
* On a miss, run `fn` and store the result with TTL.
|
|
15
|
+
*
|
|
16
|
+
* # Properties
|
|
17
|
+
*
|
|
18
|
+
* - **Single-flight originator isolation**: the stored in-flight promise is
|
|
19
|
+
* the RAW `fn(signal)` — NOT raceAbort-wrapped. An originator's signal
|
|
20
|
+
* abort no longer causes all cache-miss joiners to reject. Each caller
|
|
21
|
+
* races against their OWN signal only.
|
|
22
|
+
* - **Generation-safe inflight cleanup**: stale originators don't delete
|
|
23
|
+
* newer entries (same pattern as dedupePolicy).
|
|
24
|
+
* - **Signal-aware async store path**: between `await store.get()` and
|
|
25
|
+
* `await store.set()`, we re-check `signal.aborted`. The previous
|
|
26
|
+
* version would happily return a cached value even after the caller
|
|
27
|
+
* aborted mid-Redis-latency.
|
|
28
|
+
* - **Cache hit honours abort**: if `signal.aborted` is true at policy
|
|
29
|
+
* entry, we reject immediately — consistent with `act()`'s contract.
|
|
30
|
+
*
|
|
31
|
+
* # Single-flight (cache stampede prevention)
|
|
32
|
+
*
|
|
33
|
+
* On a sync store, the policy also stores the in-flight Promise under a
|
|
34
|
+
* separate `__inflight:cache:<key>` slot. Concurrent callers that miss the
|
|
35
|
+
* cache but find an in-flight Promise join it instead of launching duplicate
|
|
36
|
+
* work. This is the same mechanism `dedupePolicy` uses, applied internally
|
|
37
|
+
* to cache misses so users don't need to combine `cache` + `dedupe` to
|
|
38
|
+
* avoid stampedes.
|
|
39
|
+
*
|
|
40
|
+
* On an async store, single-flight is not possible (the same sync-store
|
|
41
|
+
* constraint as dedupe applies). Stampedes are a known limitation — document
|
|
42
|
+
* and pair with dedupe at a higher layer if you need single-flight semantics.
|
|
9
43
|
*
|
|
10
|
-
*
|
|
44
|
+
* # Fail-open writes
|
|
11
45
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
46
|
+
* If `store.set()` throws (e.g. Redis transient error), we swallow the error
|
|
47
|
+
* and return the value anyway. The caller gets their result; the next call
|
|
48
|
+
* will simply re-run `fn` and try to cache again. Caching is an optimisation,
|
|
49
|
+
* not a correctness requirement.
|
|
50
|
+
*
|
|
51
|
+
* # Cache hit semantics
|
|
52
|
+
*
|
|
53
|
+
* On a cache hit, `meta.source` is set to `'cache'` and `meta.attempts` is
|
|
54
|
+
* set to `0` — no work was performed.
|
|
55
|
+
*
|
|
56
|
+
* Failures are NEVER cached. Only successful values are stored.
|
|
16
57
|
*/
|
|
17
58
|
function cachePolicy(opts) {
|
|
18
|
-
return (fn, ctx) => async () => {
|
|
59
|
+
return (fn, ctx) => async (signal) => {
|
|
60
|
+
// Honour abort on cache hit too. Consistent with act()'s
|
|
61
|
+
// "reject immediately if signal aborted" contract.
|
|
62
|
+
if (signal.aborted)
|
|
63
|
+
return Promise.reject(signal.reason);
|
|
19
64
|
const key = NS + ctx.key;
|
|
65
|
+
const inflightKey = INFLIGHT_NS + ctx.key;
|
|
66
|
+
// ─── Sync store: fast path with single-flight ────────────────────────
|
|
20
67
|
if ((0, base_js_1.isSyncStore)(ctx.store)) {
|
|
21
|
-
//
|
|
22
|
-
|
|
68
|
+
// Capture the narrowed sync store so closures (cleanup) keep the
|
|
69
|
+
// type information — TS doesn't carry `isSyncStore` narrowing
|
|
70
|
+
// into nested function bodies.
|
|
71
|
+
const store = ctx.store;
|
|
72
|
+
const obs = ctx.observability;
|
|
73
|
+
// 1. Cache hit?
|
|
74
|
+
const hit = store.get(key);
|
|
23
75
|
if (hit) {
|
|
24
76
|
ctx.meta.source = 'cache';
|
|
77
|
+
ctx.meta.attempts = 0;
|
|
78
|
+
// Emit cache-hit event if hooks are registered.
|
|
79
|
+
// Compute actual age from the entry's insertion timestamp.
|
|
80
|
+
if (obs) {
|
|
81
|
+
const ageMs = Date.now() - hit.insertedAt;
|
|
82
|
+
obs.hooks.onCacheHit?.({
|
|
83
|
+
type: 'cache-hit', key: ctx.key, traceId: obs.traceId,
|
|
84
|
+
timestamp: Date.now(), ageMs: Math.max(0, ageMs),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
25
87
|
return hit.value;
|
|
26
88
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
89
|
+
// Emit cache-miss event if hooks are registered.
|
|
90
|
+
if (obs) {
|
|
91
|
+
obs.hooks.onCacheMiss?.({
|
|
92
|
+
type: 'cache-miss', key: ctx.key, traceId: obs.traceId,
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// 2. In-flight single-flight hit? Join it.
|
|
97
|
+
//
|
|
98
|
+
// The stored promise is RAW fn(signal). Joiners race it
|
|
99
|
+
// against their OWN signal only — originator abort doesn't
|
|
100
|
+
// propagate to joiners.
|
|
101
|
+
const inflight = store.get(inflightKey);
|
|
102
|
+
if (inflight) {
|
|
103
|
+
// Race the in-flight promise against OUR OWN signal. If we
|
|
104
|
+
// abort, we reject — but other joiners and the originator
|
|
105
|
+
// continue unaffected.
|
|
106
|
+
try {
|
|
107
|
+
const value = await raceWithOwnSignal(inflight.promise, signal);
|
|
108
|
+
// Mirror originator's meta so joiner's ActResult reflects
|
|
109
|
+
// the real effort (attempts, source).
|
|
110
|
+
ctx.meta.attempts = inflight.meta.attempts;
|
|
111
|
+
ctx.meta.source = inflight.meta.source;
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
// Copy meta on failure too (same as dedupe joiners)
|
|
116
|
+
if (signal.aborted) {
|
|
117
|
+
ctx.meta.attempts = 0;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
ctx.meta.attempts = inflight.meta.attempts;
|
|
121
|
+
ctx.meta.source = inflight.meta.source;
|
|
122
|
+
}
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 3. Originator: launch fn, cache on success (fail-open).
|
|
127
|
+
//
|
|
128
|
+
// The stored promise is the RAW fn(signal) — NOT raceAbort-wrapped.
|
|
129
|
+
// Originator's own await is wrapped in raceWithOwnSignal so they
|
|
130
|
+
// can bail on their signal without affecting joiners.
|
|
131
|
+
const generation = nextGeneration();
|
|
132
|
+
const rawPromise = Promise.resolve(fn(signal)).then((value) => {
|
|
133
|
+
try {
|
|
134
|
+
store.set(key, { value, insertedAt: Date.now() }, opts.ttl);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Fail-open: cache write failure should not surface to caller.
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
},
|
|
141
|
+
// Re-throw the error so joiners see it. Don't cache failures.
|
|
142
|
+
(err) => { throw err; });
|
|
143
|
+
// Publish the in-flight entry for single-flight. If store.set
|
|
144
|
+
// throws, single-flight is disabled for this call — concurrent
|
|
145
|
+
// callers will all run fn. Still correct, just less efficient.
|
|
146
|
+
try {
|
|
147
|
+
store.set(inflightKey, { promise: rawPromise, generation, meta: ctx.meta });
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Single-flight unavailable; proceed without publishing.
|
|
151
|
+
// Originator still gets their value (or error).
|
|
152
|
+
}
|
|
153
|
+
// Cleanup the in-flight slot on settle — generation-safe.
|
|
154
|
+
const cleanup = () => {
|
|
155
|
+
const current = store.get(inflightKey);
|
|
156
|
+
if (current && current.generation === generation) {
|
|
157
|
+
try {
|
|
158
|
+
store.delete(inflightKey);
|
|
159
|
+
}
|
|
160
|
+
catch { /* ignore */ }
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
rawPromise.then(cleanup, cleanup);
|
|
164
|
+
// Originator races the raw promise against their OWN signal.
|
|
165
|
+
return raceWithOwnSignal(rawPromise, signal);
|
|
30
166
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
167
|
+
// Async store: no single-flight (race window unavoidable).
|
|
168
|
+
// Re-check signal.aborted between awaits.
|
|
169
|
+
const obs = ctx.observability;
|
|
170
|
+
const hit = await ctx.store.get(key);
|
|
171
|
+
if (signal.aborted)
|
|
172
|
+
return Promise.reject(signal.reason);
|
|
173
|
+
if (hit) {
|
|
174
|
+
ctx.meta.source = 'cache';
|
|
175
|
+
ctx.meta.attempts = 0;
|
|
176
|
+
if (obs) {
|
|
177
|
+
const ageMs = Date.now() - hit.insertedAt;
|
|
178
|
+
obs.hooks.onCacheHit?.({
|
|
179
|
+
type: 'cache-hit', key: ctx.key, traceId: obs.traceId,
|
|
180
|
+
timestamp: Date.now(), ageMs: Math.max(0, ageMs),
|
|
181
|
+
});
|
|
37
182
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
183
|
+
return hit.value;
|
|
184
|
+
}
|
|
185
|
+
if (obs) {
|
|
186
|
+
obs.hooks.onCacheMiss?.({
|
|
187
|
+
type: 'cache-miss', key: ctx.key, traceId: obs.traceId,
|
|
188
|
+
timestamp: Date.now(),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const value = await fn(signal);
|
|
192
|
+
if (signal.aborted)
|
|
193
|
+
return Promise.reject(signal.reason);
|
|
194
|
+
try {
|
|
195
|
+
await ctx.store.set(key, { value, insertedAt: Date.now() }, opts.ttl);
|
|
41
196
|
}
|
|
197
|
+
catch {
|
|
198
|
+
// Fail-open: see sync path comment.
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
42
201
|
};
|
|
43
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Race a promise against a signal — but if the signal aborts, mark the
|
|
205
|
+
* underlying promise as handled (so its eventual rejection doesn't surface
|
|
206
|
+
* as an unhandled rejection). The underlying promise keeps running for any
|
|
207
|
+
* other consumers (e.g. joiners in single-flight).
|
|
208
|
+
*
|
|
209
|
+
* This is `raceAbort` from `utils/abort.ts`, inlined here to avoid an import
|
|
210
|
+
* cycle (cache.ts already imports from stores, dedupe imports from abort —
|
|
211
|
+
* keeping cache independent of abort reduces the import graph depth).
|
|
212
|
+
*
|
|
213
|
+
* Internal — not exported.
|
|
214
|
+
*/
|
|
215
|
+
function raceWithOwnSignal(promise, signal) {
|
|
216
|
+
if (signal.aborted) {
|
|
217
|
+
promise.catch(() => { }); // mark as handled
|
|
218
|
+
return Promise.reject(signal.reason);
|
|
219
|
+
}
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
let settled = false;
|
|
222
|
+
const onAbort = () => {
|
|
223
|
+
if (settled)
|
|
224
|
+
return;
|
|
225
|
+
settled = true;
|
|
226
|
+
signal.removeEventListener('abort', onAbort);
|
|
227
|
+
reject(signal.reason);
|
|
228
|
+
};
|
|
229
|
+
signal.addEventListener('abort', onAbort);
|
|
230
|
+
promise.then((value) => {
|
|
231
|
+
if (settled)
|
|
232
|
+
return;
|
|
233
|
+
settled = true;
|
|
234
|
+
signal.removeEventListener('abort', onAbort);
|
|
235
|
+
resolve(value);
|
|
236
|
+
}, (error) => {
|
|
237
|
+
if (settled)
|
|
238
|
+
return;
|
|
239
|
+
settled = true;
|
|
240
|
+
signal.removeEventListener('abort', onAbort);
|
|
241
|
+
reject(error);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
package/dist/policies/cache.d.ts
CHANGED
|
@@ -1,14 +1,49 @@
|
|
|
1
1
|
import type { CacheOptions, PolicyApplier } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
|
-
* Short-
|
|
4
|
-
* On a miss,
|
|
3
|
+
* Short-circuit the entire downstream chain on a cache hit.
|
|
4
|
+
* On a miss, run `fn` and store the result with TTL.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* # Properties
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* - **Single-flight originator isolation**: the stored in-flight promise is
|
|
9
|
+
* the RAW `fn(signal)` — NOT raceAbort-wrapped. An originator's signal
|
|
10
|
+
* abort no longer causes all cache-miss joiners to reject. Each caller
|
|
11
|
+
* races against their OWN signal only.
|
|
12
|
+
* - **Generation-safe inflight cleanup**: stale originators don't delete
|
|
13
|
+
* newer entries (same pattern as dedupePolicy).
|
|
14
|
+
* - **Signal-aware async store path**: between `await store.get()` and
|
|
15
|
+
* `await store.set()`, we re-check `signal.aborted`. The previous
|
|
16
|
+
* version would happily return a cached value even after the caller
|
|
17
|
+
* aborted mid-Redis-latency.
|
|
18
|
+
* - **Cache hit honours abort**: if `signal.aborted` is true at policy
|
|
19
|
+
* entry, we reject immediately — consistent with `act()`'s contract.
|
|
20
|
+
*
|
|
21
|
+
* # Single-flight (cache stampede prevention)
|
|
22
|
+
*
|
|
23
|
+
* On a sync store, the policy also stores the in-flight Promise under a
|
|
24
|
+
* separate `__inflight:cache:<key>` slot. Concurrent callers that miss the
|
|
25
|
+
* cache but find an in-flight Promise join it instead of launching duplicate
|
|
26
|
+
* work. This is the same mechanism `dedupePolicy` uses, applied internally
|
|
27
|
+
* to cache misses so users don't need to combine `cache` + `dedupe` to
|
|
28
|
+
* avoid stampedes.
|
|
29
|
+
*
|
|
30
|
+
* On an async store, single-flight is not possible (the same sync-store
|
|
31
|
+
* constraint as dedupe applies). Stampedes are a known limitation — document
|
|
32
|
+
* and pair with dedupe at a higher layer if you need single-flight semantics.
|
|
33
|
+
*
|
|
34
|
+
* # Fail-open writes
|
|
35
|
+
*
|
|
36
|
+
* If `store.set()` throws (e.g. Redis transient error), we swallow the error
|
|
37
|
+
* and return the value anyway. The caller gets their result; the next call
|
|
38
|
+
* will simply re-run `fn` and try to cache again. Caching is an optimisation,
|
|
39
|
+
* not a correctness requirement.
|
|
40
|
+
*
|
|
41
|
+
* # Cache hit semantics
|
|
42
|
+
*
|
|
43
|
+
* On a cache hit, `meta.source` is set to `'cache'` and `meta.attempts` is
|
|
44
|
+
* set to `0` — no work was performed.
|
|
45
|
+
*
|
|
46
|
+
* Failures are NEVER cached. Only successful values are stored.
|
|
12
47
|
*/
|
|
13
48
|
export declare function cachePolicy<T>(opts: CacheOptions): PolicyApplier<T>;
|
|
14
49
|
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/policies/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,YAAY,EAAE,aAAa,EAAiB,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/policies/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,YAAY,EAAE,aAAa,EAAiB,MAAM,mBAAmB,CAAA;AA8B1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CAkJnE"}
|
package/dist/policies/cache.js
CHANGED
|
@@ -1,41 +1,242 @@
|
|
|
1
1
|
import { isSyncStore } from '../stores/base.js';
|
|
2
2
|
const NS = 'cache:';
|
|
3
|
+
const INFLIGHT_NS = '__inflight:cache:';
|
|
4
|
+
let generationCounter = 0;
|
|
5
|
+
function nextGeneration() {
|
|
6
|
+
generationCounter = (generationCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
7
|
+
return generationCounter;
|
|
8
|
+
}
|
|
3
9
|
/**
|
|
4
|
-
* Short-
|
|
5
|
-
* On a miss,
|
|
10
|
+
* Short-circuit the entire downstream chain on a cache hit.
|
|
11
|
+
* On a miss, run `fn` and store the result with TTL.
|
|
12
|
+
*
|
|
13
|
+
* # Properties
|
|
14
|
+
*
|
|
15
|
+
* - **Single-flight originator isolation**: the stored in-flight promise is
|
|
16
|
+
* the RAW `fn(signal)` — NOT raceAbort-wrapped. An originator's signal
|
|
17
|
+
* abort no longer causes all cache-miss joiners to reject. Each caller
|
|
18
|
+
* races against their OWN signal only.
|
|
19
|
+
* - **Generation-safe inflight cleanup**: stale originators don't delete
|
|
20
|
+
* newer entries (same pattern as dedupePolicy).
|
|
21
|
+
* - **Signal-aware async store path**: between `await store.get()` and
|
|
22
|
+
* `await store.set()`, we re-check `signal.aborted`. The previous
|
|
23
|
+
* version would happily return a cached value even after the caller
|
|
24
|
+
* aborted mid-Redis-latency.
|
|
25
|
+
* - **Cache hit honours abort**: if `signal.aborted` is true at policy
|
|
26
|
+
* entry, we reject immediately — consistent with `act()`'s contract.
|
|
27
|
+
*
|
|
28
|
+
* # Single-flight (cache stampede prevention)
|
|
29
|
+
*
|
|
30
|
+
* On a sync store, the policy also stores the in-flight Promise under a
|
|
31
|
+
* separate `__inflight:cache:<key>` slot. Concurrent callers that miss the
|
|
32
|
+
* cache but find an in-flight Promise join it instead of launching duplicate
|
|
33
|
+
* work. This is the same mechanism `dedupePolicy` uses, applied internally
|
|
34
|
+
* to cache misses so users don't need to combine `cache` + `dedupe` to
|
|
35
|
+
* avoid stampedes.
|
|
36
|
+
*
|
|
37
|
+
* On an async store, single-flight is not possible (the same sync-store
|
|
38
|
+
* constraint as dedupe applies). Stampedes are a known limitation — document
|
|
39
|
+
* and pair with dedupe at a higher layer if you need single-flight semantics.
|
|
6
40
|
*
|
|
7
|
-
*
|
|
41
|
+
* # Fail-open writes
|
|
8
42
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
43
|
+
* If `store.set()` throws (e.g. Redis transient error), we swallow the error
|
|
44
|
+
* and return the value anyway. The caller gets their result; the next call
|
|
45
|
+
* will simply re-run `fn` and try to cache again. Caching is an optimisation,
|
|
46
|
+
* not a correctness requirement.
|
|
47
|
+
*
|
|
48
|
+
* # Cache hit semantics
|
|
49
|
+
*
|
|
50
|
+
* On a cache hit, `meta.source` is set to `'cache'` and `meta.attempts` is
|
|
51
|
+
* set to `0` — no work was performed.
|
|
52
|
+
*
|
|
53
|
+
* Failures are NEVER cached. Only successful values are stored.
|
|
13
54
|
*/
|
|
14
55
|
export function cachePolicy(opts) {
|
|
15
|
-
return (fn, ctx) => async () => {
|
|
56
|
+
return (fn, ctx) => async (signal) => {
|
|
57
|
+
// Honour abort on cache hit too. Consistent with act()'s
|
|
58
|
+
// "reject immediately if signal aborted" contract.
|
|
59
|
+
if (signal.aborted)
|
|
60
|
+
return Promise.reject(signal.reason);
|
|
16
61
|
const key = NS + ctx.key;
|
|
62
|
+
const inflightKey = INFLIGHT_NS + ctx.key;
|
|
63
|
+
// ─── Sync store: fast path with single-flight ────────────────────────
|
|
17
64
|
if (isSyncStore(ctx.store)) {
|
|
18
|
-
//
|
|
19
|
-
|
|
65
|
+
// Capture the narrowed sync store so closures (cleanup) keep the
|
|
66
|
+
// type information — TS doesn't carry `isSyncStore` narrowing
|
|
67
|
+
// into nested function bodies.
|
|
68
|
+
const store = ctx.store;
|
|
69
|
+
const obs = ctx.observability;
|
|
70
|
+
// 1. Cache hit?
|
|
71
|
+
const hit = store.get(key);
|
|
20
72
|
if (hit) {
|
|
21
73
|
ctx.meta.source = 'cache';
|
|
74
|
+
ctx.meta.attempts = 0;
|
|
75
|
+
// Emit cache-hit event if hooks are registered.
|
|
76
|
+
// Compute actual age from the entry's insertion timestamp.
|
|
77
|
+
if (obs) {
|
|
78
|
+
const ageMs = Date.now() - hit.insertedAt;
|
|
79
|
+
obs.hooks.onCacheHit?.({
|
|
80
|
+
type: 'cache-hit', key: ctx.key, traceId: obs.traceId,
|
|
81
|
+
timestamp: Date.now(), ageMs: Math.max(0, ageMs),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
22
84
|
return hit.value;
|
|
23
85
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
86
|
+
// Emit cache-miss event if hooks are registered.
|
|
87
|
+
if (obs) {
|
|
88
|
+
obs.hooks.onCacheMiss?.({
|
|
89
|
+
type: 'cache-miss', key: ctx.key, traceId: obs.traceId,
|
|
90
|
+
timestamp: Date.now(),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// 2. In-flight single-flight hit? Join it.
|
|
94
|
+
//
|
|
95
|
+
// The stored promise is RAW fn(signal). Joiners race it
|
|
96
|
+
// against their OWN signal only — originator abort doesn't
|
|
97
|
+
// propagate to joiners.
|
|
98
|
+
const inflight = store.get(inflightKey);
|
|
99
|
+
if (inflight) {
|
|
100
|
+
// Race the in-flight promise against OUR OWN signal. If we
|
|
101
|
+
// abort, we reject — but other joiners and the originator
|
|
102
|
+
// continue unaffected.
|
|
103
|
+
try {
|
|
104
|
+
const value = await raceWithOwnSignal(inflight.promise, signal);
|
|
105
|
+
// Mirror originator's meta so joiner's ActResult reflects
|
|
106
|
+
// the real effort (attempts, source).
|
|
107
|
+
ctx.meta.attempts = inflight.meta.attempts;
|
|
108
|
+
ctx.meta.source = inflight.meta.source;
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
// Copy meta on failure too (same as dedupe joiners)
|
|
113
|
+
if (signal.aborted) {
|
|
114
|
+
ctx.meta.attempts = 0;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
ctx.meta.attempts = inflight.meta.attempts;
|
|
118
|
+
ctx.meta.source = inflight.meta.source;
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// 3. Originator: launch fn, cache on success (fail-open).
|
|
124
|
+
//
|
|
125
|
+
// The stored promise is the RAW fn(signal) — NOT raceAbort-wrapped.
|
|
126
|
+
// Originator's own await is wrapped in raceWithOwnSignal so they
|
|
127
|
+
// can bail on their signal without affecting joiners.
|
|
128
|
+
const generation = nextGeneration();
|
|
129
|
+
const rawPromise = Promise.resolve(fn(signal)).then((value) => {
|
|
130
|
+
try {
|
|
131
|
+
store.set(key, { value, insertedAt: Date.now() }, opts.ttl);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Fail-open: cache write failure should not surface to caller.
|
|
135
|
+
}
|
|
136
|
+
return value;
|
|
137
|
+
},
|
|
138
|
+
// Re-throw the error so joiners see it. Don't cache failures.
|
|
139
|
+
(err) => { throw err; });
|
|
140
|
+
// Publish the in-flight entry for single-flight. If store.set
|
|
141
|
+
// throws, single-flight is disabled for this call — concurrent
|
|
142
|
+
// callers will all run fn. Still correct, just less efficient.
|
|
143
|
+
try {
|
|
144
|
+
store.set(inflightKey, { promise: rawPromise, generation, meta: ctx.meta });
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Single-flight unavailable; proceed without publishing.
|
|
148
|
+
// Originator still gets their value (or error).
|
|
149
|
+
}
|
|
150
|
+
// Cleanup the in-flight slot on settle — generation-safe.
|
|
151
|
+
const cleanup = () => {
|
|
152
|
+
const current = store.get(inflightKey);
|
|
153
|
+
if (current && current.generation === generation) {
|
|
154
|
+
try {
|
|
155
|
+
store.delete(inflightKey);
|
|
156
|
+
}
|
|
157
|
+
catch { /* ignore */ }
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
rawPromise.then(cleanup, cleanup);
|
|
161
|
+
// Originator races the raw promise against their OWN signal.
|
|
162
|
+
return raceWithOwnSignal(rawPromise, signal);
|
|
27
163
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
164
|
+
// Async store: no single-flight (race window unavoidable).
|
|
165
|
+
// Re-check signal.aborted between awaits.
|
|
166
|
+
const obs = ctx.observability;
|
|
167
|
+
const hit = await ctx.store.get(key);
|
|
168
|
+
if (signal.aborted)
|
|
169
|
+
return Promise.reject(signal.reason);
|
|
170
|
+
if (hit) {
|
|
171
|
+
ctx.meta.source = 'cache';
|
|
172
|
+
ctx.meta.attempts = 0;
|
|
173
|
+
if (obs) {
|
|
174
|
+
const ageMs = Date.now() - hit.insertedAt;
|
|
175
|
+
obs.hooks.onCacheHit?.({
|
|
176
|
+
type: 'cache-hit', key: ctx.key, traceId: obs.traceId,
|
|
177
|
+
timestamp: Date.now(), ageMs: Math.max(0, ageMs),
|
|
178
|
+
});
|
|
34
179
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
180
|
+
return hit.value;
|
|
181
|
+
}
|
|
182
|
+
if (obs) {
|
|
183
|
+
obs.hooks.onCacheMiss?.({
|
|
184
|
+
type: 'cache-miss', key: ctx.key, traceId: obs.traceId,
|
|
185
|
+
timestamp: Date.now(),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const value = await fn(signal);
|
|
189
|
+
if (signal.aborted)
|
|
190
|
+
return Promise.reject(signal.reason);
|
|
191
|
+
try {
|
|
192
|
+
await ctx.store.set(key, { value, insertedAt: Date.now() }, opts.ttl);
|
|
38
193
|
}
|
|
194
|
+
catch {
|
|
195
|
+
// Fail-open: see sync path comment.
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
39
198
|
};
|
|
40
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Race a promise against a signal — but if the signal aborts, mark the
|
|
202
|
+
* underlying promise as handled (so its eventual rejection doesn't surface
|
|
203
|
+
* as an unhandled rejection). The underlying promise keeps running for any
|
|
204
|
+
* other consumers (e.g. joiners in single-flight).
|
|
205
|
+
*
|
|
206
|
+
* This is `raceAbort` from `utils/abort.ts`, inlined here to avoid an import
|
|
207
|
+
* cycle (cache.ts already imports from stores, dedupe imports from abort —
|
|
208
|
+
* keeping cache independent of abort reduces the import graph depth).
|
|
209
|
+
*
|
|
210
|
+
* Internal — not exported.
|
|
211
|
+
*/
|
|
212
|
+
function raceWithOwnSignal(promise, signal) {
|
|
213
|
+
if (signal.aborted) {
|
|
214
|
+
promise.catch(() => { }); // mark as handled
|
|
215
|
+
return Promise.reject(signal.reason);
|
|
216
|
+
}
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
let settled = false;
|
|
219
|
+
const onAbort = () => {
|
|
220
|
+
if (settled)
|
|
221
|
+
return;
|
|
222
|
+
settled = true;
|
|
223
|
+
signal.removeEventListener('abort', onAbort);
|
|
224
|
+
reject(signal.reason);
|
|
225
|
+
};
|
|
226
|
+
signal.addEventListener('abort', onAbort);
|
|
227
|
+
promise.then((value) => {
|
|
228
|
+
if (settled)
|
|
229
|
+
return;
|
|
230
|
+
settled = true;
|
|
231
|
+
signal.removeEventListener('abort', onAbort);
|
|
232
|
+
resolve(value);
|
|
233
|
+
}, (error) => {
|
|
234
|
+
if (settled)
|
|
235
|
+
return;
|
|
236
|
+
settled = true;
|
|
237
|
+
signal.removeEventListener('abort', onAbort);
|
|
238
|
+
reject(error);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
41
242
|
//# sourceMappingURL=cache.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/policies/cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,MAAM,EAAE,GAAG,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/policies/cache.ts"],"names":[],"mappings":"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"}
|