@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.
Files changed (117) hide show
  1. package/AGENTS.md +12 -10
  2. package/CLAUDE.md +12 -10
  3. package/README.md +2 -2
  4. package/changelog/0.13.x/0.13.3.md +44 -0
  5. package/changelog/0.13.x/0.13.4.md +65 -0
  6. package/changelog/template.md +7 -7
  7. package/dist/config/appRoot.d.ts.map +1 -1
  8. package/dist/config/appRoot.js +48 -16
  9. package/dist/config/appRoot.js.map +1 -1
  10. package/dist/core/app.d.ts +20 -0
  11. package/dist/core/app.d.ts.map +1 -1
  12. package/dist/core/app.js +1 -0
  13. package/dist/core/app.js.map +1 -1
  14. package/dist/core/index.d.ts +1 -0
  15. package/dist/core/index.d.ts.map +1 -1
  16. package/dist/core/index.js.map +1 -1
  17. package/dist/linter/rules/schema-rules.d.ts +19 -0
  18. package/dist/linter/rules/schema-rules.d.ts.map +1 -1
  19. package/dist/linter/rules/schema-rules.js +36 -0
  20. package/dist/linter/rules/schema-rules.js.map +1 -1
  21. package/dist/linter/rules/tool-rules.d.ts +17 -0
  22. package/dist/linter/rules/tool-rules.d.ts.map +1 -1
  23. package/dist/linter/rules/tool-rules.js +97 -1
  24. package/dist/linter/rules/tool-rules.js.map +1 -1
  25. package/dist/mcp-server/handlerContext.d.ts +6 -0
  26. package/dist/mcp-server/handlerContext.d.ts.map +1 -1
  27. package/dist/mcp-server/handlerContext.js.map +1 -1
  28. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
  29. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +17 -9
  30. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
  31. package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts +114 -0
  32. package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts.map +1 -0
  33. package/dist/mcp-server/tools/utils/inputPrevalidation.js +428 -0
  34. package/dist/mcp-server/tools/utils/inputPrevalidation.js.map +1 -0
  35. package/dist/mcp-server/tools/utils/strictenRecord.d.ts +42 -0
  36. package/dist/mcp-server/tools/utils/strictenRecord.d.ts.map +1 -0
  37. package/dist/mcp-server/tools/utils/strictenRecord.js +48 -0
  38. package/dist/mcp-server/tools/utils/strictenRecord.js.map +1 -0
  39. package/dist/mcp-server/tools/utils/toolDefinition.d.ts +27 -0
  40. package/dist/mcp-server/tools/utils/toolDefinition.d.ts.map +1 -1
  41. package/dist/mcp-server/tools/utils/toolDefinition.js +35 -6
  42. package/dist/mcp-server/tools/utils/toolDefinition.js.map +1 -1
  43. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +28 -4
  44. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  45. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +245 -23
  46. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  47. package/dist/services/canvas/core/sqlGate.d.ts +14 -1
  48. package/dist/services/canvas/core/sqlGate.d.ts.map +1 -1
  49. package/dist/services/canvas/core/sqlGate.js +69 -7
  50. package/dist/services/canvas/core/sqlGate.js.map +1 -1
  51. package/dist/services/canvas/index.d.ts +1 -1
  52. package/dist/services/canvas/index.d.ts.map +1 -1
  53. package/dist/services/canvas/index.js +1 -1
  54. package/dist/services/canvas/index.js.map +1 -1
  55. package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts +20 -0
  56. package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
  57. package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +75 -25
  58. package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
  59. package/dist/services/canvas/types.d.ts +6 -1
  60. package/dist/services/canvas/types.d.ts.map +1 -1
  61. package/dist/types-global/errors.d.ts.map +1 -1
  62. package/dist/types-global/errors.js +4 -3
  63. package/dist/types-global/errors.js.map +1 -1
  64. package/dist/utils/index.d.ts +3 -2
  65. package/dist/utils/index.d.ts.map +1 -1
  66. package/dist/utils/index.js +3 -2
  67. package/dist/utils/index.js.map +1 -1
  68. package/dist/utils/internal/error-handler/errorHandler.d.ts +33 -0
  69. package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
  70. package/dist/utils/internal/error-handler/errorHandler.js +45 -3
  71. package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
  72. package/dist/utils/network/httpError.d.ts +13 -2
  73. package/dist/utils/network/httpError.d.ts.map +1 -1
  74. package/dist/utils/network/httpError.js +4 -2
  75. package/dist/utils/network/httpError.js.map +1 -1
  76. package/dist/utils/network/pacer.d.ts +117 -0
  77. package/dist/utils/network/pacer.d.ts.map +1 -0
  78. package/dist/utils/network/pacer.js +304 -0
  79. package/dist/utils/network/pacer.js.map +1 -0
  80. package/dist/utils/network/retry.d.ts +119 -3
  81. package/dist/utils/network/retry.d.ts.map +1 -1
  82. package/dist/utils/network/retry.js +176 -35
  83. package/dist/utils/network/retry.js.map +1 -1
  84. package/dist/utils/security/rateLimiter.d.ts +19 -1
  85. package/dist/utils/security/rateLimiter.d.ts.map +1 -1
  86. package/dist/utils/security/rateLimiter.js +49 -1
  87. package/dist/utils/security/rateLimiter.js.map +1 -1
  88. package/dist/utils/telemetry/attributes.d.ts +19 -0
  89. package/dist/utils/telemetry/attributes.d.ts.map +1 -1
  90. package/dist/utils/telemetry/attributes.js +30 -0
  91. package/dist/utils/telemetry/attributes.js.map +1 -1
  92. package/framework-skills/add-tool/SKILL.md +43 -2
  93. package/framework-skills/api-canvas/SKILL.md +8 -4
  94. package/framework-skills/api-config/SKILL.md +4 -4
  95. package/framework-skills/api-errors/SKILL.md +19 -10
  96. package/framework-skills/api-linter/SKILL.md +48 -3
  97. package/framework-skills/api-telemetry/SKILL.md +26 -2
  98. package/framework-skills/api-utils/SKILL.md +7 -3
  99. package/framework-skills/api-utils/references/security.md +2 -2
  100. package/framework-skills/design-mcp-server/SKILL.md +17 -2
  101. package/framework-skills/field-test/SKILL.md +3 -1
  102. package/framework-skills/git-wrapup/SKILL.md +90 -68
  103. package/framework-skills/orchestrations/SKILL.md +3 -3
  104. package/framework-skills/release-and-publish/SKILL.md +10 -10
  105. package/framework-skills/release-pr-review/SKILL.md +19 -26
  106. package/framework-skills/report-issue-framework/SKILL.md +6 -35
  107. package/framework-skills/report-issue-local/SKILL.md +6 -36
  108. package/framework-skills/tool-defs-analysis/SKILL.md +4 -4
  109. package/package.json +7 -7
  110. package/templates/.github/ISSUE_TEMPLATE/bug_report.yml +2 -2
  111. package/templates/.github/ISSUE_TEMPLATE/feature_request.yml +2 -2
  112. package/templates/.github/workflows/codeql.yml +39 -0
  113. package/templates/AGENTS.md +3 -1
  114. package/templates/CLAUDE.md +3 -1
  115. package/templates/changelog/template.md +7 -7
  116. package/templates/package.json +2 -2
  117. 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":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,gBAAgB,EAChB,QAAQ,EACR,WAAW,EACX,gBAAgB,GACjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAgGnG,IAAI,WAOS,CAAC;AAEd;;;GAGG;AACH,SAAS,eAAe;IACtB,WAAW,KAAK;QACd,SAAS,EAAE,aAAa,CACtB,qBAAqB,EACrB,iDAAiD,EACjD,aAAa,CACd;QACD,UAAU,EAAE,mBAAmB,CAC7B,uBAAuB,EACvB,mCAAmC,EACnC,YAAY,CACb;QACD,KAAK,EAAE,aAAa,CAClB,iBAAiB,EACjB,mEAAmE,EACnE,YAAY,CACb;QACD,IAAI,EAAE,eAAe,CAAC,gBAAgB,EAAE,+CAA+C,EAAE,IAAI,CAAC;KAC/F,CAAC;IACF,OAAO,WAAW,CAAC;AACrB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,UAAU,WAAW,CAAC,OAAqB;IAC/C,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,EAAE,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,GAAG,CAAC,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IAEjG,MAAM,UAAU,GAAG,EAAE,CAAC,mBAAmB,CAAC,EAAE,IAAI,EAAE,CAAC;IACnD,4EAA4E;IAC5E,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;IACtF,MAAM,KAAK,GAAiB,EAAE,CAAC;IAE/B,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,qBAAqB,GAAG,CAAC,CAAC;IAC9B,gDAAgD;IAChD,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,IAAI,aAAwD,CAAC;IAC7D,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB;;;;;OAKG;IACH,SAAS,aAAa,CAAC,OAA0B,EAAE,EAAU;QAC3D,IAAI,QAAQ,GAAG,WAAW,CAAC;QAE3B,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACzC,IAAI,IAAI,KAAK,SAAS,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;YAC5C,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,GAAG,aAAa,CAAC,CAAC;QACtD,CAAC;QAED,KAAK,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,MAAM,EAAE,CAAC;YACzC,IAAI,QAAQ,IAAI,CAAC;gBAAE,SAAS;YAC5B,IAAI,IAAI,GAAG,CAAC,CAAC;YACb,KAAK,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC7C,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAW,CAAC;gBACnC,2DAA2D;gBAC3D,IAAI,KAAK,IAAI,EAAE,GAAG,KAAK;oBAAE,MAAM;gBAC/B,IAAI,EAAE,CAAC;gBACP,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;oBACtB,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC;oBAC7C,MAAM;gBACR,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED;;;;;OAKG;IACH,SAAS,cAAc,CAAC,GAAW,EAAE,KAAa;QAChD,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC;QACjC,IAAI,EAAE,GAAG,GAAG,CAAC;QACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YAChC,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,aAAa,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;YAChD,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrB,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,qEAAqE;IACrE,SAAS,gBAAgB,CAAC,GAAW,EAAE,KAAa;QAClD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,cAAc,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IACzE,CAAC;IAED;;;;;;OAMG;IACH,SAAS,IAAI,CAAC,UAAkB,EAAE,UAAkB;QAClD,eAAe,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;QAC3C,OAAO,WAAW,CAAC,MAAM,IAAI,0DAA0D,EAAE;YACvF,MAAM,EAAE,YAAY;YACpB,UAAU;YACV,UAAU;SACX,CAAC,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACH,SAAS,MAAM,CAAC,KAAiB;QAC/B,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YAClC,YAAY,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;YAC9B,KAAK,CAAC,SAAS,GAAG,SAAS,CAAC;QAC9B,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YAClC,KAAK,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;YACzD,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;QAC5B,CAAC;QACD,eAAe,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IACnD,CAAC;IAED,gFAAgF;IAChF,SAAS,MAAM,CAAC,KAAiB;QAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,KAAK,KAAK,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;QAC/B,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QACvB,MAAM,CAAC,KAAK,CAAC,CAAC;QACd,OAAO,IAAI,CAAC;IACd,CAAC;IAED,SAAS,kBAAkB;QACzB,IAAI,aAAa,KAAK,SAAS;YAAE,OAAO;QACxC,YAAY,CAAC,aAAa,CAAC,CAAC;QAC5B,aAAa,GAAG,SAAS,CAAC;IAC5B,CAAC;IAED;;;;OAIG;IACH,SAAS,IAAI;QACX,kBAAkB,EAAE,CAAC;QACrB,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,IAAI,aAAa,KAAK,SAAS,IAAI,MAAM,IAAI,aAAa;gBAAE,OAAO;YAEnE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YAC5C,IAAI,QAAQ,GAAG,GAAG,EAAE,CAAC;gBACnB,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE;oBAC9B,aAAa,GAAG,SAAS,CAAC;oBAC1B,IAAI,EAAE,CAAC;gBACT,CAAC,EAAE,QAAQ,GAAG,GAAG,CAAC,CAAC;gBAClB,aAAwC,CAAC,KAAK,EAAE,EAAE,CAAC;gBACpD,OAAO;YACT,CAAC;YAED,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC;YAC5B,IAAI,CAAC,KAAK;gBAAE,OAAO;YACnB,MAAM,CAAC,KAAK,CAAC,CAAC;YAEd,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACjB,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,IAAK,MAAM,CAAC,CAAC,CAAY,IAAI,GAAG,GAAG,WAAW;gBAAE,MAAM,CAAC,KAAK,EAAE,CAAC;YAEvF,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,SAAS,aAAa,CAAC,KAAc;QACnC,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,IAAI,CAAC,CAAC,KAAK,YAAY,QAAQ,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,gBAAgB,CAAC,WAAW;YAAE,OAAO;QAExF,qBAAqB,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,qBAAqB,GAAG,CAAC,CAAC,CAAC;QACnE,2EAA2E;QAC3E,MAAM,OAAO,GAAG,iBAAiB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;QAEpE,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,CAAC;QACzD,eAAe,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IACjD,CAAC;IAED,SAAS,GAAG,CACV,IAAyC,EACzC,UAAU,GAAoB,EAAE;QAEhC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC;QAEzC,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,IAAI,2BAA2B,CAAC,CAAC,CAAC;QAClF,CAAC;QACD,IAAI,MAAM,EAAE,OAAO;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAE1D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC;QAE3B,qEAAqE;QACrE,IAAI,aAAa,KAAK,SAAS,IAAI,KAAK,IAAI,aAAa,EAAE,CAAC;YAC1D,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;QACnE,CAAC;QACD,IAAI,SAAS,KAAK,SAAS,IAAI,cAAc,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,GAAG,GAAG,SAAS,EAAE,CAAC;YAC5E,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;QACnE,CAAC;QAED,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACxC,MAAM,KAAK,GAAe;gBACxB,UAAU,EAAE,GAAG;gBACf,MAAM;gBACN,MAAM;gBACN,QAAQ,EAAE,CAAC,SAAS,EAAE,EAAE;oBACtB,MAAM,EAAE,CAAC;oBACT,eAAe,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;oBACxE,wEAAwE;oBACxE,sEAAsE;oBACtE,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,eAAe,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI,CACpE,CAAC,KAAK,EAAE,EAAE;wBACR,qBAAqB,GAAG,CAAC,CAAC;wBAC1B,OAAO,CAAC,KAAK,CAAC,CAAC;wBACf,MAAM,EAAE,CAAC;wBACT,IAAI,EAAE,CAAC;oBACT,CAAC,EACD,CAAC,KAAc,EAAE,EAAE;wBACjB,aAAa,CAAC,KAAK,CAAC,CAAC;wBACrB,MAAM,CAAC,KAAK,CAAC,CAAC;wBACd,MAAM,EAAE,CAAC;wBACT,IAAI,EAAE,CAAC;oBACT,CAAC,CACF,CAAC;gBACJ,CAAC;aACF,CAAC;YAEF,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;gBAC5B,KAAK,CAAC,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE;oBAChC,KAAK,CAAC,SAAS,GAAG,SAAS,CAAC;oBAC5B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;wBAAE,OAAO;oBAC3B,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;oBAC5D,sEAAsE;oBACtE,IAAI,EAAE,CAAC;gBACT,CAAC,EAAE,SAAS,CAAC,CAAC;gBACb,KAAK,CAAC,SAAoC,CAAC,KAAK,EAAE,EAAE,CAAC;YACxD,CAAC;YAED,IAAI,MAAM,EAAE,CAAC;gBACX,KAAK,CAAC,OAAO,GAAG,GAAG,EAAE;oBACnB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;wBAAE,OAAO;oBAC3B,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;oBACtB,IAAI,EAAE,CAAC;gBACT,CAAC,CAAC;gBACF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAClE,CAAC;YAED,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAClB,eAAe,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;YAChD,IAAI,EAAE,CAAC;QACT,CAAC,CAAC,CAAC;IACL,CAAC;IAED,SAAS,OAAO;QACd,IAAI,QAAQ;YAAE,OAAO;QACrB,QAAQ,GAAG,IAAI,CAAC;QAChB,kBAAkB,EAAE,CAAC;QACrB,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;YAClD,MAAM,CAAC,KAAK,CAAC,CAAC;YACd,KAAK,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,IAAI,2BAA2B,CAAC,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO;QACP,GAAG;QACH,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO;KAC1B,CAAC;AACJ,CAAC"}
@@ -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;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AA8GD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,CAAC,CAAC,CAgE/F"}
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"}