actly 1.1.5 → 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/README.md +232 -434
- package/dist/core/act.cjs +353 -35
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +355 -37
- package/dist/core/act.js.map +1 -1
- package/dist/core/executor.cjs +2 -0
- package/dist/core/executor.d.ts +6 -1
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +2 -0
- 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 +44 -4
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- 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 +168 -31
- package/dist/policies/cache.d.ts +17 -4
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +168 -31
- 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 +74 -45
- package/dist/policies/dedupe.d.ts +10 -28
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +74 -45
- 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 +45 -8
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +45 -8
- package/dist/policies/retry.js.map +1 -1
- package/dist/policies/timeout.cjs +25 -53
- package/dist/policies/timeout.d.ts +2 -33
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +31 -48
- package/dist/policies/timeout.js.map +1 -1
- package/dist/state/store.cjs +3 -3
- package/dist/state/store.d.ts +3 -3
- package/dist/state/store.js +3 -3
- package/dist/stores/memory.cjs +126 -53
- package/dist/stores/memory.d.ts +35 -16
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +125 -53
- package/dist/stores/memory.js.map +1 -1
- package/dist/types/index.d.ts +176 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/abort.cjs +108 -38
- package/dist/utils/abort.d.ts +43 -19
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +108 -38
- package/dist/utils/abort.js.map +1 -1
- 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 +12 -1
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +12 -1
- package/dist/utils/backoff.js.map +1 -1
- 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 +90 -16
- package/dist/utils/validate.d.ts +11 -0
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +85 -16
- package/dist/utils/validate.js.map +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability hooks.
|
|
3
|
+
*
|
|
4
|
+
* Eight event types covering the entire lifecycle of an `act()` call.
|
|
5
|
+
*
|
|
6
|
+
* # Contract
|
|
7
|
+
*
|
|
8
|
+
* When `options.observability` is `null`/`undefined` (the common case),
|
|
9
|
+
* no event objects are allocated and no function calls are made. The
|
|
10
|
+
* hot path is a single null-check per policy decision.
|
|
11
|
+
*
|
|
12
|
+
* When hooks ARE registered, events are allocated lazily — only when the
|
|
13
|
+
* corresponding event actually fires. A cache hit doesn't allocate an
|
|
14
|
+
* `onRetry` event, for example.
|
|
15
|
+
*
|
|
16
|
+
* # Event shape
|
|
17
|
+
*
|
|
18
|
+
* Every event carries `key`, `traceId`, and `timestamp` for correlation.
|
|
19
|
+
* Event-specific fields are on the same object (no nesting) for flat
|
|
20
|
+
* destructuring in user code.
|
|
21
|
+
*
|
|
22
|
+
* # Ordering
|
|
23
|
+
*
|
|
24
|
+
* For a successful fresh call with retries:
|
|
25
|
+
* onAttempt (1) → onAttempt (2) → onRetry (1→2) → onAttempt (3) → onFinalSuccess
|
|
26
|
+
*
|
|
27
|
+
* For a cache hit:
|
|
28
|
+
* onCacheHit → onFinalSuccess
|
|
29
|
+
*
|
|
30
|
+
* For a dedupe joiner:
|
|
31
|
+
* onDedupeJoin → onFinalSuccess (or onFinalFailure)
|
|
32
|
+
*
|
|
33
|
+
* For a timeout:
|
|
34
|
+
* onAttempt → onTimeout → onFinalFailure
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Quick null-check helper. Policies call this once at decision points; if
|
|
38
|
+
* it returns false, no further observability work is done.
|
|
39
|
+
*/
|
|
40
|
+
export function hasObservers(ctx) {
|
|
41
|
+
return ctx.observability != null;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=observability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability.js","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AA0IH;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,GAA6C;IACxE,OAAO,GAAG,CAAC,aAAa,IAAI,IAAI,CAAA;AAClC,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bulkheadPolicy = bulkheadPolicy;
|
|
4
|
+
const executor_js_1 = require("../core/executor.js");
|
|
5
|
+
const errors_js_1 = require("../errors.js");
|
|
6
|
+
const NS = 'bulk:';
|
|
7
|
+
function getState(store, key) {
|
|
8
|
+
return store.get(NS + key) ?? { active: 0, queue: [] };
|
|
9
|
+
}
|
|
10
|
+
function setState(store, key, state) {
|
|
11
|
+
store.set(NS + key, state);
|
|
12
|
+
}
|
|
13
|
+
function bulkheadPolicy(opts) {
|
|
14
|
+
const maxConcurrent = Math.max(1, Math.floor(opts.maxConcurrent));
|
|
15
|
+
const queueTimeoutMs = opts.queueTimeoutMs ?? 0;
|
|
16
|
+
const applier = (fn, ctx) => {
|
|
17
|
+
const syncCtx = ctx;
|
|
18
|
+
return async (signal) => {
|
|
19
|
+
const key = syncCtx.key;
|
|
20
|
+
const acquireSlot = () => {
|
|
21
|
+
// If signal already aborted, reject immediately
|
|
22
|
+
if (signal.aborted)
|
|
23
|
+
return Promise.reject(signal.reason);
|
|
24
|
+
const state = getState(syncCtx.store, key);
|
|
25
|
+
if (state.active < maxConcurrent) {
|
|
26
|
+
state.active++;
|
|
27
|
+
setState(syncCtx.store, key, state);
|
|
28
|
+
return Promise.resolve();
|
|
29
|
+
}
|
|
30
|
+
if (queueTimeoutMs === 0) {
|
|
31
|
+
throw new errors_js_1.BulkheadOverflowError(key, maxConcurrent);
|
|
32
|
+
}
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const state2 = getState(syncCtx.store, key);
|
|
35
|
+
const entry = {
|
|
36
|
+
resolve,
|
|
37
|
+
reject,
|
|
38
|
+
signal,
|
|
39
|
+
};
|
|
40
|
+
// Remove entry from queue on signal abort
|
|
41
|
+
entry.onAbort = () => {
|
|
42
|
+
const s = getState(syncCtx.store, key);
|
|
43
|
+
const idx = s.queue.indexOf(entry);
|
|
44
|
+
if (idx >= 0) {
|
|
45
|
+
s.queue.splice(idx, 1);
|
|
46
|
+
setState(syncCtx.store, key, s);
|
|
47
|
+
}
|
|
48
|
+
if (entry.timer)
|
|
49
|
+
clearTimeout(entry.timer);
|
|
50
|
+
reject(signal.reason);
|
|
51
|
+
};
|
|
52
|
+
if (queueTimeoutMs > 0) {
|
|
53
|
+
entry.timer = setTimeout(() => {
|
|
54
|
+
const s = getState(syncCtx.store, key);
|
|
55
|
+
const idx = s.queue.indexOf(entry);
|
|
56
|
+
if (idx >= 0)
|
|
57
|
+
s.queue.splice(idx, 1);
|
|
58
|
+
setState(syncCtx.store, key, s);
|
|
59
|
+
signal.removeEventListener('abort', entry.onAbort);
|
|
60
|
+
reject(new errors_js_1.BulkheadOverflowError(key, maxConcurrent));
|
|
61
|
+
}, queueTimeoutMs);
|
|
62
|
+
}
|
|
63
|
+
signal.addEventListener('abort', entry.onAbort, { once: true });
|
|
64
|
+
state2.queue.push(entry);
|
|
65
|
+
setState(syncCtx.store, key, state2);
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
const releaseSlot = () => {
|
|
69
|
+
const state = getState(syncCtx.store, key);
|
|
70
|
+
state.active--;
|
|
71
|
+
if (state.queue.length > 0) {
|
|
72
|
+
const next = state.queue.shift();
|
|
73
|
+
state.active++;
|
|
74
|
+
if (next.timer)
|
|
75
|
+
clearTimeout(next.timer);
|
|
76
|
+
// Remove abort listener from the QUEUED caller's signal (not releaser's)
|
|
77
|
+
if (next.onAbort && next.signal) {
|
|
78
|
+
next.signal.removeEventListener('abort', next.onAbort);
|
|
79
|
+
}
|
|
80
|
+
next.resolve();
|
|
81
|
+
}
|
|
82
|
+
if (state.active < 0)
|
|
83
|
+
state.active = 0;
|
|
84
|
+
setState(syncCtx.store, key, state);
|
|
85
|
+
};
|
|
86
|
+
await acquireSlot();
|
|
87
|
+
try {
|
|
88
|
+
return await fn(signal);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
releaseSlot();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
applier[executor_js_1.REQUIRES_SYNC_STORE] = true;
|
|
96
|
+
return applier;
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bulkhead.d.ts","sourceRoot":"","sources":["../../src/policies/bulkhead.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,eAAe,EAAE,MAAM,mBAAmB,CAAA;AA0B7F,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,CAAC,CA0FzE"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { REQUIRES_SYNC_STORE } from '../core/executor.js';
|
|
2
|
+
import { BulkheadOverflowError } from '../errors.js';
|
|
3
|
+
const NS = 'bulk:';
|
|
4
|
+
function getState(store, key) {
|
|
5
|
+
return store.get(NS + key) ?? { active: 0, queue: [] };
|
|
6
|
+
}
|
|
7
|
+
function setState(store, key, state) {
|
|
8
|
+
store.set(NS + key, state);
|
|
9
|
+
}
|
|
10
|
+
export function bulkheadPolicy(opts) {
|
|
11
|
+
const maxConcurrent = Math.max(1, Math.floor(opts.maxConcurrent));
|
|
12
|
+
const queueTimeoutMs = opts.queueTimeoutMs ?? 0;
|
|
13
|
+
const applier = (fn, ctx) => {
|
|
14
|
+
const syncCtx = ctx;
|
|
15
|
+
return async (signal) => {
|
|
16
|
+
const key = syncCtx.key;
|
|
17
|
+
const acquireSlot = () => {
|
|
18
|
+
// If signal already aborted, reject immediately
|
|
19
|
+
if (signal.aborted)
|
|
20
|
+
return Promise.reject(signal.reason);
|
|
21
|
+
const state = getState(syncCtx.store, key);
|
|
22
|
+
if (state.active < maxConcurrent) {
|
|
23
|
+
state.active++;
|
|
24
|
+
setState(syncCtx.store, key, state);
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
}
|
|
27
|
+
if (queueTimeoutMs === 0) {
|
|
28
|
+
throw new BulkheadOverflowError(key, maxConcurrent);
|
|
29
|
+
}
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const state2 = getState(syncCtx.store, key);
|
|
32
|
+
const entry = {
|
|
33
|
+
resolve,
|
|
34
|
+
reject,
|
|
35
|
+
signal,
|
|
36
|
+
};
|
|
37
|
+
// Remove entry from queue on signal abort
|
|
38
|
+
entry.onAbort = () => {
|
|
39
|
+
const s = getState(syncCtx.store, key);
|
|
40
|
+
const idx = s.queue.indexOf(entry);
|
|
41
|
+
if (idx >= 0) {
|
|
42
|
+
s.queue.splice(idx, 1);
|
|
43
|
+
setState(syncCtx.store, key, s);
|
|
44
|
+
}
|
|
45
|
+
if (entry.timer)
|
|
46
|
+
clearTimeout(entry.timer);
|
|
47
|
+
reject(signal.reason);
|
|
48
|
+
};
|
|
49
|
+
if (queueTimeoutMs > 0) {
|
|
50
|
+
entry.timer = setTimeout(() => {
|
|
51
|
+
const s = getState(syncCtx.store, key);
|
|
52
|
+
const idx = s.queue.indexOf(entry);
|
|
53
|
+
if (idx >= 0)
|
|
54
|
+
s.queue.splice(idx, 1);
|
|
55
|
+
setState(syncCtx.store, key, s);
|
|
56
|
+
signal.removeEventListener('abort', entry.onAbort);
|
|
57
|
+
reject(new BulkheadOverflowError(key, maxConcurrent));
|
|
58
|
+
}, queueTimeoutMs);
|
|
59
|
+
}
|
|
60
|
+
signal.addEventListener('abort', entry.onAbort, { once: true });
|
|
61
|
+
state2.queue.push(entry);
|
|
62
|
+
setState(syncCtx.store, key, state2);
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
const releaseSlot = () => {
|
|
66
|
+
const state = getState(syncCtx.store, key);
|
|
67
|
+
state.active--;
|
|
68
|
+
if (state.queue.length > 0) {
|
|
69
|
+
const next = state.queue.shift();
|
|
70
|
+
state.active++;
|
|
71
|
+
if (next.timer)
|
|
72
|
+
clearTimeout(next.timer);
|
|
73
|
+
// Remove abort listener from the QUEUED caller's signal (not releaser's)
|
|
74
|
+
if (next.onAbort && next.signal) {
|
|
75
|
+
next.signal.removeEventListener('abort', next.onAbort);
|
|
76
|
+
}
|
|
77
|
+
next.resolve();
|
|
78
|
+
}
|
|
79
|
+
if (state.active < 0)
|
|
80
|
+
state.active = 0;
|
|
81
|
+
setState(syncCtx.store, key, state);
|
|
82
|
+
};
|
|
83
|
+
await acquireSlot();
|
|
84
|
+
try {
|
|
85
|
+
return await fn(signal);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
releaseSlot();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
applier[REQUIRES_SYNC_STORE] = true;
|
|
93
|
+
return applier;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=bulkhead.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bulkhead.js","sourceRoot":"","sources":["../../src/policies/bulkhead.ts"],"names":[],"mappings":"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"}
|
package/dist/policies/cache.cjs
CHANGED
|
@@ -4,11 +4,31 @@ exports.cachePolicy = cachePolicy;
|
|
|
4
4
|
const base_js_1 = require("../stores/base.js");
|
|
5
5
|
const NS = 'cache:';
|
|
6
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
|
+
}
|
|
7
12
|
/**
|
|
8
13
|
* Short-circuit the entire downstream chain on a cache hit.
|
|
9
14
|
* On a miss, run `fn` and store the result with TTL.
|
|
10
15
|
*
|
|
11
|
-
* #
|
|
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)
|
|
12
32
|
*
|
|
13
33
|
* On a sync store, the policy also stores the in-flight Promise under a
|
|
14
34
|
* separate `__inflight:cache:<key>` slot. Concurrent callers that miss the
|
|
@@ -31,73 +51,148 @@ const INFLIGHT_NS = '__inflight:cache:';
|
|
|
31
51
|
* # Cache hit semantics
|
|
32
52
|
*
|
|
33
53
|
* On a cache hit, `meta.source` is set to `'cache'` and `meta.attempts` is
|
|
34
|
-
* set to `0` — no work was performed.
|
|
35
|
-
* cache hits, which was inconsistent with the documented meaning of
|
|
36
|
-
* `attempts`. v1.1.5 fixes this.)
|
|
54
|
+
* set to `0` — no work was performed.
|
|
37
55
|
*
|
|
38
56
|
* Failures are NEVER cached. Only successful values are stored.
|
|
39
57
|
*/
|
|
40
58
|
function cachePolicy(opts) {
|
|
41
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);
|
|
42
64
|
const key = NS + ctx.key;
|
|
43
65
|
const inflightKey = INFLIGHT_NS + ctx.key;
|
|
44
66
|
// ─── Sync store: fast path with single-flight ────────────────────────
|
|
45
67
|
if ((0, base_js_1.isSyncStore)(ctx.store)) {
|
|
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;
|
|
46
73
|
// 1. Cache hit?
|
|
47
|
-
const hit =
|
|
74
|
+
const hit = store.get(key);
|
|
48
75
|
if (hit) {
|
|
49
76
|
ctx.meta.source = 'cache';
|
|
50
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
|
+
}
|
|
51
87
|
return hit.value;
|
|
52
88
|
}
|
|
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
|
+
}
|
|
53
96
|
// 2. In-flight single-flight hit? Join it.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
}
|
|
57
126
|
// 3. Originator: launch fn, cache on success (fail-open).
|
|
58
|
-
|
|
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) => {
|
|
59
133
|
try {
|
|
60
|
-
|
|
134
|
+
store.set(key, { value, insertedAt: Date.now() }, opts.ttl);
|
|
61
135
|
}
|
|
62
136
|
catch {
|
|
63
137
|
// Fail-open: cache write failure should not surface to caller.
|
|
64
|
-
// The value is still valid; next call will just re-run fn.
|
|
65
138
|
}
|
|
66
139
|
return value;
|
|
67
|
-
},
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
//
|
|
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.
|
|
72
146
|
try {
|
|
73
|
-
|
|
74
|
-
// Cleanup the in-flight slot on settle. The `.catch(() => {})`
|
|
75
|
-
// suppresses the unhandled-rejection warning that would otherwise
|
|
76
|
-
// fire on the `.finally()` chain — the original `promise` (with
|
|
77
|
-
// its rejection) is returned to the caller, which has its own
|
|
78
|
-
// try/catch via act().
|
|
79
|
-
promise.finally(() => {
|
|
80
|
-
try {
|
|
81
|
-
ctx.store.delete(inflightKey);
|
|
82
|
-
}
|
|
83
|
-
catch { /* ignore */ }
|
|
84
|
-
}).catch(() => { });
|
|
147
|
+
store.set(inflightKey, { promise: rawPromise, generation, meta: ctx.meta });
|
|
85
148
|
}
|
|
86
149
|
catch {
|
|
87
150
|
// Single-flight unavailable; proceed without publishing.
|
|
151
|
+
// Originator still gets their value (or error).
|
|
88
152
|
}
|
|
89
|
-
|
|
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);
|
|
90
166
|
}
|
|
91
|
-
//
|
|
167
|
+
// Async store: no single-flight (race window unavoidable).
|
|
168
|
+
// Re-check signal.aborted between awaits.
|
|
169
|
+
const obs = ctx.observability;
|
|
92
170
|
const hit = await ctx.store.get(key);
|
|
171
|
+
if (signal.aborted)
|
|
172
|
+
return Promise.reject(signal.reason);
|
|
93
173
|
if (hit) {
|
|
94
174
|
ctx.meta.source = 'cache';
|
|
95
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
|
+
});
|
|
182
|
+
}
|
|
96
183
|
return hit.value;
|
|
97
184
|
}
|
|
185
|
+
if (obs) {
|
|
186
|
+
obs.hooks.onCacheMiss?.({
|
|
187
|
+
type: 'cache-miss', key: ctx.key, traceId: obs.traceId,
|
|
188
|
+
timestamp: Date.now(),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
98
191
|
const value = await fn(signal);
|
|
192
|
+
if (signal.aborted)
|
|
193
|
+
return Promise.reject(signal.reason);
|
|
99
194
|
try {
|
|
100
|
-
await ctx.store.set(key, { value }, opts.ttl);
|
|
195
|
+
await ctx.store.set(key, { value, insertedAt: Date.now() }, opts.ttl);
|
|
101
196
|
}
|
|
102
197
|
catch {
|
|
103
198
|
// Fail-open: see sync path comment.
|
|
@@ -105,3 +200,45 @@ function cachePolicy(opts) {
|
|
|
105
200
|
return value;
|
|
106
201
|
};
|
|
107
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
|
@@ -3,7 +3,22 @@ import type { CacheOptions, PolicyApplier } from '../types/index.js';
|
|
|
3
3
|
* Short-circuit the entire downstream chain on a cache hit.
|
|
4
4
|
* On a miss, run `fn` and store the result with TTL.
|
|
5
5
|
*
|
|
6
|
-
* #
|
|
6
|
+
* # Properties
|
|
7
|
+
*
|
|
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)
|
|
7
22
|
*
|
|
8
23
|
* On a sync store, the policy also stores the in-flight Promise under a
|
|
9
24
|
* separate `__inflight:cache:<key>` slot. Concurrent callers that miss the
|
|
@@ -26,9 +41,7 @@ import type { CacheOptions, PolicyApplier } from '../types/index.js';
|
|
|
26
41
|
* # Cache hit semantics
|
|
27
42
|
*
|
|
28
43
|
* On a cache hit, `meta.source` is set to `'cache'` and `meta.attempts` is
|
|
29
|
-
* set to `0` — no work was performed.
|
|
30
|
-
* cache hits, which was inconsistent with the documented meaning of
|
|
31
|
-
* `attempts`. v1.1.5 fixes this.)
|
|
44
|
+
* set to `0` — no work was performed.
|
|
32
45
|
*
|
|
33
46
|
* Failures are NEVER cached. Only successful values are stored.
|
|
34
47
|
*/
|
|
@@ -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"}
|