@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.
- package/LICENSE +21 -0
- package/README.md +604 -0
- package/dist/adapters/zod.d.ts +14 -0
- package/dist/adapters/zod.d.ts.map +1 -0
- package/dist/adapters/zod.js +14 -0
- package/dist/adapters/zod.js.map +1 -0
- package/dist/core/backoff.d.ts +9 -0
- package/dist/core/backoff.d.ts.map +1 -0
- package/dist/core/backoff.js +23 -0
- package/dist/core/backoff.js.map +1 -0
- package/dist/core/client.d.ts +98 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +781 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/errors.d.ts +87 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +140 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/index.d.ts +15 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +10 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/listeners.d.ts +14 -0
- package/dist/core/listeners.d.ts.map +1 -0
- package/dist/core/listeners.js +27 -0
- package/dist/core/listeners.js.map +1 -0
- package/dist/core/metrics.d.ts +33 -0
- package/dist/core/metrics.d.ts.map +1 -0
- package/dist/core/metrics.js +24 -0
- package/dist/core/metrics.js.map +1 -0
- package/dist/core/nanoid.d.ts +2 -0
- package/dist/core/nanoid.d.ts.map +1 -0
- package/dist/core/nanoid.js +11 -0
- package/dist/core/nanoid.js.map +1 -0
- package/dist/core/redact.d.ts +12 -0
- package/dist/core/redact.d.ts.map +1 -0
- package/dist/core/redact.js +42 -0
- package/dist/core/redact.js.map +1 -0
- package/dist/core/types.d.ts +261 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +41 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/validate.d.ts +19 -0
- package/dist/core/validate.d.ts.map +1 -0
- package/dist/core/validate.js +135 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/middleware/index.d.ts +26 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +55 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/queue/bulkhead.d.ts +63 -0
- package/dist/queue/bulkhead.d.ts.map +1 -0
- package/dist/queue/bulkhead.js +192 -0
- package/dist/queue/bulkhead.js.map +1 -0
- package/dist/queue/circuit-breaker.d.ts +81 -0
- package/dist/queue/circuit-breaker.d.ts.map +1 -0
- package/dist/queue/circuit-breaker.js +283 -0
- package/dist/queue/circuit-breaker.js.map +1 -0
- package/dist/queue/executor.d.ts +67 -0
- package/dist/queue/executor.d.ts.map +1 -0
- package/dist/queue/executor.js +273 -0
- package/dist/queue/executor.js.map +1 -0
- package/dist/queue/policy.d.ts +26 -0
- package/dist/queue/policy.d.ts.map +1 -0
- package/dist/queue/policy.js +37 -0
- package/dist/queue/policy.js.map +1 -0
- package/dist/queue/retry.d.ts +58 -0
- package/dist/queue/retry.d.ts.map +1 -0
- package/dist/queue/retry.js +259 -0
- package/dist/queue/retry.js.map +1 -0
- package/dist/queue/semaphore.d.ts +32 -0
- package/dist/queue/semaphore.d.ts.map +1 -0
- package/dist/queue/semaphore.js +83 -0
- package/dist/queue/semaphore.js.map +1 -0
- package/dist/ticket/ticket.d.ts +77 -0
- package/dist/ticket/ticket.d.ts.map +1 -0
- package/dist/ticket/ticket.js +186 -0
- package/dist/ticket/ticket.js.map +1 -0
- package/package.json +85 -0
- package/src/adapters/zod.ts +16 -0
- package/src/core/backoff.ts +26 -0
- package/src/core/client.ts +1048 -0
- package/src/core/errors.ts +194 -0
- package/src/core/index.ts +56 -0
- package/src/core/listeners.ts +28 -0
- package/src/core/metrics.ts +42 -0
- package/src/core/nanoid.ts +11 -0
- package/src/core/redact.ts +46 -0
- package/src/core/types.ts +306 -0
- package/src/core/validate.ts +163 -0
- package/src/middleware/index.ts +63 -0
- package/src/queue/bulkhead.ts +243 -0
- package/src/queue/circuit-breaker.ts +373 -0
- package/src/queue/executor.ts +355 -0
- package/src/queue/policy.ts +49 -0
- package/src/queue/retry.ts +380 -0
- package/src/queue/semaphore.ts +91 -0
- 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"}
|