actly 1.2.0 → 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 (151) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/MIGRATION.md +141 -0
  3. package/README.md +326 -218
  4. package/SECURITY.md +56 -0
  5. package/dist/core/act.cjs +406 -220
  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 +405 -222
  9. package/dist/core/executor.cjs +0 -27
  10. package/dist/core/executor.d.ts +0 -41
  11. package/dist/core/executor.d.ts.map +1 -1
  12. package/dist/core/executor.js +0 -28
  13. package/dist/core/health.cjs +146 -17
  14. package/dist/core/health.d.ts +20 -5
  15. package/dist/core/health.d.ts.map +1 -1
  16. package/dist/core/health.js +140 -18
  17. package/dist/core/shutdown.cjs +19 -8
  18. package/dist/core/shutdown.d.ts +1 -4
  19. package/dist/core/shutdown.d.ts.map +1 -1
  20. package/dist/core/shutdown.js +18 -9
  21. package/dist/core/tenant.cjs +61 -19
  22. package/dist/core/tenant.d.ts +5 -10
  23. package/dist/core/tenant.d.ts.map +1 -1
  24. package/dist/core/tenant.js +61 -20
  25. package/dist/errors.cjs +70 -103
  26. package/dist/errors.d.ts +33 -90
  27. package/dist/errors.d.ts.map +1 -1
  28. package/dist/errors.js +66 -103
  29. package/dist/index.cjs +25 -22
  30. package/dist/index.d.ts +12 -8
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +7 -15
  33. package/dist/observability.cjs +0 -39
  34. package/dist/observability.d.ts +19 -78
  35. package/dist/observability.d.ts.map +1 -1
  36. package/dist/observability.js +0 -40
  37. package/dist/policies/bulkhead.cjs +44 -4
  38. package/dist/policies/bulkhead.d.ts +0 -0
  39. package/dist/policies/bulkhead.d.ts.map +1 -1
  40. package/dist/policies/bulkhead.js +44 -5
  41. package/dist/policies/cache.cjs +22 -142
  42. package/dist/policies/cache.d.ts +0 -46
  43. package/dist/policies/cache.d.ts.map +1 -1
  44. package/dist/policies/cache.js +22 -143
  45. package/dist/policies/circuitBreaker.cjs +125 -31
  46. package/dist/policies/circuitBreaker.d.ts +0 -0
  47. package/dist/policies/circuitBreaker.d.ts.map +1 -1
  48. package/dist/policies/circuitBreaker.js +125 -32
  49. package/dist/policies/dedupe.cjs +19 -66
  50. package/dist/policies/dedupe.d.ts +0 -23
  51. package/dist/policies/dedupe.d.ts.map +1 -1
  52. package/dist/policies/dedupe.js +19 -67
  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 +15 -5
  58. package/dist/policies/rateLimit.d.ts +0 -0
  59. package/dist/policies/rateLimit.d.ts.map +1 -1
  60. package/dist/policies/rateLimit.js +15 -6
  61. package/dist/policies/retry.cjs +100 -65
  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 +100 -66
  65. package/dist/policies/timeout.cjs +32 -60
  66. package/dist/policies/timeout.d.ts +0 -13
  67. package/dist/policies/timeout.d.ts.map +1 -1
  68. package/dist/policies/timeout.js +32 -71
  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 +75 -88
  74. package/dist/stores/memory.d.ts +7 -74
  75. package/dist/stores/memory.d.ts.map +1 -1
  76. package/dist/stores/memory.js +75 -89
  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 +17 -307
  83. package/dist/types/index.d.ts.map +1 -1
  84. package/dist/types/index.js +0 -2
  85. package/dist/utils/abort.cjs +14 -111
  86. package/dist/utils/abort.d.ts +3 -74
  87. package/dist/utils/abort.d.ts.map +1 -1
  88. package/dist/utils/abort.js +14 -112
  89. package/dist/utils/abortPool.cjs +1 -2
  90. package/dist/utils/abortPool.d.ts +0 -0
  91. package/dist/utils/abortPool.d.ts.map +1 -1
  92. package/dist/utils/abortPool.js +1 -3
  93. package/dist/utils/backoff.cjs +3 -21
  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 +3 -22
  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 +13 -45
  102. package/dist/utils/key.d.ts +0 -27
  103. package/dist/utils/key.d.ts.map +1 -1
  104. package/dist/utils/key.js +13 -46
  105. package/dist/utils/limits.cjs +9 -27
  106. package/dist/utils/limits.d.ts +9 -27
  107. package/dist/utils/limits.d.ts.map +1 -1
  108. package/dist/utils/limits.js +9 -28
  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 +22 -1
  114. package/dist/utils/sanitize.d.ts +0 -0
  115. package/dist/utils/sanitize.d.ts.map +1 -1
  116. package/dist/utils/sanitize.js +22 -2
  117. package/dist/utils/validate.cjs +81 -18
  118. package/dist/utils/validate.d.ts +6 -19
  119. package/dist/utils/validate.d.ts.map +1 -1
  120. package/dist/utils/validate.js +81 -19
  121. package/package.json +38 -4
  122. package/dist/core/act.js.map +0 -1
  123. package/dist/core/executor.js.map +0 -1
  124. package/dist/core/health.js.map +0 -1
  125. package/dist/core/shutdown.js.map +0 -1
  126. package/dist/core/tenant.js.map +0 -1
  127. package/dist/errors.js.map +0 -1
  128. package/dist/index.js.map +0 -1
  129. package/dist/observability.js.map +0 -1
  130. package/dist/policies/bulkhead.js.map +0 -1
  131. package/dist/policies/cache.js.map +0 -1
  132. package/dist/policies/circuitBreaker.js.map +0 -1
  133. package/dist/policies/dedupe.js.map +0 -1
  134. package/dist/policies/rateLimit.js.map +0 -1
  135. package/dist/policies/retry.js.map +0 -1
  136. package/dist/policies/timeout.js.map +0 -1
  137. package/dist/state/store.cjs +0 -14
  138. package/dist/state/store.d.ts +0 -11
  139. package/dist/state/store.d.ts.map +0 -1
  140. package/dist/state/store.js +0 -11
  141. package/dist/state/store.js.map +0 -1
  142. package/dist/stores/base.js.map +0 -1
  143. package/dist/stores/memory.js.map +0 -1
  144. package/dist/types/index.js.map +0 -1
  145. package/dist/utils/abort.js.map +0 -1
  146. package/dist/utils/abortPool.js.map +0 -1
  147. package/dist/utils/backoff.js.map +0 -1
  148. package/dist/utils/key.js.map +0 -1
  149. package/dist/utils/limits.js.map +0 -1
  150. package/dist/utils/sanitize.js.map +0 -1
  151. package/dist/utils/validate.js.map +0 -1
@@ -1,73 +1,25 @@
1
- /**
2
- * Observability hooks.
3
- *
4
- * Eight event types covering the entire lifecycle of an `act()` call.
5
- *
6
- * # Contract
7
- *
8
- * When `options.observability` is `null`/`undefined` (the common case),
9
- * no event objects are allocated and no function calls are made. The
10
- * hot path is a single null-check per policy decision.
11
- *
12
- * When hooks ARE registered, events are allocated lazily — only when the
13
- * corresponding event actually fires. A cache hit doesn't allocate an
14
- * `onRetry` event, for example.
15
- *
16
- * # Event shape
17
- *
18
- * Every event carries `key`, `traceId`, and `timestamp` for correlation.
19
- * Event-specific fields are on the same object (no nesting) for flat
20
- * destructuring in user code.
21
- *
22
- * # Ordering
23
- *
24
- * For a successful fresh call with retries:
25
- * onAttempt (1) → onAttempt (2) → onRetry (1→2) → onAttempt (3) → onFinalSuccess
26
- *
27
- * For a cache hit:
28
- * onCacheHit → onFinalSuccess
29
- *
30
- * For a dedupe joiner:
31
- * onDedupeJoin → onFinalSuccess (or onFinalFailure)
32
- *
33
- * For a timeout:
34
- * onAttempt → onTimeout → onFinalFailure
35
- */
36
- import type { ActSource } from './types/index.js';
37
- /** Stable discriminator for telemetry. */
38
- export type ActlyEventType = 'attempt' | 'retry' | 'cache-hit' | 'cache-miss' | 'dedupe-join' | 'timeout' | 'final-success' | 'final-failure';
39
- /** Common fields on every event. */
1
+ import type { ActSource, ActlyFailedBy } from './types/index.js';
2
+ export type ActlyEventType = 'attempt' | 'retry' | 'cache-hit' | 'cache-miss' | 'dedupe-join' | 'timeout' | 'final-success' | 'final-failure' | 'backpressure' | 'watchdog';
40
3
  export interface ActlyEventBase {
41
- /** The `key` passed to `act()`. */
42
4
  readonly key: string;
43
- /** Auto-generated trace ID (or user-supplied via `options.traceId`). */
44
5
  readonly traceId: string;
45
- /** Event timestamp (ms since epoch). */
46
6
  readonly timestamp: number;
47
- /** Discriminator for switch statements. */
48
7
  readonly type: ActlyEventType;
49
8
  }
50
9
  export interface AttemptEvent extends ActlyEventBase {
51
10
  readonly type: 'attempt';
52
- /** 1-based attempt number. */
53
11
  readonly attempt: number;
54
- /** Duration of this attempt in ms (set after attempt settles). */
55
12
  readonly durationMs?: number;
56
- /** Error from this attempt, if it failed. */
57
13
  readonly error?: unknown;
58
14
  }
59
15
  export interface RetryEvent extends ActlyEventBase {
60
16
  readonly type: 'retry';
61
- /** The attempt that just failed. */
62
17
  readonly attempt: number;
63
- /** The delay (ms) before the next attempt. */
64
18
  readonly delayMs: number;
65
- /** Error that triggered the retry. */
66
19
  readonly error: unknown;
67
20
  }
68
21
  export interface CacheHitEvent extends ActlyEventBase {
69
22
  readonly type: 'cache-hit';
70
- /** Age of the cached value in ms. */
71
23
  readonly ageMs: number;
72
24
  }
73
25
  export interface CacheMissEvent extends ActlyEventBase {
@@ -75,41 +27,40 @@ export interface CacheMissEvent extends ActlyEventBase {
75
27
  }
76
28
  export interface DedupeJoinEvent extends ActlyEventBase {
77
29
  readonly type: 'dedupe-join';
78
- /** This caller's position in the joiner queue (1 = first joiner). */
79
30
  readonly joinerPosition: number;
80
31
  }
81
32
  export interface TimeoutEvent extends ActlyEventBase {
82
33
  readonly type: 'timeout';
83
- /** Which deadline fired. */
84
34
  readonly kind: 'per-attempt' | 'total';
85
- /** The configured ms. */
86
35
  readonly ms: number;
87
36
  }
88
37
  export interface FinalSuccessEvent extends ActlyEventBase {
89
38
  readonly type: 'final-success';
90
- /** Where the value came from. */
91
39
  readonly source: ActSource;
92
- /** Total attempts made (0 for cache hit). */
93
40
  readonly attempts: number;
94
- /** Total wall-clock duration of the act() call. */
95
41
  readonly durationMs: number;
96
42
  }
97
43
  export interface FinalFailureEvent extends ActlyEventBase {
98
44
  readonly type: 'final-failure';
99
- /** Total attempts made. */
100
45
  readonly attempts: number;
101
- /** Total wall-clock duration of the act() call. */
102
46
  readonly durationMs: number;
103
- /** Stable reason for failure — use for telemetry tags. */
104
- readonly failedBy: 'abort' | 'timeout' | 'total-timeout' | 'retry-exhausted' | 'fn-error' | 'validation';
105
- /** The final error. */
47
+ readonly failedBy: ActlyFailedBy;
106
48
  readonly error: unknown;
107
49
  }
108
- export type ActlyEvent = AttemptEvent | RetryEvent | CacheHitEvent | CacheMissEvent | DedupeJoinEvent | TimeoutEvent | FinalSuccessEvent | FinalFailureEvent;
109
- /**
110
- * User-supplied observability hooks. All optional. When absent, zero
111
- * overhead is incurred on the hot path.
112
- */
50
+ export type ActlyEvent = AttemptEvent | RetryEvent | CacheHitEvent | CacheMissEvent | DedupeJoinEvent | TimeoutEvent | FinalSuccessEvent | FinalFailureEvent | BackpressureEvent | WatchdogEvent;
51
+ export interface BackpressureEvent extends ActlyEventBase {
52
+ readonly type: 'backpressure';
53
+ readonly source: 'bulkhead';
54
+ readonly queueLength: number;
55
+ readonly maxConcurrent: number;
56
+ readonly maxQueueSize: number;
57
+ readonly utilization: number;
58
+ }
59
+ export interface WatchdogEvent extends ActlyEventBase {
60
+ readonly type: 'watchdog';
61
+ readonly elapsedMs: number;
62
+ readonly scope: string;
63
+ }
113
64
  export interface ObservabilityHooks {
114
65
  onAttempt?: (event: AttemptEvent) => void;
115
66
  onRetry?: (event: RetryEvent) => void;
@@ -119,24 +70,14 @@ export interface ObservabilityHooks {
119
70
  onTimeout?: (event: TimeoutEvent) => void;
120
71
  onFinalSuccess?: (event: FinalSuccessEvent) => void;
121
72
  onFinalFailure?: (event: FinalFailureEvent) => void;
73
+ onBackpressure?: (event: BackpressureEvent) => void;
74
+ onWatchdog?: (event: WatchdogEvent) => void;
122
75
  }
123
- /**
124
- * Internal: thread observability through the policy chain without changing
125
- * every policy's signature. We attach it to `PolicyContext` as an optional
126
- * field — policies that emit events check for its presence.
127
- *
128
- * `traceId` is also stored here so policies can include it in events.
129
- */
130
76
  export interface ObservabilityContext {
131
77
  traceId: string;
132
78
  hooks: ObservabilityHooks;
133
- /** Counter for dedupe joiner position. Per-key, but we approximate by call. */
134
79
  joinerCounter: number;
135
80
  }
136
- /**
137
- * Quick null-check helper. Policies call this once at decision points; if
138
- * it returns false, no further observability work is done.
139
- */
140
81
  export declare function hasObservers(ctx: {
141
82
  observability?: ObservabilityContext;
142
83
  }): ctx is {
@@ -1 +1 @@
1
- {"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"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"}
@@ -1,43 +1,3 @@
1
- /**
2
- * Observability hooks.
3
- *
4
- * Eight event types covering the entire lifecycle of an `act()` call.
5
- *
6
- * # Contract
7
- *
8
- * When `options.observability` is `null`/`undefined` (the common case),
9
- * no event objects are allocated and no function calls are made. The
10
- * hot path is a single null-check per policy decision.
11
- *
12
- * When hooks ARE registered, events are allocated lazily — only when the
13
- * corresponding event actually fires. A cache hit doesn't allocate an
14
- * `onRetry` event, for example.
15
- *
16
- * # Event shape
17
- *
18
- * Every event carries `key`, `traceId`, and `timestamp` for correlation.
19
- * Event-specific fields are on the same object (no nesting) for flat
20
- * destructuring in user code.
21
- *
22
- * # Ordering
23
- *
24
- * For a successful fresh call with retries:
25
- * onAttempt (1) → onAttempt (2) → onRetry (1→2) → onAttempt (3) → onFinalSuccess
26
- *
27
- * For a cache hit:
28
- * onCacheHit → onFinalSuccess
29
- *
30
- * For a dedupe joiner:
31
- * onDedupeJoin → onFinalSuccess (or onFinalFailure)
32
- *
33
- * For a timeout:
34
- * onAttempt → onTimeout → onFinalFailure
35
- */
36
- /**
37
- * Quick null-check helper. Policies call this once at decision points; if
38
- * it returns false, no further observability work is done.
39
- */
40
1
  export function hasObservers(ctx) {
41
2
  return ctx.observability != null;
42
3
  }
43
- //# sourceMappingURL=observability.js.map
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.bulkheadPolicy = bulkheadPolicy;
4
4
  const executor_js_1 = require("../core/executor.js");
5
5
  const errors_js_1 = require("../errors.js");
6
+ const safeCall_js_1 = require("../utils/safeCall.js");
6
7
  const NS = 'bulk:';
7
8
  function getState(store, key) {
8
9
  return store.get(NS + key) ?? { active: 0, queue: [] };
@@ -13,12 +14,12 @@ function setState(store, key, state) {
13
14
  function bulkheadPolicy(opts) {
14
15
  const maxConcurrent = Math.max(1, Math.floor(opts.maxConcurrent));
15
16
  const queueTimeoutMs = opts.queueTimeoutMs ?? 0;
17
+ const maxQueueSize = opts.maxQueueSize ?? Number.POSITIVE_INFINITY;
16
18
  const applier = (fn, ctx) => {
17
19
  const syncCtx = ctx;
18
20
  return async (signal) => {
19
21
  const key = syncCtx.key;
20
22
  const acquireSlot = () => {
21
- // If signal already aborted, reject immediately
22
23
  if (signal.aborted)
23
24
  return Promise.reject(signal.reason);
24
25
  const state = getState(syncCtx.store, key);
@@ -30,14 +31,19 @@ function bulkheadPolicy(opts) {
30
31
  if (queueTimeoutMs === 0) {
31
32
  throw new errors_js_1.BulkheadOverflowError(key, maxConcurrent);
32
33
  }
34
+ const currentState = getState(syncCtx.store, key);
35
+ if (currentState.queue.length >= maxQueueSize) {
36
+ throw new errors_js_1.BulkheadOverflowError(key, maxConcurrent);
37
+ }
33
38
  return new Promise((resolve, reject) => {
34
39
  const state2 = getState(syncCtx.store, key);
35
40
  const entry = {
36
41
  resolve,
37
42
  reject,
43
+ timer: undefined,
44
+ onAbort: undefined,
38
45
  signal,
39
46
  };
40
- // Remove entry from queue on signal abort
41
47
  entry.onAbort = () => {
42
48
  const s = getState(syncCtx.store, key);
43
49
  const idx = s.queue.indexOf(entry);
@@ -62,6 +68,30 @@ function bulkheadPolicy(opts) {
62
68
  }
63
69
  signal.addEventListener('abort', entry.onAbort, { once: true });
64
70
  state2.queue.push(entry);
71
+ const obs = syncCtx.observability;
72
+ if (obs && maxQueueSize !== Number.POSITIVE_INFINITY) {
73
+ const utilization = state2.queue.length / maxQueueSize;
74
+ const wasOver80 = state2.backpressureEmitted === true;
75
+ if (utilization >= 0.8 && !wasOver80) {
76
+ ;
77
+ state2.backpressureEmitted = true;
78
+ (0, safeCall_js_1.safeCall)(obs.hooks.onBackpressure, {
79
+ type: 'backpressure',
80
+ key: syncCtx.key,
81
+ traceId: obs.traceId,
82
+ timestamp: Date.now(),
83
+ source: 'bulkhead',
84
+ queueLength: state2.queue.length,
85
+ maxConcurrent,
86
+ maxQueueSize,
87
+ utilization,
88
+ });
89
+ }
90
+ else if (utilization < 0.8 && wasOver80) {
91
+ ;
92
+ state2.backpressureEmitted = false;
93
+ }
94
+ }
65
95
  setState(syncCtx.store, key, state2);
66
96
  });
67
97
  };
@@ -73,7 +103,6 @@ function bulkheadPolicy(opts) {
73
103
  state.active++;
74
104
  if (next.timer)
75
105
  clearTimeout(next.timer);
76
- // Remove abort listener from the QUEUED caller's signal (not releaser's)
77
106
  if (next.onAbort && next.signal) {
78
107
  next.signal.removeEventListener('abort', next.onAbort);
79
108
  }
@@ -81,7 +110,18 @@ function bulkheadPolicy(opts) {
81
110
  }
82
111
  if (state.active < 0)
83
112
  state.active = 0;
84
- setState(syncCtx.store, key, state);
113
+ if (maxQueueSize !== Number.POSITIVE_INFINITY && state.backpressureEmitted === true) {
114
+ if (state.queue.length / maxQueueSize < 0.8) {
115
+ ;
116
+ state.backpressureEmitted = false;
117
+ }
118
+ }
119
+ if (state.active === 0 && state.queue.length === 0) {
120
+ syncCtx.store.delete(NS + key);
121
+ }
122
+ else {
123
+ setState(syncCtx.store, key, state);
124
+ }
85
125
  };
86
126
  await acquireSlot();
87
127
  try {
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"bulkhead.d.ts","sourceRoot":"","sources":["../../src/policies/bulkhead.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,eAAe,EAAE,MAAM,mBAAmB,CAAA;AA0B7F,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,CAAC,CA0FzE"}
1
+ {"version":3,"file":"bulkhead.d.ts","sourceRoot":"","sources":["../../src/policies/bulkhead.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,eAAe,EAAE,MAAM,mBAAmB,CAAA;AA2B7F,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,CAAC,CAkJzE"}
@@ -1,5 +1,6 @@
1
1
  import { REQUIRES_SYNC_STORE } from '../core/executor.js';
2
2
  import { BulkheadOverflowError } from '../errors.js';
3
+ import { safeCall } from '../utils/safeCall.js';
3
4
  const NS = 'bulk:';
4
5
  function getState(store, key) {
5
6
  return store.get(NS + key) ?? { active: 0, queue: [] };
@@ -10,12 +11,12 @@ function setState(store, key, state) {
10
11
  export function bulkheadPolicy(opts) {
11
12
  const maxConcurrent = Math.max(1, Math.floor(opts.maxConcurrent));
12
13
  const queueTimeoutMs = opts.queueTimeoutMs ?? 0;
14
+ const maxQueueSize = opts.maxQueueSize ?? Number.POSITIVE_INFINITY;
13
15
  const applier = (fn, ctx) => {
14
16
  const syncCtx = ctx;
15
17
  return async (signal) => {
16
18
  const key = syncCtx.key;
17
19
  const acquireSlot = () => {
18
- // If signal already aborted, reject immediately
19
20
  if (signal.aborted)
20
21
  return Promise.reject(signal.reason);
21
22
  const state = getState(syncCtx.store, key);
@@ -27,14 +28,19 @@ export function bulkheadPolicy(opts) {
27
28
  if (queueTimeoutMs === 0) {
28
29
  throw new BulkheadOverflowError(key, maxConcurrent);
29
30
  }
31
+ const currentState = getState(syncCtx.store, key);
32
+ if (currentState.queue.length >= maxQueueSize) {
33
+ throw new BulkheadOverflowError(key, maxConcurrent);
34
+ }
30
35
  return new Promise((resolve, reject) => {
31
36
  const state2 = getState(syncCtx.store, key);
32
37
  const entry = {
33
38
  resolve,
34
39
  reject,
40
+ timer: undefined,
41
+ onAbort: undefined,
35
42
  signal,
36
43
  };
37
- // Remove entry from queue on signal abort
38
44
  entry.onAbort = () => {
39
45
  const s = getState(syncCtx.store, key);
40
46
  const idx = s.queue.indexOf(entry);
@@ -59,6 +65,30 @@ export function bulkheadPolicy(opts) {
59
65
  }
60
66
  signal.addEventListener('abort', entry.onAbort, { once: true });
61
67
  state2.queue.push(entry);
68
+ const obs = syncCtx.observability;
69
+ if (obs && maxQueueSize !== Number.POSITIVE_INFINITY) {
70
+ const utilization = state2.queue.length / maxQueueSize;
71
+ const wasOver80 = state2.backpressureEmitted === true;
72
+ if (utilization >= 0.8 && !wasOver80) {
73
+ ;
74
+ state2.backpressureEmitted = true;
75
+ safeCall(obs.hooks.onBackpressure, {
76
+ type: 'backpressure',
77
+ key: syncCtx.key,
78
+ traceId: obs.traceId,
79
+ timestamp: Date.now(),
80
+ source: 'bulkhead',
81
+ queueLength: state2.queue.length,
82
+ maxConcurrent,
83
+ maxQueueSize,
84
+ utilization,
85
+ });
86
+ }
87
+ else if (utilization < 0.8 && wasOver80) {
88
+ ;
89
+ state2.backpressureEmitted = false;
90
+ }
91
+ }
62
92
  setState(syncCtx.store, key, state2);
63
93
  });
64
94
  };
@@ -70,7 +100,6 @@ export function bulkheadPolicy(opts) {
70
100
  state.active++;
71
101
  if (next.timer)
72
102
  clearTimeout(next.timer);
73
- // Remove abort listener from the QUEUED caller's signal (not releaser's)
74
103
  if (next.onAbort && next.signal) {
75
104
  next.signal.removeEventListener('abort', next.onAbort);
76
105
  }
@@ -78,7 +107,18 @@ export function bulkheadPolicy(opts) {
78
107
  }
79
108
  if (state.active < 0)
80
109
  state.active = 0;
81
- setState(syncCtx.store, key, state);
110
+ if (maxQueueSize !== Number.POSITIVE_INFINITY && state.backpressureEmitted === true) {
111
+ if (state.queue.length / maxQueueSize < 0.8) {
112
+ ;
113
+ state.backpressureEmitted = false;
114
+ }
115
+ }
116
+ if (state.active === 0 && state.queue.length === 0) {
117
+ syncCtx.store.delete(NS + key);
118
+ }
119
+ else {
120
+ setState(syncCtx.store, key, state);
121
+ }
82
122
  };
83
123
  await acquireSlot();
84
124
  try {
@@ -92,4 +132,3 @@ export function bulkheadPolicy(opts) {
92
132
  applier[REQUIRES_SYNC_STORE] = true;
93
133
  return applier;
94
134
  }
95
- //# sourceMappingURL=bulkhead.js.map