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.
- package/README.md +232 -434
- package/dist/core/act.cjs +353 -35
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +355 -37
- package/dist/core/act.js.map +1 -1
- package/dist/core/executor.cjs +2 -0
- package/dist/core/executor.d.ts +6 -1
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +2 -0
- package/dist/core/executor.js.map +1 -1
- package/dist/core/health.cjs +34 -0
- package/dist/core/health.d.ts +18 -0
- package/dist/core/health.d.ts.map +1 -0
- package/dist/core/health.js +28 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/shutdown.cjs +60 -0
- package/dist/core/shutdown.d.ts +8 -0
- package/dist/core/shutdown.d.ts.map +1 -0
- package/dist/core/shutdown.js +56 -0
- package/dist/core/shutdown.js.map +1 -0
- package/dist/core/tenant.cjs +71 -0
- package/dist/core/tenant.d.ts +29 -0
- package/dist/core/tenant.d.ts.map +1 -0
- package/dist/core/tenant.js +68 -0
- package/dist/core/tenant.js.map +1 -0
- package/dist/errors.cjs +182 -0
- package/dist/errors.d.ts +151 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +171 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.cjs +44 -4
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/observability.cjs +45 -0
- package/dist/observability.d.ts +145 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +43 -0
- package/dist/observability.js.map +1 -0
- package/dist/policies/bulkhead.cjs +97 -0
- package/dist/policies/bulkhead.d.ts +3 -0
- package/dist/policies/bulkhead.d.ts.map +1 -0
- package/dist/policies/bulkhead.js +95 -0
- package/dist/policies/bulkhead.js.map +1 -0
- package/dist/policies/cache.cjs +168 -31
- package/dist/policies/cache.d.ts +17 -4
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +168 -31
- package/dist/policies/cache.js.map +1 -1
- package/dist/policies/circuitBreaker.cjs +85 -0
- package/dist/policies/circuitBreaker.d.ts +3 -0
- package/dist/policies/circuitBreaker.d.ts.map +1 -0
- package/dist/policies/circuitBreaker.js +83 -0
- package/dist/policies/circuitBreaker.js.map +1 -0
- package/dist/policies/dedupe.cjs +74 -45
- package/dist/policies/dedupe.d.ts +10 -28
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +74 -45
- package/dist/policies/dedupe.js.map +1 -1
- package/dist/policies/rateLimit.cjs +35 -0
- package/dist/policies/rateLimit.d.ts +3 -0
- package/dist/policies/rateLimit.d.ts.map +1 -0
- package/dist/policies/rateLimit.js +33 -0
- package/dist/policies/rateLimit.js.map +1 -0
- package/dist/policies/retry.cjs +45 -8
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +45 -8
- package/dist/policies/retry.js.map +1 -1
- package/dist/policies/timeout.cjs +25 -53
- package/dist/policies/timeout.d.ts +2 -33
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +31 -48
- package/dist/policies/timeout.js.map +1 -1
- package/dist/state/store.cjs +3 -3
- package/dist/state/store.d.ts +3 -3
- package/dist/state/store.js +3 -3
- package/dist/stores/memory.cjs +126 -53
- package/dist/stores/memory.d.ts +35 -16
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +125 -53
- package/dist/stores/memory.js.map +1 -1
- package/dist/types/index.d.ts +176 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/abort.cjs +108 -38
- package/dist/utils/abort.d.ts +43 -19
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +108 -38
- package/dist/utils/abort.js.map +1 -1
- package/dist/utils/abortPool.cjs +26 -0
- package/dist/utils/abortPool.d.ts +4 -0
- package/dist/utils/abortPool.d.ts.map +1 -0
- package/dist/utils/abortPool.js +22 -0
- package/dist/utils/abortPool.js.map +1 -0
- package/dist/utils/backoff.cjs +12 -1
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +12 -1
- package/dist/utils/backoff.js.map +1 -1
- package/dist/utils/key.cjs +77 -0
- package/dist/utils/key.d.ts +29 -0
- package/dist/utils/key.d.ts.map +1 -0
- package/dist/utils/key.js +75 -0
- package/dist/utils/key.js.map +1 -0
- package/dist/utils/limits.cjs +40 -0
- package/dist/utils/limits.d.ts +39 -0
- package/dist/utils/limits.d.ts.map +1 -0
- package/dist/utils/limits.js +38 -0
- package/dist/utils/limits.js.map +1 -0
- package/dist/utils/sanitize.cjs +29 -0
- package/dist/utils/sanitize.d.ts +3 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +26 -0
- package/dist/utils/sanitize.js.map +1 -0
- package/dist/utils/validate.cjs +90 -16
- package/dist/utils/validate.d.ts +11 -0
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +85 -16
- package/dist/utils/validate.js.map +1 -1
- 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
|
-
* #
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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;
|
|
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"}
|
package/dist/policies/dedupe.js
CHANGED
|
@@ -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
|
-
* #
|
|
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
|
-
*
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
//
|
|
56
|
-
// signal
|
|
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
|
-
|
|
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
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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;
|
|
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 @@
|
|
|
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"}
|
package/dist/policies/retry.cjs
CHANGED
|
@@ -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
|
-
|
|
62
|
-
//
|
|
63
|
-
|
|
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
|
|
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
|
|
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;
|
|
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"}
|
package/dist/policies/retry.js
CHANGED
|
@@ -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
|
-
|
|
59
|
-
//
|
|
60
|
-
|
|
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
|
|
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
|
|
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;
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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,
|
|
38
|
+
return (fn, ctx) => async (parentSignal) => {
|
|
80
39
|
const controller = new AbortController();
|
|
81
|
-
|
|
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
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
}
|