@vereda/http 1.0.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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +604 -0
  3. package/dist/adapters/zod.d.ts +14 -0
  4. package/dist/adapters/zod.d.ts.map +1 -0
  5. package/dist/adapters/zod.js +14 -0
  6. package/dist/adapters/zod.js.map +1 -0
  7. package/dist/core/backoff.d.ts +9 -0
  8. package/dist/core/backoff.d.ts.map +1 -0
  9. package/dist/core/backoff.js +23 -0
  10. package/dist/core/backoff.js.map +1 -0
  11. package/dist/core/client.d.ts +98 -0
  12. package/dist/core/client.d.ts.map +1 -0
  13. package/dist/core/client.js +781 -0
  14. package/dist/core/client.js.map +1 -0
  15. package/dist/core/errors.d.ts +87 -0
  16. package/dist/core/errors.d.ts.map +1 -0
  17. package/dist/core/errors.js +140 -0
  18. package/dist/core/errors.js.map +1 -0
  19. package/dist/core/index.d.ts +15 -0
  20. package/dist/core/index.d.ts.map +1 -0
  21. package/dist/core/index.js +10 -0
  22. package/dist/core/index.js.map +1 -0
  23. package/dist/core/listeners.d.ts +14 -0
  24. package/dist/core/listeners.d.ts.map +1 -0
  25. package/dist/core/listeners.js +27 -0
  26. package/dist/core/listeners.js.map +1 -0
  27. package/dist/core/metrics.d.ts +33 -0
  28. package/dist/core/metrics.d.ts.map +1 -0
  29. package/dist/core/metrics.js +24 -0
  30. package/dist/core/metrics.js.map +1 -0
  31. package/dist/core/nanoid.d.ts +2 -0
  32. package/dist/core/nanoid.d.ts.map +1 -0
  33. package/dist/core/nanoid.js +11 -0
  34. package/dist/core/nanoid.js.map +1 -0
  35. package/dist/core/redact.d.ts +12 -0
  36. package/dist/core/redact.d.ts.map +1 -0
  37. package/dist/core/redact.js +42 -0
  38. package/dist/core/redact.js.map +1 -0
  39. package/dist/core/types.d.ts +261 -0
  40. package/dist/core/types.d.ts.map +1 -0
  41. package/dist/core/types.js +41 -0
  42. package/dist/core/types.js.map +1 -0
  43. package/dist/core/validate.d.ts +19 -0
  44. package/dist/core/validate.d.ts.map +1 -0
  45. package/dist/core/validate.js +135 -0
  46. package/dist/core/validate.js.map +1 -0
  47. package/dist/middleware/index.d.ts +26 -0
  48. package/dist/middleware/index.d.ts.map +1 -0
  49. package/dist/middleware/index.js +55 -0
  50. package/dist/middleware/index.js.map +1 -0
  51. package/dist/queue/bulkhead.d.ts +63 -0
  52. package/dist/queue/bulkhead.d.ts.map +1 -0
  53. package/dist/queue/bulkhead.js +192 -0
  54. package/dist/queue/bulkhead.js.map +1 -0
  55. package/dist/queue/circuit-breaker.d.ts +81 -0
  56. package/dist/queue/circuit-breaker.d.ts.map +1 -0
  57. package/dist/queue/circuit-breaker.js +283 -0
  58. package/dist/queue/circuit-breaker.js.map +1 -0
  59. package/dist/queue/executor.d.ts +67 -0
  60. package/dist/queue/executor.d.ts.map +1 -0
  61. package/dist/queue/executor.js +273 -0
  62. package/dist/queue/executor.js.map +1 -0
  63. package/dist/queue/policy.d.ts +26 -0
  64. package/dist/queue/policy.d.ts.map +1 -0
  65. package/dist/queue/policy.js +37 -0
  66. package/dist/queue/policy.js.map +1 -0
  67. package/dist/queue/retry.d.ts +58 -0
  68. package/dist/queue/retry.d.ts.map +1 -0
  69. package/dist/queue/retry.js +259 -0
  70. package/dist/queue/retry.js.map +1 -0
  71. package/dist/queue/semaphore.d.ts +32 -0
  72. package/dist/queue/semaphore.d.ts.map +1 -0
  73. package/dist/queue/semaphore.js +83 -0
  74. package/dist/queue/semaphore.js.map +1 -0
  75. package/dist/ticket/ticket.d.ts +77 -0
  76. package/dist/ticket/ticket.d.ts.map +1 -0
  77. package/dist/ticket/ticket.js +186 -0
  78. package/dist/ticket/ticket.js.map +1 -0
  79. package/package.json +85 -0
  80. package/src/adapters/zod.ts +16 -0
  81. package/src/core/backoff.ts +26 -0
  82. package/src/core/client.ts +1048 -0
  83. package/src/core/errors.ts +194 -0
  84. package/src/core/index.ts +56 -0
  85. package/src/core/listeners.ts +28 -0
  86. package/src/core/metrics.ts +42 -0
  87. package/src/core/nanoid.ts +11 -0
  88. package/src/core/redact.ts +46 -0
  89. package/src/core/types.ts +306 -0
  90. package/src/core/validate.ts +163 -0
  91. package/src/middleware/index.ts +63 -0
  92. package/src/queue/bulkhead.ts +243 -0
  93. package/src/queue/circuit-breaker.ts +373 -0
  94. package/src/queue/executor.ts +355 -0
  95. package/src/queue/policy.ts +49 -0
  96. package/src/queue/retry.ts +380 -0
  97. package/src/queue/semaphore.ts +91 -0
  98. package/src/ticket/ticket.ts +246 -0
@@ -0,0 +1,192 @@
1
+ import { CancelledError, QueueFullError } from "../core/errors.js";
2
+ import { DEFAULT_CONCURRENCY, DEFAULT_MAX_QUEUE_SIZE } from "../core/types.js";
3
+ /** Default TTL (ms) before an idle partition registry entry (bulkhead or
4
+ * circuit breaker) is swept from its registry. Shared by both registries
5
+ * so the eviction policy stays in one place. */
6
+ export const DEFAULT_PARTITION_TTL_MS = 60_000;
7
+ export class Bulkhead {
8
+ name;
9
+ concurrency;
10
+ maxQueueSize;
11
+ _limitFirstAttempts;
12
+ running = 0;
13
+ _waitQueue = [];
14
+ constructor(name, config = {}) {
15
+ this.name = name;
16
+ this.concurrency = config.concurrency ?? DEFAULT_CONCURRENCY;
17
+ this.maxQueueSize = config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
18
+ this._limitFirstAttempts = config.limitFirstAttempts ?? false;
19
+ }
20
+ get queueSize() {
21
+ return this._waitQueue.length;
22
+ }
23
+ get runningCount() {
24
+ return this.running;
25
+ }
26
+ get concurrencyLimit() {
27
+ return this.concurrency;
28
+ }
29
+ get maxQueueSizeLimit() {
30
+ return this.maxQueueSize;
31
+ }
32
+ get limitFirstAttempts() {
33
+ return this._limitFirstAttempts;
34
+ }
35
+ /** Acquire a concurrency slot, execute `task`, release the slot.
36
+ * Rejects with `QueueFullError` when the queue is at capacity.
37
+ * `this.running` is decremented *before* the returned Promise resolves,
38
+ * giving consumers an accurate count immediately after `await`.
39
+ * When a global `semaphore` is provided, a permit is acquired after the
40
+ * partition slot and released before it (D4).
41
+ * `onDequeue`, if given, fires once — right before `task()` starts —
42
+ * with the ms elapsed since `run()` was called, covering both the
43
+ * partition wait and the semaphore wait.
44
+ * If `signal` aborts while the caller is still waiting (for a partition
45
+ * slot or a global permit), it leaves the queue and the promise rejects
46
+ * with `CancelledError` without running `task` (B9). Once `task` has
47
+ * started, cancelling it is the task's own job. */
48
+ run(task, semaphore, onDequeue, signal) {
49
+ const enqueuedAt = Date.now();
50
+ return new Promise((resolve, reject) => {
51
+ if (signal?.aborted) {
52
+ reject(new CancelledError());
53
+ return;
54
+ }
55
+ const onAbort = () => {
56
+ const idx = this._waitQueue.indexOf(execute);
57
+ if (idx !== -1)
58
+ this._waitQueue.splice(idx, 1);
59
+ reject(new CancelledError());
60
+ };
61
+ const execute = () => {
62
+ signal?.removeEventListener("abort", onAbort);
63
+ this.running++;
64
+ // `release`, when given, must run BEFORE _releaseSlot(): releasing
65
+ // the slot synchronously drains the next waiter, which may call
66
+ // semaphore.acquire() immediately — if the permit hasn't actually
67
+ // been freed yet (i.e. this ran after _releaseSlot instead of
68
+ // before), that acquire can spuriously see none available and
69
+ // reject even though one was about to free.
70
+ const runTask = (release) => {
71
+ onDequeue?.(Date.now() - enqueuedAt);
72
+ return task().then((result) => {
73
+ release?.();
74
+ this._releaseSlot();
75
+ resolve(result);
76
+ }, (err) => {
77
+ release?.();
78
+ this._releaseSlot();
79
+ reject(err);
80
+ });
81
+ };
82
+ if (semaphore) {
83
+ semaphore.acquire(signal).then((release) => {
84
+ // void: outcomes are routed to the outer resolve/reject inside runTask.
85
+ void runTask(release);
86
+ }, (err) => {
87
+ // The partition slot was granted but the global semaphore
88
+ // rejected (queue full, or cancelled while waiting for a
89
+ // permit) — release the slot we already
90
+ // counted, or it leaks as a permanently phantom-running slot.
91
+ // Report how long this attempt actually waited before being
92
+ // told no; it never reached runTask's own onDequeue call.
93
+ onDequeue?.(Date.now() - enqueuedAt);
94
+ this._releaseSlot();
95
+ reject(err);
96
+ });
97
+ }
98
+ else {
99
+ // void: outcomes are routed to the outer resolve/reject inside runTask.
100
+ void runTask();
101
+ }
102
+ };
103
+ if (this.running < this.concurrency) {
104
+ execute();
105
+ }
106
+ else if (this._waitQueue.length < this.maxQueueSize) {
107
+ signal?.addEventListener("abort", onAbort, { once: true });
108
+ this._waitQueue.push(execute);
109
+ }
110
+ else {
111
+ reject(new QueueFullError(this.name, this._waitQueue.length, this.maxQueueSize));
112
+ }
113
+ });
114
+ }
115
+ /** Free a counted slot and hand it to the next waiter, if any. Every path
116
+ * that decrements `running` — task success, task failure, and a rejected
117
+ * semaphore acquire after the slot was already granted — must go through
118
+ * this, or a slot leaks as permanently phantom-running. */
119
+ _releaseSlot() {
120
+ this.running--;
121
+ this._drainWaitQueue();
122
+ }
123
+ _drainWaitQueue() {
124
+ while (this.running < this.concurrency && this._waitQueue.length > 0) {
125
+ const next = this._waitQueue.shift();
126
+ next();
127
+ }
128
+ }
129
+ }
130
+ export class BulkheadRegistry {
131
+ bulkheads = new Map();
132
+ ttlMs;
133
+ globalConfig;
134
+ partitionConfigs;
135
+ sweepInterval;
136
+ semaphore;
137
+ callCounter = 0;
138
+ constructor(globalConfig = {}, partitionConfigs = {}, ttlMs = DEFAULT_PARTITION_TTL_MS, semaphore) {
139
+ this.ttlMs = ttlMs;
140
+ this.globalConfig = globalConfig;
141
+ this.partitionConfigs = partitionConfigs;
142
+ this.sweepInterval = 10;
143
+ this.semaphore = semaphore;
144
+ }
145
+ get(partitionName) {
146
+ this.callCounter++;
147
+ if (!this.bulkheads.has(partitionName)) {
148
+ const partitionConfig = this.partitionConfigs[partitionName] ?? {};
149
+ const merged = {
150
+ ...this.globalConfig,
151
+ ...partitionConfig,
152
+ };
153
+ this.bulkheads.set(partitionName, [new Bulkhead(partitionName, merged), Date.now()]);
154
+ }
155
+ const [bh] = this.bulkheads.get(partitionName);
156
+ // Update last accessed time
157
+ this.bulkheads.set(partitionName, [bh, Date.now()]);
158
+ // Sweep stale entries periodically
159
+ if (this.callCounter % this.sweepInterval === 0 || this.bulkheads.size > 100) {
160
+ this.prune();
161
+ }
162
+ return bh;
163
+ }
164
+ prune() {
165
+ const now = Date.now();
166
+ for (const [key, [, lastAccessed]] of this.bulkheads) {
167
+ if (now - lastAccessed > this.ttlMs) {
168
+ this.bulkheads.delete(key);
169
+ }
170
+ }
171
+ }
172
+ delete(partitionName) {
173
+ this.bulkheads.delete(partitionName);
174
+ }
175
+ getAll() {
176
+ const result = [];
177
+ for (const [, [bh]] of this.bulkheads) {
178
+ result.push({
179
+ name: bh.name,
180
+ running: bh.runningCount,
181
+ queued: bh.queueSize,
182
+ concurrency: bh.concurrencyLimit,
183
+ maxQueueSize: bh.maxQueueSizeLimit,
184
+ });
185
+ }
186
+ return result;
187
+ }
188
+ getSemaphore() {
189
+ return this.semaphore;
190
+ }
191
+ }
192
+ //# sourceMappingURL=bulkhead.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bulkhead.js","sourceRoot":"","sources":["../../src/queue/bulkhead.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ import type { AppError } from "../core/errors.ts";
2
+ import { type CircuitBreakerConfig, type PartitionConfig } from "../core/types.ts";
3
+ /** One admission through the breaker, returned by `tryAcquire()`. Exactly one
4
+ * of its methods takes effect (later calls are no-ops), so a caller can
5
+ * report the outcome and still `release()` unconditionally in a `finally`.
6
+ * In half-open, the permit owns a trial slot; every exit path — success,
7
+ * failure, a non-failure error, cancellation, a veto — must hand it back,
8
+ * or the breaker is wedged half-open with no trial slots left (B1). */
9
+ export interface CircuitPermit {
10
+ /** The attempt succeeded. */
11
+ success(): void;
12
+ /** The attempt failed with `error`. Errors the breaker doesn't classify as
13
+ * failures (e.g. a 404) count as successes — the host answered. An error
14
+ * that shows the attempt never reached the host (e.g. a body factory that
15
+ * threw) is ignored instead — same effect as calling `release()` — since
16
+ * it carries no information about the host's health. */
17
+ failure(error: AppError): void;
18
+ /** The permit was not used to reach the server (cancelled, vetoed, queue
19
+ * full…). Frees a half-open trial slot without recording an outcome. */
20
+ release(): void;
21
+ }
22
+ export declare class CircuitBreaker {
23
+ private readonly partition;
24
+ private readonly config;
25
+ private readonly onStateChange?;
26
+ private state;
27
+ private consecutiveFailures;
28
+ private openedAt;
29
+ private halfOpenInFlight;
30
+ /** Bumped on every open -> half-open transition, so a permit can tell
31
+ * whether the trial slot it reserved still belongs to the current
32
+ * half-open episode or to one that has since ended. */
33
+ private halfOpenGeneration;
34
+ private readonly window?;
35
+ constructor(partition: string, config?: CircuitBreakerConfig, onStateChange?: (partition: string, state: "open" | "closed") => void);
36
+ /** Whether a request may proceed against this partition right now.
37
+ * Always true when the breaker is disabled. Handles the open -> half-open
38
+ * transition on `resetTimeoutMs` elapse, and reserves a half-open trial
39
+ * slot atomically so concurrent callers don't over-admit trials. */
40
+ canRequest(): boolean;
41
+ /** Admit a request, or return `null` when the circuit rejects it. Prefer
42
+ * this over the raw `canRequest()`/`record*()` pair: the returned permit
43
+ * guarantees a reserved half-open trial slot is always given back. */
44
+ tryAcquire(): CircuitPermit | null;
45
+ /** Record a successful attempt outcome. No-op when disabled. */
46
+ recordSuccess(): void;
47
+ /** Record a failed attempt outcome. No-op when disabled or when the error
48
+ * is not classified as a failure. */
49
+ recordFailure(error: AppError): void;
50
+ /** Record a failure the caller has already classified via `isFailure`, so a
51
+ * user-supplied classifier runs exactly once per outcome. No-op when disabled. */
52
+ private recordClassifiedFailure;
53
+ /** Not consulted for a `NEVER_SENT_KINDS` error (`tryAcquire()` short-circuits
54
+ * before calling this) — such an error never reached the host, so there is
55
+ * nothing for a caller-supplied classifier to legitimately weigh in on. */
56
+ private isFailure;
57
+ /** An attempt reached the server but ended in an error the breaker does
58
+ * not count as a failure — e.g. a 404, or a 200 whose body failed `parse`.
59
+ * The breaker measures availability, and the host answered, so this is
60
+ * recorded as a success in every state (the Resilience4j default for
61
+ * unrecorded errors): closed resets the consecutive-failure run and counts
62
+ * as a non-failure in the rolling window; a half-open trial closes the
63
+ * circuit. Classify it as a failure via `isFailure` to change that. */
64
+ private recordNeutral;
65
+ private resetCounters;
66
+ private evaluateTripCondition;
67
+ }
68
+ export declare class CircuitBreakerRegistry {
69
+ private readonly breakers;
70
+ private readonly ttlMs;
71
+ private readonly globalConfig;
72
+ private readonly partitionConfigs;
73
+ private readonly sweepInterval;
74
+ private readonly onStateChange?;
75
+ private callCounter;
76
+ constructor(globalConfig?: CircuitBreakerConfig, partitionConfigs?: Record<string, PartitionConfig>, ttlMs?: number, onStateChange?: (partition: string, state: "open" | "closed") => void);
77
+ get(partitionName: string): CircuitBreaker;
78
+ prune(): void;
79
+ delete(partitionName: string): void;
80
+ }
81
+ //# sourceMappingURL=circuit-breaker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../../src/queue/circuit-breaker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EACN,KAAK,oBAAoB,EAIzB,KAAK,eAAe,EACpB,MAAM,kBAAkB,CAAC;AAoF1B;;;;;wEAKwE;AACxE,MAAM,WAAW,aAAa;IAC7B,6BAA6B;IAC7B,OAAO,IAAI,IAAI,CAAC;IAChB;;;;6DAIyD;IACzD,OAAO,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC/B;6EACyE;IACzE,OAAO,IAAI,IAAI,CAAC;CAChB;AAED,qBAAa,cAAc;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAwD;IAEvF,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,gBAAgB,CAAK;IAC7B;;4DAEwD;IACxD,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAgB;gBAGvC,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE,oBAAyB,EACjC,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,KAAK,IAAI;IAUtE;;;yEAGqE;IACrE,UAAU,IAAI,OAAO;IA6BrB;;2EAEuE;IACvE,UAAU,IAAI,aAAa,GAAG,IAAI;IAiDlC,gEAAgE;IAChE,aAAa,IAAI,IAAI;IAiBrB;0CACsC;IACtC,aAAa,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;IAMpC;uFACmF;IACnF,OAAO,CAAC,uBAAuB;IAsB/B;;gFAE4E;IAC5E,OAAO,CAAC,SAAS;IAIjB;;;;;;4EAMwE;IACxE,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,qBAAqB;CAU7B;AAQD,qBAAa,sBAAsB;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0C;IACnE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAuB;IACpD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkC;IACnE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAwD;IACvF,OAAO,CAAC,WAAW,CAAK;gBAGvB,YAAY,GAAE,oBAAyB,EACvC,gBAAgB,GAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAM,EACtD,KAAK,GAAE,MAAiC,EACxC,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,KAAK,IAAI;IAStE,GAAG,CAAC,aAAa,EAAE,MAAM,GAAG,cAAc;IAuB1C,KAAK,IAAI,IAAI;IASb,MAAM,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI;CAGnC"}
@@ -0,0 +1,283 @@
1
+ import { DEFAULT_FAILURE_THRESHOLD, DEFAULT_HALF_OPEN_MAX_ATTEMPTS, DEFAULT_RESET_TIMEOUT_MS, } from "../core/types.js";
2
+ import { DEFAULT_PARTITION_TTL_MS } from "./bulkhead.js";
3
+ import { RETRIABLE_KINDS } from "./policy.js";
4
+ /** Error kinds that mean the attempt never left the process — no request was
5
+ * ever dispatched, so the host's health is untouched. Currently only a body
6
+ * factory (`RequestOptions.body` as a function) that threw before `fetch`
7
+ * was called (`src/queue/executor.ts`). A raw-`ReadableStream` body is
8
+ * rejected even earlier, before the breaker is asked for a permit at all
9
+ * (`client.ts` validates it ahead of `tryAcquire()`), so it never reaches
10
+ * this classification in the first place. */
11
+ const NEVER_SENT_KINDS = new Set(["configuration"]);
12
+ const DEFAULT_BUCKET_COUNT = 10;
13
+ class RollingWindow {
14
+ sizeMs;
15
+ bucketMs;
16
+ buckets = [];
17
+ constructor(sizeMs, bucketCount = DEFAULT_BUCKET_COUNT) {
18
+ this.sizeMs = sizeMs;
19
+ this.bucketMs = Math.max(1, Math.floor(sizeMs / bucketCount));
20
+ }
21
+ recordSuccess(now = Date.now()) {
22
+ const bucket = this.currentBucket(now);
23
+ bucket.total++;
24
+ }
25
+ recordFailure(now = Date.now()) {
26
+ const bucket = this.currentBucket(now);
27
+ bucket.total++;
28
+ bucket.failures++;
29
+ }
30
+ totalRequests(now = Date.now()) {
31
+ this.prune(now);
32
+ return this.buckets.reduce((sum, b) => sum + b.total, 0);
33
+ }
34
+ failureRate(now = Date.now()) {
35
+ this.prune(now);
36
+ const total = this.buckets.reduce((sum, b) => sum + b.total, 0);
37
+ if (total === 0)
38
+ return 0;
39
+ const failures = this.buckets.reduce((sum, b) => sum + b.failures, 0);
40
+ return (failures / total) * 100;
41
+ }
42
+ currentBucket(now) {
43
+ this.prune(now);
44
+ const bucketStart = Math.floor(now / this.bucketMs) * this.bucketMs;
45
+ const last = this.buckets[this.buckets.length - 1];
46
+ if (last && last.start === bucketStart) {
47
+ return last;
48
+ }
49
+ const bucket = { start: bucketStart, total: 0, failures: 0 };
50
+ this.buckets.push(bucket);
51
+ return bucket;
52
+ }
53
+ prune(now) {
54
+ const cutoff = now - this.sizeMs;
55
+ this.buckets = this.buckets.filter((b) => b.start >= cutoff);
56
+ }
57
+ }
58
+ export class CircuitBreaker {
59
+ partition;
60
+ config;
61
+ onStateChange;
62
+ state = "closed";
63
+ consecutiveFailures = 0;
64
+ openedAt = 0;
65
+ halfOpenInFlight = 0;
66
+ /** Bumped on every open -> half-open transition, so a permit can tell
67
+ * whether the trial slot it reserved still belongs to the current
68
+ * half-open episode or to one that has since ended. */
69
+ halfOpenGeneration = 0;
70
+ window;
71
+ constructor(partition, config = {}, onStateChange) {
72
+ this.partition = partition;
73
+ this.config = config;
74
+ this.onStateChange = onStateChange;
75
+ if (config.window) {
76
+ this.window = new RollingWindow(config.window.sizeMs);
77
+ }
78
+ }
79
+ /** Whether a request may proceed against this partition right now.
80
+ * Always true when the breaker is disabled. Handles the open -> half-open
81
+ * transition on `resetTimeoutMs` elapse, and reserves a half-open trial
82
+ * slot atomically so concurrent callers don't over-admit trials. */
83
+ canRequest() {
84
+ if (!this.config.enabled)
85
+ return true;
86
+ if (this.state === "open") {
87
+ const resetTimeoutMs = this.config.resetTimeoutMs ?? DEFAULT_RESET_TIMEOUT_MS;
88
+ if (Date.now() - this.openedAt >= resetTimeoutMs) {
89
+ this.state = "half-open";
90
+ this.halfOpenInFlight = 0;
91
+ this.halfOpenGeneration++;
92
+ }
93
+ else {
94
+ return false;
95
+ }
96
+ }
97
+ if (this.state === "half-open") {
98
+ const maxAttempts = this.config.halfOpenMaxAttempts ?? DEFAULT_HALF_OPEN_MAX_ATTEMPTS;
99
+ if (this.halfOpenInFlight >= maxAttempts) {
100
+ return false;
101
+ }
102
+ // Reserve the slot immediately (side-effecting) so two callers
103
+ // arriving before either trial resolves can't both be admitted.
104
+ this.halfOpenInFlight++;
105
+ return true;
106
+ }
107
+ // closed
108
+ return true;
109
+ }
110
+ /** Admit a request, or return `null` when the circuit rejects it. Prefer
111
+ * this over the raw `canRequest()`/`record*()` pair: the returned permit
112
+ * guarantees a reserved half-open trial slot is always given back. */
113
+ tryAcquire() {
114
+ if (!this.canRequest())
115
+ return null;
116
+ // null = admitted while closed (or disabled) — no trial slot held.
117
+ const trialGeneration = this.config.enabled && this.state === "half-open" ? this.halfOpenGeneration : null;
118
+ const isCurrentTrial = () => trialGeneration !== null && trialGeneration === this.halfOpenGeneration;
119
+ // A request admitted while closed that completes during a later
120
+ // half-open episode must not decide that episode: only its own trials do.
121
+ const mayRecord = () => this.state !== "half-open" || isCurrentTrial();
122
+ const freeTrialSlot = () => {
123
+ if (this.state === "half-open" && isCurrentTrial()) {
124
+ this.halfOpenInFlight = Math.max(0, this.halfOpenInFlight - 1);
125
+ }
126
+ };
127
+ let settled = false;
128
+ const settle = (fn) => {
129
+ if (settled)
130
+ return;
131
+ settled = true;
132
+ fn();
133
+ };
134
+ return {
135
+ success: () => settle(() => {
136
+ if (mayRecord())
137
+ this.recordSuccess();
138
+ }),
139
+ failure: (error) => settle(() => {
140
+ if (!mayRecord())
141
+ return;
142
+ // Never-sent outcomes are ignored outright — not even isFailure is
143
+ // consulted, since there is no host-health signal to classify.
144
+ if (NEVER_SENT_KINDS.has(error.kind)) {
145
+ freeTrialSlot();
146
+ return;
147
+ }
148
+ // Classify once here and record the already-classified failure
149
+ // directly — `recordFailure()` would consult isFailure a second time.
150
+ if (this.isFailure(error)) {
151
+ this.recordClassifiedFailure();
152
+ }
153
+ else {
154
+ this.recordNeutral();
155
+ freeTrialSlot();
156
+ }
157
+ }),
158
+ release: () => settle(freeTrialSlot),
159
+ };
160
+ }
161
+ /** Record a successful attempt outcome. No-op when disabled. */
162
+ recordSuccess() {
163
+ if (!this.config.enabled)
164
+ return;
165
+ if (this.state === "half-open") {
166
+ this.halfOpenInFlight = Math.max(0, this.halfOpenInFlight - 1);
167
+ this.state = "closed";
168
+ this.resetCounters();
169
+ this.onStateChange?.(this.partition, "closed");
170
+ return;
171
+ }
172
+ if (this.state === "closed") {
173
+ this.consecutiveFailures = 0;
174
+ this.window?.recordSuccess();
175
+ }
176
+ }
177
+ /** Record a failed attempt outcome. No-op when disabled or when the error
178
+ * is not classified as a failure. */
179
+ recordFailure(error) {
180
+ if (!this.config.enabled)
181
+ return;
182
+ if (!this.isFailure(error))
183
+ return;
184
+ this.recordClassifiedFailure();
185
+ }
186
+ /** Record a failure the caller has already classified via `isFailure`, so a
187
+ * user-supplied classifier runs exactly once per outcome. No-op when disabled. */
188
+ recordClassifiedFailure() {
189
+ if (!this.config.enabled)
190
+ return;
191
+ if (this.state === "half-open") {
192
+ this.halfOpenInFlight = Math.max(0, this.halfOpenInFlight - 1);
193
+ this.state = "open";
194
+ this.openedAt = Date.now();
195
+ this.onStateChange?.(this.partition, "open");
196
+ return;
197
+ }
198
+ if (this.state === "closed") {
199
+ this.consecutiveFailures++;
200
+ this.window?.recordFailure();
201
+ if (this.evaluateTripCondition()) {
202
+ this.state = "open";
203
+ this.openedAt = Date.now();
204
+ this.onStateChange?.(this.partition, "open");
205
+ }
206
+ }
207
+ }
208
+ /** Not consulted for a `NEVER_SENT_KINDS` error (`tryAcquire()` short-circuits
209
+ * before calling this) — such an error never reached the host, so there is
210
+ * nothing for a caller-supplied classifier to legitimately weigh in on. */
211
+ isFailure(error) {
212
+ return this.config.isFailure ? this.config.isFailure(error) : RETRIABLE_KINDS.has(error.kind);
213
+ }
214
+ /** An attempt reached the server but ended in an error the breaker does
215
+ * not count as a failure — e.g. a 404, or a 200 whose body failed `parse`.
216
+ * The breaker measures availability, and the host answered, so this is
217
+ * recorded as a success in every state (the Resilience4j default for
218
+ * unrecorded errors): closed resets the consecutive-failure run and counts
219
+ * as a non-failure in the rolling window; a half-open trial closes the
220
+ * circuit. Classify it as a failure via `isFailure` to change that. */
221
+ recordNeutral() {
222
+ this.recordSuccess();
223
+ }
224
+ resetCounters() {
225
+ this.consecutiveFailures = 0;
226
+ this.halfOpenInFlight = 0;
227
+ }
228
+ evaluateTripCondition() {
229
+ const windowConfig = this.config.window;
230
+ if (this.window && windowConfig) {
231
+ return (this.window.totalRequests() >= windowConfig.minimumRequests &&
232
+ this.window.failureRate() > windowConfig.failureRatePercent);
233
+ }
234
+ return this.consecutiveFailures >= (this.config.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD);
235
+ }
236
+ }
237
+ export class CircuitBreakerRegistry {
238
+ breakers = new Map();
239
+ ttlMs;
240
+ globalConfig;
241
+ partitionConfigs;
242
+ sweepInterval;
243
+ onStateChange;
244
+ callCounter = 0;
245
+ constructor(globalConfig = {}, partitionConfigs = {}, ttlMs = DEFAULT_PARTITION_TTL_MS, onStateChange) {
246
+ this.ttlMs = ttlMs;
247
+ this.globalConfig = globalConfig;
248
+ this.partitionConfigs = partitionConfigs;
249
+ this.sweepInterval = 10;
250
+ this.onStateChange = onStateChange;
251
+ }
252
+ get(partitionName) {
253
+ this.callCounter++;
254
+ if (!this.breakers.has(partitionName)) {
255
+ const partitionConfig = this.partitionConfigs[partitionName]?.circuitBreaker ?? {};
256
+ const merged = {
257
+ ...this.globalConfig,
258
+ ...partitionConfig,
259
+ };
260
+ this.breakers.set(partitionName, [new CircuitBreaker(partitionName, merged, this.onStateChange), Date.now()]);
261
+ }
262
+ const [cb] = this.breakers.get(partitionName);
263
+ // Update last accessed time
264
+ this.breakers.set(partitionName, [cb, Date.now()]);
265
+ // Sweep stale entries periodically
266
+ if (this.callCounter % this.sweepInterval === 0 || this.breakers.size > 100) {
267
+ this.prune();
268
+ }
269
+ return cb;
270
+ }
271
+ prune() {
272
+ const now = Date.now();
273
+ for (const [key, [, lastAccessed]] of this.breakers) {
274
+ if (now - lastAccessed > this.ttlMs) {
275
+ this.breakers.delete(key);
276
+ }
277
+ }
278
+ }
279
+ delete(partitionName) {
280
+ this.breakers.delete(partitionName);
281
+ }
282
+ }
283
+ //# sourceMappingURL=circuit-breaker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-breaker.js","sourceRoot":"","sources":["../../src/queue/circuit-breaker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import type { AppError } from "../core/errors.ts";
2
+ import type { RequestOptions, Result, RetryConfig, TimeoutConfig } from "../core/types.ts";
3
+ export interface ExecuteRequest {
4
+ url: string;
5
+ options: RequestOptions<unknown>;
6
+ timeoutConfig: TimeoutConfig;
7
+ retryConfig: RetryConfig;
8
+ signal: AbortSignal;
9
+ /** Which attempt this is: 0 = the first attempt, 1 = the first retry, etc.
10
+ * Exposed to middleware via `RequestContext.attempt`. */
11
+ attempt: number;
12
+ ticketId: string;
13
+ partition: string;
14
+ /** Absolute `Date.now()` timestamp of the ticket's whole-ticket deadline
15
+ * (`startTime + timeout.totalMs`), or undefined when `totalMs` isn't
16
+ * bounded. Used only to bound the read window of a Response handed back
17
+ * to the caller unread (see `handedOff` in `executeRequest`) — never to
18
+ * reclassify an in-attempt timeout. */
19
+ deadlineAt?: number;
20
+ /** `url` as it may appear in errors/logs (query- and userinfo-redacted
21
+ * unless disabled) — used to build the `TimeoutError`/`DeadlineExceededError`
22
+ * that bounds a handed-off Response's body-read window, so that error is
23
+ * redacted the same as every other error the client builds. */
24
+ displayUrl: string;
25
+ /** Custom fetch function. Falls back to globalThis.fetch. */
26
+ fetch?: typeof globalThis.fetch;
27
+ }
28
+ export type ExecuteResult = {
29
+ kind: "success";
30
+ result: Result<unknown>;
31
+ } | {
32
+ kind: "timeout";
33
+ } | {
34
+ kind: "cancelled";
35
+ } | {
36
+ kind: "error";
37
+ error: AppError;
38
+ };
39
+ /**
40
+ * Executes a single HTTP request attempt.
41
+ * Returns a discriminated union describing what happened,
42
+ * so the caller (retry loop) can decide whether to retry or resolve.
43
+ */
44
+ export declare function executeRequest(req: ExecuteRequest, middleware: MiddlewareFn[]): Promise<ExecuteResult>;
45
+ /** What a middleware function sees and can rewrite for a single attempt.
46
+ * `headers` is a real `Headers` instance (case-insensitive lookups/sets) and
47
+ * is fresh per attempt — mutating it does not affect other attempts or the
48
+ * caller's original `RequestOptions`. */
49
+ export interface RequestContext {
50
+ url: string;
51
+ method: string;
52
+ headers: Headers;
53
+ body?: BodyInit;
54
+ signal: AbortSignal;
55
+ /** 0 = the first attempt, 1 = the first retry, etc. */
56
+ attempt: number;
57
+ ticketId: string;
58
+ partition: string;
59
+ }
60
+ export type NextFn = (ctx: RequestContext) => Promise<Response>;
61
+ export type MiddlewareFn = (ctx: RequestContext, next: NextFn) => Promise<Response>;
62
+ export declare function composeMiddleware(middlewares: MiddlewareFn[], core: NextFn): NextFn;
63
+ /** Parse a `Retry-After` header value into a delay in ms, or undefined when
64
+ * absent/unparseable. Integer seconds → ms; HTTP-date → ms from now, clamped
65
+ * to ≥ 0; anything else (garbage, negative) → undefined. */
66
+ export declare function parseRetryAfter(header: string | null): number | undefined;
67
+ //# sourceMappingURL=executor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/queue/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAWlD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAI3F,MAAM,WAAW,cAAc;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,WAAW,EAAE,WAAW,CAAC;IACzB,MAAM,EAAE,WAAW,CAAC;IACpB;8DAC0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;4CAIwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;oEAGgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CAChC;AAED,MAAM,MAAM,aAAa,GACtB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEtC;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,cAAc,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAkO5G;AAMD;;;0CAG0C;AAC1C,MAAM,WAAW,cAAc;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,MAAM,GAAG,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAChE,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAmBpF,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnF;AAMD;;6DAE6D;AAC7D,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CASzE"}