actly 1.1.5 → 1.3.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/CHANGELOG.md +234 -0
- package/MIGRATION.md +141 -0
- package/README.md +334 -428
- package/SECURITY.md +56 -0
- package/dist/core/act.cjs +630 -126
- package/dist/core/act.d.ts +6 -84
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +630 -129
- package/dist/core/executor.cjs +1 -26
- package/dist/core/executor.d.ts +2 -38
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +1 -27
- package/dist/core/health.cjs +163 -0
- package/dist/core/health.d.ts +33 -0
- package/dist/core/health.d.ts.map +1 -0
- package/dist/core/health.js +150 -0
- package/dist/core/shutdown.cjs +71 -0
- package/dist/core/shutdown.d.ts +5 -0
- package/dist/core/shutdown.d.ts.map +1 -0
- package/dist/core/shutdown.js +65 -0
- package/dist/core/tenant.cjs +113 -0
- package/dist/core/tenant.d.ts +24 -0
- package/dist/core/tenant.d.ts.map +1 -0
- package/dist/core/tenant.js +109 -0
- package/dist/errors.cjs +149 -0
- package/dist/errors.d.ts +94 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +134 -0
- package/dist/index.cjs +51 -8
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -6
- package/dist/observability.cjs +6 -0
- package/dist/observability.d.ts +86 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +3 -0
- package/dist/policies/bulkhead.cjs +137 -0
- package/dist/policies/bulkhead.d.ts +3 -0
- package/dist/policies/bulkhead.d.ts.map +1 -0
- package/dist/policies/bulkhead.js +134 -0
- package/dist/policies/cache.cjs +84 -67
- package/dist/policies/cache.d.ts +0 -33
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +84 -68
- package/dist/policies/circuitBreaker.cjs +179 -0
- package/dist/policies/circuitBreaker.d.ts +3 -0
- package/dist/policies/circuitBreaker.d.ts.map +1 -0
- package/dist/policies/circuitBreaker.js +176 -0
- package/dist/policies/dedupe.cjs +46 -64
- package/dist/policies/dedupe.d.ts +0 -41
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +46 -65
- package/dist/policies/noop.cjs +6 -0
- package/dist/policies/noop.d.ts +3 -0
- package/dist/policies/noop.d.ts.map +1 -0
- package/dist/policies/noop.js +3 -0
- package/dist/policies/rateLimit.cjs +45 -0
- package/dist/policies/rateLimit.d.ts +3 -0
- package/dist/policies/rateLimit.d.ts.map +1 -0
- package/dist/policies/rateLimit.js +42 -0
- package/dist/policies/retry.cjs +129 -57
- package/dist/policies/retry.d.ts +0 -27
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +129 -58
- package/dist/policies/timeout.cjs +47 -103
- package/dist/policies/timeout.d.ts +2 -46
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +46 -102
- package/dist/stores/base.cjs +0 -10
- package/dist/stores/base.d.ts +4 -67
- package/dist/stores/base.d.ts.map +1 -1
- package/dist/stores/base.js +0 -11
- package/dist/stores/memory.cjs +147 -87
- package/dist/stores/memory.d.ts +11 -59
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +146 -88
- package/dist/testing/index.cjs +56 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +52 -0
- package/dist/types/index.cjs +0 -1
- package/dist/types/index.d.ts +62 -179
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -2
- package/dist/utils/abort.cjs +46 -73
- package/dist/utils/abort.d.ts +4 -51
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +46 -74
- package/dist/utils/abortPool.cjs +25 -0
- package/dist/utils/abortPool.d.ts +4 -0
- package/dist/utils/abortPool.d.ts.map +1 -0
- package/dist/utils/abortPool.js +20 -0
- package/dist/utils/backoff.cjs +8 -15
- package/dist/utils/backoff.d.ts +0 -11
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +8 -16
- package/dist/utils/decorator.cjs +44 -0
- package/dist/utils/decorator.d.ts +3 -0
- package/dist/utils/decorator.d.ts.map +1 -0
- package/dist/utils/decorator.js +41 -0
- package/dist/utils/key.cjs +45 -0
- package/dist/utils/key.d.ts +2 -0
- package/dist/utils/key.d.ts.map +1 -0
- package/dist/utils/key.js +42 -0
- package/dist/utils/limits.cjs +22 -0
- package/dist/utils/limits.d.ts +21 -0
- package/dist/utils/limits.d.ts.map +1 -0
- package/dist/utils/limits.js +19 -0
- package/dist/utils/safeCall.cjs +28 -0
- package/dist/utils/safeCall.d.ts +2 -0
- package/dist/utils/safeCall.d.ts.map +1 -0
- package/dist/utils/safeCall.js +25 -0
- package/dist/utils/sanitize.cjs +50 -0
- package/dist/utils/sanitize.d.ts +3 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +46 -0
- package/dist/utils/validate.cjs +163 -26
- package/dist/utils/validate.d.ts +6 -8
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +158 -27
- package/package.json +42 -7
- package/dist/core/act.js.map +0 -1
- package/dist/core/executor.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/policies/cache.js.map +0 -1
- package/dist/policies/dedupe.js.map +0 -1
- package/dist/policies/retry.js.map +0 -1
- package/dist/policies/timeout.js.map +0 -1
- package/dist/state/store.cjs +0 -14
- package/dist/state/store.d.ts +0 -11
- package/dist/state/store.d.ts.map +0 -1
- package/dist/state/store.js +0 -11
- package/dist/state/store.js.map +0 -1
- package/dist/stores/base.js.map +0 -1
- package/dist/stores/memory.js.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/abort.js.map +0 -1
- package/dist/utils/backoff.js.map +0 -1
- package/dist/utils/validate.js.map +0 -1
package/dist/utils/abort.js
CHANGED
|
@@ -1,87 +1,64 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// Centralised utilities for composing AbortSignals. These exist because
|
|
4
|
-
// Node 18 lacks `AbortSignal.any` (added in Node 20) and we want to keep
|
|
5
|
-
// the `engines` floor at 18 for backwards compatibility with existing
|
|
6
|
-
// consumers.
|
|
7
|
-
/**
|
|
8
|
-
* Polyfill for `AbortSignal.any(signals)` (Node 20+).
|
|
9
|
-
*
|
|
10
|
-
* Returns a single signal that aborts when ANY of the input signals aborts,
|
|
11
|
-
* with the same reason. If any input is already aborted, the returned signal
|
|
12
|
-
* is aborted synchronously.
|
|
13
|
-
*
|
|
14
|
-
* Listener registration is `{ once: true }` — once any signal fires, we stop
|
|
15
|
-
* listening on the others. The composite signal cannot be "un-aborted".
|
|
16
|
-
*/
|
|
1
|
+
const NATIVE_ANY = AbortSignal.any;
|
|
17
2
|
export function anySignal(signals) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
return
|
|
23
|
-
|
|
3
|
+
const filtered = signals.filter((s) => s != null);
|
|
4
|
+
if (filtered.length === 0)
|
|
5
|
+
return new AbortController().signal;
|
|
6
|
+
if (filtered.length === 1)
|
|
7
|
+
return filtered[0];
|
|
8
|
+
if (NATIVE_ANY)
|
|
9
|
+
return NATIVE_ANY.call(AbortSignal, filtered);
|
|
24
10
|
const controller = new AbortController();
|
|
25
|
-
|
|
11
|
+
const listeners = [];
|
|
12
|
+
for (const signal of filtered) {
|
|
26
13
|
if (signal.aborted) {
|
|
27
14
|
controller.abort(signal.reason);
|
|
28
15
|
break;
|
|
29
16
|
}
|
|
30
|
-
|
|
17
|
+
const onAbort = () => {
|
|
18
|
+
controller.abort(signal.reason);
|
|
19
|
+
for (const off of listeners)
|
|
20
|
+
off();
|
|
21
|
+
};
|
|
22
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
23
|
+
listeners.push(() => signal.removeEventListener('abort', onAbort));
|
|
24
|
+
}
|
|
25
|
+
if (controller.signal.aborted) {
|
|
26
|
+
for (const off of listeners)
|
|
27
|
+
off();
|
|
31
28
|
}
|
|
32
29
|
return controller.signal;
|
|
33
30
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Race a promise against an AbortSignal.
|
|
36
|
-
*
|
|
37
|
-
* - If the signal is already aborted, rejects immediately with `signal.reason`.
|
|
38
|
-
* - If the signal aborts while the promise is pending, rejects with `signal.reason`.
|
|
39
|
-
* - If the promise settles first, returns its value (or rejects with its error).
|
|
40
|
-
*
|
|
41
|
-
* The listener is registered with `{ once: true }` and never leaks: either
|
|
42
|
-
* the signal fires (listener auto-removed) or the promise settles (the
|
|
43
|
-
* signal will eventually be GC'd along with the listener).
|
|
44
|
-
*
|
|
45
|
-
* Used by `dedupePolicy` so joiners can cancel their own `await` even if the
|
|
46
|
-
* originator's `fn` is still running.
|
|
47
|
-
*/
|
|
48
31
|
export function raceAbort(promise, signal) {
|
|
49
|
-
if (signal.aborted)
|
|
32
|
+
if (signal.aborted) {
|
|
33
|
+
promise.catch(() => { });
|
|
50
34
|
return Promise.reject(signal.reason);
|
|
35
|
+
}
|
|
51
36
|
return new Promise((resolve, reject) => {
|
|
52
37
|
let settled = false;
|
|
53
38
|
const onAbort = () => {
|
|
54
39
|
if (settled)
|
|
55
40
|
return;
|
|
56
41
|
settled = true;
|
|
42
|
+
signal.removeEventListener('abort', onAbort);
|
|
57
43
|
reject(signal.reason);
|
|
58
44
|
};
|
|
59
|
-
signal.addEventListener('abort', onAbort
|
|
45
|
+
signal.addEventListener('abort', onAbort);
|
|
60
46
|
promise.then((value) => {
|
|
61
47
|
if (settled)
|
|
62
48
|
return;
|
|
63
49
|
settled = true;
|
|
50
|
+
signal.removeEventListener('abort', onAbort);
|
|
64
51
|
resolve(value);
|
|
65
52
|
}, (error) => {
|
|
66
53
|
if (settled)
|
|
67
54
|
return;
|
|
68
55
|
settled = true;
|
|
56
|
+
signal.removeEventListener('abort', onAbort);
|
|
69
57
|
reject(error);
|
|
70
58
|
});
|
|
71
59
|
});
|
|
72
60
|
}
|
|
73
|
-
|
|
74
|
-
* Sleep for `ms` milliseconds, but abort early if `signal` fires.
|
|
75
|
-
*
|
|
76
|
-
* Resolves normally on timer expiry. Rejects with `signal.reason` if the
|
|
77
|
-
* signal aborts before the timer fires. If the signal is already aborted
|
|
78
|
-
* when called, rejects synchronously (in microtask).
|
|
79
|
-
*
|
|
80
|
-
* Used by `retryPolicy` to make backoff delays interruptible: when an outer
|
|
81
|
-
* `totalTimeout` fires mid-delay, the delay rejects immediately instead of
|
|
82
|
-
* blocking the retry loop until the timer would have elapsed.
|
|
83
|
-
*/
|
|
84
|
-
export function sleep(ms, signal) {
|
|
61
|
+
export function sleep(ms, signal, opts) {
|
|
85
62
|
if (ms <= 0) {
|
|
86
63
|
if (signal?.aborted)
|
|
87
64
|
return Promise.reject(signal.reason);
|
|
@@ -94,43 +71,38 @@ export function sleep(ms, signal) {
|
|
|
94
71
|
signal?.removeEventListener('abort', onAbort);
|
|
95
72
|
resolve();
|
|
96
73
|
}, ms);
|
|
74
|
+
if (opts?.unref && typeof timer.unref === 'function') {
|
|
75
|
+
timer.unref();
|
|
76
|
+
}
|
|
97
77
|
const onAbort = () => {
|
|
98
78
|
clearTimeout(timer);
|
|
99
79
|
reject(signal.reason);
|
|
100
80
|
};
|
|
101
|
-
signal?.addEventListener('abort', onAbort
|
|
81
|
+
signal?.addEventListener('abort', onAbort);
|
|
102
82
|
});
|
|
103
83
|
}
|
|
104
|
-
/**
|
|
105
|
-
* True if `err` is an `AbortError` (DOMException name or Error name).
|
|
106
|
-
*
|
|
107
|
-
* The default `shouldRetry` predicate uses this to skip retrying on
|
|
108
|
-
* cancellations — if the caller aborted, retrying would just abort again.
|
|
109
|
-
*/
|
|
110
84
|
export function isAbortError(err) {
|
|
111
85
|
if (err == null || typeof err !== 'object')
|
|
112
86
|
return false;
|
|
87
|
+
if (err.code === 'ACTLY_ABORT')
|
|
88
|
+
return true;
|
|
113
89
|
const name = err.name;
|
|
114
|
-
|
|
90
|
+
if (name === 'AbortError')
|
|
91
|
+
return true;
|
|
92
|
+
if (name === 'TimeoutError' &&
|
|
115
93
|
err instanceof Error &&
|
|
116
|
-
// DOMException with name 'TimeoutError' is what AbortSignal.timeout throws.
|
|
117
|
-
// Distinguish from our own TimeoutError class by checking for DOMException.
|
|
118
94
|
typeof DOMException !== 'undefined' &&
|
|
119
|
-
err instanceof DOMException
|
|
95
|
+
err instanceof DOMException) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
120
99
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Link a parent signal to a child controller: when the parent aborts, the
|
|
123
|
-
* child is aborted with the same reason. No-op if the parent is already
|
|
124
|
-
* aborted (the caller should check `parent.aborted` separately if it cares
|
|
125
|
-
* about synchronous abort).
|
|
126
|
-
*
|
|
127
|
-
* The listener is `{ once: true }` — no leak.
|
|
128
|
-
*/
|
|
129
100
|
export function linkSignal(parent, child) {
|
|
130
101
|
if (parent.aborted) {
|
|
131
102
|
child.abort(parent.reason);
|
|
132
|
-
return;
|
|
103
|
+
return () => { };
|
|
133
104
|
}
|
|
134
|
-
|
|
105
|
+
const onAbort = () => child.abort(parent.reason);
|
|
106
|
+
parent.addEventListener('abort', onAbort);
|
|
107
|
+
return () => parent.removeEventListener('abort', onAbort);
|
|
135
108
|
}
|
|
136
|
-
//# sourceMappingURL=abort.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.acquireController = acquireController;
|
|
4
|
+
exports.releaseController = releaseController;
|
|
5
|
+
exports.poolSize = poolSize;
|
|
6
|
+
const pool = [];
|
|
7
|
+
const MAX_POOL_SIZE = 64;
|
|
8
|
+
function acquireController() {
|
|
9
|
+
while (pool.length > 0) {
|
|
10
|
+
const c = pool.pop();
|
|
11
|
+
if (!c.signal.aborted)
|
|
12
|
+
return c;
|
|
13
|
+
}
|
|
14
|
+
return new AbortController();
|
|
15
|
+
}
|
|
16
|
+
function releaseController(c) {
|
|
17
|
+
if (c.signal.aborted)
|
|
18
|
+
return;
|
|
19
|
+
if (pool.length >= MAX_POOL_SIZE)
|
|
20
|
+
return;
|
|
21
|
+
pool.push(c);
|
|
22
|
+
}
|
|
23
|
+
function poolSize() {
|
|
24
|
+
return pool.length;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abortPool.d.ts","sourceRoot":"","sources":["../../src/utils/abortPool.ts"],"names":[],"mappings":"AAMA,wBAAgB,iBAAiB,IAAI,eAAe,CAOnD;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,eAAe,GAAG,IAAI,CAK1D;AAED,wBAAgB,QAAQ,IAAI,MAAM,CAEjC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const pool = [];
|
|
2
|
+
const MAX_POOL_SIZE = 64;
|
|
3
|
+
export function acquireController() {
|
|
4
|
+
while (pool.length > 0) {
|
|
5
|
+
const c = pool.pop();
|
|
6
|
+
if (!c.signal.aborted)
|
|
7
|
+
return c;
|
|
8
|
+
}
|
|
9
|
+
return new AbortController();
|
|
10
|
+
}
|
|
11
|
+
export function releaseController(c) {
|
|
12
|
+
if (c.signal.aborted)
|
|
13
|
+
return;
|
|
14
|
+
if (pool.length >= MAX_POOL_SIZE)
|
|
15
|
+
return;
|
|
16
|
+
pool.push(c);
|
|
17
|
+
}
|
|
18
|
+
export function poolSize() {
|
|
19
|
+
return pool.length;
|
|
20
|
+
}
|
package/dist/utils/backoff.cjs
CHANGED
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.computeDelay = computeDelay;
|
|
4
|
-
/**
|
|
5
|
-
* Compute the delay before the next retry attempt, applying backoff, cap, and
|
|
6
|
-
* jitter in that order.
|
|
7
|
-
*
|
|
8
|
-
* Order matters:
|
|
9
|
-
* 1. `backoff` grows the base delay geometrically/linearly.
|
|
10
|
-
* 2. `maxDelay` caps the result (prevents exponential blowup).
|
|
11
|
-
* 3. `jitter` randomises within `[0, delay]` (prevents thundering herd).
|
|
12
|
-
*
|
|
13
|
-
* Returns 0 if `delayMs` is 0 or undefined — skipping the sleep entirely.
|
|
14
|
-
*/
|
|
15
4
|
function computeDelay(attempt, opts) {
|
|
16
5
|
const base = opts.delayMs ?? 0;
|
|
17
6
|
if (base === 0)
|
|
18
7
|
return 0;
|
|
19
|
-
// Step 1: backoff
|
|
20
8
|
let delay;
|
|
21
9
|
switch (opts.backoff ?? 'none') {
|
|
22
10
|
case 'linear':
|
|
@@ -27,17 +15,22 @@ function computeDelay(attempt, opts) {
|
|
|
27
15
|
break;
|
|
28
16
|
default: delay = base;
|
|
29
17
|
}
|
|
30
|
-
// Step 2: cap (guard against Infinity and NaN before Math.min)
|
|
31
18
|
const max = opts.maxDelay ?? Number.POSITIVE_INFINITY;
|
|
32
19
|
if (!Number.isFinite(delay))
|
|
33
20
|
delay = max;
|
|
34
21
|
delay = Math.min(delay, max);
|
|
35
|
-
// Step 3: jitter
|
|
36
22
|
switch (opts.jitter ?? 'full') {
|
|
37
23
|
case 'none': return delay;
|
|
38
24
|
case 'full': return Math.random() * delay;
|
|
39
25
|
case 'equal': return delay / 2 + Math.random() * delay / 2;
|
|
40
|
-
case 'decorrelated':
|
|
26
|
+
case 'decorrelated': {
|
|
27
|
+
if (delay < base)
|
|
28
|
+
return Math.random() * delay;
|
|
29
|
+
const lo = base;
|
|
30
|
+
const hi = delay;
|
|
31
|
+
const result = lo + Math.random() * (hi - lo);
|
|
32
|
+
return Math.max(0, Math.min(result, delay));
|
|
33
|
+
}
|
|
41
34
|
default: return delay;
|
|
42
35
|
}
|
|
43
36
|
}
|
package/dist/utils/backoff.d.ts
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
1
|
import type { RetryOptions } from '../types/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* Compute the delay before the next retry attempt, applying backoff, cap, and
|
|
4
|
-
* jitter in that order.
|
|
5
|
-
*
|
|
6
|
-
* Order matters:
|
|
7
|
-
* 1. `backoff` grows the base delay geometrically/linearly.
|
|
8
|
-
* 2. `maxDelay` caps the result (prevents exponential blowup).
|
|
9
|
-
* 3. `jitter` randomises within `[0, delay]` (prevents thundering herd).
|
|
10
|
-
*
|
|
11
|
-
* Returns 0 if `delayMs` is 0 or undefined — skipping the sleep entirely.
|
|
12
|
-
*/
|
|
13
2
|
export declare function computeDelay(attempt: number, opts: RetryOptions): number;
|
|
14
3
|
//# sourceMappingURL=backoff.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../../src/utils/backoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../../src/utils/backoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AASrD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CAgCxE"}
|
package/dist/utils/backoff.js
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compute the delay before the next retry attempt, applying backoff, cap, and
|
|
3
|
-
* jitter in that order.
|
|
4
|
-
*
|
|
5
|
-
* Order matters:
|
|
6
|
-
* 1. `backoff` grows the base delay geometrically/linearly.
|
|
7
|
-
* 2. `maxDelay` caps the result (prevents exponential blowup).
|
|
8
|
-
* 3. `jitter` randomises within `[0, delay]` (prevents thundering herd).
|
|
9
|
-
*
|
|
10
|
-
* Returns 0 if `delayMs` is 0 or undefined — skipping the sleep entirely.
|
|
11
|
-
*/
|
|
12
1
|
export function computeDelay(attempt, opts) {
|
|
13
2
|
const base = opts.delayMs ?? 0;
|
|
14
3
|
if (base === 0)
|
|
15
4
|
return 0;
|
|
16
|
-
// Step 1: backoff
|
|
17
5
|
let delay;
|
|
18
6
|
switch (opts.backoff ?? 'none') {
|
|
19
7
|
case 'linear':
|
|
@@ -24,18 +12,22 @@ export function computeDelay(attempt, opts) {
|
|
|
24
12
|
break;
|
|
25
13
|
default: delay = base;
|
|
26
14
|
}
|
|
27
|
-
// Step 2: cap (guard against Infinity and NaN before Math.min)
|
|
28
15
|
const max = opts.maxDelay ?? Number.POSITIVE_INFINITY;
|
|
29
16
|
if (!Number.isFinite(delay))
|
|
30
17
|
delay = max;
|
|
31
18
|
delay = Math.min(delay, max);
|
|
32
|
-
// Step 3: jitter
|
|
33
19
|
switch (opts.jitter ?? 'full') {
|
|
34
20
|
case 'none': return delay;
|
|
35
21
|
case 'full': return Math.random() * delay;
|
|
36
22
|
case 'equal': return delay / 2 + Math.random() * delay / 2;
|
|
37
|
-
case 'decorrelated':
|
|
23
|
+
case 'decorrelated': {
|
|
24
|
+
if (delay < base)
|
|
25
|
+
return Math.random() * delay;
|
|
26
|
+
const lo = base;
|
|
27
|
+
const hi = delay;
|
|
28
|
+
const result = lo + Math.random() * (hi - lo);
|
|
29
|
+
return Math.max(0, Math.min(result, delay));
|
|
30
|
+
}
|
|
38
31
|
default: return delay;
|
|
39
32
|
}
|
|
40
33
|
}
|
|
41
|
-
//# sourceMappingURL=backoff.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.usePolicy = usePolicy;
|
|
4
|
+
function usePolicy(options) {
|
|
5
|
+
return function (_target, propertyKey, descriptor) {
|
|
6
|
+
if (!descriptor || !descriptor.value) {
|
|
7
|
+
throw new Error(`Actly: @usePolicy can only be applied to methods with a function value. ` +
|
|
8
|
+
`Got descriptor without value for property "${String(propertyKey)}".`);
|
|
9
|
+
}
|
|
10
|
+
const originalMethod = descriptor.value;
|
|
11
|
+
let actPromise;
|
|
12
|
+
const getAct = () => {
|
|
13
|
+
if (!actPromise)
|
|
14
|
+
actPromise = Promise.resolve().then(() => require('../core/act.js')).then(m => m.act);
|
|
15
|
+
return actPromise;
|
|
16
|
+
};
|
|
17
|
+
const wrappedMethod = async function (...args) {
|
|
18
|
+
const act = await getAct();
|
|
19
|
+
const ctorName = this?.constructor?.name ?? 'Anonymous';
|
|
20
|
+
const key = `${ctorName}.${String(propertyKey)}`;
|
|
21
|
+
const firstArg = args[0];
|
|
22
|
+
const isSignal = firstArg != null &&
|
|
23
|
+
typeof firstArg === 'object' &&
|
|
24
|
+
typeof firstArg.aborted === 'boolean' &&
|
|
25
|
+
typeof firstArg.addEventListener === 'function';
|
|
26
|
+
const signal = isSignal ? firstArg : undefined;
|
|
27
|
+
const fnArgs = signal ? args.slice(1) : args;
|
|
28
|
+
const result = await act(key, async (actSignal) => originalMethod.apply(this, [actSignal, ...fnArgs]), signal ? { ...options, signal } : options);
|
|
29
|
+
if (result.ok)
|
|
30
|
+
return result.value;
|
|
31
|
+
throw result.error;
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
Object.defineProperty(wrappedMethod, 'name', {
|
|
35
|
+
value: typeof propertyKey === 'symbol' ? propertyKey.description ?? 'wrapped' : propertyKey,
|
|
36
|
+
configurable: true,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
}
|
|
41
|
+
descriptor.value = wrappedMethod;
|
|
42
|
+
return descriptor;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorator.d.ts","sourceRoot":"","sources":["../../src/utils/decorator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAa,MAAM,mBAAmB,CAAA;AA0B9D,wBAAgB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,eAAe,CAoE9D"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function usePolicy(options) {
|
|
2
|
+
return function (_target, propertyKey, descriptor) {
|
|
3
|
+
if (!descriptor || !descriptor.value) {
|
|
4
|
+
throw new Error(`Actly: @usePolicy can only be applied to methods with a function value. ` +
|
|
5
|
+
`Got descriptor without value for property "${String(propertyKey)}".`);
|
|
6
|
+
}
|
|
7
|
+
const originalMethod = descriptor.value;
|
|
8
|
+
let actPromise;
|
|
9
|
+
const getAct = () => {
|
|
10
|
+
if (!actPromise)
|
|
11
|
+
actPromise = import('../core/act.js').then(m => m.act);
|
|
12
|
+
return actPromise;
|
|
13
|
+
};
|
|
14
|
+
const wrappedMethod = async function (...args) {
|
|
15
|
+
const act = await getAct();
|
|
16
|
+
const ctorName = this?.constructor?.name ?? 'Anonymous';
|
|
17
|
+
const key = `${ctorName}.${String(propertyKey)}`;
|
|
18
|
+
const firstArg = args[0];
|
|
19
|
+
const isSignal = firstArg != null &&
|
|
20
|
+
typeof firstArg === 'object' &&
|
|
21
|
+
typeof firstArg.aborted === 'boolean' &&
|
|
22
|
+
typeof firstArg.addEventListener === 'function';
|
|
23
|
+
const signal = isSignal ? firstArg : undefined;
|
|
24
|
+
const fnArgs = signal ? args.slice(1) : args;
|
|
25
|
+
const result = await act(key, async (actSignal) => originalMethod.apply(this, [actSignal, ...fnArgs]), signal ? { ...options, signal } : options);
|
|
26
|
+
if (result.ok)
|
|
27
|
+
return result.value;
|
|
28
|
+
throw result.error;
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
Object.defineProperty(wrappedMethod, 'name', {
|
|
32
|
+
value: typeof propertyKey === 'symbol' ? propertyKey.description ?? 'wrapped' : propertyKey,
|
|
33
|
+
configurable: true,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
}
|
|
38
|
+
descriptor.value = wrappedMethod;
|
|
39
|
+
return descriptor;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitizeKey = sanitizeKey;
|
|
4
|
+
const limits_js_1 = require("./limits.js");
|
|
5
|
+
const RESERVED_PREFIXES = ['dedupe:', 'cache:', 'inflight:', 'tenant:'];
|
|
6
|
+
const FORBIDDEN_LITERALS = new Set([
|
|
7
|
+
'__proto__',
|
|
8
|
+
'constructor',
|
|
9
|
+
'prototype',
|
|
10
|
+
'hasOwnProperty',
|
|
11
|
+
'toString',
|
|
12
|
+
'valueOf',
|
|
13
|
+
'isPrototypeOf',
|
|
14
|
+
'propertyIsEnumerable',
|
|
15
|
+
'toLocaleString',
|
|
16
|
+
]);
|
|
17
|
+
const UNSAFE_CHAR = /[\x00-\x08\x0b\x0c\x0d\x0e-\x1f\x7f]/;
|
|
18
|
+
function sanitizeKey(key) {
|
|
19
|
+
if (typeof key !== 'string') {
|
|
20
|
+
throw new TypeError(`Actly: key must be a string, got ${key === null ? 'null' : typeof key}`);
|
|
21
|
+
}
|
|
22
|
+
if (key.length === 0) {
|
|
23
|
+
throw new RangeError("Actly: key must be non-empty. An empty key collapses every caller " +
|
|
24
|
+
"onto the same dedupe/cache slot — almost certainly a bug.");
|
|
25
|
+
}
|
|
26
|
+
if (key.length > limits_js_1.LIMITS.MAX_KEY_LENGTH) {
|
|
27
|
+
throw new RangeError(`Actly: key length ${key.length} exceeds limit ${limits_js_1.LIMITS.MAX_KEY_LENGTH}. ` +
|
|
28
|
+
`Long keys bloat stores and slow iteration. Hash externally if you need longer keys.`);
|
|
29
|
+
}
|
|
30
|
+
if (FORBIDDEN_LITERALS.has(key)) {
|
|
31
|
+
throw new RangeError(`Actly: key ${JSON.stringify(key)} is forbidden (prototype-pollution vector). ` +
|
|
32
|
+
`Pick a different key.`);
|
|
33
|
+
}
|
|
34
|
+
if (UNSAFE_CHAR.test(key)) {
|
|
35
|
+
throw new RangeError(`Actly: key contains control characters or CRLF, which break store backends ` +
|
|
36
|
+
`and log serializers. Got ${JSON.stringify(key)}.`);
|
|
37
|
+
}
|
|
38
|
+
for (const prefix of RESERVED_PREFIXES) {
|
|
39
|
+
if (key.startsWith(prefix)) {
|
|
40
|
+
throw new RangeError(`Actly: key must not start with reserved prefix "${prefix}" ` +
|
|
41
|
+
`(got ${JSON.stringify(key)}). These namespaces are used internally.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return key;
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../../src/utils/key.ts"],"names":[],"mappings":"AAwCA,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAqC/C"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { LIMITS } from './limits.js';
|
|
2
|
+
const RESERVED_PREFIXES = ['dedupe:', 'cache:', 'inflight:', 'tenant:'];
|
|
3
|
+
const FORBIDDEN_LITERALS = new Set([
|
|
4
|
+
'__proto__',
|
|
5
|
+
'constructor',
|
|
6
|
+
'prototype',
|
|
7
|
+
'hasOwnProperty',
|
|
8
|
+
'toString',
|
|
9
|
+
'valueOf',
|
|
10
|
+
'isPrototypeOf',
|
|
11
|
+
'propertyIsEnumerable',
|
|
12
|
+
'toLocaleString',
|
|
13
|
+
]);
|
|
14
|
+
const UNSAFE_CHAR = /[\x00-\x08\x0b\x0c\x0d\x0e-\x1f\x7f]/;
|
|
15
|
+
export function sanitizeKey(key) {
|
|
16
|
+
if (typeof key !== 'string') {
|
|
17
|
+
throw new TypeError(`Actly: key must be a string, got ${key === null ? 'null' : typeof key}`);
|
|
18
|
+
}
|
|
19
|
+
if (key.length === 0) {
|
|
20
|
+
throw new RangeError("Actly: key must be non-empty. An empty key collapses every caller " +
|
|
21
|
+
"onto the same dedupe/cache slot — almost certainly a bug.");
|
|
22
|
+
}
|
|
23
|
+
if (key.length > LIMITS.MAX_KEY_LENGTH) {
|
|
24
|
+
throw new RangeError(`Actly: key length ${key.length} exceeds limit ${LIMITS.MAX_KEY_LENGTH}. ` +
|
|
25
|
+
`Long keys bloat stores and slow iteration. Hash externally if you need longer keys.`);
|
|
26
|
+
}
|
|
27
|
+
if (FORBIDDEN_LITERALS.has(key)) {
|
|
28
|
+
throw new RangeError(`Actly: key ${JSON.stringify(key)} is forbidden (prototype-pollution vector). ` +
|
|
29
|
+
`Pick a different key.`);
|
|
30
|
+
}
|
|
31
|
+
if (UNSAFE_CHAR.test(key)) {
|
|
32
|
+
throw new RangeError(`Actly: key contains control characters or CRLF, which break store backends ` +
|
|
33
|
+
`and log serializers. Got ${JSON.stringify(key)}.`);
|
|
34
|
+
}
|
|
35
|
+
for (const prefix of RESERVED_PREFIXES) {
|
|
36
|
+
if (key.startsWith(prefix)) {
|
|
37
|
+
throw new RangeError(`Actly: key must not start with reserved prefix "${prefix}" ` +
|
|
38
|
+
`(got ${JSON.stringify(key)}). These namespaces are used internally.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return key;
|
|
42
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LIMITS = void 0;
|
|
4
|
+
exports.LIMITS = Object.freeze({
|
|
5
|
+
MAX_KEY_LENGTH: 1024,
|
|
6
|
+
MAX_RETRY_ATTEMPTS: 100,
|
|
7
|
+
MAX_TIMEOUT_MS: 100_000_000,
|
|
8
|
+
MAX_CACHE_TTL: 86_400_000,
|
|
9
|
+
MAX_RETRY_DELAY_MS: 300_000,
|
|
10
|
+
MAX_INFLIGHT_TTL: 86_400_000,
|
|
11
|
+
DEFAULT_INFLIGHT_TTL: 5 * 60_000,
|
|
12
|
+
DEFAULT_STORE_MAX_SIZE: 10_000,
|
|
13
|
+
DEFAULT_STORE_CLEANUP_INTERVAL_MS: 60_000,
|
|
14
|
+
MAX_GLOBAL_INFLIGHT: 100_000,
|
|
15
|
+
MAX_CIRCUIT_BREAKER_WINDOW: 1_000_000,
|
|
16
|
+
MAX_BULKHEAD_CONCURRENCY: 1_000_000,
|
|
17
|
+
MAX_BULKHEAD_QUEUE: 1_000_000,
|
|
18
|
+
MAX_RATE_LIMIT_CALLS: 1_000_000,
|
|
19
|
+
MAX_SANITIZED_ERROR_LENGTH: 8_192,
|
|
20
|
+
MAX_HEDGE_DELAY_MS: 300_000,
|
|
21
|
+
MAX_TENANTS: 10_000,
|
|
22
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const LIMITS: Readonly<{
|
|
2
|
+
MAX_KEY_LENGTH: 1024;
|
|
3
|
+
MAX_RETRY_ATTEMPTS: 100;
|
|
4
|
+
MAX_TIMEOUT_MS: 100000000;
|
|
5
|
+
MAX_CACHE_TTL: 86400000;
|
|
6
|
+
MAX_RETRY_DELAY_MS: 300000;
|
|
7
|
+
MAX_INFLIGHT_TTL: 86400000;
|
|
8
|
+
DEFAULT_INFLIGHT_TTL: number;
|
|
9
|
+
DEFAULT_STORE_MAX_SIZE: 10000;
|
|
10
|
+
DEFAULT_STORE_CLEANUP_INTERVAL_MS: 60000;
|
|
11
|
+
MAX_GLOBAL_INFLIGHT: 100000;
|
|
12
|
+
MAX_CIRCUIT_BREAKER_WINDOW: 1000000;
|
|
13
|
+
MAX_BULKHEAD_CONCURRENCY: 1000000;
|
|
14
|
+
MAX_BULKHEAD_QUEUE: 1000000;
|
|
15
|
+
MAX_RATE_LIMIT_CALLS: 1000000;
|
|
16
|
+
MAX_SANITIZED_ERROR_LENGTH: 8192;
|
|
17
|
+
MAX_HEDGE_DELAY_MS: 300000;
|
|
18
|
+
MAX_TENANTS: 10000;
|
|
19
|
+
}>;
|
|
20
|
+
export type Limits = typeof LIMITS;
|
|
21
|
+
//# sourceMappingURL=limits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../../src/utils/limits.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;EA8EjB,CAAA;AAEF,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const LIMITS = Object.freeze({
|
|
2
|
+
MAX_KEY_LENGTH: 1024,
|
|
3
|
+
MAX_RETRY_ATTEMPTS: 100,
|
|
4
|
+
MAX_TIMEOUT_MS: 100_000_000,
|
|
5
|
+
MAX_CACHE_TTL: 86_400_000,
|
|
6
|
+
MAX_RETRY_DELAY_MS: 300_000,
|
|
7
|
+
MAX_INFLIGHT_TTL: 86_400_000,
|
|
8
|
+
DEFAULT_INFLIGHT_TTL: 5 * 60_000,
|
|
9
|
+
DEFAULT_STORE_MAX_SIZE: 10_000,
|
|
10
|
+
DEFAULT_STORE_CLEANUP_INTERVAL_MS: 60_000,
|
|
11
|
+
MAX_GLOBAL_INFLIGHT: 100_000,
|
|
12
|
+
MAX_CIRCUIT_BREAKER_WINDOW: 1_000_000,
|
|
13
|
+
MAX_BULKHEAD_CONCURRENCY: 1_000_000,
|
|
14
|
+
MAX_BULKHEAD_QUEUE: 1_000_000,
|
|
15
|
+
MAX_RATE_LIMIT_CALLS: 1_000_000,
|
|
16
|
+
MAX_SANITIZED_ERROR_LENGTH: 8_192,
|
|
17
|
+
MAX_HEDGE_DELAY_MS: 300_000,
|
|
18
|
+
MAX_TENANTS: 10_000,
|
|
19
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.safeCall = safeCall;
|
|
4
|
+
const IS_DEV = typeof process !== 'undefined' &&
|
|
5
|
+
typeof process.env === 'object' &&
|
|
6
|
+
process.env !== null &&
|
|
7
|
+
process.env.NODE_ENV !== 'production';
|
|
8
|
+
function safeCall(fn, ...args) {
|
|
9
|
+
if (!fn)
|
|
10
|
+
return;
|
|
11
|
+
try {
|
|
12
|
+
const result = fn(...args);
|
|
13
|
+
if (result !== null && result !== undefined && typeof result.then === 'function') {
|
|
14
|
+
Promise.resolve(result).catch((err) => {
|
|
15
|
+
if (IS_DEV) {
|
|
16
|
+
console.warn('Actly: async observability hook rejected — error swallowed to protect main path. ' +
|
|
17
|
+
'Fix the hook to prevent silent observability loss.', err);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
if (IS_DEV) {
|
|
24
|
+
console.warn('Actly: observability hook threw — error swallowed to protect main path. ' +
|
|
25
|
+
'Fix the hook to prevent silent observability loss.', err);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safeCall.d.ts","sourceRoot":"","sources":["../../src/utils/safeCall.ts"],"names":[],"mappings":"AAkBA,wBAAgB,QAAQ,CAAC,CAAC,SAAS,OAAO,EAAE,EAC1C,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC,GAAG,SAAS,EACtC,GAAG,IAAI,EAAE,CAAC,GACT,IAAI,CA2BN"}
|