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.
Files changed (139) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/MIGRATION.md +141 -0
  3. package/README.md +334 -428
  4. package/SECURITY.md +56 -0
  5. package/dist/core/act.cjs +630 -126
  6. package/dist/core/act.d.ts +6 -84
  7. package/dist/core/act.d.ts.map +1 -1
  8. package/dist/core/act.js +630 -129
  9. package/dist/core/executor.cjs +1 -26
  10. package/dist/core/executor.d.ts +2 -38
  11. package/dist/core/executor.d.ts.map +1 -1
  12. package/dist/core/executor.js +1 -27
  13. package/dist/core/health.cjs +163 -0
  14. package/dist/core/health.d.ts +33 -0
  15. package/dist/core/health.d.ts.map +1 -0
  16. package/dist/core/health.js +150 -0
  17. package/dist/core/shutdown.cjs +71 -0
  18. package/dist/core/shutdown.d.ts +5 -0
  19. package/dist/core/shutdown.d.ts.map +1 -0
  20. package/dist/core/shutdown.js +65 -0
  21. package/dist/core/tenant.cjs +113 -0
  22. package/dist/core/tenant.d.ts +24 -0
  23. package/dist/core/tenant.d.ts.map +1 -0
  24. package/dist/core/tenant.js +109 -0
  25. package/dist/errors.cjs +149 -0
  26. package/dist/errors.d.ts +94 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +134 -0
  29. package/dist/index.cjs +51 -8
  30. package/dist/index.d.ts +22 -3
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +14 -6
  33. package/dist/observability.cjs +6 -0
  34. package/dist/observability.d.ts +86 -0
  35. package/dist/observability.d.ts.map +1 -0
  36. package/dist/observability.js +3 -0
  37. package/dist/policies/bulkhead.cjs +137 -0
  38. package/dist/policies/bulkhead.d.ts +3 -0
  39. package/dist/policies/bulkhead.d.ts.map +1 -0
  40. package/dist/policies/bulkhead.js +134 -0
  41. package/dist/policies/cache.cjs +84 -67
  42. package/dist/policies/cache.d.ts +0 -33
  43. package/dist/policies/cache.d.ts.map +1 -1
  44. package/dist/policies/cache.js +84 -68
  45. package/dist/policies/circuitBreaker.cjs +179 -0
  46. package/dist/policies/circuitBreaker.d.ts +3 -0
  47. package/dist/policies/circuitBreaker.d.ts.map +1 -0
  48. package/dist/policies/circuitBreaker.js +176 -0
  49. package/dist/policies/dedupe.cjs +46 -64
  50. package/dist/policies/dedupe.d.ts +0 -41
  51. package/dist/policies/dedupe.d.ts.map +1 -1
  52. package/dist/policies/dedupe.js +46 -65
  53. package/dist/policies/noop.cjs +6 -0
  54. package/dist/policies/noop.d.ts +3 -0
  55. package/dist/policies/noop.d.ts.map +1 -0
  56. package/dist/policies/noop.js +3 -0
  57. package/dist/policies/rateLimit.cjs +45 -0
  58. package/dist/policies/rateLimit.d.ts +3 -0
  59. package/dist/policies/rateLimit.d.ts.map +1 -0
  60. package/dist/policies/rateLimit.js +42 -0
  61. package/dist/policies/retry.cjs +129 -57
  62. package/dist/policies/retry.d.ts +0 -27
  63. package/dist/policies/retry.d.ts.map +1 -1
  64. package/dist/policies/retry.js +129 -58
  65. package/dist/policies/timeout.cjs +47 -103
  66. package/dist/policies/timeout.d.ts +2 -46
  67. package/dist/policies/timeout.d.ts.map +1 -1
  68. package/dist/policies/timeout.js +46 -102
  69. package/dist/stores/base.cjs +0 -10
  70. package/dist/stores/base.d.ts +4 -67
  71. package/dist/stores/base.d.ts.map +1 -1
  72. package/dist/stores/base.js +0 -11
  73. package/dist/stores/memory.cjs +147 -87
  74. package/dist/stores/memory.d.ts +11 -59
  75. package/dist/stores/memory.d.ts.map +1 -1
  76. package/dist/stores/memory.js +146 -88
  77. package/dist/testing/index.cjs +56 -0
  78. package/dist/testing/index.d.ts +6 -0
  79. package/dist/testing/index.d.ts.map +1 -0
  80. package/dist/testing/index.js +52 -0
  81. package/dist/types/index.cjs +0 -1
  82. package/dist/types/index.d.ts +62 -179
  83. package/dist/types/index.d.ts.map +1 -1
  84. package/dist/types/index.js +0 -2
  85. package/dist/utils/abort.cjs +46 -73
  86. package/dist/utils/abort.d.ts +4 -51
  87. package/dist/utils/abort.d.ts.map +1 -1
  88. package/dist/utils/abort.js +46 -74
  89. package/dist/utils/abortPool.cjs +25 -0
  90. package/dist/utils/abortPool.d.ts +4 -0
  91. package/dist/utils/abortPool.d.ts.map +1 -0
  92. package/dist/utils/abortPool.js +20 -0
  93. package/dist/utils/backoff.cjs +8 -15
  94. package/dist/utils/backoff.d.ts +0 -11
  95. package/dist/utils/backoff.d.ts.map +1 -1
  96. package/dist/utils/backoff.js +8 -16
  97. package/dist/utils/decorator.cjs +44 -0
  98. package/dist/utils/decorator.d.ts +3 -0
  99. package/dist/utils/decorator.d.ts.map +1 -0
  100. package/dist/utils/decorator.js +41 -0
  101. package/dist/utils/key.cjs +45 -0
  102. package/dist/utils/key.d.ts +2 -0
  103. package/dist/utils/key.d.ts.map +1 -0
  104. package/dist/utils/key.js +42 -0
  105. package/dist/utils/limits.cjs +22 -0
  106. package/dist/utils/limits.d.ts +21 -0
  107. package/dist/utils/limits.d.ts.map +1 -0
  108. package/dist/utils/limits.js +19 -0
  109. package/dist/utils/safeCall.cjs +28 -0
  110. package/dist/utils/safeCall.d.ts +2 -0
  111. package/dist/utils/safeCall.d.ts.map +1 -0
  112. package/dist/utils/safeCall.js +25 -0
  113. package/dist/utils/sanitize.cjs +50 -0
  114. package/dist/utils/sanitize.d.ts +3 -0
  115. package/dist/utils/sanitize.d.ts.map +1 -0
  116. package/dist/utils/sanitize.js +46 -0
  117. package/dist/utils/validate.cjs +163 -26
  118. package/dist/utils/validate.d.ts +6 -8
  119. package/dist/utils/validate.d.ts.map +1 -1
  120. package/dist/utils/validate.js +158 -27
  121. package/package.json +42 -7
  122. package/dist/core/act.js.map +0 -1
  123. package/dist/core/executor.js.map +0 -1
  124. package/dist/index.js.map +0 -1
  125. package/dist/policies/cache.js.map +0 -1
  126. package/dist/policies/dedupe.js.map +0 -1
  127. package/dist/policies/retry.js.map +0 -1
  128. package/dist/policies/timeout.js.map +0 -1
  129. package/dist/state/store.cjs +0 -14
  130. package/dist/state/store.d.ts +0 -11
  131. package/dist/state/store.d.ts.map +0 -1
  132. package/dist/state/store.js +0 -11
  133. package/dist/state/store.js.map +0 -1
  134. package/dist/stores/base.js.map +0 -1
  135. package/dist/stores/memory.js.map +0 -1
  136. package/dist/types/index.js.map +0 -1
  137. package/dist/utils/abort.js.map +0 -1
  138. package/dist/utils/backoff.js.map +0 -1
  139. package/dist/utils/validate.js.map +0 -1
@@ -1,87 +1,64 @@
1
- // ─── AbortSignal helpers ──────────────────────────────────────────────────────
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
- // Fast path: native implementation (Node 20+, modern browsers, Bun).
19
- // The cast is safe — the runtime check guards the call.
20
- const native = AbortSignal.any;
21
- if (typeof native === 'function')
22
- return native.call(AbortSignal, signals);
23
- // Polyfill for Node 18.
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
- for (const signal of signals) {
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
- signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });
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, { once: true });
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, { once: true });
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
- return name === 'AbortError' || name === 'TimeoutError' &&
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
- parent.addEventListener('abort', () => child.abort(parent.reason), { once: true });
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,4 @@
1
+ export declare function acquireController(): AbortController;
2
+ export declare function releaseController(c: AbortController): void;
3
+ export declare function poolSize(): number;
4
+ //# sourceMappingURL=abortPool.d.ts.map
@@ -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
+ }
@@ -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': return base + Math.random() * (delay - base);
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
  }
@@ -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;AAErD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CAyBxE"}
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"}
@@ -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': return base + Math.random() * (delay - base);
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,3 @@
1
+ import type { ActOptions } from '../types/index.js';
2
+ export declare function usePolicy(options: ActOptions): MethodDecorator;
3
+ //# sourceMappingURL=decorator.d.ts.map
@@ -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,2 @@
1
+ export declare function sanitizeKey(key: string): string;
2
+ //# sourceMappingURL=key.d.ts.map
@@ -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,2 @@
1
+ export declare function safeCall<T extends unknown[]>(fn: ((...args: T) => void) | undefined, ...args: T): void;
2
+ //# sourceMappingURL=safeCall.d.ts.map
@@ -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"}