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.
Files changed (130) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +262 -232
  3. package/dist/core/act.cjs +505 -39
  4. package/dist/core/act.d.ts +81 -10
  5. package/dist/core/act.d.ts.map +1 -1
  6. package/dist/core/act.js +504 -40
  7. package/dist/core/act.js.map +1 -1
  8. package/dist/core/executor.cjs +21 -9
  9. package/dist/core/executor.d.ts +31 -6
  10. package/dist/core/executor.d.ts.map +1 -1
  11. package/dist/core/executor.js +21 -9
  12. package/dist/core/executor.js.map +1 -1
  13. package/dist/core/health.cjs +34 -0
  14. package/dist/core/health.d.ts +18 -0
  15. package/dist/core/health.d.ts.map +1 -0
  16. package/dist/core/health.js +28 -0
  17. package/dist/core/health.js.map +1 -0
  18. package/dist/core/shutdown.cjs +60 -0
  19. package/dist/core/shutdown.d.ts +8 -0
  20. package/dist/core/shutdown.d.ts.map +1 -0
  21. package/dist/core/shutdown.js +56 -0
  22. package/dist/core/shutdown.js.map +1 -0
  23. package/dist/core/tenant.cjs +71 -0
  24. package/dist/core/tenant.d.ts +29 -0
  25. package/dist/core/tenant.d.ts.map +1 -0
  26. package/dist/core/tenant.js +68 -0
  27. package/dist/core/tenant.js.map +1 -0
  28. package/dist/errors.cjs +182 -0
  29. package/dist/errors.d.ts +151 -0
  30. package/dist/errors.d.ts.map +1 -0
  31. package/dist/errors.js +171 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/index.cjs +56 -6
  34. package/dist/index.d.ts +22 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +24 -4
  37. package/dist/index.js.map +1 -1
  38. package/dist/observability.cjs +45 -0
  39. package/dist/observability.d.ts +145 -0
  40. package/dist/observability.d.ts.map +1 -0
  41. package/dist/observability.js +43 -0
  42. package/dist/observability.js.map +1 -0
  43. package/dist/policies/bulkhead.cjs +97 -0
  44. package/dist/policies/bulkhead.d.ts +3 -0
  45. package/dist/policies/bulkhead.d.ts.map +1 -0
  46. package/dist/policies/bulkhead.js +95 -0
  47. package/dist/policies/bulkhead.js.map +1 -0
  48. package/dist/policies/cache.cjs +223 -22
  49. package/dist/policies/cache.d.ts +42 -7
  50. package/dist/policies/cache.d.ts.map +1 -1
  51. package/dist/policies/cache.js +223 -22
  52. package/dist/policies/cache.js.map +1 -1
  53. package/dist/policies/circuitBreaker.cjs +85 -0
  54. package/dist/policies/circuitBreaker.d.ts +3 -0
  55. package/dist/policies/circuitBreaker.d.ts.map +1 -0
  56. package/dist/policies/circuitBreaker.js +83 -0
  57. package/dist/policies/circuitBreaker.js.map +1 -0
  58. package/dist/policies/dedupe.cjs +98 -21
  59. package/dist/policies/dedupe.d.ts +20 -13
  60. package/dist/policies/dedupe.d.ts.map +1 -1
  61. package/dist/policies/dedupe.js +98 -21
  62. package/dist/policies/dedupe.js.map +1 -1
  63. package/dist/policies/rateLimit.cjs +35 -0
  64. package/dist/policies/rateLimit.d.ts +3 -0
  65. package/dist/policies/rateLimit.d.ts.map +1 -0
  66. package/dist/policies/rateLimit.js +33 -0
  67. package/dist/policies/rateLimit.js.map +1 -0
  68. package/dist/policies/retry.cjs +103 -26
  69. package/dist/policies/retry.d.ts +25 -2
  70. package/dist/policies/retry.d.ts.map +1 -1
  71. package/dist/policies/retry.js +103 -26
  72. package/dist/policies/retry.js.map +1 -1
  73. package/dist/policies/timeout.cjs +78 -36
  74. package/dist/policies/timeout.d.ts +7 -18
  75. package/dist/policies/timeout.d.ts.map +1 -1
  76. package/dist/policies/timeout.js +84 -31
  77. package/dist/policies/timeout.js.map +1 -1
  78. package/dist/state/store.cjs +9 -2
  79. package/dist/state/store.d.ts +9 -0
  80. package/dist/state/store.d.ts.map +1 -1
  81. package/dist/state/store.js +9 -2
  82. package/dist/state/store.js.map +1 -1
  83. package/dist/stores/base.cjs +4 -4
  84. package/dist/stores/base.d.ts +32 -56
  85. package/dist/stores/base.d.ts.map +1 -1
  86. package/dist/stores/base.js +4 -4
  87. package/dist/stores/base.js.map +1 -1
  88. package/dist/stores/memory.cjs +161 -39
  89. package/dist/stores/memory.d.ts +64 -24
  90. package/dist/stores/memory.d.ts.map +1 -1
  91. package/dist/stores/memory.js +160 -39
  92. package/dist/stores/memory.js.map +1 -1
  93. package/dist/types/index.d.ts +315 -37
  94. package/dist/types/index.d.ts.map +1 -1
  95. package/dist/utils/abort.cjs +212 -0
  96. package/dist/utils/abort.d.ts +79 -0
  97. package/dist/utils/abort.d.ts.map +1 -0
  98. package/dist/utils/abort.js +206 -0
  99. package/dist/utils/abort.js.map +1 -0
  100. package/dist/utils/abortPool.cjs +26 -0
  101. package/dist/utils/abortPool.d.ts +4 -0
  102. package/dist/utils/abortPool.d.ts.map +1 -0
  103. package/dist/utils/abortPool.js +22 -0
  104. package/dist/utils/abortPool.js.map +1 -0
  105. package/dist/utils/backoff.cjs +54 -0
  106. package/dist/utils/backoff.d.ts +14 -0
  107. package/dist/utils/backoff.d.ts.map +1 -0
  108. package/dist/utils/backoff.js +52 -0
  109. package/dist/utils/backoff.js.map +1 -0
  110. package/dist/utils/key.cjs +77 -0
  111. package/dist/utils/key.d.ts +29 -0
  112. package/dist/utils/key.d.ts.map +1 -0
  113. package/dist/utils/key.js +75 -0
  114. package/dist/utils/key.js.map +1 -0
  115. package/dist/utils/limits.cjs +40 -0
  116. package/dist/utils/limits.d.ts +39 -0
  117. package/dist/utils/limits.d.ts.map +1 -0
  118. package/dist/utils/limits.js +38 -0
  119. package/dist/utils/limits.js.map +1 -0
  120. package/dist/utils/sanitize.cjs +29 -0
  121. package/dist/utils/sanitize.d.ts +3 -0
  122. package/dist/utils/sanitize.d.ts.map +1 -0
  123. package/dist/utils/sanitize.js +26 -0
  124. package/dist/utils/sanitize.js.map +1 -0
  125. package/dist/utils/validate.cjs +153 -0
  126. package/dist/utils/validate.d.ts +26 -0
  127. package/dist/utils/validate.d.ts.map +1 -0
  128. package/dist/utils/validate.js +141 -0
  129. package/dist/utils/validate.js.map +1 -0
  130. package/package.json +20 -37
@@ -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-circuits the entire downstream chain on a cache hit.
8
- * On a miss, runs fn and stores the result with TTL.
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
- * Must be the OUTERMOST policy so a hit skips dedupe, timeout, and retry.
44
+ * # Fail-open writes
11
45
  *
12
- * Supports both SyncStateStore and AsyncStateStore. The async branch
13
- * introduces two await points (get + set) but has no correctness
14
- * requirement for same-tick execution worst case is a cache stampede
15
- * on a simultaneous miss, which is standard behaviour without locking.
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
- // Fast path synchronous store, no await needed
22
- const hit = ctx.store.get(key);
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
- const value = await fn();
28
- ctx.store.set(key, { value }, opts.ttl);
29
- return value;
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
- else {
32
- // Async store path
33
- const hit = await ctx.store.get(key);
34
- if (hit) {
35
- ctx.meta.source = 'cache';
36
- return hit.value;
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
- const value = await fn();
39
- await ctx.store.set(key, { value }, opts.ttl);
40
- return value;
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
+ }
@@ -1,14 +1,49 @@
1
1
  import type { CacheOptions, PolicyApplier } from '../types/index.js';
2
2
  /**
3
- * Short-circuits the entire downstream chain on a cache hit.
4
- * On a miss, runs fn and stores the result with TTL.
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
- * Must be the OUTERMOST policy so a hit skips dedupe, timeout, and retry.
6
+ * # Properties
7
7
  *
8
- * Supports both SyncStateStore and AsyncStateStore. The async branch
9
- * introduces two await points (get + set) but has no correctness
10
- * requirement for same-tick execution worst case is a cache stampede
11
- * on a simultaneous miss, which is standard behaviour without locking.
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;AAU1F;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CA6BnE"}
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"}
@@ -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-circuits the entire downstream chain on a cache hit.
5
- * On a miss, runs fn and stores the result with TTL.
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
- * Must be the OUTERMOST policy so a hit skips dedupe, timeout, and retry.
41
+ * # Fail-open writes
8
42
  *
9
- * Supports both SyncStateStore and AsyncStateStore. The async branch
10
- * introduces two await points (get + set) but has no correctness
11
- * requirement for same-tick execution worst case is a cache stampede
12
- * on a simultaneous miss, which is standard behaviour without locking.
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
- // Fast path synchronous store, no await needed
19
- const hit = ctx.store.get(key);
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
- const value = await fn();
25
- ctx.store.set(key, { value }, opts.ttl);
26
- return value;
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
- else {
29
- // Async store path
30
- const hit = await ctx.store.get(key);
31
- if (hit) {
32
- ctx.meta.source = 'cache';
33
- return hit.value;
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
- const value = await fn();
36
- await ctx.store.set(key, { value }, opts.ttl);
37
- return value;
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;AAOnB;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAI,IAAkB;IAC/C,OAAO,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE,CACpD,KAAK,IAAI,EAAE;QACT,MAAM,GAAG,GAAG,EAAE,GAAG,GAAG,CAAC,GAAG,CAAA;QAExB,IAAI,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,iDAAiD;YACjD,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAgB,GAAG,CAAC,CAAA;YAC7C,IAAI,GAAG,EAAE,CAAC;gBACR,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAA;gBACzB,OAAO,GAAG,CAAC,KAAK,CAAA;YAClB,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,EAAE,EAAE,CAAA;YACxB,GAAG,CAAC,KAAK,CAAC,GAAG,CAAgB,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;YACtD,OAAO,KAAK,CAAA;QACd,CAAC;aAAM,CAAC;YACN,mBAAmB;YACnB,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,GAAG,CAAgB,GAAG,CAAC,CAAA;YACnD,IAAI,GAAG,EAAE,CAAC;gBACR,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAA;gBACzB,OAAO,GAAG,CAAC,KAAK,CAAA;YAClB,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,EAAE,EAAE,CAAA;YACxB,MAAM,GAAG,CAAC,KAAK,CAAC,GAAG,CAAgB,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;YAC5D,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC,CAAA;AACL,CAAC"}
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/policies/cache.ts"],"names":[],"mappings":"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"}