@fetchkit/ffetch 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -79,6 +79,7 @@ var index_exports = {};
79
79
  __export(index_exports, {
80
80
  AbortError: () => AbortError,
81
81
  CircuitOpenError: () => CircuitOpenError,
82
+ HttpError: () => HttpError,
82
83
  NetworkError: () => NetworkError,
83
84
  RetryLimitError: () => RetryLimitError,
84
85
  TimeoutError: () => TimeoutError,
@@ -97,7 +98,29 @@ var defaultDelay = (ctx) => {
97
98
  }
98
99
  return 2 ** ctx.attempt * 200 + Math.random() * 100;
99
100
  };
100
- async function retry(fn, retries, delay, shouldRetry2 = () => true, request) {
101
+ function waitForRetryDelay(ms, signal) {
102
+ if (ms <= 0) return Promise.resolve();
103
+ return new Promise((resolve) => {
104
+ if (!signal) {
105
+ setTimeout(resolve, ms);
106
+ return;
107
+ }
108
+ if (signal.aborted) {
109
+ resolve();
110
+ return;
111
+ }
112
+ const onAbort = () => {
113
+ clearTimeout(timer);
114
+ resolve();
115
+ };
116
+ const timer = setTimeout(() => {
117
+ signal.removeEventListener("abort", onAbort);
118
+ resolve();
119
+ }, ms);
120
+ signal.addEventListener("abort", onAbort, { once: true });
121
+ });
122
+ }
123
+ async function retry(fn, retries, delay, shouldRetry2 = () => true, request, signal) {
101
124
  let lastErr;
102
125
  let lastRes;
103
126
  for (let i = 0; i <= retries; i++) {
@@ -113,7 +136,7 @@ async function retry(fn, retries, delay, shouldRetry2 = () => true, request) {
113
136
  ctx.error = void 0;
114
137
  if (i < retries && shouldRetry2(ctx)) {
115
138
  const wait = typeof delay === "function" ? delay(ctx) : delay;
116
- await new Promise((r) => setTimeout(r, wait));
139
+ await waitForRetryDelay(wait, signal);
117
140
  continue;
118
141
  }
119
142
  return lastRes;
@@ -122,7 +145,7 @@ async function retry(fn, retries, delay, shouldRetry2 = () => true, request) {
122
145
  ctx.error = err;
123
146
  if (i === retries || !shouldRetry2(ctx)) throw err;
124
147
  const wait = typeof delay === "function" ? delay(ctx) : delay;
125
- await new Promise((r) => setTimeout(r, wait));
148
+ await waitForRetryDelay(wait, signal);
126
149
  }
127
150
  }
128
151
  throw lastErr;
@@ -189,250 +212,260 @@ function createClient(opts = {}) {
189
212
  entry.controller?.abort();
190
213
  }
191
214
  }
192
- const client = async (input, init = {}) => {
193
- let request = new Request(input, init);
194
- const effectiveHooks = { ...clientDefaultHooks, ...init.hooks || {} };
195
- if (effectiveHooks.transformRequest) {
196
- request = await effectiveHooks.transformRequest(request);
197
- }
198
- await effectiveHooks.before?.(request);
199
- const effectiveRetries = init.retries ?? clientDefaultRetries;
200
- const effectiveRetryDelay = typeof init.retryDelay !== "undefined" ? init.retryDelay : clientDefaultRetryDelay;
201
- const effectiveShouldRetry = init.shouldRetry ?? clientDefaultShouldRetry;
202
- const effectiveTimeout = init.timeout ?? clientDefaultTimeout;
203
- const userSignal = init.signal;
204
- const transformedSignal = request.signal;
205
- const pluginContext = {
206
- request,
207
- init,
208
- state: /* @__PURE__ */ Object.create(null),
209
- metadata: {
210
- startedAt: Date.now(),
211
- timeoutMs: effectiveTimeout,
212
- signals: {
213
- user: userSignal === void 0 || userSignal === null ? void 0 : userSignal,
214
- transformed: transformedSignal
215
- },
216
- retry: {
217
- configuredRetries: effectiveRetries,
218
- configuredDelay: effectiveRetryDelay,
219
- attempt: 0
220
- }
215
+ const client = (input, init = {}) => {
216
+ const execute = async () => {
217
+ let request = new Request(input, init);
218
+ const effectiveHooks = { ...clientDefaultHooks, ...init.hooks || {} };
219
+ if (effectiveHooks.transformRequest) {
220
+ request = await effectiveHooks.transformRequest(request);
221
221
  }
222
- };
223
- for (const plugin of plugins) {
224
- await plugin.preRequest?.(pluginContext);
225
- }
226
- const effectiveThrowOnHttpError = typeof init.throwOnHttpError !== "undefined" ? init.throwOnHttpError : opts.throwOnHttpError ?? false;
227
- function createTimeoutSignal(timeout) {
228
- if (typeof AbortSignal?.timeout === "function") {
229
- return AbortSignal.timeout(timeout);
222
+ await effectiveHooks.before?.(request);
223
+ const effectiveRetries = init.retries ?? clientDefaultRetries;
224
+ const effectiveRetryDelay = typeof init.retryDelay !== "undefined" ? init.retryDelay : clientDefaultRetryDelay;
225
+ const effectiveShouldRetry = init.shouldRetry ?? clientDefaultShouldRetry;
226
+ const effectiveTimeout = init.timeout ?? clientDefaultTimeout;
227
+ const userSignal = init.signal;
228
+ const transformedSignal = request.signal;
229
+ const pluginContext = {
230
+ request,
231
+ init,
232
+ state: /* @__PURE__ */ Object.create(null),
233
+ metadata: {
234
+ startedAt: Date.now(),
235
+ timeoutMs: effectiveTimeout,
236
+ signals: {
237
+ user: userSignal === void 0 || userSignal === null ? void 0 : userSignal,
238
+ transformed: transformedSignal
239
+ },
240
+ retry: {
241
+ configuredRetries: effectiveRetries,
242
+ configuredDelay: effectiveRetryDelay,
243
+ attempt: 0
244
+ }
245
+ }
246
+ };
247
+ for (const plugin of plugins) {
248
+ await plugin.preRequest?.(pluginContext);
230
249
  }
231
- const controller2 = new AbortController();
232
- const timeoutId = setTimeout(() => controller2.abort(), timeout);
233
- controller2.signal.addEventListener(
234
- "abort",
235
- () => clearTimeout(timeoutId),
236
- { once: true }
237
- );
238
- return controller2.signal;
239
- }
240
- let timeoutSignal = void 0;
241
- let combinedSignal = void 0;
242
- let controller = void 0;
243
- if (effectiveTimeout > 0) {
244
- timeoutSignal = createTimeoutSignal(effectiveTimeout);
245
- pluginContext.metadata.signals.timeout = timeoutSignal;
246
- }
247
- const signals = [];
248
- if (userSignal) signals.push(userSignal);
249
- if (transformedSignal && transformedSignal !== userSignal) {
250
- signals.push(transformedSignal);
251
- }
252
- if (timeoutSignal) signals.push(timeoutSignal);
253
- if (signals.length === 1) {
254
- combinedSignal = signals[0];
255
- controller = new AbortController();
256
- } else {
257
- if (typeof AbortSignal.any !== "function") {
258
- throw new Error(
259
- "AbortSignal.any is required for combining multiple signals. Please install a polyfill for environments that do not support it."
250
+ const effectiveThrowOnHttpError = typeof init.throwOnHttpError !== "undefined" ? init.throwOnHttpError : opts.throwOnHttpError ?? false;
251
+ function createTimeoutSignal(timeout) {
252
+ if (typeof AbortSignal?.timeout === "function") {
253
+ return AbortSignal.timeout(timeout);
254
+ }
255
+ const controller2 = new AbortController();
256
+ const timeoutId = setTimeout(() => controller2.abort(), timeout);
257
+ controller2.signal.addEventListener(
258
+ "abort",
259
+ () => clearTimeout(timeoutId),
260
+ { once: true }
260
261
  );
262
+ return controller2.signal;
261
263
  }
262
- combinedSignal = AbortSignal.any(signals);
263
- controller = new AbortController();
264
- }
265
- pluginContext.metadata.signals.combined = combinedSignal;
266
- const retryWithHooks = async () => {
267
- let attempt = 0;
268
- const shouldRetryWithHook = (ctx) => {
269
- attempt = ctx.attempt;
270
- pluginContext.metadata.retry.attempt = attempt;
271
- pluginContext.metadata.retry.lastError = ctx.error;
272
- pluginContext.metadata.retry.lastResponse = ctx.response;
273
- const retrying = effectiveShouldRetry(ctx);
274
- pluginContext.metadata.retry.shouldRetryResult = retrying;
275
- if (retrying && attempt <= effectiveRetries) {
276
- effectiveHooks.onRetry?.(
277
- request,
278
- attempt - 1,
279
- ctx.error,
280
- ctx.response
264
+ let timeoutSignal = void 0;
265
+ let combinedSignal = void 0;
266
+ let controller = void 0;
267
+ if (effectiveTimeout > 0) {
268
+ timeoutSignal = createTimeoutSignal(effectiveTimeout);
269
+ pluginContext.metadata.signals.timeout = timeoutSignal;
270
+ }
271
+ const signals = [];
272
+ if (userSignal) signals.push(userSignal);
273
+ if (transformedSignal && transformedSignal !== userSignal) {
274
+ signals.push(transformedSignal);
275
+ }
276
+ if (timeoutSignal) signals.push(timeoutSignal);
277
+ if (signals.length === 1) {
278
+ combinedSignal = signals[0];
279
+ controller = new AbortController();
280
+ } else {
281
+ if (typeof AbortSignal.any !== "function") {
282
+ throw new Error(
283
+ "AbortSignal.any is required for combining multiple signals. Please install a polyfill for environments that do not support it."
281
284
  );
282
285
  }
283
- return retrying;
284
- };
285
- let lastResponse = void 0;
286
- try {
287
- let res = await retry(
288
- async () => {
289
- if (userSignal?.aborted) {
290
- effectiveHooks.onAbort?.(request);
291
- throw new AbortError("Request was aborted by user");
292
- }
293
- if (timeoutSignal?.aborted) {
294
- effectiveHooks.onTimeout?.(request);
295
- throw new TimeoutError("signal timed out");
296
- }
297
- if (typeof combinedSignal?.throwIfAborted === "function") {
298
- combinedSignal.throwIfAborted();
299
- } else if (combinedSignal?.aborted) {
286
+ combinedSignal = AbortSignal.any(signals);
287
+ controller = new AbortController();
288
+ }
289
+ pluginContext.metadata.signals.combined = combinedSignal;
290
+ const retryWithHooks = async () => {
291
+ let attempt = 0;
292
+ const shouldRetryWithHook = (ctx) => {
293
+ attempt = ctx.attempt;
294
+ pluginContext.metadata.retry.attempt = attempt;
295
+ pluginContext.metadata.retry.lastError = ctx.error;
296
+ pluginContext.metadata.retry.lastResponse = ctx.response;
297
+ const retrying = effectiveShouldRetry(ctx);
298
+ pluginContext.metadata.retry.shouldRetryResult = retrying;
299
+ if (retrying && attempt <= effectiveRetries) {
300
+ effectiveHooks.onRetry?.(
301
+ request,
302
+ attempt - 1,
303
+ ctx.error,
304
+ ctx.response
305
+ );
306
+ }
307
+ return retrying;
308
+ };
309
+ let lastResponse = void 0;
310
+ try {
311
+ let res = await retry(
312
+ async () => {
300
313
  if (userSignal?.aborted) {
301
314
  effectiveHooks.onAbort?.(request);
302
315
  throw new AbortError("Request was aborted by user");
303
- } else if (timeoutSignal?.aborted) {
316
+ }
317
+ if (timeoutSignal?.aborted) {
304
318
  effectiveHooks.onTimeout?.(request);
305
319
  throw new TimeoutError("signal timed out");
306
- } else {
307
- throw new AbortError(
308
- "Request was aborted",
309
- new DOMException("Aborted", "AbortError")
310
- );
311
320
  }
312
- }
313
- const reqWithSignal = new Request(request, {
314
- signal: combinedSignal
315
- });
316
- try {
317
- const handler = init.fetchHandler ?? fetchHandler ?? fetch;
318
- const response = await handler(reqWithSignal);
319
- lastResponse = response;
320
- pluginContext.metadata.retry.lastResponse = response;
321
- return response;
322
- } catch (err) {
323
- pluginContext.metadata.retry.lastError = err;
324
- if (err instanceof DOMException && err.name === "AbortError") {
325
- if (timeoutSignal?.aborted && (!userSignal || !userSignal.aborted)) {
326
- effectiveHooks.onTimeout?.(request);
327
- throw new TimeoutError("signal timed out", err);
328
- } else if (userSignal?.aborted) {
321
+ if (typeof combinedSignal?.throwIfAborted === "function") {
322
+ combinedSignal.throwIfAborted();
323
+ } else if (combinedSignal?.aborted) {
324
+ if (userSignal?.aborted) {
329
325
  effectiveHooks.onAbort?.(request);
330
326
  throw new AbortError("Request was aborted by user");
327
+ } else if (timeoutSignal?.aborted) {
328
+ effectiveHooks.onTimeout?.(request);
329
+ throw new TimeoutError("signal timed out");
331
330
  } else {
332
331
  throw new AbortError(
333
332
  "Request was aborted",
334
333
  new DOMException("Aborted", "AbortError")
335
334
  );
336
335
  }
337
- } else if (err instanceof TypeError && /NetworkError|network error|failed to fetch|lost connection|NetworkError when attempting to fetch resource/i.test(
338
- err.message
339
- )) {
340
- throw new NetworkError(err.message, err);
341
336
  }
342
- throw err;
343
- }
344
- },
345
- effectiveRetries,
346
- effectiveRetryDelay,
347
- shouldRetryWithHook,
348
- request
349
- );
350
- if (effectiveHooks.transformResponse) {
351
- res = await effectiveHooks.transformResponse(res, request);
352
- }
353
- await effectiveHooks.after?.(request, res);
354
- await effectiveHooks.onComplete?.(request, res, void 0);
355
- if (effectiveThrowOnHttpError && (res.status >= 400 && res.status < 500 && res.status !== 429 || res.status >= 500 || res.status === 429)) {
356
- const { HttpError: HttpError2 } = await Promise.resolve().then(() => (init_error(), error_exports));
357
- throw new HttpError2(
358
- `HTTP error: ${res.status} ${res.statusText}`,
359
- res
337
+ const reqWithSignal = new Request(request, {
338
+ signal: combinedSignal
339
+ });
340
+ try {
341
+ const handler = init.fetchHandler ?? fetchHandler ?? fetch;
342
+ const response = await handler(reqWithSignal);
343
+ lastResponse = response;
344
+ pluginContext.metadata.retry.lastResponse = response;
345
+ return response;
346
+ } catch (err) {
347
+ pluginContext.metadata.retry.lastError = err;
348
+ if (err instanceof DOMException && err.name === "AbortError") {
349
+ if (timeoutSignal?.aborted && (!userSignal || !userSignal.aborted)) {
350
+ effectiveHooks.onTimeout?.(request);
351
+ throw new TimeoutError("signal timed out", err);
352
+ } else if (userSignal?.aborted) {
353
+ effectiveHooks.onAbort?.(request);
354
+ throw new AbortError("Request was aborted by user");
355
+ } else {
356
+ throw new AbortError(
357
+ "Request was aborted",
358
+ new DOMException("Aborted", "AbortError")
359
+ );
360
+ }
361
+ } else if (err instanceof TypeError && /NetworkError|network error|failed to fetch|lost connection|NetworkError when attempting to fetch resource/i.test(
362
+ err.message
363
+ )) {
364
+ throw new NetworkError(err.message, err);
365
+ }
366
+ throw err;
367
+ }
368
+ },
369
+ effectiveRetries,
370
+ effectiveRetryDelay,
371
+ shouldRetryWithHook,
372
+ request,
373
+ combinedSignal
360
374
  );
361
- }
362
- return res;
363
- } catch (err) {
364
- pluginContext.metadata.retry.lastError = err;
365
- if (lastResponse) {
366
- const resp = lastResponse;
367
- if (effectiveThrowOnHttpError && (resp.status >= 400 && resp.status < 500 && resp.status !== 429 || resp.status >= 500 || resp.status === 429)) {
375
+ if (effectiveHooks.transformResponse) {
376
+ res = await effectiveHooks.transformResponse(res, request);
377
+ }
378
+ await effectiveHooks.after?.(request, res);
379
+ await effectiveHooks.onComplete?.(request, res, void 0);
380
+ if (effectiveThrowOnHttpError && (res.status >= 400 && res.status < 500 && res.status !== 429 || res.status >= 500 || res.status === 429)) {
368
381
  const { HttpError: HttpError2 } = await Promise.resolve().then(() => (init_error(), error_exports));
369
382
  throw new HttpError2(
370
- `HTTP error: ${resp.status} ${resp.statusText}`,
371
- resp
383
+ `HTTP error: ${res.status} ${res.statusText}`,
384
+ res
372
385
  );
373
386
  }
374
- return resp;
387
+ return res;
388
+ } catch (err) {
389
+ pluginContext.metadata.retry.lastError = err;
390
+ if (lastResponse) {
391
+ const resp = lastResponse;
392
+ if (effectiveThrowOnHttpError && (resp.status >= 400 && resp.status < 500 && resp.status !== 429 || resp.status >= 500 || resp.status === 429)) {
393
+ const { HttpError: HttpError2 } = await Promise.resolve().then(() => (init_error(), error_exports));
394
+ throw new HttpError2(
395
+ `HTTP error: ${resp.status} ${resp.statusText}`,
396
+ resp
397
+ );
398
+ }
399
+ return resp;
400
+ }
401
+ if (err instanceof TimeoutError) {
402
+ await effectiveHooks.onTimeout?.(request);
403
+ await effectiveHooks.onError?.(request, err);
404
+ await effectiveHooks.onComplete?.(request, void 0, err);
405
+ throw err;
406
+ }
407
+ if (err instanceof AbortError) {
408
+ await effectiveHooks.onAbort?.(request);
409
+ await effectiveHooks.onError?.(request, err);
410
+ await effectiveHooks.onComplete?.(request, void 0, err);
411
+ throw err;
412
+ }
413
+ if (err instanceof NetworkError) {
414
+ await effectiveHooks.onError?.(request, err);
415
+ await effectiveHooks.onComplete?.(request, void 0, err);
416
+ throw err;
417
+ }
418
+ const retryErr = new RetryLimitError(
419
+ typeof err === "object" && err && "message" in err && typeof err.message === "string" ? err.message : "Retry limit reached",
420
+ err
421
+ );
422
+ await effectiveHooks.onError?.(request, retryErr);
423
+ await effectiveHooks.onComplete?.(request, void 0, retryErr);
424
+ throw retryErr;
375
425
  }
376
- if (err instanceof TimeoutError) {
377
- await effectiveHooks.onTimeout?.(request);
378
- await effectiveHooks.onError?.(request, err);
379
- await effectiveHooks.onComplete?.(request, void 0, err);
380
- throw err;
426
+ };
427
+ const baseDispatch = async () => retryWithHooks();
428
+ let dispatch = baseDispatch;
429
+ for (let i = plugins.length - 1; i >= 0; i--) {
430
+ const plugin = plugins[i];
431
+ if (plugin.wrapDispatch) {
432
+ dispatch = plugin.wrapDispatch(dispatch);
381
433
  }
382
- if (err instanceof AbortError) {
383
- await effectiveHooks.onAbort?.(request);
384
- await effectiveHooks.onError?.(request, err);
385
- await effectiveHooks.onComplete?.(request, void 0, err);
386
- throw err;
434
+ }
435
+ const actualPromise = dispatch(pluginContext).then(async (response) => {
436
+ for (const plugin of plugins) {
437
+ await plugin.onSuccess?.(pluginContext, response);
387
438
  }
388
- if (err instanceof NetworkError) {
389
- await effectiveHooks.onError?.(request, err);
390
- await effectiveHooks.onComplete?.(request, void 0, err);
391
- throw err;
439
+ return response;
440
+ }).catch(async (err) => {
441
+ for (const plugin of plugins) {
442
+ await plugin.onError?.(pluginContext, err);
392
443
  }
393
- const retryErr = new RetryLimitError(
394
- typeof err === "object" && err && "message" in err && typeof err.message === "string" ? err.message : "Retry limit reached",
395
- err
396
- );
397
- await effectiveHooks.onError?.(request, retryErr);
398
- await effectiveHooks.onComplete?.(request, void 0, retryErr);
399
- throw retryErr;
400
- }
444
+ throw err;
445
+ });
446
+ const pendingEntry = {
447
+ promise: actualPromise,
448
+ request,
449
+ controller
450
+ };
451
+ pendingRequests.push(pendingEntry);
452
+ return actualPromise.finally(async () => {
453
+ for (const plugin of plugins) {
454
+ await plugin.onFinally?.(pluginContext);
455
+ }
456
+ const index = pendingRequests.indexOf(pendingEntry);
457
+ if (index > -1) {
458
+ pendingRequests.splice(index, 1);
459
+ }
460
+ });
401
461
  };
402
- const baseDispatch = async () => retryWithHooks();
403
- let dispatch = baseDispatch;
404
- for (let i = plugins.length - 1; i >= 0; i--) {
405
- const plugin = plugins[i];
406
- if (plugin.wrapDispatch) {
407
- dispatch = plugin.wrapDispatch(dispatch);
462
+ let promise = execute();
463
+ for (const plugin of plugins) {
464
+ if (plugin.decoratePromise) {
465
+ promise = plugin.decoratePromise(promise);
408
466
  }
409
467
  }
410
- const actualPromise = dispatch(pluginContext).then(async (response) => {
411
- for (const plugin of plugins) {
412
- await plugin.onSuccess?.(pluginContext, response);
413
- }
414
- return response;
415
- }).catch(async (err) => {
416
- for (const plugin of plugins) {
417
- await plugin.onError?.(pluginContext, err);
418
- }
419
- throw err;
420
- });
421
- const pendingEntry = {
422
- promise: actualPromise,
423
- request,
424
- controller
425
- };
426
- pendingRequests.push(pendingEntry);
427
- return actualPromise.finally(async () => {
428
- for (const plugin of plugins) {
429
- await plugin.onFinally?.(pluginContext);
430
- }
431
- const index = pendingRequests.indexOf(pendingEntry);
432
- if (index > -1) {
433
- pendingRequests.splice(index, 1);
434
- }
435
- });
468
+ return promise;
436
469
  };
437
470
  Object.defineProperty(client, "pendingRequests", {
438
471
  get() {
@@ -457,6 +490,7 @@ init_error();
457
490
  0 && (module.exports = {
458
491
  AbortError,
459
492
  CircuitOpenError,
493
+ HttpError,
460
494
  NetworkError,
461
495
  RetryLimitError,
462
496
  TimeoutError,