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
@@ -2,34 +2,16 @@ import type { PolicyApplier, DedupeOptions } from '../types/index.js';
2
2
  /**
3
3
  * Collapse concurrent calls that share the same key into one in-flight Promise.
4
4
  *
5
- * # How it works
6
- *
7
- * The first caller (originator) starts the work and stores
8
- * `{ promise, meta }` in the store under `dedupe:<key>`. Every subsequent
9
- * caller that arrives before the promise settles receives the SAME promise
10
- * no duplicate work.
11
- *
12
- * # Shared `meta` (fixes v1.0 trade-off)
13
- *
14
- * The originator's `ctx.meta` reference is stored alongside the promise.
15
- * Inner policies (e.g. `retryPolicy`) mutate it as they run. After the
16
- * promise settles, joiners copy `attempts` and `source` from the shared
17
- * meta into their own `ctx.meta`. This means a joiner's `ActResult.attempts`
18
- * reflects the real effort (e.g. `3` if the originator retried twice), not
19
- * the misleading default of `1`.
20
- *
21
- * # Abort safety (fixes hung-fn block)
22
- *
23
- * Joiners race the in-flight promise against their own AbortSignal via
24
- * `raceAbort`. If a joiner's signal aborts (e.g. their `totalTimeout`
25
- * fires), they reject immediately — they don't have to wait for the
26
- * originator to finish. The originator's promise continues in the
27
- * background for any other joiners that haven't aborted.
28
- *
29
- * If `inflightTtl` is set, the store entry is also TTL'd: if the
30
- * originator never settles, new callers can start fresh after the TTL
31
- * expires (the original promise still leaks unless an outer timeout
32
- * fires, but new callers aren't blocked).
5
+ * # Properties
6
+ *
7
+ * - **Generation-safe cleanup**: stale originators never delete newer
8
+ * entries when `inflightTtl` triggers replacement.
9
+ * - **Joiner isolation**: originator's caller-signal abort does NOT
10
+ * propagate to joiners. Each joiner races the shared in-flight promise
11
+ * against their OWN signal only.
12
+ * - **Truthful joiner attempts**: joiners that abort before the originator
13
+ * settles report `attempts: 0` (they did no work), not the originator's
14
+ * in-progress count.
33
15
  *
34
16
  * # INVARIANT: requires SyncStateStore
35
17
  *
@@ -1 +1 @@
1
- {"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAqB3F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,IAAI,GAAE,aAAiC,GAAG,aAAa,CAAC,CAAC,CAAC,CAkDzF"}
1
+ {"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,aAAa,EAAE,MAAM,mBAAmB,CAAA;AA6D3F;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,IAAI,GAAE,aAAiC,GAAG,aAAa,CAAC,CAAC,CAAC,CAqFzF"}
@@ -2,37 +2,29 @@ import { REQUIRES_SYNC_STORE } from '../core/executor.js';
2
2
  import { raceAbort } from '../utils/abort.js';
3
3
  // Namespace so dedupe keys never collide with cache keys in the shared store.
4
4
  const NS = 'dedupe:';
5
+ /**
6
+ * Module-level generation counter. Monotonic — guarantees uniqueness
7
+ * across the lifetime of the process. Wraps at `Number.MAX_SAFE_INTEGER`
8
+ * (which would take ~285 000 years at 1M increments/sec).
9
+ */
10
+ let generationCounter = 0;
11
+ function nextGeneration() {
12
+ generationCounter = (generationCounter + 1) % Number.MAX_SAFE_INTEGER;
13
+ return generationCounter;
14
+ }
5
15
  /**
6
16
  * Collapse concurrent calls that share the same key into one in-flight Promise.
7
17
  *
8
- * # How it works
9
- *
10
- * The first caller (originator) starts the work and stores
11
- * `{ promise, meta }` in the store under `dedupe:<key>`. Every subsequent
12
- * caller that arrives before the promise settles receives the SAME promise
13
- * — no duplicate work.
14
- *
15
- * # Shared `meta` (fixes v1.0 trade-off)
16
- *
17
- * The originator's `ctx.meta` reference is stored alongside the promise.
18
- * Inner policies (e.g. `retryPolicy`) mutate it as they run. After the
19
- * promise settles, joiners copy `attempts` and `source` from the shared
20
- * meta into their own `ctx.meta`. This means a joiner's `ActResult.attempts`
21
- * reflects the real effort (e.g. `3` if the originator retried twice), not
22
- * the misleading default of `1`.
23
- *
24
- * # Abort safety (fixes hung-fn block)
18
+ * # Properties
25
19
  *
26
- * Joiners race the in-flight promise against their own AbortSignal via
27
- * `raceAbort`. If a joiner's signal aborts (e.g. their `totalTimeout`
28
- * fires), they reject immediately — they don't have to wait for the
29
- * originator to finish. The originator's promise continues in the
30
- * background for any other joiners that haven't aborted.
31
- *
32
- * If `inflightTtl` is set, the store entry is also TTL'd: if the
33
- * originator never settles, new callers can start fresh after the TTL
34
- * expires (the original promise still leaks unless an outer timeout
35
- * fires, but new callers aren't blocked).
20
+ * - **Generation-safe cleanup**: stale originators never delete newer
21
+ * entries when `inflightTtl` triggers replacement.
22
+ * - **Joiner isolation**: originator's caller-signal abort does NOT
23
+ * propagate to joiners. Each joiner races the shared in-flight promise
24
+ * against their OWN signal only.
25
+ * - **Truthful joiner attempts**: joiners that abort before the originator
26
+ * settles report `attempts: 0` (they did no work), not the originator's
27
+ * in-progress count.
36
28
  *
37
29
  * # INVARIANT: requires SyncStateStore
38
30
  *
@@ -51,34 +43,71 @@ export function dedupePolicy(opts = { enabled: true }) {
51
43
  const syncCtx = ctx;
52
44
  return async (signal) => {
53
45
  const key = NS + syncCtx.key;
54
- // Fast path: an in-flight promise already exists. Join it.
55
- // raceAbort ensures we don't block on a hung originator if our own
56
- // signal aborts.
46
+ // ─── Fast path: an in-flight promise already exists. Join it. ──────
47
+ //
48
+ // The joiner races the in-flight promise against THEIR OWN signal.
49
+ // They do NOT inherit the originator's signal state — if the
50
+ // originator's caller cancels, joiners continue waiting (or get
51
+ // the eventual settled value).
57
52
  const existing = syncCtx.store.get(key);
58
53
  if (existing) {
59
54
  try {
60
55
  const value = await raceAbort(existing.promise, signal);
61
- return value;
62
- }
63
- finally {
64
- // Copy the originator's final meta into our own, regardless of
65
- // success or failure. By the time `existing.promise` has settled
66
- // (or our signal aborted), the originator's retry loop has set
67
- // the final attempt count.
56
+ // Success — copy originator's final meta (attempts, source)
57
+ // so the joiner's ActResult reflects the real effort.
68
58
  syncCtx.meta.attempts = existing.meta.attempts;
69
59
  syncCtx.meta.source = existing.meta.source;
60
+ return value;
61
+ }
62
+ catch (err) {
63
+ // Joiner either aborted (their own signal) or got the
64
+ // originator's settled error.
65
+ if (signal.aborted) {
66
+ // Joiner's own signal aborted before originator settled.
67
+ // They did no work — report `attempts: 0` (truthful).
68
+ syncCtx.meta.attempts = 0;
69
+ // source stays 'fresh' (default) — joiner didn't read from cache
70
+ }
71
+ else {
72
+ // Originator settled with failure — copy its final meta.
73
+ syncCtx.meta.attempts = existing.meta.attempts;
74
+ syncCtx.meta.source = existing.meta.source;
75
+ }
76
+ throw err;
70
77
  }
71
78
  }
72
- // Originator path: start the work and publish the promise.
79
+ // ─── Originator path: start the work and publish the promise. ──────
73
80
  //
74
- // We race fn against `signal` so that if our own signal aborts while
75
- // fn is pending, we reject (and the .finally cleans up the store).
76
- // The stored promise is the RACED one joiners see the same
77
- // rejection if they join before cleanup.
78
- const promise = raceAbort(Promise.resolve(fn(signal)), signal).finally(() => syncCtx.store.delete(key));
79
- const entry = { promise, meta: syncCtx.meta };
81
+ // The stored promise is the RAW fn(signal) NOT raceAbort-wrapped.
82
+ // This is critical: if we stored raceAbort(fn, signal), an
83
+ // originator signal abort would cause the stored promise to reject,
84
+ // which would propagate to all joiners. Instead:
85
+ // - stored: raw fn(signal) joiners await this
86
+ // - originator's await: raceAbort(stored, signal) originator
87
+ // can bail out on their own signal without affecting joiners
88
+ const generation = nextGeneration();
89
+ const rawPromise = Promise.resolve(fn(signal));
90
+ const entry = {
91
+ promise: rawPromise,
92
+ meta: syncCtx.meta,
93
+ generation,
94
+ };
80
95
  syncCtx.store.set(key, entry, inflightTtl);
81
- return promise;
96
+ // Cleanup on settle: only delete if generation matches. If a newer
97
+ // originator has replaced this entry (because inflightTtl expired),
98
+ // our cleanup is a no-op — the newer entry stays.
99
+ const cleanup = () => {
100
+ const current = syncCtx.store.get(key);
101
+ if (current && current.generation === generation) {
102
+ syncCtx.store.delete(key);
103
+ }
104
+ };
105
+ // Attach cleanup to the RAW promise (not the raceAbort-wrapped one)
106
+ // so cleanup runs whenever fn settles, regardless of originator abort.
107
+ rawPromise.then(cleanup, cleanup);
108
+ // Originator races the raw promise against their own signal —
109
+ // they can bail out without affecting joiners.
110
+ return raceAbort(rawPromise, signal);
82
111
  };
83
112
  };
84
113
  applier[REQUIRES_SYNC_STORE] = true;
@@ -1 +1 @@
1
- {"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAE7C,8EAA8E;AAC9E,MAAM,EAAE,GAAG,SAAS,CAAA;AAepB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,MAAM,UAAU,YAAY,CAAI,OAAsB,EAAE,OAAO,EAAE,IAAI,EAAE;IACrE,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,CAAA;IAEpC,MAAM,OAAO,GAAG,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE;QAC7D,yEAAyE;QACzE,8CAA8C;QAC9C,MAAM,OAAO,GAAG,GAAoB,CAAA;QAEpC,OAAO,KAAK,EAAE,MAAmB,EAAE,EAAE;YACnC,MAAM,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,CAAA;YAE5B,2DAA2D;YAC3D,mEAAmE;YACnE,iBAAiB;YACjB,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAiB,GAAG,CAAC,CAAA;YACvD,IAAI,QAAQ,EAAE,CAAC;gBACb,IAAI,CAAC;oBACH,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;oBACvD,OAAO,KAAK,CAAA;gBACd,CAAC;wBAAS,CAAC;oBACT,+DAA+D;oBAC/D,iEAAiE;oBACjE,+DAA+D;oBAC/D,2BAA2B;oBAC3B,OAAO,CAAC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAA;oBAC9C,OAAO,CAAC,IAAI,CAAC,MAAM,GAAK,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAA;gBAC9C,CAAC;YACH,CAAC;YAED,2DAA2D;YAC3D,EAAE;YACF,qEAAqE;YACrE,mEAAmE;YACnE,6DAA6D;YAC7D,yCAAyC;YACzC,MAAM,OAAO,GAAG,SAAS,CACvB,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,EAC3B,MAAM,CACP,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;YAE1C,MAAM,KAAK,GAAmB,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAA;YAC7D,OAAO,CAAC,KAAK,CAAC,GAAG,CAAiB,GAAG,EAAE,KAAK,EAAE,WAAW,CAAC,CAAA;YAC1D,OAAO,OAAO,CAAA;QAChB,CAAC,CAAA;IACH,CAAC,CAGA;IAAC,OAA+D,CAAC,mBAAmB,CAAC,GAAG,IAAI,CAAA;IAE7F,OAAO,OAAO,CAAA;AAChB,CAAC"}
1
+ {"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rateLimitPolicy = rateLimitPolicy;
4
+ const executor_js_1 = require("../core/executor.js");
5
+ const errors_js_1 = require("../errors.js");
6
+ const NS = 'rl:';
7
+ function getState(store, key) {
8
+ return store.get(NS + key) ?? { timestamps: [] };
9
+ }
10
+ function setState(store, key, state) {
11
+ store.set(NS + key, state);
12
+ }
13
+ function rateLimitPolicy(opts) {
14
+ const maxCalls = Math.max(1, Math.floor(opts.maxCalls));
15
+ const windowMs = opts.windowMs;
16
+ const applier = (fn, ctx) => {
17
+ const syncCtx = ctx;
18
+ return async (signal) => {
19
+ const key = syncCtx.key;
20
+ const now = Date.now();
21
+ const state = getState(syncCtx.store, key);
22
+ const cutoff = now - windowMs;
23
+ state.timestamps = state.timestamps.filter(t => t > cutoff);
24
+ if (state.timestamps.length >= maxCalls) {
25
+ setState(syncCtx.store, key, state);
26
+ throw new errors_js_1.RateLimitError(key, maxCalls, windowMs);
27
+ }
28
+ state.timestamps.push(now);
29
+ setState(syncCtx.store, key, state);
30
+ return fn(signal);
31
+ };
32
+ };
33
+ applier[executor_js_1.REQUIRES_SYNC_STORE] = true;
34
+ return applier;
35
+ }
@@ -0,0 +1,3 @@
1
+ import type { PolicyApplier, RateLimitOptions } from '../types/index.js';
2
+ export declare function rateLimitPolicy<T>(opts: RateLimitOptions): PolicyApplier<T>;
3
+ //# sourceMappingURL=rateLimit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rateLimit.d.ts","sourceRoot":"","sources":["../../src/policies/rateLimit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAmB9F,wBAAgB,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE,gBAAgB,GAAG,aAAa,CAAC,CAAC,CAAC,CA6B3E"}
@@ -0,0 +1,33 @@
1
+ import { REQUIRES_SYNC_STORE } from '../core/executor.js';
2
+ import { RateLimitError } from '../errors.js';
3
+ const NS = 'rl:';
4
+ function getState(store, key) {
5
+ return store.get(NS + key) ?? { timestamps: [] };
6
+ }
7
+ function setState(store, key, state) {
8
+ store.set(NS + key, state);
9
+ }
10
+ export function rateLimitPolicy(opts) {
11
+ const maxCalls = Math.max(1, Math.floor(opts.maxCalls));
12
+ const windowMs = opts.windowMs;
13
+ const applier = (fn, ctx) => {
14
+ const syncCtx = ctx;
15
+ return async (signal) => {
16
+ const key = syncCtx.key;
17
+ const now = Date.now();
18
+ const state = getState(syncCtx.store, key);
19
+ const cutoff = now - windowMs;
20
+ state.timestamps = state.timestamps.filter(t => t > cutoff);
21
+ if (state.timestamps.length >= maxCalls) {
22
+ setState(syncCtx.store, key, state);
23
+ throw new RateLimitError(key, maxCalls, windowMs);
24
+ }
25
+ state.timestamps.push(now);
26
+ setState(syncCtx.store, key, state);
27
+ return fn(signal);
28
+ };
29
+ };
30
+ applier[REQUIRES_SYNC_STORE] = true;
31
+ return applier;
32
+ }
33
+ //# sourceMappingURL=rateLimit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/policies/rateLimit.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAE7C,MAAM,EAAE,GAAG,KAAK,CAAA;AAMhB,SAAS,QAAQ,CAAC,KAAqB,EAAE,GAAW;IAClD,OAAO,KAAK,CAAC,GAAG,CAAiB,EAAE,GAAG,GAAG,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,CAAA;AAClE,CAAC;AAED,SAAS,QAAQ,CAAC,KAAqB,EAAE,GAAW,EAAE,KAAqB;IACzE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,GAAG,EAAE,KAAK,CAAC,CAAA;AAC5B,CAAC;AAED,MAAM,UAAU,eAAe,CAAI,IAAsB;IACvD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;IACvD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAA;IAE9B,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;YACtB,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;YAE1C,MAAM,MAAM,GAAG,GAAG,GAAG,QAAQ,CAAA;YAC7B,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,CAAA;YAE3D,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,IAAI,QAAQ,EAAE,CAAC;gBACxC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;gBACnC,MAAM,IAAI,cAAc,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;YACnD,CAAC;YAED,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YAC1B,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;YAEnC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAA;QACnB,CAAC,CAAA;IACH,CAAC,CAEA;IAAC,OAA+D,CAAC,mBAAmB,CAAC,GAAG,IAAI,CAAA;IAC7F,OAAO,OAAO,CAAA;AAChB,CAAC"}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.retryPolicy = retryPolicy;
4
4
  const backoff_js_1 = require("../utils/backoff.js");
5
5
  const abort_js_1 = require("../utils/abort.js");
6
+ const errors_js_1 = require("../errors.js");
6
7
  // ─── Default predicate ────────────────────────────────────────────────────────
7
8
  /**
8
9
  * Default `shouldRetry`: retry on any error EXCEPT abort errors.
@@ -47,41 +48,77 @@ function retryPolicy(opts) {
47
48
  const max = Math.max(1, Math.floor(opts.attempts));
48
49
  const shouldRetry = opts.shouldRetry ?? defaultShouldRetry;
49
50
  return (fn, ctx) => async (parentSignal) => {
51
+ const errors = [];
50
52
  let lastErr;
53
+ let retriedAtLeastOnce = false;
54
+ const obs = ctx.observability;
51
55
  for (let attempt = 1; attempt <= max; attempt++) {
52
56
  // Parent (totalTimeout or caller signal) already aborted — bail.
53
57
  if (parentSignal.aborted)
54
58
  throw parentSignal.reason;
55
59
  ctx.meta.attempts = attempt;
60
+ const attemptStart = Date.now();
56
61
  try {
62
+ // Emit onAttempt before each attempt. (We could emit
63
+ // after with duration/error, but before lets observers track
64
+ // in-flight attempts in real time.)
65
+ if (obs) {
66
+ obs.hooks.onAttempt?.({
67
+ type: 'attempt', key: ctx.key, traceId: obs.traceId,
68
+ timestamp: attemptStart, attempt,
69
+ });
70
+ }
57
71
  return await fn(parentSignal);
58
72
  }
59
73
  catch (err) {
60
74
  lastErr = err;
61
- // Always invoke shouldRetry so observers see every failure.
62
- // The return value only matters when there are attempts left.
63
- const retryable = shouldRetry(err, attempt);
75
+ errors.push(err);
76
+ // Invoke shouldRetry. If the predicate throws, treat as "don't retry"
77
+ // and surface the original fn error (not the predicate error).
78
+ let retryable;
79
+ try {
80
+ retryable = shouldRetry(err, attempt);
81
+ }
82
+ catch {
83
+ // Predicate threw — don't retry, surface original error
84
+ throw err;
85
+ }
64
86
  if (attempt >= max) {
65
- // Last attempt surface the error regardless of retryable.
87
+ // Last attempt. If we retried at least once (predicate
88
+ // allowed retries), wrap in RetryExhaustedError for context.
89
+ if (retriedAtLeastOnce) {
90
+ throw new errors_js_1.RetryExhaustedError({
91
+ key: ctx.key,
92
+ attempts: attempt,
93
+ lastError: err,
94
+ errors,
95
+ });
96
+ }
66
97
  throw err;
67
98
  }
68
99
  if (!retryable) {
69
- // Non-retryable error — bail immediately without consuming
70
- // remaining attempts. The error surfaces exactly as-is.
100
+ // Non-retryable error — bail immediately.
71
101
  throw err;
72
102
  }
103
+ // Mark that we retried at least once — affects final wrap.
104
+ retriedAtLeastOnce = true;
73
105
  // Parent aborted mid-attempt — don't sleep, bail.
74
106
  if (parentSignal.aborted)
75
107
  throw parentSignal.reason;
76
108
  const delay = (0, backoff_js_1.computeDelay)(attempt, opts);
109
+ // Emit onRetry before the delay sleep.
110
+ if (obs) {
111
+ obs.hooks.onRetry?.({
112
+ type: 'retry', key: ctx.key, traceId: obs.traceId,
113
+ timestamp: Date.now(), attempt, delayMs: delay, error: err,
114
+ });
115
+ }
77
116
  if (delay > 0) {
78
- // Sleep is signal-aware: rejects immediately if parent aborts.
79
117
  await (0, abort_js_1.sleep)(delay, parentSignal);
80
118
  }
81
119
  }
82
120
  }
83
121
  // Unreachable: the loop either returns or throws on every iteration.
84
- // The cast satisfies the type checker without a `throw` after the loop.
85
122
  throw lastErr;
86
123
  };
87
124
  }
@@ -1 +1 @@
1
- {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAoB1F;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CAiDnE"}
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAqB1F;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CAuFnE"}
@@ -1,5 +1,6 @@
1
1
  import { computeDelay } from '../utils/backoff.js';
2
2
  import { isAbortError, sleep } from '../utils/abort.js';
3
+ import { RetryExhaustedError } from '../errors.js';
3
4
  // ─── Default predicate ────────────────────────────────────────────────────────
4
5
  /**
5
6
  * Default `shouldRetry`: retry on any error EXCEPT abort errors.
@@ -44,41 +45,77 @@ export function retryPolicy(opts) {
44
45
  const max = Math.max(1, Math.floor(opts.attempts));
45
46
  const shouldRetry = opts.shouldRetry ?? defaultShouldRetry;
46
47
  return (fn, ctx) => async (parentSignal) => {
48
+ const errors = [];
47
49
  let lastErr;
50
+ let retriedAtLeastOnce = false;
51
+ const obs = ctx.observability;
48
52
  for (let attempt = 1; attempt <= max; attempt++) {
49
53
  // Parent (totalTimeout or caller signal) already aborted — bail.
50
54
  if (parentSignal.aborted)
51
55
  throw parentSignal.reason;
52
56
  ctx.meta.attempts = attempt;
57
+ const attemptStart = Date.now();
53
58
  try {
59
+ // Emit onAttempt before each attempt. (We could emit
60
+ // after with duration/error, but before lets observers track
61
+ // in-flight attempts in real time.)
62
+ if (obs) {
63
+ obs.hooks.onAttempt?.({
64
+ type: 'attempt', key: ctx.key, traceId: obs.traceId,
65
+ timestamp: attemptStart, attempt,
66
+ });
67
+ }
54
68
  return await fn(parentSignal);
55
69
  }
56
70
  catch (err) {
57
71
  lastErr = err;
58
- // Always invoke shouldRetry so observers see every failure.
59
- // The return value only matters when there are attempts left.
60
- const retryable = shouldRetry(err, attempt);
72
+ errors.push(err);
73
+ // Invoke shouldRetry. If the predicate throws, treat as "don't retry"
74
+ // and surface the original fn error (not the predicate error).
75
+ let retryable;
76
+ try {
77
+ retryable = shouldRetry(err, attempt);
78
+ }
79
+ catch {
80
+ // Predicate threw — don't retry, surface original error
81
+ throw err;
82
+ }
61
83
  if (attempt >= max) {
62
- // Last attempt surface the error regardless of retryable.
84
+ // Last attempt. If we retried at least once (predicate
85
+ // allowed retries), wrap in RetryExhaustedError for context.
86
+ if (retriedAtLeastOnce) {
87
+ throw new RetryExhaustedError({
88
+ key: ctx.key,
89
+ attempts: attempt,
90
+ lastError: err,
91
+ errors,
92
+ });
93
+ }
63
94
  throw err;
64
95
  }
65
96
  if (!retryable) {
66
- // Non-retryable error — bail immediately without consuming
67
- // remaining attempts. The error surfaces exactly as-is.
97
+ // Non-retryable error — bail immediately.
68
98
  throw err;
69
99
  }
100
+ // Mark that we retried at least once — affects final wrap.
101
+ retriedAtLeastOnce = true;
70
102
  // Parent aborted mid-attempt — don't sleep, bail.
71
103
  if (parentSignal.aborted)
72
104
  throw parentSignal.reason;
73
105
  const delay = computeDelay(attempt, opts);
106
+ // Emit onRetry before the delay sleep.
107
+ if (obs) {
108
+ obs.hooks.onRetry?.({
109
+ type: 'retry', key: ctx.key, traceId: obs.traceId,
110
+ timestamp: Date.now(), attempt, delayMs: delay, error: err,
111
+ });
112
+ }
74
113
  if (delay > 0) {
75
- // Sleep is signal-aware: rejects immediately if parent aborts.
76
114
  await sleep(delay, parentSignal);
77
115
  }
78
116
  }
79
117
  }
80
118
  // Unreachable: the loop either returns or throws on every iteration.
81
- // The cast satisfies the type checker without a `throw` after the loop.
82
119
  throw lastErr;
83
120
  };
84
121
  }
@@ -1 +1 @@
1
- {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAEvD,iFAAiF;AAEjF;;;;;;;GAOG;AACH,SAAS,kBAAkB,CAAC,KAAc,EAAE,QAAgB;IAC1D,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAA;AAC7B,CAAC;AAED,iFAAiF;AAEjF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,WAAW,CAAI,IAAkB;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;IAClD,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,kBAAkB,CAAA;IAE1D,OAAO,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE,CACpD,KAAK,EAAE,YAAyB,EAAE,EAAE;QAClC,IAAI,OAAgB,CAAA;QAEpB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC;YAChD,iEAAiE;YACjE,IAAI,YAAY,CAAC,OAAO;gBAAE,MAAM,YAAY,CAAC,MAAM,CAAA;YAEnD,GAAG,CAAC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAA;YAE3B,IAAI,CAAC;gBACH,OAAO,MAAM,EAAE,CAAC,YAAY,CAAC,CAAA;YAC/B,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,GAAG,GAAG,CAAA;gBAEb,4DAA4D;gBAC5D,8DAA8D;gBAC9D,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;gBAE3C,IAAI,OAAO,IAAI,GAAG,EAAE,CAAC;oBACnB,4DAA4D;oBAC5D,MAAM,GAAG,CAAA;gBACX,CAAC;gBAED,IAAI,CAAC,SAAS,EAAE,CAAC;oBACf,2DAA2D;oBAC3D,wDAAwD;oBACxD,MAAM,GAAG,CAAA;gBACX,CAAC;gBAED,kDAAkD;gBAClD,IAAI,YAAY,CAAC,OAAO;oBAAE,MAAM,YAAY,CAAC,MAAM,CAAA;gBAEnD,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;gBACzC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;oBACd,+DAA+D;oBAC/D,MAAM,KAAK,CAAC,KAAK,EAAE,YAAY,CAAC,CAAA;gBAClC,CAAC;YACH,CAAC;QACH,CAAC;QAED,qEAAqE;QACrE,wEAAwE;QACxE,MAAM,OAAO,CAAA;IACf,CAAC,CAAA;AACL,CAAC"}
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"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"}
@@ -4,50 +4,9 @@ exports.TotalTimeoutError = exports.TimeoutError = void 0;
4
4
  exports.timeoutPolicy = timeoutPolicy;
5
5
  exports.totalTimeoutPolicy = totalTimeoutPolicy;
6
6
  const abort_js_1 = require("../utils/abort.js");
7
- // ─── Errors ───────────────────────────────────────────────────────────────────
8
- /**
9
- * Thrown when a per-attempt `timeout` deadline fires.
10
- *
11
- * Carries the configured `ms` so callers can log/alert precisely:
12
- *
13
- * ```ts
14
- * if (!result.ok && result.error instanceof TimeoutError) {
15
- * console.log(`attempt timed out after ${result.error.ms}ms`)
16
- * }
17
- * ```
18
- */
19
- class TimeoutError extends Error {
20
- ms;
21
- constructor(ms) {
22
- super(`ACT timed out after ${ms}ms`);
23
- this.name = 'TimeoutError';
24
- this.ms = ms;
25
- }
26
- }
27
- exports.TimeoutError = TimeoutError;
28
- /**
29
- * Thrown when the operation-wide `totalTimeout` budget fires.
30
- *
31
- * Distinct from `TimeoutError` (per-attempt) so callers can `instanceof`-check
32
- * which deadline fired:
33
- *
34
- * ```ts
35
- * if (result.error instanceof TotalTimeoutError) {
36
- * // whole operation budget exhausted
37
- * } else if (result.error instanceof TimeoutError) {
38
- * // last attempt's per-attempt deadline fired
39
- * }
40
- * ```
41
- */
42
- class TotalTimeoutError extends Error {
43
- ms;
44
- constructor(ms) {
45
- super(`ACT total timeout exceeded after ${ms}ms`);
46
- this.name = 'TotalTimeoutError';
47
- this.ms = ms;
48
- }
49
- }
50
- exports.TotalTimeoutError = TotalTimeoutError;
7
+ const errors_js_1 = require("../errors.js");
8
+ Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function () { return errors_js_1.TimeoutError; } });
9
+ Object.defineProperty(exports, "TotalTimeoutError", { enumerable: true, get: function () { return errors_js_1.TotalTimeoutError; } });
51
10
  // ─── Policy ───────────────────────────────────────────────────────────────────
52
11
  /**
53
12
  * Build a timeout policy that throws `ErrorCtor` on deadline.
@@ -76,16 +35,21 @@ exports.TotalTimeoutError = TotalTimeoutError;
76
35
  * reason (could be `TotalTimeoutError`, an `AbortError`, or anything else).
77
36
  */
78
37
  function makeTimeoutPolicy(opts, ErrorCtor) {
79
- return (fn, _ctx) => async (parentSignal) => {
38
+ return (fn, ctx) => async (parentSignal) => {
80
39
  const controller = new AbortController();
81
- const timerError = new ErrorCtor(opts.ms);
40
+ // Pass key to the error ctor for better debugging context.
41
+ const timerError = new ErrorCtor(opts.ms, { key: ctx.key });
82
42
  // Arm the per-attempt timer. The error object is allocated once so the
83
43
  // stack trace points here (the policy frame), not at setTimeout's
84
44
  // internal callback.
85
45
  const timer = setTimeout(() => controller.abort(timerError), opts.ms);
86
- // Link parent -> child. If parent is already aborted, child aborts
87
- // synchronously with parent's reason.
88
- (0, abort_js_1.linkSignal)(parentSignal, controller);
46
+ // NOTE: do NOT `unref()` this timer. The timeout IS the operation
47
+ // the caller is awaiting. unref'ing would let Node exit the process
48
+ // while a timeout was pending — silently dropping the operation.
49
+ // The timer is cleared in the finally block below.
50
+ // Link parent → child. Capture the unlink function so we can clean
51
+ // up the listener on success path (contract).
52
+ const unlink = (0, abort_js_1.linkSignal)(parentSignal, controller);
89
53
  try {
90
54
  // Race fn against the abort event. If fn settles first, we get its
91
55
  // result/error. If the signal aborts first, we reject with reason.
@@ -97,12 +61,20 @@ function makeTimeoutPolicy(opts, ErrorCtor) {
97
61
  reject(controller.signal.reason);
98
62
  return;
99
63
  }
100
- controller.signal.addEventListener('abort', () => reject(controller.signal.reason), { once: true });
101
- Promise.resolve(fn(controller.signal)).then((value) => resolve(value), (error) => reject(error));
64
+ const onAbort = () => reject(controller.signal.reason);
65
+ controller.signal.addEventListener('abort', onAbort, { once: true });
66
+ Promise.resolve(fn(controller.signal)).then((value) => {
67
+ controller.signal.removeEventListener('abort', onAbort);
68
+ resolve(value);
69
+ }, (error) => {
70
+ controller.signal.removeEventListener('abort', onAbort);
71
+ reject(error);
72
+ });
102
73
  });
103
74
  }
104
75
  finally {
105
76
  clearTimeout(timer);
77
+ unlink();
106
78
  }
107
79
  };
108
80
  }
@@ -113,7 +85,7 @@ function makeTimeoutPolicy(opts, ErrorCtor) {
113
85
  * own clock.
114
86
  */
115
87
  function timeoutPolicy(opts) {
116
- return makeTimeoutPolicy(opts, TimeoutError);
88
+ return makeTimeoutPolicy(opts, errors_js_1.TimeoutError);
117
89
  }
118
90
  /**
119
91
  * Operation-wide timeout. Races the ENTIRE chain (all retry attempts +
@@ -123,5 +95,5 @@ function timeoutPolicy(opts) {
123
95
  * policy runs and stops regardless of what the inner chain is doing.
124
96
  */
125
97
  function totalTimeoutPolicy(opts) {
126
- return makeTimeoutPolicy(opts, TotalTimeoutError);
98
+ return makeTimeoutPolicy(opts, errors_js_1.TotalTimeoutError);
127
99
  }