@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.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AbortError,
3
3
  CircuitOpenError,
4
+ HttpError,
4
5
  NetworkError,
5
6
  RetryLimitError,
6
7
  TimeoutError
@@ -17,7 +18,29 @@ var defaultDelay = (ctx) => {
17
18
  }
18
19
  return 2 ** ctx.attempt * 200 + Math.random() * 100;
19
20
  };
20
- async function retry(fn, retries, delay, shouldRetry2 = () => true, request) {
21
+ function waitForRetryDelay(ms, signal) {
22
+ if (ms <= 0) return Promise.resolve();
23
+ return new Promise((resolve) => {
24
+ if (!signal) {
25
+ setTimeout(resolve, ms);
26
+ return;
27
+ }
28
+ if (signal.aborted) {
29
+ resolve();
30
+ return;
31
+ }
32
+ const onAbort = () => {
33
+ clearTimeout(timer);
34
+ resolve();
35
+ };
36
+ const timer = setTimeout(() => {
37
+ signal.removeEventListener("abort", onAbort);
38
+ resolve();
39
+ }, ms);
40
+ signal.addEventListener("abort", onAbort, { once: true });
41
+ });
42
+ }
43
+ async function retry(fn, retries, delay, shouldRetry2 = () => true, request, signal) {
21
44
  let lastErr;
22
45
  let lastRes;
23
46
  for (let i = 0; i <= retries; i++) {
@@ -33,7 +56,7 @@ async function retry(fn, retries, delay, shouldRetry2 = () => true, request) {
33
56
  ctx.error = void 0;
34
57
  if (i < retries && shouldRetry2(ctx)) {
35
58
  const wait = typeof delay === "function" ? delay(ctx) : delay;
36
- await new Promise((r) => setTimeout(r, wait));
59
+ await waitForRetryDelay(wait, signal);
37
60
  continue;
38
61
  }
39
62
  return lastRes;
@@ -42,7 +65,7 @@ async function retry(fn, retries, delay, shouldRetry2 = () => true, request) {
42
65
  ctx.error = err;
43
66
  if (i === retries || !shouldRetry2(ctx)) throw err;
44
67
  const wait = typeof delay === "function" ? delay(ctx) : delay;
45
- await new Promise((r) => setTimeout(r, wait));
68
+ await waitForRetryDelay(wait, signal);
46
69
  }
47
70
  }
48
71
  throw lastErr;
@@ -107,250 +130,260 @@ function createClient(opts = {}) {
107
130
  entry.controller?.abort();
108
131
  }
109
132
  }
110
- const client = async (input, init = {}) => {
111
- let request = new Request(input, init);
112
- const effectiveHooks = { ...clientDefaultHooks, ...init.hooks || {} };
113
- if (effectiveHooks.transformRequest) {
114
- request = await effectiveHooks.transformRequest(request);
115
- }
116
- await effectiveHooks.before?.(request);
117
- const effectiveRetries = init.retries ?? clientDefaultRetries;
118
- const effectiveRetryDelay = typeof init.retryDelay !== "undefined" ? init.retryDelay : clientDefaultRetryDelay;
119
- const effectiveShouldRetry = init.shouldRetry ?? clientDefaultShouldRetry;
120
- const effectiveTimeout = init.timeout ?? clientDefaultTimeout;
121
- const userSignal = init.signal;
122
- const transformedSignal = request.signal;
123
- const pluginContext = {
124
- request,
125
- init,
126
- state: /* @__PURE__ */ Object.create(null),
127
- metadata: {
128
- startedAt: Date.now(),
129
- timeoutMs: effectiveTimeout,
130
- signals: {
131
- user: userSignal === void 0 || userSignal === null ? void 0 : userSignal,
132
- transformed: transformedSignal
133
- },
134
- retry: {
135
- configuredRetries: effectiveRetries,
136
- configuredDelay: effectiveRetryDelay,
137
- attempt: 0
138
- }
133
+ const client = (input, init = {}) => {
134
+ const execute = async () => {
135
+ let request = new Request(input, init);
136
+ const effectiveHooks = { ...clientDefaultHooks, ...init.hooks || {} };
137
+ if (effectiveHooks.transformRequest) {
138
+ request = await effectiveHooks.transformRequest(request);
139
139
  }
140
- };
141
- for (const plugin of plugins) {
142
- await plugin.preRequest?.(pluginContext);
143
- }
144
- const effectiveThrowOnHttpError = typeof init.throwOnHttpError !== "undefined" ? init.throwOnHttpError : opts.throwOnHttpError ?? false;
145
- function createTimeoutSignal(timeout) {
146
- if (typeof AbortSignal?.timeout === "function") {
147
- return AbortSignal.timeout(timeout);
140
+ await effectiveHooks.before?.(request);
141
+ const effectiveRetries = init.retries ?? clientDefaultRetries;
142
+ const effectiveRetryDelay = typeof init.retryDelay !== "undefined" ? init.retryDelay : clientDefaultRetryDelay;
143
+ const effectiveShouldRetry = init.shouldRetry ?? clientDefaultShouldRetry;
144
+ const effectiveTimeout = init.timeout ?? clientDefaultTimeout;
145
+ const userSignal = init.signal;
146
+ const transformedSignal = request.signal;
147
+ const pluginContext = {
148
+ request,
149
+ init,
150
+ state: /* @__PURE__ */ Object.create(null),
151
+ metadata: {
152
+ startedAt: Date.now(),
153
+ timeoutMs: effectiveTimeout,
154
+ signals: {
155
+ user: userSignal === void 0 || userSignal === null ? void 0 : userSignal,
156
+ transformed: transformedSignal
157
+ },
158
+ retry: {
159
+ configuredRetries: effectiveRetries,
160
+ configuredDelay: effectiveRetryDelay,
161
+ attempt: 0
162
+ }
163
+ }
164
+ };
165
+ for (const plugin of plugins) {
166
+ await plugin.preRequest?.(pluginContext);
148
167
  }
149
- const controller2 = new AbortController();
150
- const timeoutId = setTimeout(() => controller2.abort(), timeout);
151
- controller2.signal.addEventListener(
152
- "abort",
153
- () => clearTimeout(timeoutId),
154
- { once: true }
155
- );
156
- return controller2.signal;
157
- }
158
- let timeoutSignal = void 0;
159
- let combinedSignal = void 0;
160
- let controller = void 0;
161
- if (effectiveTimeout > 0) {
162
- timeoutSignal = createTimeoutSignal(effectiveTimeout);
163
- pluginContext.metadata.signals.timeout = timeoutSignal;
164
- }
165
- const signals = [];
166
- if (userSignal) signals.push(userSignal);
167
- if (transformedSignal && transformedSignal !== userSignal) {
168
- signals.push(transformedSignal);
169
- }
170
- if (timeoutSignal) signals.push(timeoutSignal);
171
- if (signals.length === 1) {
172
- combinedSignal = signals[0];
173
- controller = new AbortController();
174
- } else {
175
- if (typeof AbortSignal.any !== "function") {
176
- throw new Error(
177
- "AbortSignal.any is required for combining multiple signals. Please install a polyfill for environments that do not support it."
168
+ const effectiveThrowOnHttpError = typeof init.throwOnHttpError !== "undefined" ? init.throwOnHttpError : opts.throwOnHttpError ?? false;
169
+ function createTimeoutSignal(timeout) {
170
+ if (typeof AbortSignal?.timeout === "function") {
171
+ return AbortSignal.timeout(timeout);
172
+ }
173
+ const controller2 = new AbortController();
174
+ const timeoutId = setTimeout(() => controller2.abort(), timeout);
175
+ controller2.signal.addEventListener(
176
+ "abort",
177
+ () => clearTimeout(timeoutId),
178
+ { once: true }
178
179
  );
180
+ return controller2.signal;
179
181
  }
180
- combinedSignal = AbortSignal.any(signals);
181
- controller = new AbortController();
182
- }
183
- pluginContext.metadata.signals.combined = combinedSignal;
184
- const retryWithHooks = async () => {
185
- let attempt = 0;
186
- const shouldRetryWithHook = (ctx) => {
187
- attempt = ctx.attempt;
188
- pluginContext.metadata.retry.attempt = attempt;
189
- pluginContext.metadata.retry.lastError = ctx.error;
190
- pluginContext.metadata.retry.lastResponse = ctx.response;
191
- const retrying = effectiveShouldRetry(ctx);
192
- pluginContext.metadata.retry.shouldRetryResult = retrying;
193
- if (retrying && attempt <= effectiveRetries) {
194
- effectiveHooks.onRetry?.(
195
- request,
196
- attempt - 1,
197
- ctx.error,
198
- ctx.response
182
+ let timeoutSignal = void 0;
183
+ let combinedSignal = void 0;
184
+ let controller = void 0;
185
+ if (effectiveTimeout > 0) {
186
+ timeoutSignal = createTimeoutSignal(effectiveTimeout);
187
+ pluginContext.metadata.signals.timeout = timeoutSignal;
188
+ }
189
+ const signals = [];
190
+ if (userSignal) signals.push(userSignal);
191
+ if (transformedSignal && transformedSignal !== userSignal) {
192
+ signals.push(transformedSignal);
193
+ }
194
+ if (timeoutSignal) signals.push(timeoutSignal);
195
+ if (signals.length === 1) {
196
+ combinedSignal = signals[0];
197
+ controller = new AbortController();
198
+ } else {
199
+ if (typeof AbortSignal.any !== "function") {
200
+ throw new Error(
201
+ "AbortSignal.any is required for combining multiple signals. Please install a polyfill for environments that do not support it."
199
202
  );
200
203
  }
201
- return retrying;
202
- };
203
- let lastResponse = void 0;
204
- try {
205
- let res = await retry(
206
- async () => {
207
- if (userSignal?.aborted) {
208
- effectiveHooks.onAbort?.(request);
209
- throw new AbortError("Request was aborted by user");
210
- }
211
- if (timeoutSignal?.aborted) {
212
- effectiveHooks.onTimeout?.(request);
213
- throw new TimeoutError("signal timed out");
214
- }
215
- if (typeof combinedSignal?.throwIfAborted === "function") {
216
- combinedSignal.throwIfAborted();
217
- } else if (combinedSignal?.aborted) {
204
+ combinedSignal = AbortSignal.any(signals);
205
+ controller = new AbortController();
206
+ }
207
+ pluginContext.metadata.signals.combined = combinedSignal;
208
+ const retryWithHooks = async () => {
209
+ let attempt = 0;
210
+ const shouldRetryWithHook = (ctx) => {
211
+ attempt = ctx.attempt;
212
+ pluginContext.metadata.retry.attempt = attempt;
213
+ pluginContext.metadata.retry.lastError = ctx.error;
214
+ pluginContext.metadata.retry.lastResponse = ctx.response;
215
+ const retrying = effectiveShouldRetry(ctx);
216
+ pluginContext.metadata.retry.shouldRetryResult = retrying;
217
+ if (retrying && attempt <= effectiveRetries) {
218
+ effectiveHooks.onRetry?.(
219
+ request,
220
+ attempt - 1,
221
+ ctx.error,
222
+ ctx.response
223
+ );
224
+ }
225
+ return retrying;
226
+ };
227
+ let lastResponse = void 0;
228
+ try {
229
+ let res = await retry(
230
+ async () => {
218
231
  if (userSignal?.aborted) {
219
232
  effectiveHooks.onAbort?.(request);
220
233
  throw new AbortError("Request was aborted by user");
221
- } else if (timeoutSignal?.aborted) {
234
+ }
235
+ if (timeoutSignal?.aborted) {
222
236
  effectiveHooks.onTimeout?.(request);
223
237
  throw new TimeoutError("signal timed out");
224
- } else {
225
- throw new AbortError(
226
- "Request was aborted",
227
- new DOMException("Aborted", "AbortError")
228
- );
229
238
  }
230
- }
231
- const reqWithSignal = new Request(request, {
232
- signal: combinedSignal
233
- });
234
- try {
235
- const handler = init.fetchHandler ?? fetchHandler ?? fetch;
236
- const response = await handler(reqWithSignal);
237
- lastResponse = response;
238
- pluginContext.metadata.retry.lastResponse = response;
239
- return response;
240
- } catch (err) {
241
- pluginContext.metadata.retry.lastError = err;
242
- if (err instanceof DOMException && err.name === "AbortError") {
243
- if (timeoutSignal?.aborted && (!userSignal || !userSignal.aborted)) {
244
- effectiveHooks.onTimeout?.(request);
245
- throw new TimeoutError("signal timed out", err);
246
- } else if (userSignal?.aborted) {
239
+ if (typeof combinedSignal?.throwIfAborted === "function") {
240
+ combinedSignal.throwIfAborted();
241
+ } else if (combinedSignal?.aborted) {
242
+ if (userSignal?.aborted) {
247
243
  effectiveHooks.onAbort?.(request);
248
244
  throw new AbortError("Request was aborted by user");
245
+ } else if (timeoutSignal?.aborted) {
246
+ effectiveHooks.onTimeout?.(request);
247
+ throw new TimeoutError("signal timed out");
249
248
  } else {
250
249
  throw new AbortError(
251
250
  "Request was aborted",
252
251
  new DOMException("Aborted", "AbortError")
253
252
  );
254
253
  }
255
- } else if (err instanceof TypeError && /NetworkError|network error|failed to fetch|lost connection|NetworkError when attempting to fetch resource/i.test(
256
- err.message
257
- )) {
258
- throw new NetworkError(err.message, err);
259
254
  }
260
- throw err;
261
- }
262
- },
263
- effectiveRetries,
264
- effectiveRetryDelay,
265
- shouldRetryWithHook,
266
- request
267
- );
268
- if (effectiveHooks.transformResponse) {
269
- res = await effectiveHooks.transformResponse(res, request);
270
- }
271
- await effectiveHooks.after?.(request, res);
272
- await effectiveHooks.onComplete?.(request, res, void 0);
273
- if (effectiveThrowOnHttpError && (res.status >= 400 && res.status < 500 && res.status !== 429 || res.status >= 500 || res.status === 429)) {
274
- const { HttpError } = await import("./error-XBHPSMAQ.js");
275
- throw new HttpError(
276
- `HTTP error: ${res.status} ${res.statusText}`,
277
- res
255
+ const reqWithSignal = new Request(request, {
256
+ signal: combinedSignal
257
+ });
258
+ try {
259
+ const handler = init.fetchHandler ?? fetchHandler ?? fetch;
260
+ const response = await handler(reqWithSignal);
261
+ lastResponse = response;
262
+ pluginContext.metadata.retry.lastResponse = response;
263
+ return response;
264
+ } catch (err) {
265
+ pluginContext.metadata.retry.lastError = err;
266
+ if (err instanceof DOMException && err.name === "AbortError") {
267
+ if (timeoutSignal?.aborted && (!userSignal || !userSignal.aborted)) {
268
+ effectiveHooks.onTimeout?.(request);
269
+ throw new TimeoutError("signal timed out", err);
270
+ } else if (userSignal?.aborted) {
271
+ effectiveHooks.onAbort?.(request);
272
+ throw new AbortError("Request was aborted by user");
273
+ } else {
274
+ throw new AbortError(
275
+ "Request was aborted",
276
+ new DOMException("Aborted", "AbortError")
277
+ );
278
+ }
279
+ } else if (err instanceof TypeError && /NetworkError|network error|failed to fetch|lost connection|NetworkError when attempting to fetch resource/i.test(
280
+ err.message
281
+ )) {
282
+ throw new NetworkError(err.message, err);
283
+ }
284
+ throw err;
285
+ }
286
+ },
287
+ effectiveRetries,
288
+ effectiveRetryDelay,
289
+ shouldRetryWithHook,
290
+ request,
291
+ combinedSignal
278
292
  );
279
- }
280
- return res;
281
- } catch (err) {
282
- pluginContext.metadata.retry.lastError = err;
283
- if (lastResponse) {
284
- const resp = lastResponse;
285
- if (effectiveThrowOnHttpError && (resp.status >= 400 && resp.status < 500 && resp.status !== 429 || resp.status >= 500 || resp.status === 429)) {
286
- const { HttpError } = await import("./error-XBHPSMAQ.js");
287
- throw new HttpError(
288
- `HTTP error: ${resp.status} ${resp.statusText}`,
289
- resp
293
+ if (effectiveHooks.transformResponse) {
294
+ res = await effectiveHooks.transformResponse(res, request);
295
+ }
296
+ await effectiveHooks.after?.(request, res);
297
+ await effectiveHooks.onComplete?.(request, res, void 0);
298
+ if (effectiveThrowOnHttpError && (res.status >= 400 && res.status < 500 && res.status !== 429 || res.status >= 500 || res.status === 429)) {
299
+ const { HttpError: HttpError2 } = await import("./error-XBHPSMAQ.js");
300
+ throw new HttpError2(
301
+ `HTTP error: ${res.status} ${res.statusText}`,
302
+ res
290
303
  );
291
304
  }
292
- return resp;
305
+ return res;
306
+ } catch (err) {
307
+ pluginContext.metadata.retry.lastError = err;
308
+ if (lastResponse) {
309
+ const resp = lastResponse;
310
+ if (effectiveThrowOnHttpError && (resp.status >= 400 && resp.status < 500 && resp.status !== 429 || resp.status >= 500 || resp.status === 429)) {
311
+ const { HttpError: HttpError2 } = await import("./error-XBHPSMAQ.js");
312
+ throw new HttpError2(
313
+ `HTTP error: ${resp.status} ${resp.statusText}`,
314
+ resp
315
+ );
316
+ }
317
+ return resp;
318
+ }
319
+ if (err instanceof TimeoutError) {
320
+ await effectiveHooks.onTimeout?.(request);
321
+ await effectiveHooks.onError?.(request, err);
322
+ await effectiveHooks.onComplete?.(request, void 0, err);
323
+ throw err;
324
+ }
325
+ if (err instanceof AbortError) {
326
+ await effectiveHooks.onAbort?.(request);
327
+ await effectiveHooks.onError?.(request, err);
328
+ await effectiveHooks.onComplete?.(request, void 0, err);
329
+ throw err;
330
+ }
331
+ if (err instanceof NetworkError) {
332
+ await effectiveHooks.onError?.(request, err);
333
+ await effectiveHooks.onComplete?.(request, void 0, err);
334
+ throw err;
335
+ }
336
+ const retryErr = new RetryLimitError(
337
+ typeof err === "object" && err && "message" in err && typeof err.message === "string" ? err.message : "Retry limit reached",
338
+ err
339
+ );
340
+ await effectiveHooks.onError?.(request, retryErr);
341
+ await effectiveHooks.onComplete?.(request, void 0, retryErr);
342
+ throw retryErr;
293
343
  }
294
- if (err instanceof TimeoutError) {
295
- await effectiveHooks.onTimeout?.(request);
296
- await effectiveHooks.onError?.(request, err);
297
- await effectiveHooks.onComplete?.(request, void 0, err);
298
- throw err;
344
+ };
345
+ const baseDispatch = async () => retryWithHooks();
346
+ let dispatch = baseDispatch;
347
+ for (let i = plugins.length - 1; i >= 0; i--) {
348
+ const plugin = plugins[i];
349
+ if (plugin.wrapDispatch) {
350
+ dispatch = plugin.wrapDispatch(dispatch);
299
351
  }
300
- if (err instanceof AbortError) {
301
- await effectiveHooks.onAbort?.(request);
302
- await effectiveHooks.onError?.(request, err);
303
- await effectiveHooks.onComplete?.(request, void 0, err);
304
- throw err;
352
+ }
353
+ const actualPromise = dispatch(pluginContext).then(async (response) => {
354
+ for (const plugin of plugins) {
355
+ await plugin.onSuccess?.(pluginContext, response);
305
356
  }
306
- if (err instanceof NetworkError) {
307
- await effectiveHooks.onError?.(request, err);
308
- await effectiveHooks.onComplete?.(request, void 0, err);
309
- throw err;
357
+ return response;
358
+ }).catch(async (err) => {
359
+ for (const plugin of plugins) {
360
+ await plugin.onError?.(pluginContext, err);
310
361
  }
311
- const retryErr = new RetryLimitError(
312
- typeof err === "object" && err && "message" in err && typeof err.message === "string" ? err.message : "Retry limit reached",
313
- err
314
- );
315
- await effectiveHooks.onError?.(request, retryErr);
316
- await effectiveHooks.onComplete?.(request, void 0, retryErr);
317
- throw retryErr;
318
- }
362
+ throw err;
363
+ });
364
+ const pendingEntry = {
365
+ promise: actualPromise,
366
+ request,
367
+ controller
368
+ };
369
+ pendingRequests.push(pendingEntry);
370
+ return actualPromise.finally(async () => {
371
+ for (const plugin of plugins) {
372
+ await plugin.onFinally?.(pluginContext);
373
+ }
374
+ const index = pendingRequests.indexOf(pendingEntry);
375
+ if (index > -1) {
376
+ pendingRequests.splice(index, 1);
377
+ }
378
+ });
319
379
  };
320
- const baseDispatch = async () => retryWithHooks();
321
- let dispatch = baseDispatch;
322
- for (let i = plugins.length - 1; i >= 0; i--) {
323
- const plugin = plugins[i];
324
- if (plugin.wrapDispatch) {
325
- dispatch = plugin.wrapDispatch(dispatch);
380
+ let promise = execute();
381
+ for (const plugin of plugins) {
382
+ if (plugin.decoratePromise) {
383
+ promise = plugin.decoratePromise(promise);
326
384
  }
327
385
  }
328
- const actualPromise = dispatch(pluginContext).then(async (response) => {
329
- for (const plugin of plugins) {
330
- await plugin.onSuccess?.(pluginContext, response);
331
- }
332
- return response;
333
- }).catch(async (err) => {
334
- for (const plugin of plugins) {
335
- await plugin.onError?.(pluginContext, err);
336
- }
337
- throw err;
338
- });
339
- const pendingEntry = {
340
- promise: actualPromise,
341
- request,
342
- controller
343
- };
344
- pendingRequests.push(pendingEntry);
345
- return actualPromise.finally(async () => {
346
- for (const plugin of plugins) {
347
- await plugin.onFinally?.(pluginContext);
348
- }
349
- const index = pendingRequests.indexOf(pendingEntry);
350
- if (index > -1) {
351
- pendingRequests.splice(index, 1);
352
- }
353
- });
386
+ return promise;
354
387
  };
355
388
  Object.defineProperty(client, "pendingRequests", {
356
389
  get() {
@@ -371,6 +404,7 @@ function createClient(opts = {}) {
371
404
  export {
372
405
  AbortError,
373
406
  CircuitOpenError,
407
+ HttpError,
374
408
  NetworkError,
375
409
  RetryLimitError,
376
410
  TimeoutError,