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
@@ -0,0 +1,25 @@
1
+ const IS_DEV = typeof process !== 'undefined' &&
2
+ typeof process.env === 'object' &&
3
+ process.env !== null &&
4
+ process.env.NODE_ENV !== 'production';
5
+ export function safeCall(fn, ...args) {
6
+ if (!fn)
7
+ return;
8
+ try {
9
+ const result = fn(...args);
10
+ if (result !== null && result !== undefined && typeof result.then === 'function') {
11
+ Promise.resolve(result).catch((err) => {
12
+ if (IS_DEV) {
13
+ console.warn('Actly: async observability hook rejected — error swallowed to protect main path. ' +
14
+ 'Fix the hook to prevent silent observability loss.', err);
15
+ }
16
+ });
17
+ }
18
+ }
19
+ catch (err) {
20
+ if (IS_DEV) {
21
+ console.warn('Actly: observability hook threw — error swallowed to protect main path. ' +
22
+ 'Fix the hook to prevent silent observability loss.', err);
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeErrorMessage = sanitizeErrorMessage;
4
+ exports.sanitizeError = sanitizeError;
5
+ const limits_js_1 = require("./limits.js");
6
+ const errors_js_1 = require("../errors.js");
7
+ const FORBIDDEN_CHARS = /[\x00-\x1f\x7f]/g;
8
+ const HTML_ENTITIES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;' };
9
+ function sanitizeErrorMessage(msg) {
10
+ let str;
11
+ if (msg instanceof Error) {
12
+ str = String(msg.message ?? '');
13
+ }
14
+ else if (typeof msg === 'string') {
15
+ str = msg;
16
+ }
17
+ else {
18
+ str = String(msg ?? '');
19
+ }
20
+ str = str.replace(/[&<>"']/g, (c) => HTML_ENTITIES[c]);
21
+ str = str.replace(FORBIDDEN_CHARS, '');
22
+ if (str.length > limits_js_1.LIMITS.MAX_SANITIZED_ERROR_LENGTH) {
23
+ str = str.slice(0, limits_js_1.LIMITS.MAX_SANITIZED_ERROR_LENGTH - 3) + '...';
24
+ }
25
+ return str;
26
+ }
27
+ function sanitizeError(err) {
28
+ if (err instanceof Error) {
29
+ const sanitized = new Error(sanitizeErrorMessage(err.message));
30
+ sanitized.name = err.name;
31
+ try {
32
+ sanitized.stack = err.stack;
33
+ }
34
+ catch { }
35
+ if (err instanceof errors_js_1.ActlyError) {
36
+ const code = err.code;
37
+ const key = err.key;
38
+ Object.defineProperty(sanitized, 'code', { value: code, enumerable: true });
39
+ if (key !== undefined) {
40
+ Object.defineProperty(sanitized, 'key', { value: key, enumerable: true });
41
+ }
42
+ }
43
+ try {
44
+ Object.defineProperty(sanitized, 'cause', { value: err, enumerable: false });
45
+ }
46
+ catch { }
47
+ return sanitized;
48
+ }
49
+ return sanitizeErrorMessage(err);
50
+ }
@@ -0,0 +1,3 @@
1
+ export declare function sanitizeErrorMessage(msg: unknown): string;
2
+ export declare function sanitizeError(err: unknown): unknown;
3
+ //# sourceMappingURL=sanitize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize.ts"],"names":[],"mappings":"AAQA,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAkBzD;AAQD,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAmBnD"}
@@ -0,0 +1,46 @@
1
+ import { LIMITS } from './limits.js';
2
+ import { ActlyError } from '../errors.js';
3
+ const FORBIDDEN_CHARS = /[\x00-\x1f\x7f]/g;
4
+ const HTML_ENTITIES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;' };
5
+ export function sanitizeErrorMessage(msg) {
6
+ let str;
7
+ if (msg instanceof Error) {
8
+ str = String(msg.message ?? '');
9
+ }
10
+ else if (typeof msg === 'string') {
11
+ str = msg;
12
+ }
13
+ else {
14
+ str = String(msg ?? '');
15
+ }
16
+ str = str.replace(/[&<>"']/g, (c) => HTML_ENTITIES[c]);
17
+ str = str.replace(FORBIDDEN_CHARS, '');
18
+ if (str.length > LIMITS.MAX_SANITIZED_ERROR_LENGTH) {
19
+ str = str.slice(0, LIMITS.MAX_SANITIZED_ERROR_LENGTH - 3) + '...';
20
+ }
21
+ return str;
22
+ }
23
+ export function sanitizeError(err) {
24
+ if (err instanceof Error) {
25
+ const sanitized = new Error(sanitizeErrorMessage(err.message));
26
+ sanitized.name = err.name;
27
+ try {
28
+ sanitized.stack = err.stack;
29
+ }
30
+ catch { }
31
+ if (err instanceof ActlyError) {
32
+ const code = err.code;
33
+ const key = err.key;
34
+ Object.defineProperty(sanitized, 'code', { value: code, enumerable: true });
35
+ if (key !== undefined) {
36
+ Object.defineProperty(sanitized, 'key', { value: key, enumerable: true });
37
+ }
38
+ }
39
+ try {
40
+ Object.defineProperty(sanitized, 'cause', { value: err, enumerable: false });
41
+ }
42
+ catch { }
43
+ return sanitized;
44
+ }
45
+ return sanitizeErrorMessage(err);
46
+ }
@@ -6,54 +6,78 @@ exports.assertTimeoutOptions = assertTimeoutOptions;
6
6
  exports.assertCacheOptions = assertCacheOptions;
7
7
  exports.assertDedupeOptions = assertDedupeOptions;
8
8
  exports.assertOptions = assertOptions;
9
- /**
10
- * Validate user-facing option shapes. Throws `RangeError` / `TypeError` on
11
- * invalid input — these are programmer errors, not runtime failures, so
12
- * throwing (rather than returning an `ActFailure`) is the right call.
13
- *
14
- * Called once at the top of `act()` so policies can assume well-formed input.
15
- */
9
+ exports.assertAuditOptions = assertAuditOptions;
10
+ exports.assertCircuitBreakerOptions = assertCircuitBreakerOptions;
11
+ exports.assertBulkheadOptions = assertBulkheadOptions;
12
+ exports.assertRateLimitOptions = assertRateLimitOptions;
13
+ exports.assertHedgeOptions = assertHedgeOptions;
14
+ const limits_js_1 = require("./limits.js");
15
+ const key_js_1 = require("./key.js");
16
16
  function assertKey(key) {
17
- if (typeof key !== 'string') {
18
- throw new TypeError(`Actly: key must be a string, got ${typeof key}`);
19
- }
20
- if (key.length === 0) {
21
- throw new RangeError("Actly: key must be non-empty. An empty key collapses every caller " +
22
- "onto the same dedupe/cache slot — almost certainly a bug.");
23
- }
24
- // Reject reserved internal prefixes so user keys cannot collide with
25
- // dedupe/cache namespace prefixes.
26
- if (key.startsWith('dedupe:') || key.startsWith('cache:') || key.startsWith('__inflight:')) {
27
- throw new RangeError(`Actly: key must not start with reserved prefix "dedupe:", "cache:", or "__inflight:" (got ${JSON.stringify(key)}).`);
28
- }
17
+ (0, key_js_1.sanitizeKey)(key);
29
18
  }
30
19
  function assertRetryOptions(opts) {
31
20
  if (!Number.isInteger(opts.attempts) || opts.attempts < 1) {
32
21
  throw new RangeError(`Actly: retry.attempts must be a positive integer, got ${opts.attempts}`);
33
22
  }
23
+ if (opts.attempts > limits_js_1.LIMITS.MAX_RETRY_ATTEMPTS) {
24
+ throw new RangeError(`Actly: retry.attempts ${opts.attempts} exceeds limit ${limits_js_1.LIMITS.MAX_RETRY_ATTEMPTS}. ` +
25
+ `If you genuinely need more, use an outer supervisor.`);
26
+ }
34
27
  if (opts.delayMs !== undefined) {
35
- assertNonNegativeFinite('retry.delayMs', opts.delayMs);
28
+ assertNonNegativeFinite('retry.delayMs', opts.delayMs, limits_js_1.LIMITS.MAX_RETRY_DELAY_MS);
36
29
  }
37
30
  if (opts.maxDelay !== undefined) {
38
- assertNonNegativeFinite('retry.maxDelay', opts.maxDelay);
31
+ assertNonNegativeFinite('retry.maxDelay', opts.maxDelay, limits_js_1.LIMITS.MAX_RETRY_DELAY_MS);
32
+ }
33
+ if (opts.backoff !== undefined && !BACKOFF_MODES.has(opts.backoff)) {
34
+ throw new RangeError(`Actly: retry.backoff must be one of ${[...BACKOFF_MODES].map((m) => JSON.stringify(m)).join(' | ')}, ` +
35
+ `got ${JSON.stringify(opts.backoff)}`);
36
+ }
37
+ if (opts.jitter !== undefined && !JITTER_MODES.has(opts.jitter)) {
38
+ throw new RangeError(`Actly: retry.jitter must be one of ${[...JITTER_MODES].map((m) => JSON.stringify(m)).join(' | ')}, ` +
39
+ `got ${JSON.stringify(opts.jitter)}`);
39
40
  }
40
41
  if (opts.shouldRetry !== undefined && typeof opts.shouldRetry !== 'function') {
41
42
  throw new TypeError(`Actly: retry.shouldRetry must be a function, got ${typeof opts.shouldRetry}`);
42
43
  }
44
+ if (opts.shouldRetryResult !== undefined && typeof opts.shouldRetryResult !== 'function') {
45
+ throw new TypeError(`Actly: retry.shouldRetryResult must be a function, got ${typeof opts.shouldRetryResult}`);
46
+ }
47
+ if (opts.backoffFn !== undefined && typeof opts.backoffFn !== 'function') {
48
+ throw new TypeError(`Actly: retry.backoffFn must be a function, got ${typeof opts.backoffFn}`);
49
+ }
50
+ if (opts.dangerouslyUnref !== undefined && typeof opts.dangerouslyUnref !== 'boolean') {
51
+ throw new TypeError(`Actly: retry.dangerouslyUnref must be a boolean, got ${typeof opts.dangerouslyUnref}`);
52
+ }
43
53
  }
44
54
  function assertTimeoutOptions(opts, field) {
45
55
  if (typeof opts.ms !== 'number' || !Number.isFinite(opts.ms) || opts.ms <= 0) {
46
56
  throw new RangeError(`Actly: ${field}.ms must be a positive finite number, got ${opts.ms}`);
47
57
  }
58
+ if (opts.ms > limits_js_1.LIMITS.MAX_TIMEOUT_MS) {
59
+ throw new RangeError(`Actly: ${field}.ms ${opts.ms} exceeds limit ${limits_js_1.LIMITS.MAX_TIMEOUT_MS}.`);
60
+ }
61
+ if (opts.strategy !== undefined && opts.strategy !== 'race' && opts.strategy !== 'cooperative') {
62
+ throw new RangeError(`Actly: ${field}.strategy must be 'race' or 'cooperative', got ${JSON.stringify(opts.strategy)}`);
63
+ }
48
64
  }
49
65
  function assertCacheOptions(opts) {
50
66
  if (typeof opts.ttl !== 'number' || !Number.isFinite(opts.ttl) || opts.ttl <= 0) {
51
67
  throw new RangeError(`Actly: cache.ttl must be a positive finite number, got ${opts.ttl}`);
52
68
  }
69
+ if (opts.ttl > limits_js_1.LIMITS.MAX_CACHE_TTL) {
70
+ throw new RangeError(`Actly: cache.ttl ${opts.ttl} exceeds limit ${limits_js_1.LIMITS.MAX_CACHE_TTL} (~24h).`);
71
+ }
53
72
  }
54
73
  function assertDedupeOptions(opts) {
55
74
  if (opts.inflightTtl !== undefined) {
56
- assertNonNegativeFinite('dedupe.inflightTtl', opts.inflightTtl);
75
+ if (opts.inflightTtl === Number.POSITIVE_INFINITY)
76
+ return;
77
+ if (opts.inflightTtl === 0) {
78
+ throw new RangeError(`Actly: dedupe.inflightTtl must be > 0 (or Infinity). Got 0 — "immediate expiry" is not a valid configuration; use a small positive number (e.g. 1) instead.`);
79
+ }
80
+ assertNonNegativeFinite('dedupe.inflightTtl', opts.inflightTtl, limits_js_1.LIMITS.MAX_INFLIGHT_TTL);
57
81
  }
58
82
  }
59
83
  function assertOptions(options) {
@@ -65,15 +89,128 @@ function assertOptions(options) {
65
89
  assertTimeoutOptions(options.totalTimeout, 'totalTimeout');
66
90
  if (options.cache)
67
91
  assertCacheOptions(options.cache);
68
- if (options.dedupe && typeof options.dedupe !== 'boolean') {
92
+ if (options.dedupe != null && typeof options.dedupe !== 'boolean') {
69
93
  assertDedupeOptions(options.dedupe);
70
94
  }
71
- if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) {
72
- throw new TypeError(`Actly: signal must be an AbortSignal, got ${options.signal === null ? 'null' : typeof options.signal}`);
95
+ if (options.dedupe != null &&
96
+ typeof options.dedupe !== 'boolean' && typeof options.dedupe !== 'object') {
97
+ throw new TypeError(`Actly: dedupe must be a boolean, an object, or undefined, got ${typeof options.dedupe}`);
98
+ }
99
+ if (options.signal !== undefined && options.signal !== null) {
100
+ const s = options.signal;
101
+ if (typeof s !== 'object' ||
102
+ typeof s.aborted !== 'boolean' ||
103
+ typeof s.addEventListener !== 'function' ||
104
+ typeof s.removeEventListener !== 'function') {
105
+ throw new TypeError(`Actly: signal must be an AbortSignal (object with .aborted boolean, .addEventListener function, and .removeEventListener function), got ${options.signal === null ? 'null' : typeof options.signal}`);
106
+ }
107
+ }
108
+ if (options.circuitBreaker)
109
+ assertCircuitBreakerOptions(options.circuitBreaker);
110
+ if (options.bulkhead)
111
+ assertBulkheadOptions(options.bulkhead);
112
+ if (options.rateLimit)
113
+ assertRateLimitOptions(options.rateLimit);
114
+ if (options.hedge)
115
+ assertHedgeOptions(options.hedge);
116
+ if (options.audit)
117
+ assertAuditOptions(options.audit);
118
+ }
119
+ function assertAuditOptions(opts) {
120
+ if (typeof opts.log !== 'function') {
121
+ throw new TypeError(`Actly: audit.log must be a function, got ${opts.log === null ? 'null' : typeof opts.log}`);
122
+ }
123
+ }
124
+ function assertCircuitBreakerOptions(opts) {
125
+ if (!Number.isInteger(opts.threshold) || opts.threshold < 1) {
126
+ throw new RangeError(`Actly: circuitBreaker.threshold must be a positive integer, got ${opts.threshold}`);
127
+ }
128
+ if (typeof opts.cooldownMs !== 'number' || !Number.isFinite(opts.cooldownMs) || opts.cooldownMs <= 0) {
129
+ throw new RangeError(`Actly: circuitBreaker.cooldownMs must be a positive finite number, got ${opts.cooldownMs}`);
130
+ }
131
+ if (opts.resetTimeoutMs !== undefined) {
132
+ if (opts.resetTimeoutMs === Number.POSITIVE_INFINITY)
133
+ return;
134
+ assertNonNegativeFinite('circuitBreaker.resetTimeoutMs', opts.resetTimeoutMs, Number.POSITIVE_INFINITY);
135
+ }
136
+ if (opts.strategy !== undefined && opts.strategy !== 'consecutive' && opts.strategy !== 'count') {
137
+ throw new RangeError(`Actly: circuitBreaker.strategy must be 'consecutive' or 'count', got ${JSON.stringify(opts.strategy)}`);
138
+ }
139
+ if (opts.countSize !== undefined) {
140
+ if (!Number.isInteger(opts.countSize) || opts.countSize < 1) {
141
+ throw new RangeError(`Actly: circuitBreaker.countSize must be a positive integer, got ${opts.countSize}`);
142
+ }
143
+ if (opts.countSize > limits_js_1.LIMITS.MAX_CIRCUIT_BREAKER_WINDOW) {
144
+ throw new RangeError(`Actly: circuitBreaker.countSize ${opts.countSize} exceeds limit ${limits_js_1.LIMITS.MAX_CIRCUIT_BREAKER_WINDOW}.`);
145
+ }
146
+ }
147
+ if (opts.countThreshold !== undefined) {
148
+ if (!Number.isFinite(opts.countThreshold) || opts.countThreshold < 0 || opts.countThreshold > 1) {
149
+ throw new RangeError(`Actly: circuitBreaker.countThreshold must be a finite number between 0 and 1, got ${opts.countThreshold}`);
150
+ }
151
+ }
152
+ if (opts.countMinimumCalls !== undefined) {
153
+ if (!Number.isInteger(opts.countMinimumCalls) || opts.countMinimumCalls < 1) {
154
+ throw new RangeError(`Actly: circuitBreaker.countMinimumCalls must be a positive integer, got ${opts.countMinimumCalls}`);
155
+ }
156
+ if (opts.countMinimumCalls > limits_js_1.LIMITS.MAX_CIRCUIT_BREAKER_WINDOW) {
157
+ throw new RangeError(`Actly: circuitBreaker.countMinimumCalls ${opts.countMinimumCalls} exceeds limit ${limits_js_1.LIMITS.MAX_CIRCUIT_BREAKER_WINDOW}.`);
158
+ }
159
+ }
160
+ }
161
+ function assertBulkheadOptions(opts) {
162
+ if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent < 1) {
163
+ throw new RangeError(`Actly: bulkhead.maxConcurrent must be a positive integer, got ${opts.maxConcurrent}`);
164
+ }
165
+ if (opts.maxConcurrent > limits_js_1.LIMITS.MAX_BULKHEAD_CONCURRENCY) {
166
+ throw new RangeError(`Actly: bulkhead.maxConcurrent ${opts.maxConcurrent} exceeds limit ${limits_js_1.LIMITS.MAX_BULKHEAD_CONCURRENCY}.`);
167
+ }
168
+ if (opts.queueTimeoutMs !== undefined) {
169
+ assertNonNegativeFinite('bulkhead.queueTimeoutMs', opts.queueTimeoutMs, Number.POSITIVE_INFINITY);
170
+ }
171
+ if (opts.maxQueueSize !== undefined) {
172
+ if (opts.maxQueueSize !== Number.POSITIVE_INFINITY &&
173
+ (!Number.isInteger(opts.maxQueueSize) || opts.maxQueueSize < 1)) {
174
+ throw new RangeError(`Actly: bulkhead.maxQueueSize must be a positive integer or Infinity, got ${opts.maxQueueSize}`);
175
+ }
176
+ if (opts.maxQueueSize !== Number.POSITIVE_INFINITY &&
177
+ opts.maxQueueSize > limits_js_1.LIMITS.MAX_BULKHEAD_QUEUE) {
178
+ throw new RangeError(`Actly: bulkhead.maxQueueSize ${opts.maxQueueSize} exceeds limit ${limits_js_1.LIMITS.MAX_BULKHEAD_QUEUE}.`);
179
+ }
73
180
  }
74
181
  }
75
- function assertNonNegativeFinite(field, value) {
182
+ function assertRateLimitOptions(opts) {
183
+ if (!Number.isInteger(opts.maxCalls) || opts.maxCalls < 1) {
184
+ throw new RangeError(`Actly: rateLimit.maxCalls must be a positive integer, got ${opts.maxCalls}`);
185
+ }
186
+ if (opts.maxCalls > limits_js_1.LIMITS.MAX_RATE_LIMIT_CALLS) {
187
+ throw new RangeError(`Actly: rateLimit.maxCalls ${opts.maxCalls} exceeds limit ${limits_js_1.LIMITS.MAX_RATE_LIMIT_CALLS}.`);
188
+ }
189
+ if (typeof opts.windowMs !== 'number' || !Number.isFinite(opts.windowMs) || opts.windowMs <= 0) {
190
+ throw new RangeError(`Actly: rateLimit.windowMs must be a positive finite number, got ${opts.windowMs}`);
191
+ }
192
+ }
193
+ function assertHedgeOptions(opts) {
194
+ if (typeof opts.delayMs !== 'number' || !Number.isFinite(opts.delayMs) || opts.delayMs <= 0) {
195
+ throw new RangeError(`Actly: hedge.delayMs must be a positive finite number, got ${opts.delayMs}`);
196
+ }
197
+ if (opts.delayMs > limits_js_1.LIMITS.MAX_HEDGE_DELAY_MS) {
198
+ throw new RangeError(`Actly: hedge.delayMs ${opts.delayMs} exceeds limit ${limits_js_1.LIMITS.MAX_HEDGE_DELAY_MS}.`);
199
+ }
200
+ if (opts.placement !== undefined && opts.placement !== 'outside-retry' && opts.placement !== 'inside-retry') {
201
+ throw new RangeError(`Actly: hedge.placement must be 'outside-retry' or 'inside-retry', got ${JSON.stringify(opts.placement)}`);
202
+ }
203
+ if (opts.keepLoser !== undefined && typeof opts.keepLoser !== 'boolean') {
204
+ throw new TypeError(`Actly: hedge.keepLoser must be a boolean, got ${typeof opts.keepLoser}`);
205
+ }
206
+ }
207
+ function assertNonNegativeFinite(field, value, max) {
76
208
  if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
77
209
  throw new RangeError(`Actly: ${field} must be a non-negative finite number, got ${value}`);
78
210
  }
211
+ if (value > max) {
212
+ throw new RangeError(`Actly: ${field} ${value} exceeds limit ${max}.`);
213
+ }
79
214
  }
215
+ const BACKOFF_MODES = new Set(['none', 'linear', 'exponential']);
216
+ const JITTER_MODES = new Set(['none', 'full', 'equal', 'decorrelated']);
@@ -1,15 +1,13 @@
1
- import type { ActOptions, CacheOptions, DedupeOptions, RetryOptions, TimeoutOptions } from '../types/index.js';
2
- /**
3
- * Validate user-facing option shapes. Throws `RangeError` / `TypeError` on
4
- * invalid input — these are programmer errors, not runtime failures, so
5
- * throwing (rather than returning an `ActFailure`) is the right call.
6
- *
7
- * Called once at the top of `act()` so policies can assume well-formed input.
8
- */
1
+ import type { ActOptions, AuditOptions, BulkheadOptions, CacheOptions, CircuitBreakerOptions, DedupeOptions, HedgeOptions, RateLimitOptions, RetryOptions, TimeoutOptions } from '../types/index.js';
9
2
  export declare function assertKey(key: string): void;
10
3
  export declare function assertRetryOptions(opts: RetryOptions): void;
11
4
  export declare function assertTimeoutOptions(opts: TimeoutOptions, field: string): void;
12
5
  export declare function assertCacheOptions(opts: CacheOptions): void;
13
6
  export declare function assertDedupeOptions(opts: DedupeOptions): void;
14
7
  export declare function assertOptions(options: ActOptions): void;
8
+ export declare function assertAuditOptions(opts: AuditOptions): void;
9
+ export declare function assertCircuitBreakerOptions(opts: CircuitBreakerOptions): void;
10
+ export declare function assertBulkheadOptions(opts: BulkheadOptions): void;
11
+ export declare function assertRateLimitOptions(opts: RateLimitOptions): void;
12
+ export declare function assertHedgeOptions(opts: HedgeOptions): void;
15
13
  //# sourceMappingURL=validate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/utils/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,cAAc,EACf,MAAM,mBAAmB,CAAA;AAE1B;;;;;;GAMG;AAEH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAiB3C;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAiB3D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAM9E;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAM3D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAI7D;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAavD"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/utils/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,qBAAqB,EACrB,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACf,MAAM,mBAAmB,CAAA;AAW1B,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAE3C;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAkD3D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAgB9E;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAW3D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAa7D;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAoCvD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAM3D;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,qBAAqB,GAAG,IAAI,CAuD7E;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CA4BjE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAgBnE;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAuB3D"}
@@ -1,51 +1,70 @@
1
- /**
2
- * Validate user-facing option shapes. Throws `RangeError` / `TypeError` on
3
- * invalid input — these are programmer errors, not runtime failures, so
4
- * throwing (rather than returning an `ActFailure`) is the right call.
5
- *
6
- * Called once at the top of `act()` so policies can assume well-formed input.
7
- */
1
+ import { LIMITS } from './limits.js';
2
+ import { sanitizeKey } from './key.js';
8
3
  export function assertKey(key) {
9
- if (typeof key !== 'string') {
10
- throw new TypeError(`Actly: key must be a string, got ${typeof key}`);
11
- }
12
- if (key.length === 0) {
13
- throw new RangeError("Actly: key must be non-empty. An empty key collapses every caller " +
14
- "onto the same dedupe/cache slot — almost certainly a bug.");
15
- }
16
- // Reject reserved internal prefixes so user keys cannot collide with
17
- // dedupe/cache namespace prefixes.
18
- if (key.startsWith('dedupe:') || key.startsWith('cache:') || key.startsWith('__inflight:')) {
19
- throw new RangeError(`Actly: key must not start with reserved prefix "dedupe:", "cache:", or "__inflight:" (got ${JSON.stringify(key)}).`);
20
- }
4
+ sanitizeKey(key);
21
5
  }
22
6
  export function assertRetryOptions(opts) {
23
7
  if (!Number.isInteger(opts.attempts) || opts.attempts < 1) {
24
8
  throw new RangeError(`Actly: retry.attempts must be a positive integer, got ${opts.attempts}`);
25
9
  }
10
+ if (opts.attempts > LIMITS.MAX_RETRY_ATTEMPTS) {
11
+ throw new RangeError(`Actly: retry.attempts ${opts.attempts} exceeds limit ${LIMITS.MAX_RETRY_ATTEMPTS}. ` +
12
+ `If you genuinely need more, use an outer supervisor.`);
13
+ }
26
14
  if (opts.delayMs !== undefined) {
27
- assertNonNegativeFinite('retry.delayMs', opts.delayMs);
15
+ assertNonNegativeFinite('retry.delayMs', opts.delayMs, LIMITS.MAX_RETRY_DELAY_MS);
28
16
  }
29
17
  if (opts.maxDelay !== undefined) {
30
- assertNonNegativeFinite('retry.maxDelay', opts.maxDelay);
18
+ assertNonNegativeFinite('retry.maxDelay', opts.maxDelay, LIMITS.MAX_RETRY_DELAY_MS);
19
+ }
20
+ if (opts.backoff !== undefined && !BACKOFF_MODES.has(opts.backoff)) {
21
+ throw new RangeError(`Actly: retry.backoff must be one of ${[...BACKOFF_MODES].map((m) => JSON.stringify(m)).join(' | ')}, ` +
22
+ `got ${JSON.stringify(opts.backoff)}`);
23
+ }
24
+ if (opts.jitter !== undefined && !JITTER_MODES.has(opts.jitter)) {
25
+ throw new RangeError(`Actly: retry.jitter must be one of ${[...JITTER_MODES].map((m) => JSON.stringify(m)).join(' | ')}, ` +
26
+ `got ${JSON.stringify(opts.jitter)}`);
31
27
  }
32
28
  if (opts.shouldRetry !== undefined && typeof opts.shouldRetry !== 'function') {
33
29
  throw new TypeError(`Actly: retry.shouldRetry must be a function, got ${typeof opts.shouldRetry}`);
34
30
  }
31
+ if (opts.shouldRetryResult !== undefined && typeof opts.shouldRetryResult !== 'function') {
32
+ throw new TypeError(`Actly: retry.shouldRetryResult must be a function, got ${typeof opts.shouldRetryResult}`);
33
+ }
34
+ if (opts.backoffFn !== undefined && typeof opts.backoffFn !== 'function') {
35
+ throw new TypeError(`Actly: retry.backoffFn must be a function, got ${typeof opts.backoffFn}`);
36
+ }
37
+ if (opts.dangerouslyUnref !== undefined && typeof opts.dangerouslyUnref !== 'boolean') {
38
+ throw new TypeError(`Actly: retry.dangerouslyUnref must be a boolean, got ${typeof opts.dangerouslyUnref}`);
39
+ }
35
40
  }
36
41
  export function assertTimeoutOptions(opts, field) {
37
42
  if (typeof opts.ms !== 'number' || !Number.isFinite(opts.ms) || opts.ms <= 0) {
38
43
  throw new RangeError(`Actly: ${field}.ms must be a positive finite number, got ${opts.ms}`);
39
44
  }
45
+ if (opts.ms > LIMITS.MAX_TIMEOUT_MS) {
46
+ throw new RangeError(`Actly: ${field}.ms ${opts.ms} exceeds limit ${LIMITS.MAX_TIMEOUT_MS}.`);
47
+ }
48
+ if (opts.strategy !== undefined && opts.strategy !== 'race' && opts.strategy !== 'cooperative') {
49
+ throw new RangeError(`Actly: ${field}.strategy must be 'race' or 'cooperative', got ${JSON.stringify(opts.strategy)}`);
50
+ }
40
51
  }
41
52
  export function assertCacheOptions(opts) {
42
53
  if (typeof opts.ttl !== 'number' || !Number.isFinite(opts.ttl) || opts.ttl <= 0) {
43
54
  throw new RangeError(`Actly: cache.ttl must be a positive finite number, got ${opts.ttl}`);
44
55
  }
56
+ if (opts.ttl > LIMITS.MAX_CACHE_TTL) {
57
+ throw new RangeError(`Actly: cache.ttl ${opts.ttl} exceeds limit ${LIMITS.MAX_CACHE_TTL} (~24h).`);
58
+ }
45
59
  }
46
60
  export function assertDedupeOptions(opts) {
47
61
  if (opts.inflightTtl !== undefined) {
48
- assertNonNegativeFinite('dedupe.inflightTtl', opts.inflightTtl);
62
+ if (opts.inflightTtl === Number.POSITIVE_INFINITY)
63
+ return;
64
+ if (opts.inflightTtl === 0) {
65
+ throw new RangeError(`Actly: dedupe.inflightTtl must be > 0 (or Infinity). Got 0 — "immediate expiry" is not a valid configuration; use a small positive number (e.g. 1) instead.`);
66
+ }
67
+ assertNonNegativeFinite('dedupe.inflightTtl', opts.inflightTtl, LIMITS.MAX_INFLIGHT_TTL);
49
68
  }
50
69
  }
51
70
  export function assertOptions(options) {
@@ -57,16 +76,128 @@ export function assertOptions(options) {
57
76
  assertTimeoutOptions(options.totalTimeout, 'totalTimeout');
58
77
  if (options.cache)
59
78
  assertCacheOptions(options.cache);
60
- if (options.dedupe && typeof options.dedupe !== 'boolean') {
79
+ if (options.dedupe != null && typeof options.dedupe !== 'boolean') {
61
80
  assertDedupeOptions(options.dedupe);
62
81
  }
63
- if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) {
64
- throw new TypeError(`Actly: signal must be an AbortSignal, got ${options.signal === null ? 'null' : typeof options.signal}`);
82
+ if (options.dedupe != null &&
83
+ typeof options.dedupe !== 'boolean' && typeof options.dedupe !== 'object') {
84
+ throw new TypeError(`Actly: dedupe must be a boolean, an object, or undefined, got ${typeof options.dedupe}`);
85
+ }
86
+ if (options.signal !== undefined && options.signal !== null) {
87
+ const s = options.signal;
88
+ if (typeof s !== 'object' ||
89
+ typeof s.aborted !== 'boolean' ||
90
+ typeof s.addEventListener !== 'function' ||
91
+ typeof s.removeEventListener !== 'function') {
92
+ throw new TypeError(`Actly: signal must be an AbortSignal (object with .aborted boolean, .addEventListener function, and .removeEventListener function), got ${options.signal === null ? 'null' : typeof options.signal}`);
93
+ }
94
+ }
95
+ if (options.circuitBreaker)
96
+ assertCircuitBreakerOptions(options.circuitBreaker);
97
+ if (options.bulkhead)
98
+ assertBulkheadOptions(options.bulkhead);
99
+ if (options.rateLimit)
100
+ assertRateLimitOptions(options.rateLimit);
101
+ if (options.hedge)
102
+ assertHedgeOptions(options.hedge);
103
+ if (options.audit)
104
+ assertAuditOptions(options.audit);
105
+ }
106
+ export function assertAuditOptions(opts) {
107
+ if (typeof opts.log !== 'function') {
108
+ throw new TypeError(`Actly: audit.log must be a function, got ${opts.log === null ? 'null' : typeof opts.log}`);
109
+ }
110
+ }
111
+ export function assertCircuitBreakerOptions(opts) {
112
+ if (!Number.isInteger(opts.threshold) || opts.threshold < 1) {
113
+ throw new RangeError(`Actly: circuitBreaker.threshold must be a positive integer, got ${opts.threshold}`);
114
+ }
115
+ if (typeof opts.cooldownMs !== 'number' || !Number.isFinite(opts.cooldownMs) || opts.cooldownMs <= 0) {
116
+ throw new RangeError(`Actly: circuitBreaker.cooldownMs must be a positive finite number, got ${opts.cooldownMs}`);
117
+ }
118
+ if (opts.resetTimeoutMs !== undefined) {
119
+ if (opts.resetTimeoutMs === Number.POSITIVE_INFINITY)
120
+ return;
121
+ assertNonNegativeFinite('circuitBreaker.resetTimeoutMs', opts.resetTimeoutMs, Number.POSITIVE_INFINITY);
122
+ }
123
+ if (opts.strategy !== undefined && opts.strategy !== 'consecutive' && opts.strategy !== 'count') {
124
+ throw new RangeError(`Actly: circuitBreaker.strategy must be 'consecutive' or 'count', got ${JSON.stringify(opts.strategy)}`);
125
+ }
126
+ if (opts.countSize !== undefined) {
127
+ if (!Number.isInteger(opts.countSize) || opts.countSize < 1) {
128
+ throw new RangeError(`Actly: circuitBreaker.countSize must be a positive integer, got ${opts.countSize}`);
129
+ }
130
+ if (opts.countSize > LIMITS.MAX_CIRCUIT_BREAKER_WINDOW) {
131
+ throw new RangeError(`Actly: circuitBreaker.countSize ${opts.countSize} exceeds limit ${LIMITS.MAX_CIRCUIT_BREAKER_WINDOW}.`);
132
+ }
133
+ }
134
+ if (opts.countThreshold !== undefined) {
135
+ if (!Number.isFinite(opts.countThreshold) || opts.countThreshold < 0 || opts.countThreshold > 1) {
136
+ throw new RangeError(`Actly: circuitBreaker.countThreshold must be a finite number between 0 and 1, got ${opts.countThreshold}`);
137
+ }
138
+ }
139
+ if (opts.countMinimumCalls !== undefined) {
140
+ if (!Number.isInteger(opts.countMinimumCalls) || opts.countMinimumCalls < 1) {
141
+ throw new RangeError(`Actly: circuitBreaker.countMinimumCalls must be a positive integer, got ${opts.countMinimumCalls}`);
142
+ }
143
+ if (opts.countMinimumCalls > LIMITS.MAX_CIRCUIT_BREAKER_WINDOW) {
144
+ throw new RangeError(`Actly: circuitBreaker.countMinimumCalls ${opts.countMinimumCalls} exceeds limit ${LIMITS.MAX_CIRCUIT_BREAKER_WINDOW}.`);
145
+ }
146
+ }
147
+ }
148
+ export function assertBulkheadOptions(opts) {
149
+ if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent < 1) {
150
+ throw new RangeError(`Actly: bulkhead.maxConcurrent must be a positive integer, got ${opts.maxConcurrent}`);
151
+ }
152
+ if (opts.maxConcurrent > LIMITS.MAX_BULKHEAD_CONCURRENCY) {
153
+ throw new RangeError(`Actly: bulkhead.maxConcurrent ${opts.maxConcurrent} exceeds limit ${LIMITS.MAX_BULKHEAD_CONCURRENCY}.`);
154
+ }
155
+ if (opts.queueTimeoutMs !== undefined) {
156
+ assertNonNegativeFinite('bulkhead.queueTimeoutMs', opts.queueTimeoutMs, Number.POSITIVE_INFINITY);
157
+ }
158
+ if (opts.maxQueueSize !== undefined) {
159
+ if (opts.maxQueueSize !== Number.POSITIVE_INFINITY &&
160
+ (!Number.isInteger(opts.maxQueueSize) || opts.maxQueueSize < 1)) {
161
+ throw new RangeError(`Actly: bulkhead.maxQueueSize must be a positive integer or Infinity, got ${opts.maxQueueSize}`);
162
+ }
163
+ if (opts.maxQueueSize !== Number.POSITIVE_INFINITY &&
164
+ opts.maxQueueSize > LIMITS.MAX_BULKHEAD_QUEUE) {
165
+ throw new RangeError(`Actly: bulkhead.maxQueueSize ${opts.maxQueueSize} exceeds limit ${LIMITS.MAX_BULKHEAD_QUEUE}.`);
166
+ }
65
167
  }
66
168
  }
67
- function assertNonNegativeFinite(field, value) {
169
+ export function assertRateLimitOptions(opts) {
170
+ if (!Number.isInteger(opts.maxCalls) || opts.maxCalls < 1) {
171
+ throw new RangeError(`Actly: rateLimit.maxCalls must be a positive integer, got ${opts.maxCalls}`);
172
+ }
173
+ if (opts.maxCalls > LIMITS.MAX_RATE_LIMIT_CALLS) {
174
+ throw new RangeError(`Actly: rateLimit.maxCalls ${opts.maxCalls} exceeds limit ${LIMITS.MAX_RATE_LIMIT_CALLS}.`);
175
+ }
176
+ if (typeof opts.windowMs !== 'number' || !Number.isFinite(opts.windowMs) || opts.windowMs <= 0) {
177
+ throw new RangeError(`Actly: rateLimit.windowMs must be a positive finite number, got ${opts.windowMs}`);
178
+ }
179
+ }
180
+ export function assertHedgeOptions(opts) {
181
+ if (typeof opts.delayMs !== 'number' || !Number.isFinite(opts.delayMs) || opts.delayMs <= 0) {
182
+ throw new RangeError(`Actly: hedge.delayMs must be a positive finite number, got ${opts.delayMs}`);
183
+ }
184
+ if (opts.delayMs > LIMITS.MAX_HEDGE_DELAY_MS) {
185
+ throw new RangeError(`Actly: hedge.delayMs ${opts.delayMs} exceeds limit ${LIMITS.MAX_HEDGE_DELAY_MS}.`);
186
+ }
187
+ if (opts.placement !== undefined && opts.placement !== 'outside-retry' && opts.placement !== 'inside-retry') {
188
+ throw new RangeError(`Actly: hedge.placement must be 'outside-retry' or 'inside-retry', got ${JSON.stringify(opts.placement)}`);
189
+ }
190
+ if (opts.keepLoser !== undefined && typeof opts.keepLoser !== 'boolean') {
191
+ throw new TypeError(`Actly: hedge.keepLoser must be a boolean, got ${typeof opts.keepLoser}`);
192
+ }
193
+ }
194
+ function assertNonNegativeFinite(field, value, max) {
68
195
  if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
69
196
  throw new RangeError(`Actly: ${field} must be a non-negative finite number, got ${value}`);
70
197
  }
198
+ if (value > max) {
199
+ throw new RangeError(`Actly: ${field} ${value} exceeds limit ${max}.`);
200
+ }
71
201
  }
72
- //# sourceMappingURL=validate.js.map
202
+ const BACKOFF_MODES = new Set(['none', 'linear', 'exponential']);
203
+ const JITTER_MODES = new Set(['none', 'full', 'equal', 'decorrelated']);