actly 1.0.2 → 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.
- package/LICENSE +1 -1
- package/README.md +612 -0
- package/dist/core/act.cjs +187 -37
- package/dist/core/act.d.ts +81 -10
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +184 -36
- package/dist/core/act.js.map +1 -1
- package/dist/core/executor.cjs +30 -4
- package/dist/core/executor.d.ts +33 -11
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +29 -4
- package/dist/core/executor.js.map +1 -1
- package/dist/index.cjs +15 -5
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/policies/cache.cjs +91 -7
- package/dist/policies/cache.d.ts +30 -3
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +91 -7
- package/dist/policies/cache.js.map +1 -1
- package/dist/policies/dedupe.cjs +79 -16
- package/dist/policies/dedupe.d.ts +39 -7
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +79 -16
- package/dist/policies/dedupe.js.map +1 -1
- package/dist/policies/retry.cjs +65 -25
- package/dist/policies/retry.d.ts +25 -2
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +65 -25
- package/dist/policies/retry.js.map +1 -1
- package/dist/policies/timeout.cjs +87 -17
- package/dist/policies/timeout.d.ts +28 -8
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +87 -17
- package/dist/policies/timeout.js.map +1 -1
- package/dist/state/store.cjs +11 -38
- package/dist/state/store.d.ts +10 -12
- package/dist/state/store.d.ts.map +1 -1
- package/dist/state/store.js +10 -37
- package/dist/state/store.js.map +1 -1
- package/dist/stores/base.cjs +20 -0
- package/dist/stores/base.d.ts +88 -0
- package/dist/stores/base.d.ts.map +1 -0
- package/dist/stores/base.js +17 -0
- package/dist/stores/base.js.map +1 -0
- package/dist/stores/memory.cjs +140 -0
- package/dist/stores/memory.d.ts +79 -0
- package/dist/stores/memory.d.ts.map +1 -0
- package/dist/stores/memory.js +137 -0
- package/dist/stores/memory.js.map +1 -0
- package/dist/types/index.d.ts +151 -34
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/abort.cjs +142 -0
- package/dist/utils/abort.d.ts +55 -0
- package/dist/utils/abort.d.ts.map +1 -0
- package/dist/utils/abort.js +136 -0
- package/dist/utils/abort.js.map +1 -0
- package/dist/utils/backoff.cjs +43 -0
- package/dist/utils/backoff.d.ts +14 -0
- package/dist/utils/backoff.d.ts.map +1 -0
- package/dist/utils/backoff.js +41 -0
- package/dist/utils/backoff.js.map +1 -0
- package/dist/utils/validate.cjs +79 -0
- package/dist/utils/validate.d.ts +15 -0
- package/dist/utils/validate.d.ts.map +1 -0
- package/dist/utils/validate.js +72 -0
- package/dist/utils/validate.js.map +1 -0
- package/package.json +19 -37
package/dist/policies/dedupe.cjs
CHANGED
|
@@ -1,26 +1,89 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.dedupePolicy = dedupePolicy;
|
|
4
|
-
|
|
4
|
+
const executor_js_1 = require("../core/executor.js");
|
|
5
|
+
const abort_js_1 = require("../utils/abort.js");
|
|
6
|
+
// Namespace so dedupe keys never collide with cache keys in the shared store.
|
|
5
7
|
const NS = 'dedupe:';
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
9
|
+
* Collapse concurrent calls that share the same key into one in-flight Promise.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
* the first resolves gets the same Promise back — no duplicate work.
|
|
11
|
+
* # How it works
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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)
|
|
28
|
+
*
|
|
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.
|
|
14
48
|
*/
|
|
15
|
-
function dedupePolicy() {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
49
|
+
function dedupePolicy(opts = { enabled: true }) {
|
|
50
|
+
const inflightTtl = opts.inflightTtl;
|
|
51
|
+
const applier = (fn, ctx) => {
|
|
52
|
+
// Cast is safe: execute() verifies isSyncStore(ctx.store) before calling
|
|
53
|
+
// any policy tagged with REQUIRES_SYNC_STORE.
|
|
54
|
+
const syncCtx = ctx;
|
|
55
|
+
return async (signal) => {
|
|
56
|
+
const key = NS + syncCtx.key;
|
|
57
|
+
// Fast path: an in-flight promise already exists. Join it.
|
|
58
|
+
// raceAbort ensures we don't block on a hung originator if our own
|
|
59
|
+
// signal aborts.
|
|
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);
|
|
84
|
+
return promise;
|
|
85
|
+
};
|
|
25
86
|
};
|
|
87
|
+
applier[executor_js_1.REQUIRES_SYNC_STORE] = true;
|
|
88
|
+
return applier;
|
|
26
89
|
}
|
|
@@ -1,12 +1,44 @@
|
|
|
1
|
-
import type { PolicyApplier } from '../types/index.js';
|
|
1
|
+
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
|
-
*
|
|
6
|
-
* the first resolves gets the same Promise back — no duplicate work.
|
|
5
|
+
* # How it works
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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).
|
|
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.
|
|
10
42
|
*/
|
|
11
|
-
export declare function dedupePolicy<T>(): PolicyApplier<T>;
|
|
43
|
+
export declare function dedupePolicy<T>(opts?: DedupeOptions): PolicyApplier<T>;
|
|
12
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;
|
|
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"}
|
package/dist/policies/dedupe.js
CHANGED
|
@@ -1,24 +1,87 @@
|
|
|
1
|
-
|
|
1
|
+
import { REQUIRES_SYNC_STORE } from '../core/executor.js';
|
|
2
|
+
import { raceAbort } from '../utils/abort.js';
|
|
3
|
+
// Namespace so dedupe keys never collide with cache keys in the shared store.
|
|
2
4
|
const NS = 'dedupe:';
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
6
|
+
* Collapse concurrent calls that share the same key into one in-flight Promise.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
* the first resolves gets the same Promise back — no duplicate work.
|
|
8
|
+
* # How it works
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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)
|
|
25
|
+
*
|
|
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.
|
|
11
45
|
*/
|
|
12
|
-
export function dedupePolicy() {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
46
|
+
export function dedupePolicy(opts = { enabled: true }) {
|
|
47
|
+
const inflightTtl = opts.inflightTtl;
|
|
48
|
+
const applier = (fn, ctx) => {
|
|
49
|
+
// Cast is safe: execute() verifies isSyncStore(ctx.store) before calling
|
|
50
|
+
// any policy tagged with REQUIRES_SYNC_STORE.
|
|
51
|
+
const syncCtx = ctx;
|
|
52
|
+
return async (signal) => {
|
|
53
|
+
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.
|
|
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);
|
|
81
|
+
return promise;
|
|
82
|
+
};
|
|
22
83
|
};
|
|
84
|
+
applier[REQUIRES_SYNC_STORE] = true;
|
|
85
|
+
return applier;
|
|
23
86
|
}
|
|
24
87
|
//# sourceMappingURL=dedupe.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"AAEA,
|
|
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"}
|
package/dist/policies/retry.cjs
CHANGED
|
@@ -1,47 +1,87 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.retryPolicy = retryPolicy;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
// ─── Policy ──────────────────────────────────────────────────────────────────
|
|
18
|
+
// ─── Policy ───────────────────────────────────────────────────────────────────
|
|
17
19
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
//
|
|
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
|
}
|
package/dist/policies/retry.d.ts
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
import type { PolicyApplier, RetryOptions } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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;
|
|
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"}
|
package/dist/policies/retry.js
CHANGED
|
@@ -1,44 +1,84 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
// ─── Policy ──────────────────────────────────────────────────────────────────
|
|
15
|
+
// ─── Policy ───────────────────────────────────────────────────────────────────
|
|
14
16
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
//
|
|
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":"
|
|
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"}
|