@kizunajs/fetch 2.0.0-beta.19 → 2.0.0-beta.20
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/index.cjs +84 -4
- package/dist/index.mjs +84 -4
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -57,6 +57,26 @@ export interface ClientConfig {
|
|
|
57
57
|
credentials?: RequestCredentials;
|
|
58
58
|
fetch?: typeof fetch;
|
|
59
59
|
onRequest?: (request: OutgoingRequest) => void | Promise<void>;
|
|
60
|
+
retry?: RetryOptions;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Retries a GET, HEAD, OPTIONS, PUT or DELETE that failed on the network or
|
|
65
|
+
* was answered 429 or 503, waiting what Retry-After asks or backing off.
|
|
66
|
+
*/
|
|
67
|
+
export interface RetryOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Attempts in all, the first included.
|
|
70
|
+
*/
|
|
71
|
+
maximumAttempts?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Milliseconds the first backoff waits at most.
|
|
74
|
+
*/
|
|
75
|
+
initialDelay?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Milliseconds any wait may take.
|
|
78
|
+
*/
|
|
79
|
+
maximumDelay?: number;
|
|
60
80
|
}
|
|
61
81
|
|
|
62
82
|
/**
|
|
@@ -281,6 +301,61 @@ const readStream = (body: ReadableStream<Uint8Array>, contentType: string): Asyn
|
|
|
281
301
|
return readBytes(body);
|
|
282
302
|
};
|
|
283
303
|
|
|
304
|
+
const IDEMPOTENT_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']);
|
|
305
|
+
|
|
306
|
+
const RETRY_STATUSES = new Set([429, 503]);
|
|
307
|
+
|
|
308
|
+
const retryAfter = (header: string | null): number | undefined => {
|
|
309
|
+
if (header === null) return undefined;
|
|
310
|
+
if (/^\d+$/.test(header.trim())) return Number(header.trim()) * 1000;
|
|
311
|
+
const date = Date.parse(header);
|
|
312
|
+
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const wait = (milliseconds: number, signal: AbortSignal | null | undefined): Promise<void> =>
|
|
316
|
+
new Promise((resolve, reject) => {
|
|
317
|
+
if (signal?.aborted) {
|
|
318
|
+
reject(signal.reason);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const onAbort = () => {
|
|
322
|
+
clearTimeout(timer);
|
|
323
|
+
reject(signal?.reason);
|
|
324
|
+
};
|
|
325
|
+
const timer = setTimeout(() => {
|
|
326
|
+
signal?.removeEventListener('abort', onAbort);
|
|
327
|
+
resolve();
|
|
328
|
+
}, milliseconds);
|
|
329
|
+
signal?.addEventListener('abort', onAbort, {
|
|
330
|
+
once: true,
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const send = async (url: string, init: RequestInit, route: GeneratedRoute, config: ClientConfig): Promise<Response> => {
|
|
335
|
+
const fetcher = config.fetch ?? fetch;
|
|
336
|
+
const retry = config.retry;
|
|
337
|
+
if (retry === undefined || !IDEMPOTENT_METHODS.has(route.method)) return fetcher(url, init);
|
|
338
|
+
const maximumAttempts = retry.maximumAttempts ?? 3;
|
|
339
|
+
const initialDelay = retry.initialDelay ?? 500;
|
|
340
|
+
const maximumDelay = retry.maximumDelay ?? 30_000;
|
|
341
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
342
|
+
const backoff = Math.random() * Math.min(maximumDelay, initialDelay * 2 ** (attempt - 1));
|
|
343
|
+
let response: Response;
|
|
344
|
+
try {
|
|
345
|
+
response = await fetcher(url, init);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
if (attempt >= maximumAttempts || init.signal?.aborted || !(error instanceof TypeError)) throw error;
|
|
348
|
+
await wait(backoff, init.signal);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (attempt >= maximumAttempts || !RETRY_STATUSES.has(response.status)) return response;
|
|
352
|
+
const asked = retryAfter(response.headers.get('retry-after'));
|
|
353
|
+
if (asked !== undefined && asked > maximumDelay) return response;
|
|
354
|
+
await response.body?.cancel();
|
|
355
|
+
await wait(asked ?? backoff, init.signal);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
284
359
|
const buildMethod = (route: GeneratedRoute, config: ClientConfig) => {
|
|
285
360
|
const call = async (args: CallArgs = {}) => {
|
|
286
361
|
const query = args.query === undefined ? '' : buildSearchParams(args.query).toString();
|
|
@@ -302,10 +377,15 @@ const buildMethod = (route: GeneratedRoute, config: ClientConfig) => {
|
|
|
302
377
|
route,
|
|
303
378
|
});
|
|
304
379
|
|
|
305
|
-
const response = await (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
380
|
+
const response = await send(
|
|
381
|
+
url,
|
|
382
|
+
{
|
|
383
|
+
...init,
|
|
384
|
+
...args.fetchOptions,
|
|
385
|
+
},
|
|
386
|
+
route,
|
|
387
|
+
config
|
|
388
|
+
);
|
|
309
389
|
const responseHeaders: Record<string, string> = {};
|
|
310
390
|
response.headers.forEach((value, name) => {
|
|
311
391
|
responseHeaders[name] = value;
|
package/dist/index.mjs
CHANGED
|
@@ -56,6 +56,26 @@ export interface ClientConfig {
|
|
|
56
56
|
credentials?: RequestCredentials;
|
|
57
57
|
fetch?: typeof fetch;
|
|
58
58
|
onRequest?: (request: OutgoingRequest) => void | Promise<void>;
|
|
59
|
+
retry?: RetryOptions;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Retries a GET, HEAD, OPTIONS, PUT or DELETE that failed on the network or
|
|
64
|
+
* was answered 429 or 503, waiting what Retry-After asks or backing off.
|
|
65
|
+
*/
|
|
66
|
+
export interface RetryOptions {
|
|
67
|
+
/**
|
|
68
|
+
* Attempts in all, the first included.
|
|
69
|
+
*/
|
|
70
|
+
maximumAttempts?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Milliseconds the first backoff waits at most.
|
|
73
|
+
*/
|
|
74
|
+
initialDelay?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Milliseconds any wait may take.
|
|
77
|
+
*/
|
|
78
|
+
maximumDelay?: number;
|
|
59
79
|
}
|
|
60
80
|
|
|
61
81
|
/**
|
|
@@ -280,6 +300,61 @@ const readStream = (body: ReadableStream<Uint8Array>, contentType: string): Asyn
|
|
|
280
300
|
return readBytes(body);
|
|
281
301
|
};
|
|
282
302
|
|
|
303
|
+
const IDEMPOTENT_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']);
|
|
304
|
+
|
|
305
|
+
const RETRY_STATUSES = new Set([429, 503]);
|
|
306
|
+
|
|
307
|
+
const retryAfter = (header: string | null): number | undefined => {
|
|
308
|
+
if (header === null) return undefined;
|
|
309
|
+
if (/^\d+$/.test(header.trim())) return Number(header.trim()) * 1000;
|
|
310
|
+
const date = Date.parse(header);
|
|
311
|
+
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const wait = (milliseconds: number, signal: AbortSignal | null | undefined): Promise<void> =>
|
|
315
|
+
new Promise((resolve, reject) => {
|
|
316
|
+
if (signal?.aborted) {
|
|
317
|
+
reject(signal.reason);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const onAbort = () => {
|
|
321
|
+
clearTimeout(timer);
|
|
322
|
+
reject(signal?.reason);
|
|
323
|
+
};
|
|
324
|
+
const timer = setTimeout(() => {
|
|
325
|
+
signal?.removeEventListener('abort', onAbort);
|
|
326
|
+
resolve();
|
|
327
|
+
}, milliseconds);
|
|
328
|
+
signal?.addEventListener('abort', onAbort, {
|
|
329
|
+
once: true,
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const send = async (url: string, init: RequestInit, route: GeneratedRoute, config: ClientConfig): Promise<Response> => {
|
|
334
|
+
const fetcher = config.fetch ?? fetch;
|
|
335
|
+
const retry = config.retry;
|
|
336
|
+
if (retry === undefined || !IDEMPOTENT_METHODS.has(route.method)) return fetcher(url, init);
|
|
337
|
+
const maximumAttempts = retry.maximumAttempts ?? 3;
|
|
338
|
+
const initialDelay = retry.initialDelay ?? 500;
|
|
339
|
+
const maximumDelay = retry.maximumDelay ?? 30_000;
|
|
340
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
341
|
+
const backoff = Math.random() * Math.min(maximumDelay, initialDelay * 2 ** (attempt - 1));
|
|
342
|
+
let response: Response;
|
|
343
|
+
try {
|
|
344
|
+
response = await fetcher(url, init);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (attempt >= maximumAttempts || init.signal?.aborted || !(error instanceof TypeError)) throw error;
|
|
347
|
+
await wait(backoff, init.signal);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (attempt >= maximumAttempts || !RETRY_STATUSES.has(response.status)) return response;
|
|
351
|
+
const asked = retryAfter(response.headers.get('retry-after'));
|
|
352
|
+
if (asked !== undefined && asked > maximumDelay) return response;
|
|
353
|
+
await response.body?.cancel();
|
|
354
|
+
await wait(asked ?? backoff, init.signal);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
283
358
|
const buildMethod = (route: GeneratedRoute, config: ClientConfig) => {
|
|
284
359
|
const call = async (args: CallArgs = {}) => {
|
|
285
360
|
const query = args.query === undefined ? '' : buildSearchParams(args.query).toString();
|
|
@@ -301,10 +376,15 @@ const buildMethod = (route: GeneratedRoute, config: ClientConfig) => {
|
|
|
301
376
|
route,
|
|
302
377
|
});
|
|
303
378
|
|
|
304
|
-
const response = await (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
379
|
+
const response = await send(
|
|
380
|
+
url,
|
|
381
|
+
{
|
|
382
|
+
...init,
|
|
383
|
+
...args.fetchOptions,
|
|
384
|
+
},
|
|
385
|
+
route,
|
|
386
|
+
config
|
|
387
|
+
);
|
|
308
388
|
const responseHeaders: Record<string, string> = {};
|
|
309
389
|
response.headers.forEach((value, name) => {
|
|
310
390
|
responseHeaders[name] = value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kizunajs/fetch",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.20",
|
|
4
4
|
"description": "Fetch client generation for Kizuna.js APIs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kizunajs",
|
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
"access": "public"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"kizunajs": "2.0.0-beta.
|
|
59
|
+
"kizunajs": "2.0.0-beta.20",
|
|
60
60
|
"zod": "^4.0.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"kizunajs": "2.0.0-beta.
|
|
64
|
-
"@kizunajs/express": "2.0.0-beta.
|
|
63
|
+
"kizunajs": "2.0.0-beta.20",
|
|
64
|
+
"@kizunajs/express": "2.0.0-beta.20",
|
|
65
65
|
"@types/express": "^5.0.0",
|
|
66
66
|
"express": "^5.0.0",
|
|
67
67
|
"tsdown": "^0.21.0",
|