actly 1.1.0 → 1.1.5

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 (70) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +284 -52
  3. package/dist/core/act.cjs +183 -35
  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 +181 -35
  7. package/dist/core/act.js.map +1 -1
  8. package/dist/core/executor.cjs +19 -9
  9. package/dist/core/executor.d.ts +25 -5
  10. package/dist/core/executor.d.ts.map +1 -1
  11. package/dist/core/executor.js +19 -9
  12. package/dist/core/executor.js.map +1 -1
  13. package/dist/index.cjs +13 -3
  14. package/dist/index.d.ts +7 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +8 -4
  17. package/dist/index.js.map +1 -1
  18. package/dist/policies/cache.cjs +85 -21
  19. package/dist/policies/cache.d.ts +29 -7
  20. package/dist/policies/cache.d.ts.map +1 -1
  21. package/dist/policies/cache.js +85 -21
  22. package/dist/policies/cache.js.map +1 -1
  23. package/dist/policies/dedupe.cjs +68 -20
  24. package/dist/policies/dedupe.d.ts +38 -13
  25. package/dist/policies/dedupe.d.ts.map +1 -1
  26. package/dist/policies/dedupe.js +68 -20
  27. package/dist/policies/dedupe.js.map +1 -1
  28. package/dist/policies/retry.cjs +65 -25
  29. package/dist/policies/retry.d.ts +25 -2
  30. package/dist/policies/retry.d.ts.map +1 -1
  31. package/dist/policies/retry.js +65 -25
  32. package/dist/policies/retry.js.map +1 -1
  33. package/dist/policies/timeout.cjs +87 -17
  34. package/dist/policies/timeout.d.ts +28 -8
  35. package/dist/policies/timeout.d.ts.map +1 -1
  36. package/dist/policies/timeout.js +87 -17
  37. package/dist/policies/timeout.js.map +1 -1
  38. package/dist/state/store.cjs +9 -2
  39. package/dist/state/store.d.ts +9 -0
  40. package/dist/state/store.d.ts.map +1 -1
  41. package/dist/state/store.js +9 -2
  42. package/dist/state/store.js.map +1 -1
  43. package/dist/stores/base.cjs +4 -4
  44. package/dist/stores/base.d.ts +32 -56
  45. package/dist/stores/base.d.ts.map +1 -1
  46. package/dist/stores/base.js +4 -4
  47. package/dist/stores/base.js.map +1 -1
  48. package/dist/stores/memory.cjs +77 -28
  49. package/dist/stores/memory.d.ts +44 -23
  50. package/dist/stores/memory.d.ts.map +1 -1
  51. package/dist/stores/memory.js +77 -28
  52. package/dist/stores/memory.js.map +1 -1
  53. package/dist/types/index.d.ts +141 -36
  54. package/dist/types/index.d.ts.map +1 -1
  55. package/dist/utils/abort.cjs +142 -0
  56. package/dist/utils/abort.d.ts +55 -0
  57. package/dist/utils/abort.d.ts.map +1 -0
  58. package/dist/utils/abort.js +136 -0
  59. package/dist/utils/abort.js.map +1 -0
  60. package/dist/utils/backoff.cjs +43 -0
  61. package/dist/utils/backoff.d.ts +14 -0
  62. package/dist/utils/backoff.d.ts.map +1 -0
  63. package/dist/utils/backoff.js +41 -0
  64. package/dist/utils/backoff.js.map +1 -0
  65. package/dist/utils/validate.cjs +79 -0
  66. package/dist/utils/validate.d.ts +15 -0
  67. package/dist/utils/validate.d.ts.map +1 -0
  68. package/dist/utils/validate.js +72 -0
  69. package/dist/utils/validate.js.map +1 -0
  70. package/package.json +19 -37
@@ -2,37 +2,85 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.dedupePolicy = dedupePolicy;
4
4
  const executor_js_1 = require("../core/executor.js");
5
- // Namespace so dedupe keys never collide with cache keys in the shared store
5
+ const abort_js_1 = require("../utils/abort.js");
6
+ // Namespace so dedupe keys never collide with cache keys in the shared store.
6
7
  const NS = 'dedupe:';
7
8
  /**
8
- * Collapses concurrent calls that share the same key into one in-flight Promise.
9
+ * Collapse concurrent calls that share the same key into one in-flight Promise.
9
10
  *
10
- * The first caller starts the work. Every subsequent caller that arrives before
11
- * the first resolves gets the same Promise back — no duplicate work.
11
+ * # How it works
12
12
  *
13
- * INVARIANT: requires a SyncStateStore see stores/base.ts.
14
- * The read-then-write that makes deduplication work must happen in a single
15
- * synchronous frame. An async store would introduce an await between get() and
16
- * set(), letting two concurrent callers both see a miss and both launch work.
17
- * The REQUIRES_SYNC_STORE symbol on the returned PolicyApplier lets execute()
18
- * enforce this at runtime for JS callers that bypass TypeScript.
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)
19
28
  *
20
- * Known tradeoff (v1): deduped callers see attempts=1 in their ActResult because
21
- * the retry counter belongs to the originating call's meta object.
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).
39
+ *
40
+ * # INVARIANT: requires SyncStateStore
41
+ *
42
+ * The read-then-write that makes deduplication work must happen in a single
43
+ * synchronous frame. An async store would introduce an `await` between
44
+ * `get()` and `set()`, letting two concurrent callers both see a miss and
45
+ * both launch work. The `REQUIRES_SYNC_STORE` symbol on the returned
46
+ * `PolicyApplier` lets `execute()` enforce this at runtime for JS callers
47
+ * that bypass TypeScript.
22
48
  */
23
- function dedupePolicy() {
49
+ function dedupePolicy(opts = { enabled: true }) {
50
+ const inflightTtl = opts.inflightTtl;
24
51
  const applier = (fn, ctx) => {
25
52
  // Cast is safe: execute() verifies isSyncStore(ctx.store) before calling
26
53
  // any policy tagged with REQUIRES_SYNC_STORE.
27
54
  const syncCtx = ctx;
28
- return async () => {
55
+ return async (signal) => {
29
56
  const key = NS + syncCtx.key;
30
- const inflight = syncCtx.store.get(key);
31
- if (inflight)
32
- return inflight;
33
- // No TTL .finally() cleans up regardless of outcome
34
- const promise = fn().finally(() => syncCtx.store.delete(key));
35
- syncCtx.store.set(key, promise);
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.
60
+ const existing = syncCtx.store.get(key);
61
+ if (existing) {
62
+ try {
63
+ 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.
71
+ syncCtx.meta.attempts = existing.meta.attempts;
72
+ syncCtx.meta.source = existing.meta.source;
73
+ }
74
+ }
75
+ // Originator path: start the work and publish the promise.
76
+ //
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 };
83
+ syncCtx.store.set(key, entry, inflightTtl);
36
84
  return promise;
37
85
  };
38
86
  };
@@ -1,19 +1,44 @@
1
- import type { PolicyApplier } from '../types/index.js';
1
+ import type { PolicyApplier, DedupeOptions } from '../types/index.js';
2
2
  /**
3
- * Collapses concurrent calls that share the same key into one in-flight Promise.
3
+ * Collapse concurrent calls that share the same key into one in-flight Promise.
4
4
  *
5
- * The first caller starts the work. Every subsequent caller that arrives before
6
- * the first resolves gets the same Promise back — no duplicate work.
5
+ * # How it works
7
6
  *
8
- * INVARIANT: requires a SyncStateStore see stores/base.ts.
9
- * The read-then-write that makes deduplication work must happen in a single
10
- * synchronous frame. An async store would introduce an await between get() and
11
- * set(), letting two concurrent callers both see a miss and both launch work.
12
- * The REQUIRES_SYNC_STORE symbol on the returned PolicyApplier lets execute()
13
- * enforce this at runtime for JS callers that bypass TypeScript.
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)
14
22
  *
15
- * Known tradeoff (v1): deduped callers see attempts=1 in their ActResult because
16
- * the retry counter belongs to the originating call's meta object.
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).
33
+ *
34
+ * # INVARIANT: requires SyncStateStore
35
+ *
36
+ * The read-then-write that makes deduplication work must happen in a single
37
+ * synchronous frame. An async store would introduce an `await` between
38
+ * `get()` and `set()`, letting two concurrent callers both see a miss and
39
+ * both launch work. The `REQUIRES_SYNC_STORE` symbol on the returned
40
+ * `PolicyApplier` lets `execute()` enforce this at runtime for JS callers
41
+ * that bypass TypeScript.
17
42
  */
18
- export declare function dedupePolicy<T>(): PolicyApplier<T>;
43
+ export declare function dedupePolicy<T>(opts?: DedupeOptions): PolicyApplier<T>;
19
44
  //# sourceMappingURL=dedupe.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,MAAM,mBAAmB,CAAA;AAY5E;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,CAuBlD"}
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,35 +1,83 @@
1
1
  import { REQUIRES_SYNC_STORE } from '../core/executor.js';
2
- // Namespace so dedupe keys never collide with cache keys in the shared store
2
+ import { raceAbort } from '../utils/abort.js';
3
+ // Namespace so dedupe keys never collide with cache keys in the shared store.
3
4
  const NS = 'dedupe:';
4
5
  /**
5
- * Collapses concurrent calls that share the same key into one in-flight Promise.
6
+ * Collapse concurrent calls that share the same key into one in-flight Promise.
6
7
  *
7
- * The first caller starts the work. Every subsequent caller that arrives before
8
- * the first resolves gets the same Promise back — no duplicate work.
8
+ * # How it works
9
9
  *
10
- * INVARIANT: requires a SyncStateStore see stores/base.ts.
11
- * The read-then-write that makes deduplication work must happen in a single
12
- * synchronous frame. An async store would introduce an await between get() and
13
- * set(), letting two concurrent callers both see a miss and both launch work.
14
- * The REQUIRES_SYNC_STORE symbol on the returned PolicyApplier lets execute()
15
- * enforce this at runtime for JS callers that bypass TypeScript.
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)
16
25
  *
17
- * Known tradeoff (v1): deduped callers see attempts=1 in their ActResult because
18
- * the retry counter belongs to the originating call's meta object.
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).
36
+ *
37
+ * # INVARIANT: requires SyncStateStore
38
+ *
39
+ * The read-then-write that makes deduplication work must happen in a single
40
+ * synchronous frame. An async store would introduce an `await` between
41
+ * `get()` and `set()`, letting two concurrent callers both see a miss and
42
+ * both launch work. The `REQUIRES_SYNC_STORE` symbol on the returned
43
+ * `PolicyApplier` lets `execute()` enforce this at runtime for JS callers
44
+ * that bypass TypeScript.
19
45
  */
20
- export function dedupePolicy() {
46
+ export function dedupePolicy(opts = { enabled: true }) {
47
+ const inflightTtl = opts.inflightTtl;
21
48
  const applier = (fn, ctx) => {
22
49
  // Cast is safe: execute() verifies isSyncStore(ctx.store) before calling
23
50
  // any policy tagged with REQUIRES_SYNC_STORE.
24
51
  const syncCtx = ctx;
25
- return async () => {
52
+ return async (signal) => {
26
53
  const key = NS + syncCtx.key;
27
- const inflight = syncCtx.store.get(key);
28
- if (inflight)
29
- return inflight;
30
- // No TTL .finally() cleans up regardless of outcome
31
- const promise = fn().finally(() => syncCtx.store.delete(key));
32
- syncCtx.store.set(key, promise);
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.
57
+ const existing = syncCtx.store.get(key);
58
+ if (existing) {
59
+ try {
60
+ 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.
68
+ syncCtx.meta.attempts = existing.meta.attempts;
69
+ syncCtx.meta.source = existing.meta.source;
70
+ }
71
+ }
72
+ // Originator path: start the work and publish the promise.
73
+ //
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 };
80
+ syncCtx.store.set(key, entry, inflightTtl);
33
81
  return promise;
34
82
  };
35
83
  };
@@ -1 +1 @@
1
- {"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAEzD,6EAA6E;AAC7E,MAAM,EAAE,GAAG,SAAS,CAAA;AAOpB;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,OAAO,GAAG,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE;QAC7D,yEAAyE;QACzE,8CAA8C;QAC9C,MAAM,OAAO,GAAG,GAAoB,CAAA;QAEpC,OAAO,KAAK,IAAI,EAAE;YAChB,MAAM,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,CAAA;YAE5B,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAa,GAAG,CAAC,CAAA;YACnD,IAAI,QAAQ;gBAAE,OAAO,QAAQ,CAAA;YAE7B,sDAAsD;YACtD,MAAM,OAAO,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;YAC7D,OAAO,CAAC,KAAK,CAAC,GAAG,CAAa,GAAG,EAAE,OAAO,CAAC,CAAA;YAC3C,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":"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,47 +1,87 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.retryPolicy = retryPolicy;
4
- // ─── Helpers ─────────────────────────────────────────────────────────────────
5
- function computeDelay(attempt, opts) {
6
- const base = opts.delayMs ?? 0;
7
- if (base === 0)
8
- return 0;
9
- switch (opts.backoff ?? 'none') {
10
- case 'linear': return base * attempt;
11
- case 'exponential': return base * 2 ** (attempt - 1);
12
- default: return base;
13
- }
4
+ const backoff_js_1 = require("../utils/backoff.js");
5
+ const abort_js_1 = require("../utils/abort.js");
6
+ // ─── Default predicate ────────────────────────────────────────────────────────
7
+ /**
8
+ * Default `shouldRetry`: retry on any error EXCEPT abort errors.
9
+ *
10
+ * Abort errors indicate the caller or a timeout cancelled the operation —
11
+ * retrying would just abort again on the next attempt, wasting delay budget.
12
+ *
13
+ * User-supplied `shouldRetry` overrides this entirely.
14
+ */
15
+ function defaultShouldRetry(error, _attempt) {
16
+ return !(0, abort_js_1.isAbortError)(error);
14
17
  }
15
- const sleep = (ms) => new Promise(r => setTimeout(r, ms));
16
- // ─── Policy ──────────────────────────────────────────────────────────────────
18
+ // ─── Policy ───────────────────────────────────────────────────────────────────
17
19
  /**
18
- * Retries fn up to opts.attempts times on any thrown error.
19
- * Writes the live attempt count into ctx.meta.attempts.
20
+ * Retry `fn` up to `opts.attempts` times on retryable errors.
21
+ *
22
+ * Writes the live attempt count into `ctx.meta.attempts` so `act()` can
23
+ * report it in the final `ActResult`.
24
+ *
25
+ * # Signal awareness
26
+ *
27
+ * - Before each attempt, checks `parentSignal.aborted`. If the parent (e.g.
28
+ * `totalTimeout` or caller) has aborted, throws the parent's reason
29
+ * immediately — no more attempts.
30
+ * - Sleeps between attempts use `sleep(delay, parentSignal)`. If the parent
31
+ * aborts mid-delay, the sleep rejects immediately instead of blocking
32
+ * the loop until the timer would have elapsed.
33
+ *
34
+ * # `shouldRetry` invocation
35
+ *
36
+ * Called after EVERY failure, including the last attempt. This preserves the
37
+ * predicate's contract for observers / metrics that rely on it being called
38
+ * per-attempt. The return value is only consulted when `attempt < max`.
39
+ *
40
+ * # Default predicate
41
+ *
42
+ * If `shouldRetry` is omitted, uses `defaultShouldRetry` which retries on
43
+ * every error EXCEPT abort errors (so timeouts and caller cancellations
44
+ * don't waste retry budget).
20
45
  */
21
46
  function retryPolicy(opts) {
22
- const max = Math.max(1, opts.attempts);
23
- return (fn, ctx) => async () => {
47
+ const max = Math.max(1, Math.floor(opts.attempts));
48
+ const shouldRetry = opts.shouldRetry ?? defaultShouldRetry;
49
+ return (fn, ctx) => async (parentSignal) => {
24
50
  let lastErr;
25
51
  for (let attempt = 1; attempt <= max; attempt++) {
52
+ // Parent (totalTimeout or caller signal) already aborted — bail.
53
+ if (parentSignal.aborted)
54
+ throw parentSignal.reason;
26
55
  ctx.meta.attempts = attempt;
27
56
  try {
28
- return await fn();
57
+ return await fn(parentSignal);
29
58
  }
30
59
  catch (err) {
31
60
  lastErr = err;
32
- if (attempt < max) {
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);
64
+ if (attempt >= max) {
65
+ // Last attempt — surface the error regardless of retryable.
66
+ throw err;
67
+ }
68
+ if (!retryable) {
33
69
  // Non-retryable error — bail immediately without consuming
34
70
  // remaining attempts. The error surfaces exactly as-is.
35
- if (opts.shouldRetry && !opts.shouldRetry(err, attempt)) {
36
- throw err;
37
- }
38
- const delay = computeDelay(attempt, opts);
39
- if (delay > 0)
40
- await sleep(delay);
71
+ throw err;
72
+ }
73
+ // Parent aborted mid-attempt — don't sleep, bail.
74
+ if (parentSignal.aborted)
75
+ throw parentSignal.reason;
76
+ const delay = (0, backoff_js_1.computeDelay)(attempt, opts);
77
+ if (delay > 0) {
78
+ // Sleep is signal-aware: rejects immediately if parent aborts.
79
+ await (0, abort_js_1.sleep)(delay, parentSignal);
41
80
  }
42
81
  }
43
82
  }
44
- // Every attempt failed surface the last error upstream
83
+ // Unreachable: the loop either returns or throws on every iteration.
84
+ // The cast satisfies the type checker without a `throw` after the loop.
45
85
  throw lastErr;
46
86
  };
47
87
  }
@@ -1,7 +1,30 @@
1
1
  import type { PolicyApplier, RetryOptions } from '../types/index.js';
2
2
  /**
3
- * Retries fn up to opts.attempts times on any thrown error.
4
- * Writes the live attempt count into ctx.meta.attempts.
3
+ * Retry `fn` up to `opts.attempts` times on retryable errors.
4
+ *
5
+ * Writes the live attempt count into `ctx.meta.attempts` so `act()` can
6
+ * report it in the final `ActResult`.
7
+ *
8
+ * # Signal awareness
9
+ *
10
+ * - Before each attempt, checks `parentSignal.aborted`. If the parent (e.g.
11
+ * `totalTimeout` or caller) has aborted, throws the parent's reason
12
+ * immediately — no more attempts.
13
+ * - Sleeps between attempts use `sleep(delay, parentSignal)`. If the parent
14
+ * aborts mid-delay, the sleep rejects immediately instead of blocking
15
+ * the loop until the timer would have elapsed.
16
+ *
17
+ * # `shouldRetry` invocation
18
+ *
19
+ * Called after EVERY failure, including the last attempt. This preserves the
20
+ * predicate's contract for observers / metrics that rely on it being called
21
+ * per-attempt. The return value is only consulted when `attempt < max`.
22
+ *
23
+ * # Default predicate
24
+ *
25
+ * If `shouldRetry` is omitted, uses `defaultShouldRetry` which retries on
26
+ * every error EXCEPT abort errors (so timeouts and caller cancellations
27
+ * don't waste retry budget).
5
28
  */
6
29
  export declare function retryPolicy<T>(opts: RetryOptions): PolicyApplier<T>;
7
30
  //# sourceMappingURL=retry.d.ts.map
@@ -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;AAmB1F;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CA8BnE"}
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,44 +1,84 @@
1
- // ─── Helpers ─────────────────────────────────────────────────────────────────
2
- function computeDelay(attempt, opts) {
3
- const base = opts.delayMs ?? 0;
4
- if (base === 0)
5
- return 0;
6
- switch (opts.backoff ?? 'none') {
7
- case 'linear': return base * attempt;
8
- case 'exponential': return base * 2 ** (attempt - 1);
9
- default: return base;
10
- }
1
+ import { computeDelay } from '../utils/backoff.js';
2
+ import { isAbortError, sleep } from '../utils/abort.js';
3
+ // ─── Default predicate ────────────────────────────────────────────────────────
4
+ /**
5
+ * Default `shouldRetry`: retry on any error EXCEPT abort errors.
6
+ *
7
+ * Abort errors indicate the caller or a timeout cancelled the operation —
8
+ * retrying would just abort again on the next attempt, wasting delay budget.
9
+ *
10
+ * User-supplied `shouldRetry` overrides this entirely.
11
+ */
12
+ function defaultShouldRetry(error, _attempt) {
13
+ return !isAbortError(error);
11
14
  }
12
- const sleep = (ms) => new Promise(r => setTimeout(r, ms));
13
- // ─── Policy ──────────────────────────────────────────────────────────────────
15
+ // ─── Policy ───────────────────────────────────────────────────────────────────
14
16
  /**
15
- * Retries fn up to opts.attempts times on any thrown error.
16
- * Writes the live attempt count into ctx.meta.attempts.
17
+ * Retry `fn` up to `opts.attempts` times on retryable errors.
18
+ *
19
+ * Writes the live attempt count into `ctx.meta.attempts` so `act()` can
20
+ * report it in the final `ActResult`.
21
+ *
22
+ * # Signal awareness
23
+ *
24
+ * - Before each attempt, checks `parentSignal.aborted`. If the parent (e.g.
25
+ * `totalTimeout` or caller) has aborted, throws the parent's reason
26
+ * immediately — no more attempts.
27
+ * - Sleeps between attempts use `sleep(delay, parentSignal)`. If the parent
28
+ * aborts mid-delay, the sleep rejects immediately instead of blocking
29
+ * the loop until the timer would have elapsed.
30
+ *
31
+ * # `shouldRetry` invocation
32
+ *
33
+ * Called after EVERY failure, including the last attempt. This preserves the
34
+ * predicate's contract for observers / metrics that rely on it being called
35
+ * per-attempt. The return value is only consulted when `attempt < max`.
36
+ *
37
+ * # Default predicate
38
+ *
39
+ * If `shouldRetry` is omitted, uses `defaultShouldRetry` which retries on
40
+ * every error EXCEPT abort errors (so timeouts and caller cancellations
41
+ * don't waste retry budget).
17
42
  */
18
43
  export function retryPolicy(opts) {
19
- const max = Math.max(1, opts.attempts);
20
- return (fn, ctx) => async () => {
44
+ const max = Math.max(1, Math.floor(opts.attempts));
45
+ const shouldRetry = opts.shouldRetry ?? defaultShouldRetry;
46
+ return (fn, ctx) => async (parentSignal) => {
21
47
  let lastErr;
22
48
  for (let attempt = 1; attempt <= max; attempt++) {
49
+ // Parent (totalTimeout or caller signal) already aborted — bail.
50
+ if (parentSignal.aborted)
51
+ throw parentSignal.reason;
23
52
  ctx.meta.attempts = attempt;
24
53
  try {
25
- return await fn();
54
+ return await fn(parentSignal);
26
55
  }
27
56
  catch (err) {
28
57
  lastErr = err;
29
- if (attempt < max) {
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);
61
+ if (attempt >= max) {
62
+ // Last attempt — surface the error regardless of retryable.
63
+ throw err;
64
+ }
65
+ if (!retryable) {
30
66
  // Non-retryable error — bail immediately without consuming
31
67
  // remaining attempts. The error surfaces exactly as-is.
32
- if (opts.shouldRetry && !opts.shouldRetry(err, attempt)) {
33
- throw err;
34
- }
35
- const delay = computeDelay(attempt, opts);
36
- if (delay > 0)
37
- await sleep(delay);
68
+ throw err;
69
+ }
70
+ // Parent aborted mid-attempt — don't sleep, bail.
71
+ if (parentSignal.aborted)
72
+ throw parentSignal.reason;
73
+ const delay = computeDelay(attempt, opts);
74
+ if (delay > 0) {
75
+ // Sleep is signal-aware: rejects immediately if parent aborts.
76
+ await sleep(delay, parentSignal);
38
77
  }
39
78
  }
40
79
  }
41
- // Every attempt failed surface the last error upstream
80
+ // Unreachable: the loop either returns or throws on every iteration.
81
+ // The cast satisfies the type checker without a `throw` after the loop.
42
82
  throw lastErr;
43
83
  };
44
84
  }
@@ -1 +1 @@
1
- {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/policies/retry.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAEhF,SAAS,YAAY,CAAC,OAAe,EAAE,IAAkB;IACvD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,CAAA;IAC9B,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IAExB,QAAQ,IAAI,CAAC,OAAO,IAAI,MAAM,EAAE,CAAC;QAC/B,KAAK,QAAQ,CAAC,CAAM,OAAO,IAAI,GAAG,OAAO,CAAA;QACzC,KAAK,aAAa,CAAC,CAAC,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAA;QACpD,OAAO,CAAC,CAAY,OAAO,IAAI,CAAA;IACjC,CAAC;AACH,CAAC;AAED,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;AAEvE,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAI,IAAkB;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;IAEtC,OAAO,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE,CACpD,KAAK,IAAI,EAAE;QACT,IAAI,OAAgB,CAAA;QAEpB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC;YAChD,GAAG,CAAC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAA;YAC3B,IAAI,CAAC;gBACH,OAAO,MAAM,EAAE,EAAE,CAAA;YACnB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,GAAG,GAAG,CAAA;gBAEb,IAAI,OAAO,GAAG,GAAG,EAAE,CAAC;oBAClB,2DAA2D;oBAC3D,wDAAwD;oBACxD,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,CAAC;wBACxD,MAAM,GAAG,CAAA;oBACX,CAAC;oBAED,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;oBACzC,IAAI,KAAK,GAAG,CAAC;wBAAE,MAAM,KAAK,CAAC,KAAK,CAAC,CAAA;gBACnC,CAAC;YACH,CAAC;QACH,CAAC;QAED,yDAAyD;QACzD,MAAM,OAAO,CAAA;IACf,CAAC,CAAA;AACL,CAAC"}
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"}