@cyanheads/mcp-ts-core 0.13.2 → 0.13.4
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/AGENTS.md +12 -10
- package/CLAUDE.md +12 -10
- package/README.md +2 -2
- package/changelog/0.13.x/0.13.3.md +44 -0
- package/changelog/0.13.x/0.13.4.md +65 -0
- package/changelog/template.md +7 -7
- package/dist/config/appRoot.d.ts.map +1 -1
- package/dist/config/appRoot.js +48 -16
- package/dist/config/appRoot.js.map +1 -1
- package/dist/core/app.d.ts +20 -0
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +1 -0
- package/dist/core/app.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/linter/rules/schema-rules.d.ts +19 -0
- package/dist/linter/rules/schema-rules.d.ts.map +1 -1
- package/dist/linter/rules/schema-rules.js +36 -0
- package/dist/linter/rules/schema-rules.js.map +1 -1
- package/dist/linter/rules/tool-rules.d.ts +17 -0
- package/dist/linter/rules/tool-rules.d.ts.map +1 -1
- package/dist/linter/rules/tool-rules.js +97 -1
- package/dist/linter/rules/tool-rules.js.map +1 -1
- package/dist/mcp-server/handlerContext.d.ts +6 -0
- package/dist/mcp-server/handlerContext.d.ts.map +1 -1
- package/dist/mcp-server/handlerContext.js.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +17 -9
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts +114 -0
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts.map +1 -0
- package/dist/mcp-server/tools/utils/inputPrevalidation.js +428 -0
- package/dist/mcp-server/tools/utils/inputPrevalidation.js.map +1 -0
- package/dist/mcp-server/tools/utils/strictenRecord.d.ts +42 -0
- package/dist/mcp-server/tools/utils/strictenRecord.d.ts.map +1 -0
- package/dist/mcp-server/tools/utils/strictenRecord.js +48 -0
- package/dist/mcp-server/tools/utils/strictenRecord.js.map +1 -0
- package/dist/mcp-server/tools/utils/toolDefinition.d.ts +27 -0
- package/dist/mcp-server/tools/utils/toolDefinition.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolDefinition.js +35 -6
- package/dist/mcp-server/tools/utils/toolDefinition.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +28 -4
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +245 -23
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/services/canvas/core/sqlGate.d.ts +14 -1
- package/dist/services/canvas/core/sqlGate.d.ts.map +1 -1
- package/dist/services/canvas/core/sqlGate.js +69 -7
- package/dist/services/canvas/core/sqlGate.js.map +1 -1
- package/dist/services/canvas/index.d.ts +1 -1
- package/dist/services/canvas/index.d.ts.map +1 -1
- package/dist/services/canvas/index.js +1 -1
- package/dist/services/canvas/index.js.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts +20 -0
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +75 -25
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
- package/dist/services/canvas/types.d.ts +6 -1
- package/dist/services/canvas/types.d.ts.map +1 -1
- package/dist/types-global/errors.d.ts.map +1 -1
- package/dist/types-global/errors.js +4 -3
- package/dist/types-global/errors.js.map +1 -1
- package/dist/utils/index.d.ts +3 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -2
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.d.ts +33 -0
- package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.js +45 -3
- package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
- package/dist/utils/network/httpError.d.ts +13 -2
- package/dist/utils/network/httpError.d.ts.map +1 -1
- package/dist/utils/network/httpError.js +4 -2
- package/dist/utils/network/httpError.js.map +1 -1
- package/dist/utils/network/pacer.d.ts +117 -0
- package/dist/utils/network/pacer.d.ts.map +1 -0
- package/dist/utils/network/pacer.js +304 -0
- package/dist/utils/network/pacer.js.map +1 -0
- package/dist/utils/network/retry.d.ts +119 -3
- package/dist/utils/network/retry.d.ts.map +1 -1
- package/dist/utils/network/retry.js +176 -35
- package/dist/utils/network/retry.js.map +1 -1
- package/dist/utils/security/rateLimiter.d.ts +19 -1
- package/dist/utils/security/rateLimiter.d.ts.map +1 -1
- package/dist/utils/security/rateLimiter.js +49 -1
- package/dist/utils/security/rateLimiter.js.map +1 -1
- package/dist/utils/telemetry/attributes.d.ts +19 -0
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +30 -0
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/framework-skills/add-tool/SKILL.md +43 -2
- package/framework-skills/api-canvas/SKILL.md +8 -4
- package/framework-skills/api-config/SKILL.md +4 -4
- package/framework-skills/api-errors/SKILL.md +19 -10
- package/framework-skills/api-linter/SKILL.md +48 -3
- package/framework-skills/api-telemetry/SKILL.md +26 -2
- package/framework-skills/api-utils/SKILL.md +7 -3
- package/framework-skills/api-utils/references/security.md +2 -2
- package/framework-skills/design-mcp-server/SKILL.md +17 -2
- package/framework-skills/field-test/SKILL.md +3 -1
- package/framework-skills/git-wrapup/SKILL.md +90 -68
- package/framework-skills/orchestrations/SKILL.md +3 -3
- package/framework-skills/release-and-publish/SKILL.md +10 -10
- package/framework-skills/release-pr-review/SKILL.md +19 -26
- package/framework-skills/report-issue-framework/SKILL.md +6 -35
- package/framework-skills/report-issue-local/SKILL.md +6 -36
- package/framework-skills/tool-defs-analysis/SKILL.md +4 -4
- package/package.json +7 -7
- package/templates/.github/ISSUE_TEMPLATE/bug_report.yml +2 -2
- package/templates/.github/ISSUE_TEMPLATE/feature_request.yml +2 -2
- package/templates/.github/workflows/codeql.yml +39 -0
- package/templates/AGENTS.md +3 -1
- package/templates/CLAUDE.md +3 -1
- package/templates/changelog/template.md +7 -7
- package/templates/package.json +2 -2
- package/templates/src/index.ts +4 -3
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One sliding start-rate window, e.g. `{ requests: 5, perMs: 60_000 }` for five
|
|
3
|
+
* requests a minute. Several may be declared together (per-minute and per-hour);
|
|
4
|
+
* a start needs every one of them to allow it.
|
|
5
|
+
*/
|
|
6
|
+
export interface PacerLimit {
|
|
7
|
+
/** Width of the sliding window in milliseconds. */
|
|
8
|
+
perMs: number;
|
|
9
|
+
/** Starts permitted within the window. */
|
|
10
|
+
requests: number;
|
|
11
|
+
}
|
|
12
|
+
/** Back-off applied to the shared gate when the upstream answers with a rate limit. */
|
|
13
|
+
export interface PacerCooldownOptions {
|
|
14
|
+
/** First cooldown, doubled on each consecutive rate limit. */
|
|
15
|
+
baseMs: number;
|
|
16
|
+
/**
|
|
17
|
+
* Ceiling for both the doubling and an honored `Retry-After`, so a pathological
|
|
18
|
+
* upstream value cannot park the queue.
|
|
19
|
+
*/
|
|
20
|
+
maxMs: number;
|
|
21
|
+
}
|
|
22
|
+
/** Configuration for {@link createPacer}. */
|
|
23
|
+
export interface PacerOptions {
|
|
24
|
+
/** Shared 429 gate. Omitted, an upstream rate limit paces nothing by itself. */
|
|
25
|
+
cooldown?: PacerCooldownOptions;
|
|
26
|
+
/** Sliding start-rate windows. Every entry must allow a start. */
|
|
27
|
+
limits?: PacerLimit[];
|
|
28
|
+
/** In-flight ceiling. Unset, only the windows and the start gap bind. */
|
|
29
|
+
maxConcurrent?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Absolute backpressure on queue length, for callers that pass no
|
|
32
|
+
* `maxWaitMs`. An arrival past it is shed at once.
|
|
33
|
+
*/
|
|
34
|
+
maxQueueDepth?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Minimum spacing between consecutive starts. Not expressible through
|
|
37
|
+
* {@link PacerOptions.limits} once a window admits more than one request:
|
|
38
|
+
* `{ requests: 10, perMs: 1000 }` permits ten starts in the same millisecond,
|
|
39
|
+
* which is exactly the burst the SEC and NCBI clients space out by hand.
|
|
40
|
+
*/
|
|
41
|
+
minStartGapMs?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Author-set label for telemetry attribution. Bounded and never
|
|
44
|
+
* caller-supplied — a metric attribute set lives until process restart.
|
|
45
|
+
*/
|
|
46
|
+
name: string;
|
|
47
|
+
}
|
|
48
|
+
/** Per-call options for {@link Pacer.run}. */
|
|
49
|
+
export interface PacerRunOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Ceiling on queue time — not on the task, which bounds itself. Enqueue
|
|
52
|
+
* rejects when the projected wait already exceeds it, and a still-queued entry
|
|
53
|
+
* rejects when it elapses.
|
|
54
|
+
*/
|
|
55
|
+
maxWaitMs?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Caller cancellation. While queued, an abort removes the entry and rejects
|
|
58
|
+
* with `signal.reason`; once dispatched the signal is the task's own to honor,
|
|
59
|
+
* and it is what {@link Pacer.run} hands the task.
|
|
60
|
+
*/
|
|
61
|
+
signal?: AbortSignal;
|
|
62
|
+
}
|
|
63
|
+
/** A queue in front of one upstream. Process-local; timers and `AbortSignal` only. */
|
|
64
|
+
export interface Pacer extends Disposable {
|
|
65
|
+
/**
|
|
66
|
+
* Clears the dispatch timer and rejects every queued waiter with
|
|
67
|
+
* `RequestCancelled`. In-flight tasks are left to finish. Idempotent; wire it
|
|
68
|
+
* through `createApp({ teardown })`.
|
|
69
|
+
*/
|
|
70
|
+
dispose(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Queues `task` and runs it once a slot opens, handing it the caller's signal.
|
|
73
|
+
*
|
|
74
|
+
* @throws {McpError} `RateLimited` with `data.reason: 'pacer_shed'` when no
|
|
75
|
+
* slot fits the caller's wait budget or the queue is at capacity.
|
|
76
|
+
*/
|
|
77
|
+
run<T>(task: (signal: AbortSignal) => Promise<T>, options?: PacerRunOptions): Promise<T>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Creates a FIFO pacer for one upstream.
|
|
81
|
+
*
|
|
82
|
+
* A slot is granted when every {@link PacerOptions.limits} window,
|
|
83
|
+
* {@link PacerOptions.minStartGapMs}, {@link PacerOptions.maxConcurrent}, and the
|
|
84
|
+
* cooldown gate allow it. Each window is a sliding view over recorded *start*
|
|
85
|
+
* times, so a slow response never widens the rate the upstream sees.
|
|
86
|
+
*
|
|
87
|
+
* **Composition with `withRetry`.** Retry outside, pacer inside —
|
|
88
|
+
* `withRetry(({ signal }) => pacer.run(fn, { signal }), { signal, deadlineMs })` —
|
|
89
|
+
* so each attempt re-queues and is re-paced. The cooldown gate is an absolute
|
|
90
|
+
* instant rather than a duration counted from dequeue, so `withRetry`'s
|
|
91
|
+
* `Retry-After` sleep and the gate overlap in wall-clock instead of summing: the
|
|
92
|
+
* caller waits the window the upstream named once, not twice. Pass the retry
|
|
93
|
+
* deadline's signal into `run` and the queue wait is charged to the same budget.
|
|
94
|
+
*
|
|
95
|
+
* **Runtime.** Process-local, so on Workers the limits bind per isolate, OTel is
|
|
96
|
+
* off and the metrics are inert, and `createWorkerHandler` accepts no `teardown`
|
|
97
|
+
* to call {@link Pacer.dispose} from.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* const pacer = createPacer({
|
|
102
|
+
* name: 'courtlistener',
|
|
103
|
+
* limits: [{ requests: 5, perMs: 60_000 }, { requests: 50, perMs: 3_600_000 }],
|
|
104
|
+
* minStartGapMs: 100,
|
|
105
|
+
* maxConcurrent: 2,
|
|
106
|
+
* maxQueueDepth: 100,
|
|
107
|
+
* cooldown: { baseMs: 5_000, maxMs: 60_000 },
|
|
108
|
+
* });
|
|
109
|
+
*
|
|
110
|
+
* const res = await pacer.run(
|
|
111
|
+
* (signal) => fetchWithTimeout(url, 30_000, ctx, { signal }),
|
|
112
|
+
* { signal: ctx.signal, maxWaitMs: 45_000 },
|
|
113
|
+
* );
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export declare function createPacer(options: PacerOptions): Pacer;
|
|
117
|
+
//# sourceMappingURL=pacer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pacer.d.ts","sourceRoot":"","sources":["../../../src/utils/network/pacer.ts"],"names":[],"mappings":"AAqBA;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,uFAAuF;AACvF,MAAM,WAAW,oBAAoB;IACnC,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,6CAA6C;AAC7C,MAAM,WAAW,YAAY;IAC3B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,kEAAkE;IAClE,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC;IACtB,yEAAyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;CACd;AAED,8CAA8C;AAC9C,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,sFAAsF;AACtF,MAAM,WAAW,KAAM,SAAQ,UAAU;IACvC;;;;OAIG;IACH,OAAO,IAAI,IAAI,CAAC;IAChB;;;;;OAKG;IACH,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC1F;AAgDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,KAAK,CAmQxD"}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Outbound request pacer — a FIFO queue in front of a rate-limited
|
|
3
|
+
* upstream. Holds work back until every configured window, the minimum start gap,
|
|
4
|
+
* and the concurrency cap allow it; sheds callers whose wait budget cannot be met;
|
|
5
|
+
* and closes a shared cooldown gate when the upstream answers 429.
|
|
6
|
+
*
|
|
7
|
+
* The inbound `RateLimiter` (`utils/security`) is the mirror image: it is keyed
|
|
8
|
+
* per caller and rejects synchronously, so it cannot queue work against an
|
|
9
|
+
* upstream budget.
|
|
10
|
+
* @module src/utils/network/pacer
|
|
11
|
+
*/
|
|
12
|
+
import { JsonRpcErrorCode, McpError, rateLimited, requestCancelled, } from '../../types-global/errors.js';
|
|
13
|
+
import { parseRetryAfterMs } from './retry.js';
|
|
14
|
+
import { ATTR_MCP_PACER_NAME } from '../telemetry/attributes.js';
|
|
15
|
+
import { createCounter, createHistogram, createUpDownCounter } from '../telemetry/metrics.js';
|
|
16
|
+
let instruments;
|
|
17
|
+
/**
|
|
18
|
+
* The four pacer instruments, shared across every pacer in the process and told
|
|
19
|
+
* apart by `mcp.pacer.name`. Lazy: a server that never queues emits no series.
|
|
20
|
+
*/
|
|
21
|
+
function getPacerMetrics() {
|
|
22
|
+
instruments ??= {
|
|
23
|
+
cooldowns: createCounter('mcp.pacer.cooldowns', 'Cooldown gates closed by an upstream rate limit', '{cooldowns}'),
|
|
24
|
+
queueDepth: createUpDownCounter('mcp.pacer.queue_depth', 'Requests waiting for a pacer slot', '{requests}'),
|
|
25
|
+
sheds: createCounter('mcp.pacer.sheds', 'Requests shed before dispatch because no slot fit the wait budget', '{requests}'),
|
|
26
|
+
wait: createHistogram('mcp.pacer.wait', 'Time a request spent waiting for a pacer slot', 'ms'),
|
|
27
|
+
};
|
|
28
|
+
return instruments;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates a FIFO pacer for one upstream.
|
|
32
|
+
*
|
|
33
|
+
* A slot is granted when every {@link PacerOptions.limits} window,
|
|
34
|
+
* {@link PacerOptions.minStartGapMs}, {@link PacerOptions.maxConcurrent}, and the
|
|
35
|
+
* cooldown gate allow it. Each window is a sliding view over recorded *start*
|
|
36
|
+
* times, so a slow response never widens the rate the upstream sees.
|
|
37
|
+
*
|
|
38
|
+
* **Composition with `withRetry`.** Retry outside, pacer inside —
|
|
39
|
+
* `withRetry(({ signal }) => pacer.run(fn, { signal }), { signal, deadlineMs })` —
|
|
40
|
+
* so each attempt re-queues and is re-paced. The cooldown gate is an absolute
|
|
41
|
+
* instant rather than a duration counted from dequeue, so `withRetry`'s
|
|
42
|
+
* `Retry-After` sleep and the gate overlap in wall-clock instead of summing: the
|
|
43
|
+
* caller waits the window the upstream named once, not twice. Pass the retry
|
|
44
|
+
* deadline's signal into `run` and the queue wait is charged to the same budget.
|
|
45
|
+
*
|
|
46
|
+
* **Runtime.** Process-local, so on Workers the limits bind per isolate, OTel is
|
|
47
|
+
* off and the metrics are inert, and `createWorkerHandler` accepts no `teardown`
|
|
48
|
+
* to call {@link Pacer.dispose} from.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* const pacer = createPacer({
|
|
53
|
+
* name: 'courtlistener',
|
|
54
|
+
* limits: [{ requests: 5, perMs: 60_000 }, { requests: 50, perMs: 3_600_000 }],
|
|
55
|
+
* minStartGapMs: 100,
|
|
56
|
+
* maxConcurrent: 2,
|
|
57
|
+
* maxQueueDepth: 100,
|
|
58
|
+
* cooldown: { baseMs: 5_000, maxMs: 60_000 },
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* const res = await pacer.run(
|
|
62
|
+
* (signal) => fetchWithTimeout(url, 30_000, ctx, { signal }),
|
|
63
|
+
* { signal: ctx.signal, maxWaitMs: 45_000 },
|
|
64
|
+
* );
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function createPacer(options) {
|
|
68
|
+
const { cooldown, limits = [], maxConcurrent, maxQueueDepth, minStartGapMs = 0, name } = options;
|
|
69
|
+
const attributes = { [ATTR_MCP_PACER_NAME]: name };
|
|
70
|
+
/** Recorded start instants, ascending. Pruned to the longest constraint. */
|
|
71
|
+
const starts = [];
|
|
72
|
+
const retentionMs = Math.max(minStartGapMs, ...limits.map((limit) => limit.perMs), 0);
|
|
73
|
+
const queue = [];
|
|
74
|
+
let active = 0;
|
|
75
|
+
let consecutiveRateLimits = 0;
|
|
76
|
+
/** Absolute instant the shared gate reopens. */
|
|
77
|
+
let gateOpensAt = 0;
|
|
78
|
+
let dispatchTimer;
|
|
79
|
+
let disposed = false;
|
|
80
|
+
/**
|
|
81
|
+
* The earliest instant a start is permitted given `history`, evaluated at
|
|
82
|
+
* `at`. Exact over the cooldown gate, the start gap, and every window —
|
|
83
|
+
* concurrency is deliberately not modelled here, since a slot frees on an
|
|
84
|
+
* unknowable completion.
|
|
85
|
+
*/
|
|
86
|
+
function earliestStart(history, at) {
|
|
87
|
+
let earliest = gateOpensAt;
|
|
88
|
+
const last = history[history.length - 1];
|
|
89
|
+
if (last !== undefined && minStartGapMs > 0) {
|
|
90
|
+
earliest = Math.max(earliest, last + minStartGapMs);
|
|
91
|
+
}
|
|
92
|
+
for (const { requests, perMs } of limits) {
|
|
93
|
+
if (requests <= 0)
|
|
94
|
+
continue;
|
|
95
|
+
let seen = 0;
|
|
96
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
97
|
+
const start = history[i];
|
|
98
|
+
// A start exactly `perMs` old has already left the window.
|
|
99
|
+
if (start <= at - perMs)
|
|
100
|
+
break;
|
|
101
|
+
seen++;
|
|
102
|
+
if (seen === requests) {
|
|
103
|
+
earliest = Math.max(earliest, start + perMs);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return earliest;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* When an arrival joining behind `ahead` waiters could start, by replaying the
|
|
112
|
+
* windows forward. Exact while only the windows and the gap bind; a lower
|
|
113
|
+
* bound once `maxConcurrent` does — which is what keeps a shed from ever being
|
|
114
|
+
* a false one.
|
|
115
|
+
*/
|
|
116
|
+
function projectedStart(now, ahead) {
|
|
117
|
+
const simulated = starts.slice();
|
|
118
|
+
let at = now;
|
|
119
|
+
for (let i = 0; i <= ahead; i++) {
|
|
120
|
+
at = Math.max(at, earliestStart(simulated, at));
|
|
121
|
+
simulated.push(at);
|
|
122
|
+
}
|
|
123
|
+
return at;
|
|
124
|
+
}
|
|
125
|
+
/** Seconds until a slot opens, for the shed error's `retryAfter`. */
|
|
126
|
+
function secondsUntilSlot(now, ahead) {
|
|
127
|
+
return Math.ceil(Math.max(0, projectedStart(now, ahead) - now) / 1000);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The shed error. `rateLimited` with no `retryable: false`: to the calling
|
|
131
|
+
* agent this is an ordinary rate limit — wait `retryAfter`, call again — and
|
|
132
|
+
* that flag would tell them the opposite. `withRetry`'s default predicate
|
|
133
|
+
* reads `reason` instead, so an enclosing retry fails fast rather than
|
|
134
|
+
* sleeping past the deadline the shed exists to enforce.
|
|
135
|
+
*/
|
|
136
|
+
function shed(retryAfter, queueDepth) {
|
|
137
|
+
getPacerMetrics().sheds.add(1, attributes);
|
|
138
|
+
return rateLimited(`No ${name} request slot available within the caller's wait budget.`, {
|
|
139
|
+
reason: 'pacer_shed',
|
|
140
|
+
retryAfter,
|
|
141
|
+
queueDepth,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Releases an entry that has left the queue: clears its shed timer and abort
|
|
146
|
+
* listener, and settles the depth gauge. Leaving the line and the gauge are
|
|
147
|
+
* the same event, so every exit — dequeued, removed, disposed — goes through
|
|
148
|
+
* here.
|
|
149
|
+
*/
|
|
150
|
+
function detach(entry) {
|
|
151
|
+
if (entry.shedTimer !== undefined) {
|
|
152
|
+
clearTimeout(entry.shedTimer);
|
|
153
|
+
entry.shedTimer = undefined;
|
|
154
|
+
}
|
|
155
|
+
if (entry.onAbort && entry.signal) {
|
|
156
|
+
entry.signal.removeEventListener('abort', entry.onAbort);
|
|
157
|
+
entry.onAbort = undefined;
|
|
158
|
+
}
|
|
159
|
+
getPacerMetrics().queueDepth.add(-1, attributes);
|
|
160
|
+
}
|
|
161
|
+
/** Removes a waiter and settles the depth gauge. `false` if it already left. */
|
|
162
|
+
function remove(entry) {
|
|
163
|
+
const index = queue.indexOf(entry);
|
|
164
|
+
if (index === -1)
|
|
165
|
+
return false;
|
|
166
|
+
queue.splice(index, 1);
|
|
167
|
+
detach(entry);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
function clearDispatchTimer() {
|
|
171
|
+
if (dispatchTimer === undefined)
|
|
172
|
+
return;
|
|
173
|
+
clearTimeout(dispatchTimer);
|
|
174
|
+
dispatchTimer = undefined;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Drains as much of the queue as the constraints allow, then either arms the
|
|
178
|
+
* dispatch timer for the next opening or leaves no timer at all. FIFO: only
|
|
179
|
+
* the head is ever considered, so a cheap arrival never overtakes.
|
|
180
|
+
*/
|
|
181
|
+
function pump() {
|
|
182
|
+
clearDispatchTimer();
|
|
183
|
+
while (queue.length > 0) {
|
|
184
|
+
if (maxConcurrent !== undefined && active >= maxConcurrent)
|
|
185
|
+
return;
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
const earliest = earliestStart(starts, now);
|
|
188
|
+
if (earliest > now) {
|
|
189
|
+
dispatchTimer = setTimeout(() => {
|
|
190
|
+
dispatchTimer = undefined;
|
|
191
|
+
pump();
|
|
192
|
+
}, earliest - now);
|
|
193
|
+
dispatchTimer.unref?.();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const entry = queue.shift();
|
|
197
|
+
if (!entry)
|
|
198
|
+
return;
|
|
199
|
+
detach(entry);
|
|
200
|
+
starts.push(now);
|
|
201
|
+
while (starts.length > 0 && starts[0] <= now - retentionMs)
|
|
202
|
+
starts.shift();
|
|
203
|
+
entry.dispatch(now);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Closes the shared gate on an upstream rate limit:
|
|
208
|
+
* `min(max(baseMs · 2^(consecutive−1), retryAfter), maxMs)`, as an absolute
|
|
209
|
+
* instant. Any other error leaves it open; the first success resets the count.
|
|
210
|
+
*/
|
|
211
|
+
function noteRateLimit(error) {
|
|
212
|
+
if (!cooldown)
|
|
213
|
+
return;
|
|
214
|
+
if (!(error instanceof McpError) || error.code !== JsonRpcErrorCode.RateLimited)
|
|
215
|
+
return;
|
|
216
|
+
consecutiveRateLimits++;
|
|
217
|
+
const doubled = cooldown.baseMs * 2 ** (consecutiveRateLimits - 1);
|
|
218
|
+
// An absent or unparseable hint contributes nothing, leaving the doubling.
|
|
219
|
+
const honored = parseRetryAfterMs(error) ?? 0;
|
|
220
|
+
const waitMs = Math.min(Math.max(doubled, honored), cooldown.maxMs);
|
|
221
|
+
gateOpensAt = Math.max(gateOpensAt, Date.now() + waitMs);
|
|
222
|
+
getPacerMetrics().cooldowns.add(1, attributes);
|
|
223
|
+
}
|
|
224
|
+
function run(task, runOptions = {}) {
|
|
225
|
+
const { maxWaitMs, signal } = runOptions;
|
|
226
|
+
if (disposed) {
|
|
227
|
+
return Promise.reject(requestCancelled(`The ${name} pacer has been disposed.`));
|
|
228
|
+
}
|
|
229
|
+
if (signal?.aborted)
|
|
230
|
+
return Promise.reject(signal.reason);
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
const ahead = queue.length;
|
|
233
|
+
// Absolute backpressure first, so it rejects without arming a timer.
|
|
234
|
+
if (maxQueueDepth !== undefined && ahead >= maxQueueDepth) {
|
|
235
|
+
return Promise.reject(shed(secondsUntilSlot(now, ahead), ahead));
|
|
236
|
+
}
|
|
237
|
+
if (maxWaitMs !== undefined && projectedStart(now, ahead) - now > maxWaitMs) {
|
|
238
|
+
return Promise.reject(shed(secondsUntilSlot(now, ahead), ahead));
|
|
239
|
+
}
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
const entry = {
|
|
242
|
+
enqueuedAt: now,
|
|
243
|
+
reject,
|
|
244
|
+
signal,
|
|
245
|
+
dispatch: (startedAt) => {
|
|
246
|
+
active++;
|
|
247
|
+
getPacerMetrics().wait.record(startedAt - entry.enqueuedAt, attributes);
|
|
248
|
+
// Called inside an async wrapper so a task that throws before returning
|
|
249
|
+
// a promise rejects its own caller instead of escaping into `pump()`.
|
|
250
|
+
void (async () => task(signal ?? new AbortController().signal))().then((value) => {
|
|
251
|
+
consecutiveRateLimits = 0;
|
|
252
|
+
resolve(value);
|
|
253
|
+
active--;
|
|
254
|
+
pump();
|
|
255
|
+
}, (error) => {
|
|
256
|
+
noteRateLimit(error);
|
|
257
|
+
reject(error);
|
|
258
|
+
active--;
|
|
259
|
+
pump();
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
if (maxWaitMs !== undefined) {
|
|
264
|
+
entry.shedTimer = setTimeout(() => {
|
|
265
|
+
entry.shedTimer = undefined;
|
|
266
|
+
if (!remove(entry))
|
|
267
|
+
return;
|
|
268
|
+
reject(shed(secondsUntilSlot(Date.now(), 0), queue.length));
|
|
269
|
+
// The shed entry is gone; whoever is behind it should not wait on it.
|
|
270
|
+
pump();
|
|
271
|
+
}, maxWaitMs);
|
|
272
|
+
entry.shedTimer.unref?.();
|
|
273
|
+
}
|
|
274
|
+
if (signal) {
|
|
275
|
+
entry.onAbort = () => {
|
|
276
|
+
if (!remove(entry))
|
|
277
|
+
return;
|
|
278
|
+
reject(signal.reason);
|
|
279
|
+
pump();
|
|
280
|
+
};
|
|
281
|
+
signal.addEventListener('abort', entry.onAbort, { once: true });
|
|
282
|
+
}
|
|
283
|
+
queue.push(entry);
|
|
284
|
+
getPacerMetrics().queueDepth.add(1, attributes);
|
|
285
|
+
pump();
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
function dispose() {
|
|
289
|
+
if (disposed)
|
|
290
|
+
return;
|
|
291
|
+
disposed = true;
|
|
292
|
+
clearDispatchTimer();
|
|
293
|
+
for (const entry of queue.splice(0, queue.length)) {
|
|
294
|
+
detach(entry);
|
|
295
|
+
entry.reject(requestCancelled(`The ${name} pacer has been disposed.`));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
dispose,
|
|
300
|
+
run,
|
|
301
|
+
[Symbol.dispose]: dispose,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=pacer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pacer.js","sourceRoot":"","sources":["../../../src/utils/network/pacer.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import type { RequestContext } from '../internal/requestContext.js';
|
|
2
|
+
/**
|
|
3
|
+
* The per-attempt handle {@link withRetry} passes to its operation.
|
|
4
|
+
*
|
|
5
|
+
* Both fields describe the *total* budget, not this attempt's: they exist so an
|
|
6
|
+
* attempt can bound its own I/O against what is left, which is what keeps the
|
|
7
|
+
* deadline from overshooting by one in-flight request.
|
|
8
|
+
*/
|
|
9
|
+
export interface RetryAttempt {
|
|
10
|
+
/**
|
|
11
|
+
* Milliseconds left on {@link RetryOptions.deadlineMs} as this attempt starts.
|
|
12
|
+
* Never negative, and `Number.POSITIVE_INFINITY` when no deadline is set — so
|
|
13
|
+
* `Math.min(perAttemptMs, remainingMs)` is correct either way.
|
|
14
|
+
*/
|
|
15
|
+
readonly remainingMs: number;
|
|
16
|
+
/**
|
|
17
|
+
* `AbortSignal.any` over the deadline clock and {@link RetryOptions.signal}.
|
|
18
|
+
* Thread it into the attempt's fetch: a deadline that only fires *between*
|
|
19
|
+
* attempts cannot stop one already in flight.
|
|
20
|
+
*/
|
|
21
|
+
readonly signal: AbortSignal;
|
|
22
|
+
}
|
|
2
23
|
/** Configuration for {@link withRetry}. */
|
|
3
24
|
export interface RetryOptions {
|
|
4
25
|
/**
|
|
@@ -18,6 +39,24 @@ export interface RetryOptions {
|
|
|
18
39
|
* `state`, protocol method handles) before pino sees them.
|
|
19
40
|
*/
|
|
20
41
|
context?: RequestContext;
|
|
42
|
+
/**
|
|
43
|
+
* Total wall-clock budget in milliseconds covering every attempt, every
|
|
44
|
+
* backoff, and any honored `Retry-After` — the bound `maxRetries` and a
|
|
45
|
+
* per-attempt timeout cannot express between them. Four 30s attempts plus
|
|
46
|
+
* backoff outlast a client's 60s request timeout, so the caller gets a
|
|
47
|
+
* transport timeout instead of this server's classified error.
|
|
48
|
+
*
|
|
49
|
+
* The clock is an `AbortController` + `setTimeout`, composed into
|
|
50
|
+
* {@link RetryAttempt.signal}; pass that signal into the attempt's I/O or the
|
|
51
|
+
* deadline overshoots by one in-flight request. Expiry rejects with a
|
|
52
|
+
* `Timeout` `McpError` carrying
|
|
53
|
+
* `data: { reason: 'retry_deadline_exceeded', deadlineMs, elapsedMs, retryAttempts }`
|
|
54
|
+
* and the last attempt's error as `cause`.
|
|
55
|
+
*
|
|
56
|
+
* Unset (the default) leaves attempt counts, delays, logging, and the
|
|
57
|
+
* exhausted-error shape exactly as they are.
|
|
58
|
+
*/
|
|
59
|
+
deadlineMs?: number;
|
|
21
60
|
/**
|
|
22
61
|
* Custom predicate to determine if an error is transient and should be
|
|
23
62
|
* retried. When provided, this **replaces** the default predicate entirely
|
|
@@ -56,10 +95,63 @@ export interface RetryOptions {
|
|
|
56
95
|
operation?: string;
|
|
57
96
|
/**
|
|
58
97
|
* Optional AbortSignal. When aborted, the retry loop exits immediately
|
|
59
|
-
* without further attempts
|
|
98
|
+
* without further attempts, rethrowing unchanged — this is the caller-abort
|
|
99
|
+
* passthrough and it outranks a {@link deadlineMs} expiry, so a cancelled
|
|
100
|
+
* request is never relabelled as one that ran out of budget. Also composed
|
|
101
|
+
* into {@link RetryAttempt.signal}.
|
|
60
102
|
*/
|
|
61
103
|
signal?: AbortSignal;
|
|
62
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Parses an upstream `Retry-After` hint into milliseconds. The two HTTP helpers
|
|
107
|
+
* (`fetchWithTimeout`, `httpErrorFromResponse`) capture the raw header value into
|
|
108
|
+
* `error.data.retryAfter`; this reads it back so the retry delay can honor the
|
|
109
|
+
* wait the upstream explicitly asked for instead of blind exponential backoff.
|
|
110
|
+
*
|
|
111
|
+
* Handles both RFC 9110 §10.2.3 forms:
|
|
112
|
+
* - **delta-seconds** — a bare non-negative integer (`"30"` → `30_000`).
|
|
113
|
+
* - **HTTP-date** — an absolute instant, converted to a wait from now and
|
|
114
|
+
* clamped at `0` (a past date means "retry now").
|
|
115
|
+
*
|
|
116
|
+
* A numeric `data.retryAfter` is also accepted and interpreted as delta-seconds,
|
|
117
|
+
* matching the header's units. Returns `undefined` when the error carries no
|
|
118
|
+
* parseable hint, so callers fall back to exponential backoff.
|
|
119
|
+
*
|
|
120
|
+
* Module-level, not public: the pacer's cooldown gate reads the same hint off
|
|
121
|
+
* the same errors, and a second copy of the RFC 9110 forms would drift.
|
|
122
|
+
*/
|
|
123
|
+
export declare function parseRetryAfterMs(error: unknown): number | undefined;
|
|
124
|
+
/**
|
|
125
|
+
* Default transient check: `McpError` with a transient code, or any non-McpError
|
|
126
|
+
* (network failures, unexpected throws) which are assumed transient.
|
|
127
|
+
*
|
|
128
|
+
* **Per-error opt-out.** When a thrown `McpError` carries `data.retryable === false`,
|
|
129
|
+
* the error is treated as non-transient and fails fast — even if its code is in
|
|
130
|
+
* `TRANSIENT_CODES`. This is the in-band escape hatch for deterministic upstream
|
|
131
|
+
* failures (e.g. a query too expensive to ever succeed surfaced as HTTP 200 +
|
|
132
|
+
* error body) that arrive with a transient code but should never be retried.
|
|
133
|
+
*
|
|
134
|
+
* Absent the flag (or when it is `true`), behavior is unchanged — code-based
|
|
135
|
+
* classification applies. Non-`McpError` throws (raw network errors, unexpected
|
|
136
|
+
* throws) are always assumed transient regardless of the flag.
|
|
137
|
+
*
|
|
138
|
+
* **Composing rather than replacing.** {@link RetryOptions.isTransient} replaces
|
|
139
|
+
* this predicate outright, so a caller that wants "the framework default, except
|
|
140
|
+
* this one error" composes off this export instead of mirroring the transient
|
|
141
|
+
* code set — a copy drifts silently when the framework's classification changes:
|
|
142
|
+
*
|
|
143
|
+
* ```ts
|
|
144
|
+
* withRetry(fn, {
|
|
145
|
+
* isTransient: (error) =>
|
|
146
|
+
* !(error instanceof McpError && error.data?.reason === 'budget_exhausted') &&
|
|
147
|
+
* defaultIsTransient(error),
|
|
148
|
+
* });
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* The inverse — treating one more shape as transient — composes the same way:
|
|
152
|
+
* `defaultIsTransient(error) || isMyRetryableShape(error)`.
|
|
153
|
+
*/
|
|
154
|
+
export declare function defaultIsTransient(error: unknown): boolean;
|
|
63
155
|
/**
|
|
64
156
|
* Executes `fn` with retry logic and exponential backoff.
|
|
65
157
|
*
|
|
@@ -82,11 +174,24 @@ export interface RetryOptions {
|
|
|
82
174
|
* fast: a window that won't clear within the retry budget is surfaced to the
|
|
83
175
|
* caller immediately rather than burning attempts that cannot succeed.
|
|
84
176
|
*
|
|
177
|
+
* **Total deadline.** `deadlineMs` bounds the whole ladder — every attempt, every
|
|
178
|
+
* backoff, every honored `Retry-After` — where `maxRetries` and a per-attempt
|
|
179
|
+
* timeout together cannot. `fn` receives `{ signal, remainingMs }`; thread
|
|
180
|
+
* `signal` into the attempt's I/O, or an expiry that fires mid-attempt overshoots
|
|
181
|
+
* by one in-flight request. A backoff that would consume the remaining budget
|
|
182
|
+
* fails fast rather than sleeping into a certain timeout, and expiry rejects with
|
|
183
|
+
* a single `Timeout` error carrying `data.reason: 'retry_deadline_exceeded'` —
|
|
184
|
+
* the `RequestCancelled` that an external-signal abort produces inside
|
|
185
|
+
* `fetchWithTimeout` normalizes to it. A caller abort on `options.signal` keeps
|
|
186
|
+
* precedence and is never relabelled. Without `deadlineMs`, behavior is
|
|
187
|
+
* unchanged.
|
|
188
|
+
*
|
|
85
189
|
* When retries exhaust, the final error is enriched with attempt count in both
|
|
86
190
|
* the message and structured data, so callers know retries were already attempted.
|
|
87
191
|
*
|
|
88
192
|
* @typeParam T - Return type of the operation.
|
|
89
|
-
* @param fn - The async operation to execute with retries.
|
|
193
|
+
* @param fn - The async operation to execute with retries. Receives a
|
|
194
|
+
* {@link RetryAttempt}; a zero-argument operation is assignable unchanged.
|
|
90
195
|
* @param options - Retry configuration. All fields optional with sensible defaults.
|
|
91
196
|
* @returns The result of `fn` on success.
|
|
92
197
|
* @throws The enriched final error when all attempts are exhausted, or the original
|
|
@@ -105,6 +210,17 @@ export interface RetryOptions {
|
|
|
105
210
|
* );
|
|
106
211
|
* }
|
|
107
212
|
* ```
|
|
213
|
+
*
|
|
214
|
+
* @example Bounded by one wall-clock budget
|
|
215
|
+
* ```ts
|
|
216
|
+
* const data = await withRetry(
|
|
217
|
+
* async ({ signal, remainingMs }) => {
|
|
218
|
+
* const res = await fetchWithTimeout(url, Math.min(30_000, remainingMs), ctx, { signal });
|
|
219
|
+
* return parse(await res.text());
|
|
220
|
+
* },
|
|
221
|
+
* { operation: 'search', context: ctx, signal: ctx.signal, deadlineMs: 50_000 },
|
|
222
|
+
* );
|
|
223
|
+
* ```
|
|
108
224
|
*/
|
|
109
|
-
export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
225
|
+
export declare function withRetry<T>(fn: (attempt: RetryAttempt) => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
110
226
|
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../../src/utils/network/retry.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAYzE,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAE1C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../../src/utils/network/retry.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAYzE;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB;;;;;;;;;;;;;;;;OAgBG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAE1C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAoBpE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAgB1D;AA2FD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,EAAE,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,CAAC,CAAC,EACzC,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,CAAC,CAAC,CAuHZ"}
|