@yoonion/mimi-seed-mcp 0.15.0 → 0.15.1
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/dist/lib/http.d.ts +3 -2
- package/dist/lib/http.js +38 -20
- package/package.json +1 -1
package/dist/lib/http.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare const HTTP_MAX_ATTEMPTS = 3;
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function parseRetryAfter(value: string | null, nowMs: number): number | null;
|
|
18
18
|
export interface FetchOptions {
|
|
19
|
-
/** 한 **시도당**
|
|
19
|
+
/** 한 **시도당** 상한. 총 소요 시간의 상한은 아래 RETRY_WINDOW_MS 를 더한 값이다. */
|
|
20
20
|
timeoutMs?: number;
|
|
21
21
|
/** 총 시도 횟수. 1 이면 재시도하지 않는다. */
|
|
22
22
|
maxAttempts?: number;
|
|
@@ -27,8 +27,9 @@ export interface FetchOptions {
|
|
|
27
27
|
* 재시도 정책:
|
|
28
28
|
* - **429** 는 메서드와 무관하게 재시도한다. 레이트 리미터는 요청을 처리하기 전에
|
|
29
29
|
* 거절하므로 POST 라도 중복 생성이 일어나지 않는다.
|
|
30
|
-
* - **5xx / 네트워크
|
|
30
|
+
* - **5xx / 빠른 네트워크 오류**(ECONNRESET·DNS 등)는 idempotent 메서드에서만.
|
|
31
31
|
* POST 는 서버가 이미 처리했는지 알 수 없어 재요청이 중복 생성을 만든다.
|
|
32
|
+
* - **타임아웃은 재시도하지 않는다.** 아래 시간 예산 참고.
|
|
32
33
|
* - `Retry-After` 가 있으면 그 값을 쓰고, 없으면 지수 백오프 + 지터.
|
|
33
34
|
* - 재전송 불가능한 본문(스트림)이면 재시도하지 않는다.
|
|
34
35
|
*
|
package/dist/lib/http.js
CHANGED
|
@@ -27,6 +27,16 @@ export const HTTP_MAX_ATTEMPTS = 3;
|
|
|
27
27
|
const BASE_BACKOFF_MS = 500;
|
|
28
28
|
/** Retry-After 가 아무리 길어도 이 이상은 기다리지 않는다 — 도구 호출이 매달린 것과 같아진다. */
|
|
29
29
|
const MAX_BACKOFF_MS = 20_000;
|
|
30
|
+
/**
|
|
31
|
+
* 재시도가 **추가로** 쓸 수 있는 총 시간.
|
|
32
|
+
*
|
|
33
|
+
* 재시도 도입 전의 상한은 timeoutMs 하나였다(전송은 600초). 시도마다 그 상한을 새로
|
|
34
|
+
* 주면 총 30분까지 늘어나서, 이 모듈이 존재하는 이유("도구 호출이 매달리면 안 된다")를
|
|
35
|
+
* 재시도가 되살린다. 그래서 총 예산 = timeoutMs + 이 값으로 못박는다.
|
|
36
|
+
*/
|
|
37
|
+
const RETRY_WINDOW_MS = 30_000;
|
|
38
|
+
/** 예산이 거의 소진돼도 최소한 이만큼은 준다 — 0초 타임아웃으로 즉시 죽는 것을 막는다. */
|
|
39
|
+
const MIN_ATTEMPT_MS = 1_000;
|
|
30
40
|
/**
|
|
31
41
|
* 메서드가 재요청해도 안전한가(RFC 9110 idempotent).
|
|
32
42
|
*
|
|
@@ -103,14 +113,17 @@ function backoffFor(attempt) {
|
|
|
103
113
|
return base + Math.floor(Math.random() * (base / 2));
|
|
104
114
|
}
|
|
105
115
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
116
|
+
/** 백오프가 총 예산을 넘기지 않게 자른다. */
|
|
117
|
+
const cappedWait = (wait, deadline) => Math.max(0, Math.min(wait, deadline - Date.now()));
|
|
106
118
|
/**
|
|
107
119
|
* 타임아웃 + 재시도가 붙은 `fetch`.
|
|
108
120
|
*
|
|
109
121
|
* 재시도 정책:
|
|
110
122
|
* - **429** 는 메서드와 무관하게 재시도한다. 레이트 리미터는 요청을 처리하기 전에
|
|
111
123
|
* 거절하므로 POST 라도 중복 생성이 일어나지 않는다.
|
|
112
|
-
* - **5xx / 네트워크
|
|
124
|
+
* - **5xx / 빠른 네트워크 오류**(ECONNRESET·DNS 등)는 idempotent 메서드에서만.
|
|
113
125
|
* POST 는 서버가 이미 처리했는지 알 수 없어 재요청이 중복 생성을 만든다.
|
|
126
|
+
* - **타임아웃은 재시도하지 않는다.** 아래 시간 예산 참고.
|
|
114
127
|
* - `Retry-After` 가 있으면 그 값을 쓰고, 없으면 지수 백오프 + 지터.
|
|
115
128
|
* - 재전송 불가능한 본문(스트림)이면 재시도하지 않는다.
|
|
116
129
|
*
|
|
@@ -124,41 +137,46 @@ export async function fetchWithTimeout(input, init = {}, options = {}) {
|
|
|
124
137
|
const timeoutMs = opts.timeoutMs ?? HTTP_TIMEOUT_MS;
|
|
125
138
|
const method = (init.method ?? 'GET').toUpperCase();
|
|
126
139
|
const callerSignal = init.signal != null;
|
|
127
|
-
const
|
|
128
|
-
const maxAttempts =
|
|
140
|
+
const replayable = !callerSignal && isReplayableBody(init.body);
|
|
141
|
+
const maxAttempts = replayable ? (opts.maxAttempts ?? HTTP_MAX_ATTEMPTS) : 1;
|
|
129
142
|
const idempotent = IDEMPOTENT_METHODS.has(method);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
// 총 시간 예산. 재시도가 없던 시절의 상한(timeoutMs)에 재시도 창만 더한 값으로
|
|
144
|
+
// 고정한다 — 이게 없으면 전송용 600초 × 3회 = 30분이 되어, 이 모듈이 애초에
|
|
145
|
+
// 막으려던 "도구 호출이 매달림"을 재시도가 되살린다.
|
|
146
|
+
const deadline = Date.now() + timeoutMs + (maxAttempts > 1 ? RETRY_WINDOW_MS : 0);
|
|
147
|
+
/** 남은 예산 안에서 다음 시도를 시작해도 되는가. */
|
|
148
|
+
const hasBudget = (next) => next < maxAttempts && Date.now() < deadline;
|
|
149
|
+
for (let attempt = 0;; attempt += 1) {
|
|
150
|
+
// 마지막 시도가 예산을 넘겨 달리지 않도록, 남은 시간으로 잘라준다.
|
|
151
|
+
const attemptTimeout = Math.min(timeoutMs, Math.max(deadline - Date.now(), MIN_ATTEMPT_MS));
|
|
152
|
+
const signal = init.signal ?? AbortSignal.timeout(attemptTimeout);
|
|
134
153
|
let response;
|
|
135
154
|
try {
|
|
136
155
|
response = await fetch(input, { ...init, signal });
|
|
137
156
|
}
|
|
138
157
|
catch (error) {
|
|
139
|
-
|
|
158
|
+
// 타임아웃은 재시도하지 않는다. 이미 예산을 통째로 쓴 실패이고, 같은 상한으로
|
|
159
|
+
// 두 번 더 기다려도 얻는 게 없다 — 총 소요 시간만 배로 늘린다.
|
|
160
|
+
if (isTimeoutAbort(error)) {
|
|
161
|
+
throw new Error(`${endpointLabel(input)} 요청이 ${Math.round(attemptTimeout / 1000)}초 안에 끝나지 않아 중단했습니다. ` +
|
|
162
|
+
'네트워크 상태를 확인하고 다시 시도하세요.', { cause: error });
|
|
163
|
+
}
|
|
140
164
|
// 응답 전 실패는 서버가 요청을 받았는지 알 수 없다 → idempotent 에서만 재시도.
|
|
141
|
-
if (
|
|
142
|
-
if (isTimeoutAbort(error)) {
|
|
143
|
-
throw new Error(`${endpointLabel(input)} 요청이 ${Math.round(timeoutMs / 1000)}초 안에 끝나지 않아 중단했습니다. ` +
|
|
144
|
-
'네트워크 상태를 확인하고 다시 시도하세요.', { cause: error });
|
|
145
|
-
}
|
|
165
|
+
if (!idempotent || !hasBudget(attempt + 1))
|
|
146
166
|
throw error;
|
|
147
|
-
|
|
148
|
-
await sleep(backoffFor(attempt));
|
|
167
|
+
await sleep(cappedWait(backoffFor(attempt), deadline));
|
|
149
168
|
continue;
|
|
150
169
|
}
|
|
151
|
-
if (response.ok ||
|
|
170
|
+
if (response.ok || !TRANSIENT_STATUS.has(response.status))
|
|
152
171
|
return response;
|
|
153
172
|
// 429 는 처리 전 거절이므로 POST 도 안전하다. 그 외 일시적 오류는 idempotent 만.
|
|
154
173
|
if (response.status !== 429 && !idempotent)
|
|
155
174
|
return response;
|
|
175
|
+
if (!hasBudget(attempt + 1))
|
|
176
|
+
return response;
|
|
156
177
|
const wait = parseRetryAfter(response.headers.get('retry-after'), Date.now()) ?? backoffFor(attempt);
|
|
157
178
|
// 재시도할 응답의 본문은 읽지 않고 버린다 — 소켓을 붙잡고 있지 않도록.
|
|
158
179
|
await response.body?.cancel().catch(() => undefined);
|
|
159
|
-
await sleep(wait);
|
|
180
|
+
await sleep(cappedWait(wait, deadline));
|
|
160
181
|
}
|
|
161
|
-
// 도달 불가 — 마지막 시도(isLast)는 반드시 반환하거나 throw 한다. 그래도 unknown 을
|
|
162
|
-
// 그대로 던지지 않고 Error 로 감싼다: 호출부의 `instanceof Error` 분기가 전부 무너진다.
|
|
163
|
-
throw new Error(`${endpointLabel(input)} 요청 실패`, { cause: lastError });
|
|
164
182
|
}
|
package/package.json
CHANGED