@zayne-labs/callapi 1.6.22 → 1.7.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/README.md CHANGED
@@ -1,8 +1,19 @@
1
- # CallApi
2
-
3
- [![Build Size](https://img.shields.io/bundlephobia/minzip/@zayne-labs/callapi?label=bundle%20size&style=flat&colorA=000000&colorB=000000)](https://bundlephobia.com/result?p=@zayne-labs/callapi)[![Version](https://img.shields.io/npm/v/@zayne-labs/callapi?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/@zayne-labs/callapi)
4
-
5
- CallApi Fetch is an extra-lightweight wrapper over fetch that provides quality of life improvements beyond the bare fetch api, while keeping the API familiar.
1
+ <h1 align="center">CallApi - Advanced Fetch Client</h1>
2
+
3
+ <p align="center">
4
+ <img src="../../apps/docs/public/logo.png" alt="CallApi Logo" width="50%">
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://deno.bundlejs.com/badge?q=@zayne-labs/callapi,@zayne-labs/callapi&treeshake=%5B*%5D,%5B%7B+createFetchClient+%7D%5D&config=%7B%22compression%22:%7B%22type%22:%22brotli%22,%22quality%22:11%7D%7D"><img src="https://deno.bundlejs.com/badge?q=@zayne-labs/callapi,@zayne-labs/callapi&treeshake=%5B*%5D,%5B%7B+createFetchClient+%7D%5D&config=%7B%22compression%22:%7B%22type%22:%22brotli%22,%22quality%22:11%7D%7D" alt="bundle size"></a>
9
+ <a href="https://www.npmjs.com/package/@zayne-labs/callapi"><img src="https://img.shields.io/npm/v/@zayne-labs/callapi?style=flat&color=EFBA5F" alt="npm version"></a>
10
+ <a href="https://github.com/zayne-labs/call-api/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/@zayne-labs/callapi?style=flat&color=EFBA5F" alt="license"></a>
11
+ <a href="https://github.com/zayne-labs/call-api/graphs/commit-activity"><img src="https://img.shields.io/github/commit-activity/m/zayne-labs/call-api?style=flat&color=EFBA5F" alt="commit activity"></a>
12
+ <a href="https://www.npmjs.com/package/@zayne-labs/callapi"><img src="https://img.shields.io/npm/dm/@zayne-labs/callapi?style=flat&color=EFBA5F" alt="downloads per month"></a>
13
+ </p>
14
+
15
+ <p align="center">
16
+ CallApi Fetch is an extra-lightweight wrapper over fetch that provides quality of life improvements beyond the bare fetch api, while keeping the API familiar.</p>
6
17
 
7
18
  It takes in a url and a request options object, just like fetch, but with some additional options to make your life easier. Check out the [API Reference](https://zayne-labs-callapi.netlify.app/docs/latest/all-options) for a quick look at each option.
8
19
 
@@ -1,5 +1,41 @@
1
1
  import { StandardSchemaV1 } from '@standard-schema/spec';
2
2
 
3
+ type StreamProgressEvent = {
4
+ /**
5
+ * Current chunk of data being streamed
6
+ */
7
+ chunk: Uint8Array;
8
+ /**
9
+ * Progress in percentage
10
+ */
11
+ progress: number;
12
+ /**
13
+ * Total size of data in bytes
14
+ */
15
+ totalBytes: number;
16
+ /**
17
+ * Amount of data transferred so far
18
+ */
19
+ transferredBytes: number;
20
+ };
21
+ type RequestStreamContext = {
22
+ event: StreamProgressEvent;
23
+ options: CombinedCallApiExtraOptions;
24
+ request: CallApiRequestOptionsForHooks;
25
+ requestInstance: Request;
26
+ };
27
+ type ResponseStreamContext = {
28
+ event: StreamProgressEvent;
29
+ options: CombinedCallApiExtraOptions;
30
+ request: CallApiRequestOptionsForHooks;
31
+ response: Response;
32
+ };
33
+ declare global {
34
+ interface ReadableStream<R> {
35
+ [Symbol.asyncIterator]: () => AsyncIterableIterator<R>;
36
+ }
37
+ }
38
+
3
39
  type ValueOrFunctionResult<TValue> = TValue | (() => TValue);
4
40
  /**
5
41
  * Bearer Or Token authentication
@@ -243,7 +279,10 @@ interface CallApiPlugin<TData = never, TErrorData = never> {
243
279
  declare const definePlugin: <TPlugin extends CallApiPlugin | AnyFunction<CallApiPlugin>>(plugin: TPlugin) => TPlugin;
244
280
  type Plugins<TPluginArray extends CallApiPlugin[]> = TPluginArray;
245
281
 
246
- declare const fetchSpecificKeys: ("body" | "cache" | "credentials" | "headers" | "integrity" | "keepalive" | "method" | "mode" | "priority" | "redirect" | "referrer" | "referrerPolicy" | "signal" | "window")[];
282
+ type ModifiedRequestInit = RequestInit & {
283
+ duplex?: "full" | "half" | "none";
284
+ };
285
+ declare const fetchSpecificKeys: ("body" | "cache" | "credentials" | "headers" | "integrity" | "keepalive" | "method" | "mode" | "priority" | "redirect" | "referrer" | "referrerPolicy" | "signal" | "window" | "duplex")[];
247
286
  declare const getDefaultOptions: () => {
248
287
  baseURL: string;
249
288
  bodySerializer: {
@@ -335,7 +374,7 @@ type ResultModeOption<TErrorData, TResultMode extends ResultModeUnion> = TErrorD
335
374
  };
336
375
 
337
376
  type FetchSpecificKeysUnion = Exclude<(typeof fetchSpecificKeys)[number], "body" | "headers" | "method">;
338
- type CallApiRequestOptions<TSchemas extends CallApiSchemas = DefaultMoreOptions> = BodyOption<TSchemas> & HeadersOption<TSchemas> & MethodOption<TSchemas> & Pick<RequestInit, FetchSpecificKeysUnion>;
377
+ type CallApiRequestOptions<TSchemas extends CallApiSchemas = DefaultMoreOptions> = BodyOption<TSchemas> & HeadersOption<TSchemas> & MethodOption<TSchemas> & Pick<ModifiedRequestInit, FetchSpecificKeysUnion>;
339
378
  type CallApiRequestOptionsForHooks<TSchemas extends CallApiSchemas = DefaultMoreOptions> = Omit<CallApiRequestOptions<TSchemas>, "headers"> & {
340
379
  headers?: Record<string, string | undefined>;
341
380
  };
@@ -356,6 +395,10 @@ interface Interceptors<TData = DefaultDataType, TErrorData = DefaultDataType, TM
356
395
  * Interceptor that will be called when an error occurs during the fetch request.
357
396
  */
358
397
  onRequestError?: (context: RequestErrorContext & WithMoreOptions<TMoreOptions>) => Awaitable<unknown>;
398
+ /**
399
+ * Interceptor that will be called when upload stream progress is tracked
400
+ */
401
+ onRequestStream?: (context: RequestStreamContext & WithMoreOptions<TMoreOptions>) => Awaitable<unknown>;
359
402
  /**
360
403
  * Interceptor that will be called when any response is received from the api, whether successful or not
361
404
  */
@@ -364,6 +407,10 @@ interface Interceptors<TData = DefaultDataType, TErrorData = DefaultDataType, TM
364
407
  * Interceptor that will be called when an error response is received from the api.
365
408
  */
366
409
  onResponseError?: (context: ResponseErrorContext<TErrorData> & WithMoreOptions<TMoreOptions>) => Awaitable<unknown>;
410
+ /**
411
+ * Interceptor that will be called when download stream progress is tracked
412
+ */
413
+ onResponseStream?: (context: ResponseStreamContext & WithMoreOptions<TMoreOptions>) => Awaitable<unknown>;
367
414
  /**
368
415
  * Interceptor that will be called when a request is retried.
369
416
  */
@@ -418,6 +465,14 @@ type ExtraOptions<TData = DefaultDataType, TErrorData = DefaultDataType, TResult
418
465
  * @default "Failed to fetch data from server!"
419
466
  */
420
467
  defaultErrorMessage?: string;
468
+ /**
469
+ * If true, forces the calculation of the total byte size from the request or response body, in case the content-length header is not present or is incorrect.
470
+ * @default false
471
+ */
472
+ forceStreamSizeCalc?: boolean | {
473
+ request?: boolean;
474
+ response?: boolean;
475
+ };
421
476
  /**
422
477
  * Resolved request URL
423
478
  */
@@ -481,7 +536,7 @@ type CallApiExtraOptions<TData = DefaultDataType, TErrorData = DefaultDataType,
481
536
  };
482
537
  declare const optionsEnumToOmitFromBase: ("dedupeKey" | "extend")[];
483
538
  type BaseCallApiExtraOptions<TBaseData = DefaultDataType, TBaseErrorData = DefaultDataType, TBaseResultMode extends ResultModeUnion = ResultModeUnion, TBaseThrowOnError extends boolean = DefaultThrowOnError, TBaseResponseType extends ResponseTypeUnion = ResponseTypeUnion, TBasePluginArray extends CallApiPlugin[] = DefaultPluginArray, TBaseSchemas extends CallApiSchemas = DefaultMoreOptions> = Omit<Partial<CallApiExtraOptions<TBaseData, TBaseErrorData, TBaseResultMode, TBaseThrowOnError, TBaseResponseType, TBasePluginArray, TBaseSchemas>>, (typeof optionsEnumToOmitFromBase)[number]>;
484
- type CombinedCallApiExtraOptions = BaseCallApiExtraOptions & CallApiExtraOptions;
539
+ type CombinedCallApiExtraOptions = Interceptors & Omit<BaseCallApiExtraOptions & CallApiExtraOptions, keyof Interceptors>;
485
540
  type BaseCallApiConfig<TBaseData = DefaultDataType, TBaseErrorData = DefaultDataType, TBaseResultMode extends ResultModeUnion = ResultModeUnion, TBaseThrowOnError extends boolean = DefaultThrowOnError, TBaseResponseType extends ResponseTypeUnion = ResponseTypeUnion, TBasePluginArray extends CallApiPlugin[] = DefaultPluginArray, TBaseSchemas extends CallApiSchemas = DefaultMoreOptions> = (CallApiRequestOptions<TBaseSchemas> & BaseCallApiExtraOptions<TBaseData, TBaseErrorData, TBaseResultMode, TBaseThrowOnError, TBaseResponseType, TBasePluginArray, TBaseSchemas>) | ((context: {
486
541
  initURL: string;
487
542
  options: CallApiExtraOptions;
@@ -569,7 +624,7 @@ type ResultModeMap<TData = DefaultDataType, TErrorData = DefaultDataType, TRespo
569
624
  allWithoutResponse: CallApiResultSuccessVariant<TComputedData>["data" | "error"] | CallApiResultErrorVariant<TComputedErrorData>["data" | "error"];
570
625
  onlyError: CallApiResultSuccessVariant<TComputedData>["error"] | CallApiResultErrorVariant<TComputedErrorData>["error"];
571
626
  onlyResponse: CallApiResultErrorVariant<TComputedErrorData>["response"] | CallApiResultSuccessVariant<TComputedData>["response"];
572
- onlyResponseWithException: CallApiResultSuccessVariant<TComputedErrorData>["response"];
627
+ onlyResponseWithException: CallApiResultSuccessVariant<TComputedData>["response"];
573
628
  onlySuccess: CallApiResultErrorVariant<TComputedErrorData>["data"] | CallApiResultSuccessVariant<TComputedData>["data"];
574
629
  onlySuccessWithException: CallApiResultSuccessVariant<TComputedData>["data"];
575
630
  }>;
@@ -63,9 +63,15 @@ var resolveErrorResult = (info) => {
63
63
  onlySuccess: apiDetails.data,
64
64
  onlySuccessWithException: apiDetails.data
65
65
  };
66
- const getErrorResult = (customInfo) => {
67
- const errorResult = resultModeMap[resultMode ?? "all"];
68
- return isObject(customInfo) ? { ...errorResult, ...customInfo } : errorResult;
66
+ const getErrorResult = (customErrorInfo) => {
67
+ const errorVariantResult = resultModeMap[resultMode ?? "all"];
68
+ return customErrorInfo ? {
69
+ ...errorVariantResult,
70
+ error: {
71
+ ...errorVariantResult.error,
72
+ ...customErrorInfo
73
+ }
74
+ } : errorVariantResult;
69
75
  };
70
76
  return { apiDetails, getErrorResult };
71
77
  };
@@ -83,7 +89,7 @@ var HTTPError = class extends Error {
83
89
  }
84
90
  };
85
91
 
86
- // src/utils/type-guards.ts
92
+ // src/utils/guards.ts
87
93
  var isHTTPErrorInstance = (error) => {
88
94
  return (
89
95
  // prettier-ignore
@@ -132,6 +138,9 @@ var isSerializable = (value) => {
132
138
  var isFunction = (value) => typeof value === "function";
133
139
  var isQueryString = (value) => isString(value) && value.includes("=");
134
140
  var isString = (value) => typeof value === "string";
141
+ var isReadableStream = (value) => {
142
+ return value instanceof ReadableStream;
143
+ };
135
144
 
136
145
  // src/auth.ts
137
146
  var getValue = (value) => {
@@ -181,6 +190,7 @@ var optionsEnumToOmitFromBase = defineEnum(["extend", "dedupeKey"]);
181
190
  var fetchSpecificKeys = defineEnum([
182
191
  "body",
183
192
  "integrity",
193
+ "duplex",
184
194
  "method",
185
195
  "headers",
186
196
  "signal",
@@ -323,6 +333,108 @@ var waitUntil = (delay) => {
323
333
  return promise;
324
334
  };
325
335
 
336
+ // src/stream.ts
337
+ var createProgressEvent = (options) => {
338
+ const { chunk, totalBytes, transferredBytes } = options;
339
+ return {
340
+ chunk,
341
+ progress: Math.round(transferredBytes / totalBytes * 100) || 0,
342
+ totalBytes,
343
+ transferredBytes
344
+ };
345
+ };
346
+ var calculateTotalBytesFromBody = async (requestBody, existingTotalBytes) => {
347
+ let totalBytes = existingTotalBytes;
348
+ if (!requestBody) {
349
+ return totalBytes;
350
+ }
351
+ for await (const chunk of requestBody) {
352
+ totalBytes += chunk.byteLength;
353
+ }
354
+ return totalBytes;
355
+ };
356
+ var toStreamableRequest = async (context) => {
357
+ const { options, request, requestInstance } = context;
358
+ if (!options.onRequestStream || !requestInstance.body) return;
359
+ const contentLength = requestInstance.headers.get("content-length") ?? new Headers(request.headers).get("content-length") ?? request.body?.size;
360
+ let totalBytes = Number(contentLength ?? 0);
361
+ const shouldForceContentLengthCalc = isObject(options.forceStreamSizeCalc) ? options.forceStreamSizeCalc.request : options.forceStreamSizeCalc;
362
+ if (!contentLength && shouldForceContentLengthCalc) {
363
+ totalBytes = await calculateTotalBytesFromBody(requestInstance.clone().body, totalBytes);
364
+ }
365
+ let transferredBytes = 0;
366
+ await executeHooks(
367
+ options.onRequestStream({
368
+ event: createProgressEvent({ chunk: new Uint8Array(), totalBytes, transferredBytes }),
369
+ options,
370
+ request,
371
+ requestInstance
372
+ })
373
+ );
374
+ const body = requestInstance.body;
375
+ void new ReadableStream({
376
+ start: async (controller) => {
377
+ if (!body) return;
378
+ for await (const chunk of body) {
379
+ transferredBytes += chunk.byteLength;
380
+ totalBytes = Math.max(totalBytes, transferredBytes);
381
+ await executeHooks(
382
+ options.onRequestStream?.({
383
+ event: createProgressEvent({ chunk, totalBytes, transferredBytes }),
384
+ options,
385
+ request,
386
+ requestInstance
387
+ })
388
+ );
389
+ controller.enqueue(chunk);
390
+ }
391
+ controller.close();
392
+ }
393
+ });
394
+ };
395
+ var toStreamableResponse = async (context) => {
396
+ const { options, request, response } = context;
397
+ if (!options.onResponseStream || !response.body) {
398
+ return response;
399
+ }
400
+ const contentLength = response.headers.get("content-length");
401
+ let totalBytes = Number(contentLength ?? 0);
402
+ const shouldForceContentLengthCalc = isObject(options.forceStreamSizeCalc) ? options.forceStreamSizeCalc.response : options.forceStreamSizeCalc;
403
+ if (!contentLength && shouldForceContentLengthCalc) {
404
+ totalBytes = await calculateTotalBytesFromBody(response.clone().body, totalBytes);
405
+ }
406
+ let transferredBytes = 0;
407
+ await executeHooks(
408
+ options.onResponseStream({
409
+ event: createProgressEvent({ chunk: new Uint8Array(), totalBytes, transferredBytes }),
410
+ options,
411
+ request,
412
+ response
413
+ })
414
+ );
415
+ const body = response.body;
416
+ const stream = new ReadableStream({
417
+ start: async (controller) => {
418
+ if (!body) return;
419
+ for await (const chunk of body) {
420
+ transferredBytes += chunk.byteLength;
421
+ totalBytes = Math.max(totalBytes, transferredBytes);
422
+ await executeHooks(
423
+ options.onResponseStream?.({
424
+ event: createProgressEvent({ chunk, totalBytes, transferredBytes }),
425
+ options,
426
+ request,
427
+ response
428
+ })
429
+ );
430
+ controller.enqueue(chunk);
431
+ }
432
+ controller.close();
433
+ }
434
+ });
435
+ return new Response(stream, response);
436
+ };
437
+
326
438
  // src/dedupe.ts
327
439
  var createDedupeStrategy = async (context) => {
328
440
  const { $RequestInfoCache, newFetchController, options, request } = context;
@@ -347,12 +459,29 @@ var createDedupeStrategy = async (context) => {
347
459
  prevRequestInfo.controller.abort(reason);
348
460
  return Promise.resolve();
349
461
  };
350
- const handleRequestDeferStrategy = () => {
462
+ const handleRequestDeferStrategy = async () => {
351
463
  const fetchApi = getFetchImpl(options.customFetchImpl);
352
464
  const shouldUsePromiseFromCache = prevRequestInfo && options.dedupeStrategy === "defer";
353
- const responsePromise = shouldUsePromiseFromCache ? prevRequestInfo.responsePromise : fetchApi(options.fullURL, request);
465
+ const requestInstance = new Request(
466
+ options.fullURL,
467
+ isReadableStream(request.body) && !request.duplex ? { ...request, duplex: "half" } : request
468
+ );
469
+ void toStreamableRequest({
470
+ options,
471
+ request,
472
+ requestInstance: requestInstance.clone()
473
+ });
474
+ const responsePromise = shouldUsePromiseFromCache ? prevRequestInfo.responsePromise : (
475
+ // eslint-disable-next-line unicorn/no-nested-ternary -- Allow
476
+ isReadableStream(request.body) ? fetchApi(requestInstance.clone()) : fetchApi(options.fullURL, request)
477
+ );
354
478
  $RequestInfoCacheOrNull?.set(dedupeKey, { controller: newFetchController, responsePromise });
355
- return responsePromise;
479
+ const streamableResponse = toStreamableResponse({
480
+ options,
481
+ request,
482
+ response: await responsePromise
483
+ });
484
+ return streamableResponse;
356
485
  };
357
486
  const removeDedupeKeyFromCache = () => $RequestInfoCacheOrNull?.delete(dedupeKey);
358
487
  return {
@@ -367,7 +496,8 @@ var definePlugin = (plugin) => {
367
496
  return plugin;
368
497
  };
369
498
  var createMergedHook = (hooks, mergedHooksExecutionMode) => {
370
- return async (ctx) => {
499
+ if (hooks.length === 0) return;
500
+ const mergedHook = async (ctx) => {
371
501
  if (mergedHooksExecutionMode === "sequential") {
372
502
  for (const hook of hooks) {
373
503
  await hook?.(ctx);
@@ -379,13 +509,16 @@ var createMergedHook = (hooks, mergedHooksExecutionMode) => {
379
509
  await Promise.all(hookArray.map((uniqueHook) => uniqueHook?.(ctx)));
380
510
  }
381
511
  };
512
+ return mergedHook;
382
513
  };
383
514
  var hooksEnum = {
384
515
  onError: /* @__PURE__ */ new Set(),
385
516
  onRequest: /* @__PURE__ */ new Set(),
386
517
  onRequestError: /* @__PURE__ */ new Set(),
518
+ onRequestStream: /* @__PURE__ */ new Set(),
387
519
  onResponse: /* @__PURE__ */ new Set(),
388
520
  onResponseError: /* @__PURE__ */ new Set(),
521
+ onResponseStream: /* @__PURE__ */ new Set(),
389
522
  onRetry: /* @__PURE__ */ new Set(),
390
523
  onSuccess: /* @__PURE__ */ new Set()
391
524
  };
@@ -450,7 +583,7 @@ var initializePlugins = async (context) => {
450
583
  }
451
584
  const resolvedHooks = {};
452
585
  for (const [key, hookRegistry] of Object.entries(hookRegistries)) {
453
- const flattenedHookArray = [...hookRegistry].flat();
586
+ const flattenedHookArray = [...hookRegistry].flat().filter(Boolean);
454
587
  const mergedHook = createMergedHook(flattenedHookArray, options.mergedHooksExecutionMode);
455
588
  resolvedHooks[key] = mergedHook;
456
589
  }
@@ -512,31 +645,49 @@ var getExponentialDelay = (currentAttemptCount, options) => {
512
645
  return Math.min(exponentialDelay, maxDelay);
513
646
  };
514
647
  var createRetryStrategy = (ctx) => {
515
- const currentRetryCount = ctx.options["~retryCount"] ?? 0;
648
+ const { options } = ctx;
649
+ const currentRetryCount = options["~retryCount"] ?? 0;
516
650
  const getDelay = () => {
517
- if (ctx.options.retryStrategy === "exponential") {
518
- return getExponentialDelay(currentRetryCount, ctx.options);
651
+ if (options.retryStrategy === "exponential") {
652
+ return getExponentialDelay(currentRetryCount, options);
519
653
  }
520
- return getLinearDelay(ctx.options);
654
+ return getLinearDelay(options);
521
655
  };
522
656
  const shouldAttemptRetry = async () => {
523
- const customRetryCondition = await ctx.options.retryCondition?.(ctx) ?? true;
524
- const maxRetryAttempts = ctx.options.retryAttempts ?? 0;
657
+ const customRetryCondition = await options.retryCondition?.(ctx) ?? true;
658
+ const maxRetryAttempts = options.retryAttempts ?? 0;
525
659
  const baseRetryCondition = maxRetryAttempts > currentRetryCount && customRetryCondition;
526
660
  if (ctx.error.name !== "HTTPError") {
527
661
  return baseRetryCondition;
528
662
  }
529
663
  const includesMethod = (
530
664
  // eslint-disable-next-line no-implicit-coercion -- Boolean doesn't narrow
531
- !!ctx.request.method && ctx.options.retryMethods?.includes(ctx.request.method)
665
+ !!ctx.request.method && options.retryMethods?.includes(ctx.request.method)
532
666
  );
533
667
  const includesCodes = (
534
668
  // eslint-disable-next-line no-implicit-coercion -- Boolean doesn't narrow
535
- !!ctx.response?.status && ctx.options.retryStatusCodes?.includes(ctx.response.status)
669
+ !!ctx.response?.status && options.retryStatusCodes?.includes(ctx.response.status)
536
670
  );
537
671
  return includesCodes && includesMethod && baseRetryCondition;
538
672
  };
673
+ const executeRetryHook = async (shouldThrowOnError) => {
674
+ try {
675
+ return await executeHooks(options.onRetry?.(ctx));
676
+ } catch (error) {
677
+ const { apiDetails } = resolveErrorResult({
678
+ cloneResponse: options.cloneResponse,
679
+ defaultErrorMessage: options.defaultErrorMessage,
680
+ error,
681
+ resultMode: options.resultMode
682
+ });
683
+ if (shouldThrowOnError) {
684
+ throw error;
685
+ }
686
+ return apiDetails;
687
+ }
688
+ };
539
689
  return {
690
+ executeRetryHook,
540
691
  getDelay,
541
692
  shouldAttemptRetry
542
693
  };
@@ -675,7 +826,7 @@ var createFetchClient = (baseConfig = {}) => {
675
826
  const { handleRequestCancelStrategy, handleRequestDeferStrategy, removeDedupeKeyFromCache } = await createDedupeStrategy({ $RequestInfoCache, newFetchController, options, request });
676
827
  await handleRequestCancelStrategy();
677
828
  try {
678
- await executeHooks(options.onRequest({ options, request }));
829
+ await executeHooks(options.onRequest?.({ options, request }));
679
830
  request.headers = mergeAndResolveHeaders({
680
831
  auth: options.auth,
681
832
  body: request.body,
@@ -711,11 +862,11 @@ var createFetchClient = (baseConfig = {}) => {
711
862
  data: validSuccessData,
712
863
  options,
713
864
  request,
714
- response: options.cloneResponse ? response.clone() : response
865
+ response
715
866
  };
716
867
  await executeHooks(
717
- options.onSuccess(successContext),
718
- options.onResponse({ ...successContext, error: null })
868
+ options.onSuccess?.(successContext),
869
+ options.onResponse?.({ ...successContext, error: null })
719
870
  );
720
871
  return await resolveSuccessResult({
721
872
  data: successContext.data,
@@ -736,26 +887,11 @@ var createFetchClient = (baseConfig = {}) => {
736
887
  response: apiDetails.response
737
888
  };
738
889
  const shouldThrowOnError = isFunction(options.throwOnError) ? options.throwOnError(errorContext) : options.throwOnError;
739
- const handleThrowOnError = (errorObject) => {
740
- if (!shouldThrowOnError) return;
741
- throw errorObject;
742
- };
743
- const handleRetryAndGetResult = async (customInfo = {}) => {
744
- const { getDelay, shouldAttemptRetry } = createRetryStrategy(errorContext);
890
+ const handleRetryOrGetResult = async (customInfo) => {
891
+ const { executeRetryHook, getDelay, shouldAttemptRetry } = createRetryStrategy(errorContext);
745
892
  const shouldRetry = !combinedSignal.aborted && await shouldAttemptRetry();
746
893
  if (shouldRetry) {
747
- try {
748
- await executeHooks(options.onRetry(errorContext));
749
- } catch (innerError) {
750
- const { apiDetails: innerApiDetails, getErrorResult: getInnerErrorResult } = resolveErrorResult({
751
- cloneResponse: options.cloneResponse,
752
- defaultErrorMessage: options.defaultErrorMessage,
753
- error: innerError,
754
- resultMode: options.resultMode
755
- });
756
- handleThrowOnError(innerApiDetails.error);
757
- return getInnerErrorResult();
758
- }
894
+ await executeRetryHook(shouldThrowOnError);
759
895
  const delay = getDelay();
760
896
  await waitUntil(delay);
761
897
  const updatedOptions = {
@@ -764,34 +900,36 @@ var createFetchClient = (baseConfig = {}) => {
764
900
  };
765
901
  return callApi2(initURL, updatedOptions);
766
902
  }
767
- handleThrowOnError(apiDetails.error);
768
- return customInfo.message ? getErrorResult(customInfo) : getErrorResult();
903
+ if (shouldThrowOnError) {
904
+ throw error;
905
+ }
906
+ return customInfo ? getErrorResult(customInfo) : getErrorResult();
769
907
  };
770
908
  if (isHTTPErrorInstance(error)) {
771
909
  await executeHooks(
772
- options.onResponseError(errorContext),
773
- options.onError(errorContext),
774
- options.onResponse({ ...errorContext, data: null })
910
+ options.onResponseError?.(errorContext),
911
+ options.onError?.(errorContext),
912
+ options.onResponse?.({ ...errorContext, data: null })
775
913
  );
776
- return await handleRetryAndGetResult();
914
+ return await handleRetryOrGetResult();
777
915
  }
778
916
  if (error instanceof DOMException && error.name === "AbortError") {
779
917
  const { message, name } = error;
780
- console.error(`${name}:`, message);
781
- return await handleRetryAndGetResult();
918
+ !shouldThrowOnError && console.error(`${name}:`, message);
919
+ return await handleRetryOrGetResult();
782
920
  }
783
921
  if (error instanceof DOMException && error.name === "TimeoutError") {
784
922
  const message = `Request timed out after ${options.timeout}ms`;
785
- console.error(`${error.name}:`, message);
786
- return await handleRetryAndGetResult({ message });
923
+ !shouldThrowOnError && console.error(`${error.name}:`, message);
924
+ return await handleRetryOrGetResult({ message });
787
925
  }
788
926
  await executeHooks(
789
927
  // == At this point only the request errors exist, so the request error interceptor is called
790
- options.onRequestError(errorContext),
928
+ options.onRequestError?.(errorContext),
791
929
  // == Also call the onError interceptor
792
- options.onError(errorContext)
930
+ options.onError?.(errorContext)
793
931
  );
794
- return await handleRetryAndGetResult();
932
+ return await handleRetryOrGetResult();
795
933
  } finally {
796
934
  removeDedupeKeyFromCache();
797
935
  }