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.
Files changed (119) hide show
  1. package/README.md +232 -434
  2. package/dist/core/act.cjs +353 -35
  3. package/dist/core/act.d.ts.map +1 -1
  4. package/dist/core/act.js +355 -37
  5. package/dist/core/act.js.map +1 -1
  6. package/dist/core/executor.cjs +2 -0
  7. package/dist/core/executor.d.ts +6 -1
  8. package/dist/core/executor.d.ts.map +1 -1
  9. package/dist/core/executor.js +2 -0
  10. package/dist/core/executor.js.map +1 -1
  11. package/dist/core/health.cjs +34 -0
  12. package/dist/core/health.d.ts +18 -0
  13. package/dist/core/health.d.ts.map +1 -0
  14. package/dist/core/health.js +28 -0
  15. package/dist/core/health.js.map +1 -0
  16. package/dist/core/shutdown.cjs +60 -0
  17. package/dist/core/shutdown.d.ts +8 -0
  18. package/dist/core/shutdown.d.ts.map +1 -0
  19. package/dist/core/shutdown.js +56 -0
  20. package/dist/core/shutdown.js.map +1 -0
  21. package/dist/core/tenant.cjs +71 -0
  22. package/dist/core/tenant.d.ts +29 -0
  23. package/dist/core/tenant.d.ts.map +1 -0
  24. package/dist/core/tenant.js +68 -0
  25. package/dist/core/tenant.js.map +1 -0
  26. package/dist/errors.cjs +182 -0
  27. package/dist/errors.d.ts +151 -0
  28. package/dist/errors.d.ts.map +1 -0
  29. package/dist/errors.js +171 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/index.cjs +44 -4
  32. package/dist/index.d.ts +17 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +17 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/observability.cjs +45 -0
  37. package/dist/observability.d.ts +145 -0
  38. package/dist/observability.d.ts.map +1 -0
  39. package/dist/observability.js +43 -0
  40. package/dist/observability.js.map +1 -0
  41. package/dist/policies/bulkhead.cjs +97 -0
  42. package/dist/policies/bulkhead.d.ts +3 -0
  43. package/dist/policies/bulkhead.d.ts.map +1 -0
  44. package/dist/policies/bulkhead.js +95 -0
  45. package/dist/policies/bulkhead.js.map +1 -0
  46. package/dist/policies/cache.cjs +168 -31
  47. package/dist/policies/cache.d.ts +17 -4
  48. package/dist/policies/cache.d.ts.map +1 -1
  49. package/dist/policies/cache.js +168 -31
  50. package/dist/policies/cache.js.map +1 -1
  51. package/dist/policies/circuitBreaker.cjs +85 -0
  52. package/dist/policies/circuitBreaker.d.ts +3 -0
  53. package/dist/policies/circuitBreaker.d.ts.map +1 -0
  54. package/dist/policies/circuitBreaker.js +83 -0
  55. package/dist/policies/circuitBreaker.js.map +1 -0
  56. package/dist/policies/dedupe.cjs +74 -45
  57. package/dist/policies/dedupe.d.ts +10 -28
  58. package/dist/policies/dedupe.d.ts.map +1 -1
  59. package/dist/policies/dedupe.js +74 -45
  60. package/dist/policies/dedupe.js.map +1 -1
  61. package/dist/policies/rateLimit.cjs +35 -0
  62. package/dist/policies/rateLimit.d.ts +3 -0
  63. package/dist/policies/rateLimit.d.ts.map +1 -0
  64. package/dist/policies/rateLimit.js +33 -0
  65. package/dist/policies/rateLimit.js.map +1 -0
  66. package/dist/policies/retry.cjs +45 -8
  67. package/dist/policies/retry.d.ts.map +1 -1
  68. package/dist/policies/retry.js +45 -8
  69. package/dist/policies/retry.js.map +1 -1
  70. package/dist/policies/timeout.cjs +25 -53
  71. package/dist/policies/timeout.d.ts +2 -33
  72. package/dist/policies/timeout.d.ts.map +1 -1
  73. package/dist/policies/timeout.js +31 -48
  74. package/dist/policies/timeout.js.map +1 -1
  75. package/dist/state/store.cjs +3 -3
  76. package/dist/state/store.d.ts +3 -3
  77. package/dist/state/store.js +3 -3
  78. package/dist/stores/memory.cjs +126 -53
  79. package/dist/stores/memory.d.ts +35 -16
  80. package/dist/stores/memory.d.ts.map +1 -1
  81. package/dist/stores/memory.js +125 -53
  82. package/dist/stores/memory.js.map +1 -1
  83. package/dist/types/index.d.ts +176 -3
  84. package/dist/types/index.d.ts.map +1 -1
  85. package/dist/utils/abort.cjs +108 -38
  86. package/dist/utils/abort.d.ts +43 -19
  87. package/dist/utils/abort.d.ts.map +1 -1
  88. package/dist/utils/abort.js +108 -38
  89. package/dist/utils/abort.js.map +1 -1
  90. package/dist/utils/abortPool.cjs +26 -0
  91. package/dist/utils/abortPool.d.ts +4 -0
  92. package/dist/utils/abortPool.d.ts.map +1 -0
  93. package/dist/utils/abortPool.js +22 -0
  94. package/dist/utils/abortPool.js.map +1 -0
  95. package/dist/utils/backoff.cjs +12 -1
  96. package/dist/utils/backoff.d.ts.map +1 -1
  97. package/dist/utils/backoff.js +12 -1
  98. package/dist/utils/backoff.js.map +1 -1
  99. package/dist/utils/key.cjs +77 -0
  100. package/dist/utils/key.d.ts +29 -0
  101. package/dist/utils/key.d.ts.map +1 -0
  102. package/dist/utils/key.js +75 -0
  103. package/dist/utils/key.js.map +1 -0
  104. package/dist/utils/limits.cjs +40 -0
  105. package/dist/utils/limits.d.ts +39 -0
  106. package/dist/utils/limits.d.ts.map +1 -0
  107. package/dist/utils/limits.js +38 -0
  108. package/dist/utils/limits.js.map +1 -0
  109. package/dist/utils/sanitize.cjs +29 -0
  110. package/dist/utils/sanitize.d.ts +3 -0
  111. package/dist/utils/sanitize.d.ts.map +1 -0
  112. package/dist/utils/sanitize.js +26 -0
  113. package/dist/utils/sanitize.js.map +1 -0
  114. package/dist/utils/validate.cjs +90 -16
  115. package/dist/utils/validate.d.ts +11 -0
  116. package/dist/utils/validate.d.ts.map +1 -1
  117. package/dist/utils/validate.js +85 -16
  118. package/dist/utils/validate.js.map +1 -1
  119. package/package.json +6 -5
@@ -1,11 +1,31 @@
1
1
  import { isSyncStore } from '../stores/base.js';
2
2
  const NS = 'cache:';
3
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
+ }
4
9
  /**
5
10
  * Short-circuit the entire downstream chain on a cache hit.
6
11
  * On a miss, run `fn` and store the result with TTL.
7
12
  *
8
- * # Single-flight (fixes cache stampede)
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)
9
29
  *
10
30
  * On a sync store, the policy also stores the in-flight Promise under a
11
31
  * separate `__inflight:cache:<key>` slot. Concurrent callers that miss the
@@ -28,73 +48,148 @@ const INFLIGHT_NS = '__inflight:cache:';
28
48
  * # Cache hit semantics
29
49
  *
30
50
  * On a cache hit, `meta.source` is set to `'cache'` and `meta.attempts` is
31
- * set to `0` — no work was performed. (v1.1.0 reported `attempts: 1` on
32
- * cache hits, which was inconsistent with the documented meaning of
33
- * `attempts`. v1.1.5 fixes this.)
51
+ * set to `0` — no work was performed.
34
52
  *
35
53
  * Failures are NEVER cached. Only successful values are stored.
36
54
  */
37
55
  export function cachePolicy(opts) {
38
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);
39
61
  const key = NS + ctx.key;
40
62
  const inflightKey = INFLIGHT_NS + ctx.key;
41
63
  // ─── Sync store: fast path with single-flight ────────────────────────
42
64
  if (isSyncStore(ctx.store)) {
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;
43
70
  // 1. Cache hit?
44
- const hit = ctx.store.get(key);
71
+ const hit = store.get(key);
45
72
  if (hit) {
46
73
  ctx.meta.source = 'cache';
47
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
+ }
48
84
  return hit.value;
49
85
  }
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
+ }
50
93
  // 2. In-flight single-flight hit? Join it.
51
- const inflight = ctx.store.get(inflightKey);
52
- if (inflight)
53
- return inflight;
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
+ }
54
123
  // 3. Originator: launch fn, cache on success (fail-open).
55
- const promise = Promise.resolve(fn(signal)).then((value) => {
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) => {
56
130
  try {
57
- ctx.store.set(key, { value }, opts.ttl);
131
+ store.set(key, { value, insertedAt: Date.now() }, opts.ttl);
58
132
  }
59
133
  catch {
60
134
  // Fail-open: cache write failure should not surface to caller.
61
- // The value is still valid; next call will just re-run fn.
62
135
  }
63
136
  return value;
64
- }, (err) => { throw err; });
65
- // Try to publish the in-flight promise so concurrent callers can
66
- // join (single-flight). If store.set throws, single-flight is
67
- // disabled for this call concurrent callers will all run fn.
68
- // That's still correct, just less efficient.
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.
69
143
  try {
70
- ctx.store.set(inflightKey, promise);
71
- // Cleanup the in-flight slot on settle. The `.catch(() => {})`
72
- // suppresses the unhandled-rejection warning that would otherwise
73
- // fire on the `.finally()` chain — the original `promise` (with
74
- // its rejection) is returned to the caller, which has its own
75
- // try/catch via act().
76
- promise.finally(() => {
77
- try {
78
- ctx.store.delete(inflightKey);
79
- }
80
- catch { /* ignore */ }
81
- }).catch(() => { });
144
+ store.set(inflightKey, { promise: rawPromise, generation, meta: ctx.meta });
82
145
  }
83
146
  catch {
84
147
  // Single-flight unavailable; proceed without publishing.
148
+ // Originator still gets their value (or error).
85
149
  }
86
- return promise;
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);
87
163
  }
88
- // ─── Async store: no single-flight (race window unavoidable) ─────────
164
+ // Async store: no single-flight (race window unavoidable).
165
+ // Re-check signal.aborted between awaits.
166
+ const obs = ctx.observability;
89
167
  const hit = await ctx.store.get(key);
168
+ if (signal.aborted)
169
+ return Promise.reject(signal.reason);
90
170
  if (hit) {
91
171
  ctx.meta.source = 'cache';
92
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
+ });
179
+ }
93
180
  return hit.value;
94
181
  }
182
+ if (obs) {
183
+ obs.hooks.onCacheMiss?.({
184
+ type: 'cache-miss', key: ctx.key, traceId: obs.traceId,
185
+ timestamp: Date.now(),
186
+ });
187
+ }
95
188
  const value = await fn(signal);
189
+ if (signal.aborted)
190
+ return Promise.reject(signal.reason);
96
191
  try {
97
- await ctx.store.set(key, { value }, opts.ttl);
192
+ await ctx.store.set(key, { value, insertedAt: Date.now() }, opts.ttl);
98
193
  }
99
194
  catch {
100
195
  // Fail-open: see sync path comment.
@@ -102,4 +197,46 @@ export function cachePolicy(opts) {
102
197
  return value;
103
198
  };
104
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
+ }
105
242
  //# sourceMappingURL=cache.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/policies/cache.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/policies/cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.circuitBreakerPolicy = circuitBreakerPolicy;
4
+ const executor_js_1 = require("../core/executor.js");
5
+ const errors_js_1 = require("../errors.js");
6
+ const abort_js_1 = require("../utils/abort.js");
7
+ const NS = 'cb:';
8
+ function getState(store, key) {
9
+ return store.get(NS + key) ?? { failures: 0, lastFailureTime: 0, isOpen: false, openedAt: 0, halfOpen: false };
10
+ }
11
+ function setState(store, key, state) {
12
+ store.set(NS + key, state);
13
+ }
14
+ function circuitBreakerPolicy(opts) {
15
+ const threshold = Math.max(1, Math.floor(opts.threshold));
16
+ const cooldownMs = opts.cooldownMs;
17
+ const resetTimeoutMs = opts.resetTimeoutMs ?? Number.POSITIVE_INFINITY;
18
+ const applier = (fn, ctx) => {
19
+ const syncCtx = ctx;
20
+ return async (signal) => {
21
+ const key = syncCtx.key;
22
+ const now = Date.now();
23
+ // Read state synchronously and make decisions atomically
24
+ const state = getState(syncCtx.store, key);
25
+ if (state.isOpen) {
26
+ const elapsed = now - state.openedAt;
27
+ if (elapsed >= cooldownMs) {
28
+ // Transition to half-open: allow only ONE probe call
29
+ state.isOpen = false;
30
+ state.halfOpen = true;
31
+ setState(syncCtx.store, key, state);
32
+ }
33
+ else {
34
+ throw new errors_js_1.CircuitBreakerOpenError(key, cooldownMs - elapsed);
35
+ }
36
+ }
37
+ else if (state.halfOpen) {
38
+ // Another call is already probing — block this one
39
+ throw new errors_js_1.CircuitBreakerOpenError(key, 0);
40
+ }
41
+ if (now - state.lastFailureTime > resetTimeoutMs && state.failures > 0) {
42
+ state.failures = 0;
43
+ setState(syncCtx.store, key, state);
44
+ }
45
+ try {
46
+ const result = await fn(signal);
47
+ // Re-read state (it may have changed during await) and update atomically
48
+ const updated = getState(syncCtx.store, key);
49
+ updated.failures = 0;
50
+ updated.isOpen = false;
51
+ updated.halfOpen = false;
52
+ setState(syncCtx.store, key, updated);
53
+ return result;
54
+ }
55
+ catch (err) {
56
+ // Signal aborts are not downstream failures — don't count them
57
+ if ((0, abort_js_1.isAbortError)(err) || (signal.aborted && err === signal.reason)) {
58
+ const updated = getState(syncCtx.store, key);
59
+ if (updated.halfOpen) {
60
+ // Abort during half-open: probe didn't succeed or fail —
61
+ // downstream health is still unknown. Go back to OPEN with
62
+ // fresh cooldown so next call waits before probing again.
63
+ updated.isOpen = true;
64
+ updated.openedAt = Date.now();
65
+ updated.halfOpen = false;
66
+ }
67
+ setState(syncCtx.store, key, updated);
68
+ throw err;
69
+ }
70
+ const updated = getState(syncCtx.store, key);
71
+ updated.failures++;
72
+ updated.lastFailureTime = Date.now();
73
+ updated.halfOpen = false;
74
+ if (updated.failures >= threshold) {
75
+ updated.isOpen = true;
76
+ updated.openedAt = Date.now();
77
+ }
78
+ setState(syncCtx.store, key, updated);
79
+ throw err;
80
+ }
81
+ };
82
+ };
83
+ applier[executor_js_1.REQUIRES_SYNC_STORE] = true;
84
+ return applier;
85
+ }
@@ -0,0 +1,3 @@
1
+ import type { PolicyApplier, CircuitBreakerOptions } from '../types/index.js';
2
+ export declare function circuitBreakerPolicy<T>(opts: CircuitBreakerOptions): PolicyApplier<T>;
3
+ //# sourceMappingURL=circuitBreaker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuitBreaker.d.ts","sourceRoot":"","sources":["../../src/policies/circuitBreaker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,qBAAqB,EAAE,MAAM,mBAAmB,CAAA;AAwBnG,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,IAAI,EAAE,qBAAqB,GAAG,aAAa,CAAC,CAAC,CAAC,CA2ErF"}
@@ -0,0 +1,83 @@
1
+ import { REQUIRES_SYNC_STORE } from '../core/executor.js';
2
+ import { CircuitBreakerOpenError } from '../errors.js';
3
+ import { isAbortError } from '../utils/abort.js';
4
+ const NS = 'cb:';
5
+ function getState(store, key) {
6
+ return store.get(NS + key) ?? { failures: 0, lastFailureTime: 0, isOpen: false, openedAt: 0, halfOpen: false };
7
+ }
8
+ function setState(store, key, state) {
9
+ store.set(NS + key, state);
10
+ }
11
+ export function circuitBreakerPolicy(opts) {
12
+ const threshold = Math.max(1, Math.floor(opts.threshold));
13
+ const cooldownMs = opts.cooldownMs;
14
+ const resetTimeoutMs = opts.resetTimeoutMs ?? Number.POSITIVE_INFINITY;
15
+ const applier = (fn, ctx) => {
16
+ const syncCtx = ctx;
17
+ return async (signal) => {
18
+ const key = syncCtx.key;
19
+ const now = Date.now();
20
+ // Read state synchronously and make decisions atomically
21
+ const state = getState(syncCtx.store, key);
22
+ if (state.isOpen) {
23
+ const elapsed = now - state.openedAt;
24
+ if (elapsed >= cooldownMs) {
25
+ // Transition to half-open: allow only ONE probe call
26
+ state.isOpen = false;
27
+ state.halfOpen = true;
28
+ setState(syncCtx.store, key, state);
29
+ }
30
+ else {
31
+ throw new CircuitBreakerOpenError(key, cooldownMs - elapsed);
32
+ }
33
+ }
34
+ else if (state.halfOpen) {
35
+ // Another call is already probing — block this one
36
+ throw new CircuitBreakerOpenError(key, 0);
37
+ }
38
+ if (now - state.lastFailureTime > resetTimeoutMs && state.failures > 0) {
39
+ state.failures = 0;
40
+ setState(syncCtx.store, key, state);
41
+ }
42
+ try {
43
+ const result = await fn(signal);
44
+ // Re-read state (it may have changed during await) and update atomically
45
+ const updated = getState(syncCtx.store, key);
46
+ updated.failures = 0;
47
+ updated.isOpen = false;
48
+ updated.halfOpen = false;
49
+ setState(syncCtx.store, key, updated);
50
+ return result;
51
+ }
52
+ catch (err) {
53
+ // Signal aborts are not downstream failures — don't count them
54
+ if (isAbortError(err) || (signal.aborted && err === signal.reason)) {
55
+ const updated = getState(syncCtx.store, key);
56
+ if (updated.halfOpen) {
57
+ // Abort during half-open: probe didn't succeed or fail —
58
+ // downstream health is still unknown. Go back to OPEN with
59
+ // fresh cooldown so next call waits before probing again.
60
+ updated.isOpen = true;
61
+ updated.openedAt = Date.now();
62
+ updated.halfOpen = false;
63
+ }
64
+ setState(syncCtx.store, key, updated);
65
+ throw err;
66
+ }
67
+ const updated = getState(syncCtx.store, key);
68
+ updated.failures++;
69
+ updated.lastFailureTime = Date.now();
70
+ updated.halfOpen = false;
71
+ if (updated.failures >= threshold) {
72
+ updated.isOpen = true;
73
+ updated.openedAt = Date.now();
74
+ }
75
+ setState(syncCtx.store, key, updated);
76
+ throw err;
77
+ }
78
+ };
79
+ };
80
+ applier[REQUIRES_SYNC_STORE] = true;
81
+ return applier;
82
+ }
83
+ //# sourceMappingURL=circuitBreaker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuitBreaker.js","sourceRoot":"","sources":["../../src/policies/circuitBreaker.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,MAAM,EAAE,GAAG,KAAK,CAAA;AAUhB,SAAS,QAAQ,CAAC,KAAqB,EAAE,GAAW;IAClD,OAAO,KAAK,CAAC,GAAG,CAAe,EAAE,GAAG,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;AAC9H,CAAC;AAED,SAAS,QAAQ,CAAC,KAAqB,EAAE,GAAW,EAAE,KAAmB;IACvE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,GAAG,EAAE,KAAK,CAAC,CAAA;AAC5B,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAI,IAA2B;IACjE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAA;IACzD,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAA;IAClC,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,MAAM,CAAC,iBAAiB,CAAA;IAEtE,MAAM,OAAO,GAAG,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE;QAC7D,MAAM,OAAO,GAAG,GAA+D,CAAA;QAE/E,OAAO,KAAK,EAAE,MAAmB,EAAE,EAAE;YACnC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAA;YACvB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YAEtB,yDAAyD;YACzD,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;YAE1C,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBACjB,MAAM,OAAO,GAAG,GAAG,GAAG,KAAK,CAAC,QAAQ,CAAA;gBACpC,IAAI,OAAO,IAAI,UAAU,EAAE,CAAC;oBAC1B,qDAAqD;oBACrD,KAAK,CAAC,MAAM,GAAG,KAAK,CAAA;oBACpB,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAA;oBACrB,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;gBACrC,CAAC;qBAAM,CAAC;oBACN,MAAM,IAAI,uBAAuB,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,CAAA;gBAC9D,CAAC;YACH,CAAC;iBAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;gBAC1B,mDAAmD;gBACnD,MAAM,IAAI,uBAAuB,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;YAC3C,CAAC;YAED,IAAI,GAAG,GAAG,KAAK,CAAC,eAAe,GAAG,cAAc,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC;gBACvE,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAA;gBAClB,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;YACrC,CAAC;YAED,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,CAAA;gBAC/B,yEAAyE;gBACzE,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;gBAC5C,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;gBACpB,OAAO,CAAC,MAAM,GAAG,KAAK,CAAA;gBACtB,OAAO,CAAC,QAAQ,GAAG,KAAK,CAAA;gBACxB,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,CAAC,CAAA;gBACrC,OAAO,MAAM,CAAA;YACf,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,+DAA+D;gBAC/D,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,GAAG,KAAK,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;oBACnE,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;oBAC5C,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;wBACrB,yDAAyD;wBACzD,2DAA2D;wBAC3D,0DAA0D;wBAC1D,OAAO,CAAC,MAAM,GAAG,IAAI,CAAA;wBACrB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;wBAC7B,OAAO,CAAC,QAAQ,GAAG,KAAK,CAAA;oBAC1B,CAAC;oBACD,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,CAAC,CAAA;oBACrC,MAAM,GAAG,CAAA;gBACX,CAAC;gBACD,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;gBAC5C,OAAO,CAAC,QAAQ,EAAE,CAAA;gBAClB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;gBACpC,OAAO,CAAC,QAAQ,GAAG,KAAK,CAAA;gBACxB,IAAI,OAAO,CAAC,QAAQ,IAAI,SAAS,EAAE,CAAC;oBAClC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAA;oBACrB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;gBAC/B,CAAC;gBACD,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,CAAC,CAAA;gBACrC,MAAM,GAAG,CAAA;YACX,CAAC;QACH,CAAC,CAAA;IACH,CAAC,CAEA;IAAC,OAA+D,CAAC,mBAAmB,CAAC,GAAG,IAAI,CAAA;IAC7F,OAAO,OAAO,CAAA;AAChB,CAAC"}
@@ -5,37 +5,29 @@ const executor_js_1 = require("../core/executor.js");
5
5
  const abort_js_1 = require("../utils/abort.js");
6
6
  // Namespace so dedupe keys never collide with cache keys in the shared store.
7
7
  const NS = 'dedupe:';
8
+ /**
9
+ * Module-level generation counter. Monotonic — guarantees uniqueness
10
+ * across the lifetime of the process. Wraps at `Number.MAX_SAFE_INTEGER`
11
+ * (which would take ~285 000 years at 1M increments/sec).
12
+ */
13
+ let generationCounter = 0;
14
+ function nextGeneration() {
15
+ generationCounter = (generationCounter + 1) % Number.MAX_SAFE_INTEGER;
16
+ return generationCounter;
17
+ }
8
18
  /**
9
19
  * Collapse concurrent calls that share the same key into one in-flight Promise.
10
20
  *
11
- * # How it works
12
- *
13
- * The first caller (originator) starts the work and stores
14
- * `{ promise, meta }` in the store under `dedupe:<key>`. Every subsequent
15
- * caller that arrives before the promise settles receives the SAME promise
16
- * — no duplicate work.
17
- *
18
- * # Shared `meta` (fixes v1.0 trade-off)
19
- *
20
- * The originator's `ctx.meta` reference is stored alongside the promise.
21
- * Inner policies (e.g. `retryPolicy`) mutate it as they run. After the
22
- * promise settles, joiners copy `attempts` and `source` from the shared
23
- * meta into their own `ctx.meta`. This means a joiner's `ActResult.attempts`
24
- * reflects the real effort (e.g. `3` if the originator retried twice), not
25
- * the misleading default of `1`.
26
- *
27
- * # Abort safety (fixes hung-fn block)
21
+ * # Properties
28
22
  *
29
- * Joiners race the in-flight promise against their own AbortSignal via
30
- * `raceAbort`. If a joiner's signal aborts (e.g. their `totalTimeout`
31
- * fires), they reject immediately — they don't have to wait for the
32
- * originator to finish. The originator's promise continues in the
33
- * background for any other joiners that haven't aborted.
34
- *
35
- * If `inflightTtl` is set, the store entry is also TTL'd: if the
36
- * originator never settles, new callers can start fresh after the TTL
37
- * expires (the original promise still leaks unless an outer timeout
38
- * fires, but new callers aren't blocked).
23
+ * - **Generation-safe cleanup**: stale originators never delete newer
24
+ * entries when `inflightTtl` triggers replacement.
25
+ * - **Joiner isolation**: originator's caller-signal abort does NOT
26
+ * propagate to joiners. Each joiner races the shared in-flight promise
27
+ * against their OWN signal only.
28
+ * - **Truthful joiner attempts**: joiners that abort before the originator
29
+ * settles report `attempts: 0` (they did no work), not the originator's
30
+ * in-progress count.
39
31
  *
40
32
  * # INVARIANT: requires SyncStateStore
41
33
  *
@@ -54,34 +46,71 @@ function dedupePolicy(opts = { enabled: true }) {
54
46
  const syncCtx = ctx;
55
47
  return async (signal) => {
56
48
  const key = NS + syncCtx.key;
57
- // Fast path: an in-flight promise already exists. Join it.
58
- // raceAbort ensures we don't block on a hung originator if our own
59
- // signal aborts.
49
+ // ─── Fast path: an in-flight promise already exists. Join it. ──────
50
+ //
51
+ // The joiner races the in-flight promise against THEIR OWN signal.
52
+ // They do NOT inherit the originator's signal state — if the
53
+ // originator's caller cancels, joiners continue waiting (or get
54
+ // the eventual settled value).
60
55
  const existing = syncCtx.store.get(key);
61
56
  if (existing) {
62
57
  try {
63
58
  const value = await (0, abort_js_1.raceAbort)(existing.promise, signal);
64
- return value;
65
- }
66
- finally {
67
- // Copy the originator's final meta into our own, regardless of
68
- // success or failure. By the time `existing.promise` has settled
69
- // (or our signal aborted), the originator's retry loop has set
70
- // the final attempt count.
59
+ // Success — copy originator's final meta (attempts, source)
60
+ // so the joiner's ActResult reflects the real effort.
71
61
  syncCtx.meta.attempts = existing.meta.attempts;
72
62
  syncCtx.meta.source = existing.meta.source;
63
+ return value;
64
+ }
65
+ catch (err) {
66
+ // Joiner either aborted (their own signal) or got the
67
+ // originator's settled error.
68
+ if (signal.aborted) {
69
+ // Joiner's own signal aborted before originator settled.
70
+ // They did no work — report `attempts: 0` (truthful).
71
+ syncCtx.meta.attempts = 0;
72
+ // source stays 'fresh' (default) — joiner didn't read from cache
73
+ }
74
+ else {
75
+ // Originator settled with failure — copy its final meta.
76
+ syncCtx.meta.attempts = existing.meta.attempts;
77
+ syncCtx.meta.source = existing.meta.source;
78
+ }
79
+ throw err;
73
80
  }
74
81
  }
75
- // Originator path: start the work and publish the promise.
82
+ // ─── Originator path: start the work and publish the promise. ──────
76
83
  //
77
- // We race fn against `signal` so that if our own signal aborts while
78
- // fn is pending, we reject (and the .finally cleans up the store).
79
- // The stored promise is the RACED one joiners see the same
80
- // rejection if they join before cleanup.
81
- const promise = (0, abort_js_1.raceAbort)(Promise.resolve(fn(signal)), signal).finally(() => syncCtx.store.delete(key));
82
- const entry = { promise, meta: syncCtx.meta };
84
+ // The stored promise is the RAW fn(signal) NOT raceAbort-wrapped.
85
+ // This is critical: if we stored raceAbort(fn, signal), an
86
+ // originator signal abort would cause the stored promise to reject,
87
+ // which would propagate to all joiners. Instead:
88
+ // - stored: raw fn(signal) joiners await this
89
+ // - originator's await: raceAbort(stored, signal) originator
90
+ // can bail out on their own signal without affecting joiners
91
+ const generation = nextGeneration();
92
+ const rawPromise = Promise.resolve(fn(signal));
93
+ const entry = {
94
+ promise: rawPromise,
95
+ meta: syncCtx.meta,
96
+ generation,
97
+ };
83
98
  syncCtx.store.set(key, entry, inflightTtl);
84
- return promise;
99
+ // Cleanup on settle: only delete if generation matches. If a newer
100
+ // originator has replaced this entry (because inflightTtl expired),
101
+ // our cleanup is a no-op — the newer entry stays.
102
+ const cleanup = () => {
103
+ const current = syncCtx.store.get(key);
104
+ if (current && current.generation === generation) {
105
+ syncCtx.store.delete(key);
106
+ }
107
+ };
108
+ // Attach cleanup to the RAW promise (not the raceAbort-wrapped one)
109
+ // so cleanup runs whenever fn settles, regardless of originator abort.
110
+ rawPromise.then(cleanup, cleanup);
111
+ // Originator races the raw promise against their own signal —
112
+ // they can bail out without affecting joiners.
113
+ return (0, abort_js_1.raceAbort)(rawPromise, signal);
85
114
  };
86
115
  };
87
116
  applier[executor_js_1.REQUIRES_SYNC_STORE] = true;