react-fetch-utils 1.0.0 → 1.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/Readme.md CHANGED
@@ -18,11 +18,15 @@ npm install react-fetch-utils
18
18
  ```ts
19
19
  import {
20
20
  FetchPromise,
21
+ createFetchClient,
21
22
  useQueries,
22
23
  useRequest,
23
24
  statusEnum,
24
25
  type CancellablePromise,
26
+ type FetchClientDefaults,
27
+ type FetchPromiseError,
25
28
  type FetchPromiseParams,
29
+ type FetchRequestConfig,
26
30
  type Status,
27
31
  } from "react-fetch-utils";
28
32
  ```
@@ -30,11 +34,15 @@ import {
30
34
  ## Exports
31
35
 
32
36
  - `FetchPromise`
37
+ - `createFetchClient`
33
38
  - `useQueries`
34
39
  - `useRequest`
35
40
  - `statusEnum`
36
41
  - `CancellablePromise` (type)
42
+ - `FetchClientDefaults` (type)
43
+ - `FetchPromiseError` (type)
37
44
  - `FetchPromiseParams` (type)
45
+ - `FetchRequestConfig` (type)
38
46
  - `Status` (type)
39
47
 
40
48
  ## Quick Start
@@ -86,22 +94,49 @@ function FetchPromise<T = unknown>(
86
94
  type FetchPromiseParams = {
87
95
  url: string;
88
96
  method: string;
89
- body?: object | null;
97
+ body?: unknown;
90
98
  respType?: "raw" | "json" | null;
99
+ headers?: HeadersInit | Record<string, string>;
100
+ timeoutMs?: number;
101
+ baseUrl?: string;
102
+ includeContentType?: boolean;
103
+ parseAs?: "json" | "raw" | "text" | "response";
104
+ onRequest?: (
105
+ request: FetchRequestConfig,
106
+ ) => FetchRequestConfig | void | Promise<FetchRequestConfig | void>;
107
+ validateStatus?: (status: number, response: Response) => boolean;
108
+ getAuthToken?: () => string | null | undefined | Promise<string | null | undefined>;
109
+ allowBodyForGetHead?: boolean;
91
110
  };
92
111
  ```
93
112
 
94
113
  Behavior:
95
114
 
96
- - Sends JSON body with `JSON.stringify(params.body)`.
97
- - Sends headers:
98
- - `Accept: application/json` (or `blob` when `respType: "raw"`)
99
- - `Content-Type: application/json`
100
- - Resolves with `response.json()` by default.
101
- - Resolves with `response.blob()` when `respType: "raw"`.
102
- - Rejects `401` with `{ reason: "Unauthorized", details: Response }`.
103
- - Rejects all other failures as `{ reason: "Unknown", details: error }`.
104
- - Returns a promise with `.cancel()` that aborts the underlying request.
115
+ - Backward compatible with existing `respType: "json" | "raw"`.
116
+ - New parsing modes via `parseAs`:
117
+ - `json` (default)
118
+ - `raw` (`Blob`)
119
+ - `text` (`string`)
120
+ - `response` (`Response`)
121
+ - Supports request timeout with `timeoutMs` using `AbortController`.
122
+ - Supports base URL joining with `baseUrl`.
123
+ - Supports request customization with `headers`, `onRequest`, `validateStatus`, and `getAuthToken`.
124
+ - `Accept` is set automatically only when caller did not provide it.
125
+ - `Content-Type: application/json` is added only when:
126
+ - there is a body,
127
+ - method is `POST`/`PUT`/`PATCH`,
128
+ - `includeContentType !== false`,
129
+ - caller did not already provide `Content-Type`.
130
+ - Request body behavior:
131
+ - `GET`/`HEAD` bodies are not sent by default.
132
+ - use `allowBodyForGetHead: true` to allow body on `GET`/`HEAD`.
133
+ - non-JSON bodies (`FormData`, `Blob`, `string`, `URLSearchParams`, etc.) are passed through unchanged.
134
+ - plain objects/values are JSON-stringified when body is allowed.
135
+ - Error model:
136
+ - `401` rejects with `{ reason: "Unauthorized", details: Response }`
137
+ - timeout abort rejects with `{ reason: "Timeout", details: ... }`
138
+ - other failures reject with `{ reason: "Unknown", details: ... }`
139
+ - Returned promise remains cancellable with `.cancel()` and is safe to call repeatedly.
105
140
 
106
141
  Example (JSON):
107
142
 
@@ -112,9 +147,41 @@ type User = { id: number; name: string };
112
147
 
113
148
  export function getUser() {
114
149
  return FetchPromise<User>({
115
- url: "/api/user",
150
+ url: "/user",
116
151
  method: "POST",
152
+ baseUrl: "https://api.example.com/v1",
117
153
  body: { includeDetails: true },
154
+ timeoutMs: 8000,
155
+ getAuthToken: () => localStorage.getItem("token"),
156
+ });
157
+ }
158
+ ```
159
+
160
+ Example (`text` parse mode + custom status validation):
161
+
162
+ ```ts
163
+ import { FetchPromise } from "react-fetch-utils";
164
+
165
+ export function getHealth() {
166
+ return FetchPromise<string>({
167
+ url: "/health",
168
+ method: "GET",
169
+ parseAs: "text",
170
+ validateStatus: status => status >= 200 && status < 500,
171
+ });
172
+ }
173
+ ```
174
+
175
+ Example (full `Response` mode):
176
+
177
+ ```ts
178
+ import { FetchPromise } from "react-fetch-utils";
179
+
180
+ export function getRawResponse() {
181
+ return FetchPromise<Response>({
182
+ url: "/diagnostics",
183
+ method: "GET",
184
+ parseAs: "response",
118
185
  });
119
186
  }
120
187
  ```
@@ -134,6 +201,64 @@ const download = FetchPromise<Blob>({
134
201
  download.cancel();
135
202
  ```
136
203
 
204
+ ### `createFetchClient`
205
+
206
+ Creates a reusable request function with merged defaults so app code does not need a custom API wrapper utility.
207
+
208
+ Signature:
209
+
210
+ ```ts
211
+ function createFetchClient(
212
+ defaults?: FetchClientDefaults,
213
+ ): <T = unknown>(params: FetchPromiseParams) => CancellablePromise<T>;
214
+ ```
215
+
216
+ Example (replace a custom `APIRequest` utility):
217
+
218
+ ```ts
219
+ import { createFetchClient } from "react-fetch-utils";
220
+
221
+ export const apiRequest = createFetchClient({
222
+ baseUrl: "https://api.example.com/v1",
223
+ timeoutMs: 10000,
224
+ headers: {
225
+ "X-App-Client": "web",
226
+ },
227
+ getAuthToken: async () => localStorage.getItem("access_token"),
228
+ validateStatus: status => status >= 200 && status < 300,
229
+ onRequest: request => {
230
+ request.headers.set("X-Trace-Id", crypto.randomUUID());
231
+ },
232
+ });
233
+
234
+ export const getProfile = () =>
235
+ apiRequest<{ id: string; name: string }>({
236
+ url: "/profile",
237
+ method: "GET",
238
+ parseAs: "json",
239
+ });
240
+
241
+ export const downloadReport = () =>
242
+ apiRequest<Blob>({
243
+ url: "/reports/monthly",
244
+ method: "GET",
245
+ parseAs: "raw",
246
+ });
247
+ ```
248
+
249
+ Example (`FormData` upload without forced JSON content-type):
250
+
251
+ ```ts
252
+ const formData = new FormData();
253
+ formData.append("file", file);
254
+
255
+ apiRequest({
256
+ url: "/upload",
257
+ method: "POST",
258
+ body: formData,
259
+ });
260
+ ```
261
+
137
262
  ### `useQueries`
138
263
 
139
264
  Tracks active cancellable promises and provides list management helpers.
@@ -295,12 +420,44 @@ const request: FetchPromiseParams = {
295
420
  url: "/api/items",
296
421
  method: "POST",
297
422
  body: { page: 1 },
298
- respType: "json",
423
+ headers: { Authorization: "Bearer token" },
424
+ timeoutMs: 5000,
425
+ parseAs: "json",
299
426
  };
300
427
 
301
428
  FetchPromise(request);
302
429
  ```
303
430
 
431
+ ### `FetchPromiseError` (type)
432
+
433
+ ```ts
434
+ type FetchPromiseError = {
435
+ reason: "Unauthorized" | "Timeout" | "Unknown";
436
+ details: unknown;
437
+ status?: number;
438
+ response?: Response;
439
+ originalError?: unknown;
440
+ };
441
+ ```
442
+
443
+ ### `FetchRequestConfig` (type)
444
+
445
+ ```ts
446
+ type FetchRequestConfig = {
447
+ url: string;
448
+ init: RequestInit;
449
+ headers: Headers;
450
+ };
451
+ ```
452
+
453
+ ### `FetchClientDefaults` (type)
454
+
455
+ ```ts
456
+ type FetchClientDefaults = Omit<FetchPromiseParams, "url" | "method"> & {
457
+ method?: string;
458
+ };
459
+ ```
460
+
304
461
  ### `Status` (type)
305
462
 
306
463
  ```ts
@@ -320,6 +477,7 @@ console.log(statusEnum[status]); // "fetching"
320
477
 
321
478
  - `useRequest` is built on top of `useQueries`.
322
479
  - For errors, attach `.catch(...)` where you create and consume `CancellablePromise` requests.
480
+ - `respType` is still supported for backward compatibility; prefer `parseAs` in new code.
323
481
 
324
482
  ## License
325
483
 
package/dist/index.d.mts CHANGED
@@ -1,9 +1,33 @@
1
1
  interface FetchPromiseParams {
2
2
  url: string;
3
3
  method: string;
4
- body?: object | null;
4
+ body?: unknown;
5
5
  respType?: "raw" | "json" | null;
6
+ headers?: HeadersInit | Record<string, string>;
7
+ timeoutMs?: number;
8
+ baseUrl?: string;
9
+ includeContentType?: boolean;
10
+ parseAs?: "json" | "raw" | "text" | "response";
11
+ onRequest?: (request: FetchRequestConfig) => FetchRequestConfig | void | Promise<FetchRequestConfig | void>;
12
+ validateStatus?: (status: number, response: Response) => boolean;
13
+ getAuthToken?: () => string | null | undefined | Promise<string | null | undefined>;
14
+ allowBodyForGetHead?: boolean;
6
15
  }
16
+ interface FetchRequestConfig {
17
+ url: string;
18
+ init: RequestInit;
19
+ headers: Headers;
20
+ }
21
+ interface FetchPromiseError {
22
+ reason: "Unauthorized" | "Timeout" | "Unknown";
23
+ details: unknown;
24
+ status?: number;
25
+ response?: Response;
26
+ originalError?: unknown;
27
+ }
28
+ type FetchClientDefaults = Omit<FetchPromiseParams, "url" | "method"> & {
29
+ method?: string;
30
+ };
7
31
  interface CancellablePromise<T = unknown> extends Promise<T> {
8
32
  cancel: () => void;
9
33
  }
@@ -12,6 +36,7 @@ interface CancellablePromise<T = unknown> extends Promise<T> {
12
36
  * @returns A promise with a `.cancel()` method that calls `AbortController.abort()`
13
37
  */
14
38
  declare const FetchPromise: <T = unknown>(params: FetchPromiseParams) => CancellablePromise<T>;
39
+ declare const createFetchClient: (defaults?: FetchClientDefaults) => <T = unknown>(params: FetchPromiseParams) => CancellablePromise<T>;
15
40
 
16
41
  declare function useQueries(): {
17
42
  list: CancellablePromise<unknown>[];
@@ -42,4 +67,4 @@ declare const useRequest: <T = unknown>(fetchPromise: (() => CancellablePromise<
42
67
  response: T | null;
43
68
  };
44
69
 
45
- export { type CancellablePromise, FetchPromise, type FetchPromiseParams, type Status, statusEnum, useQueries, useRequest };
70
+ export { type CancellablePromise, type FetchClientDefaults, FetchPromise, type FetchPromiseError, type FetchPromiseParams, type FetchRequestConfig, type Status, createFetchClient, statusEnum, useQueries, useRequest };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,33 @@
1
1
  interface FetchPromiseParams {
2
2
  url: string;
3
3
  method: string;
4
- body?: object | null;
4
+ body?: unknown;
5
5
  respType?: "raw" | "json" | null;
6
+ headers?: HeadersInit | Record<string, string>;
7
+ timeoutMs?: number;
8
+ baseUrl?: string;
9
+ includeContentType?: boolean;
10
+ parseAs?: "json" | "raw" | "text" | "response";
11
+ onRequest?: (request: FetchRequestConfig) => FetchRequestConfig | void | Promise<FetchRequestConfig | void>;
12
+ validateStatus?: (status: number, response: Response) => boolean;
13
+ getAuthToken?: () => string | null | undefined | Promise<string | null | undefined>;
14
+ allowBodyForGetHead?: boolean;
6
15
  }
16
+ interface FetchRequestConfig {
17
+ url: string;
18
+ init: RequestInit;
19
+ headers: Headers;
20
+ }
21
+ interface FetchPromiseError {
22
+ reason: "Unauthorized" | "Timeout" | "Unknown";
23
+ details: unknown;
24
+ status?: number;
25
+ response?: Response;
26
+ originalError?: unknown;
27
+ }
28
+ type FetchClientDefaults = Omit<FetchPromiseParams, "url" | "method"> & {
29
+ method?: string;
30
+ };
7
31
  interface CancellablePromise<T = unknown> extends Promise<T> {
8
32
  cancel: () => void;
9
33
  }
@@ -12,6 +36,7 @@ interface CancellablePromise<T = unknown> extends Promise<T> {
12
36
  * @returns A promise with a `.cancel()` method that calls `AbortController.abort()`
13
37
  */
14
38
  declare const FetchPromise: <T = unknown>(params: FetchPromiseParams) => CancellablePromise<T>;
39
+ declare const createFetchClient: (defaults?: FetchClientDefaults) => <T = unknown>(params: FetchPromiseParams) => CancellablePromise<T>;
15
40
 
16
41
  declare function useQueries(): {
17
42
  list: CancellablePromise<unknown>[];
@@ -42,4 +67,4 @@ declare const useRequest: <T = unknown>(fetchPromise: (() => CancellablePromise<
42
67
  response: T | null;
43
68
  };
44
69
 
45
- export { type CancellablePromise, FetchPromise, type FetchPromiseParams, type Status, statusEnum, useQueries, useRequest };
70
+ export { type CancellablePromise, type FetchClientDefaults, FetchPromise, type FetchPromiseError, type FetchPromiseParams, type FetchRequestConfig, type Status, createFetchClient, statusEnum, useQueries, useRequest };
package/dist/index.js CHANGED
@@ -1,8 +1,25 @@
1
1
  "use strict";
2
2
  var __defProp = Object.defineProperty;
3
+ var __defProps = Object.defineProperties;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
10
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
11
+ var __spreadValues = (a, b) => {
12
+ for (var prop in b || (b = {}))
13
+ if (__hasOwnProp.call(b, prop))
14
+ __defNormalProp(a, prop, b[prop]);
15
+ if (__getOwnPropSymbols)
16
+ for (var prop of __getOwnPropSymbols(b)) {
17
+ if (__propIsEnum.call(b, prop))
18
+ __defNormalProp(a, prop, b[prop]);
19
+ }
20
+ return a;
21
+ };
22
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
6
23
  var __export = (target, all) => {
7
24
  for (var name in all)
8
25
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -21,6 +38,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
38
  var index_exports = {};
22
39
  __export(index_exports, {
23
40
  FetchPromise: () => FetchPromise_default,
41
+ createFetchClient: () => createFetchClient,
24
42
  statusEnum: () => statusEnum,
25
43
  useQueries: () => useQueries,
26
44
  useRequest: () => useRequest
@@ -28,39 +46,221 @@ __export(index_exports, {
28
46
  module.exports = __toCommonJS(index_exports);
29
47
 
30
48
  // src/FetchPromise.ts
49
+ var METHODS_WITH_DEFAULT_BODY = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
50
+ var METHODS_WITHOUT_BODY = /* @__PURE__ */ new Set(["GET", "HEAD"]);
51
+ var isBodyInitLike = (value) => {
52
+ if (typeof value === "string") return true;
53
+ if (typeof Blob !== "undefined" && value instanceof Blob) return true;
54
+ if (typeof FormData !== "undefined" && value instanceof FormData) return true;
55
+ if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) return true;
56
+ if (typeof ReadableStream !== "undefined" && value instanceof ReadableStream) return true;
57
+ if (typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer) return true;
58
+ if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(value)) return true;
59
+ return false;
60
+ };
61
+ var mapParseMode = (parseAs, respType) => {
62
+ if (parseAs) return parseAs;
63
+ if (respType === "raw") return "raw";
64
+ return "json";
65
+ };
66
+ var mergeHeaders = (defaults, overrides) => {
67
+ const headers = new Headers(defaults != null ? defaults : {});
68
+ if (overrides) {
69
+ new Headers(overrides).forEach((value, key) => {
70
+ headers.set(key, value);
71
+ });
72
+ }
73
+ return headers;
74
+ };
75
+ var resolveUrl = (url, baseUrl) => {
76
+ if (!baseUrl) return url;
77
+ if (/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url) || url.startsWith("//")) {
78
+ return url;
79
+ }
80
+ const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
81
+ const normalizedPath = url.startsWith("/") ? url : `/${url}`;
82
+ return `${normalizedBase}${normalizedPath}`;
83
+ };
84
+ var parseJsonSafely = async (response) => {
85
+ if (response.status === 204 || response.status === 205) {
86
+ return void 0;
87
+ }
88
+ const text = await response.text();
89
+ if (!text.trim()) {
90
+ return void 0;
91
+ }
92
+ return JSON.parse(text);
93
+ };
94
+ var normalizeErrorDetails = (error) => {
95
+ if (error instanceof Error) {
96
+ return {
97
+ name: error.name,
98
+ message: error.message,
99
+ stack: error.stack
100
+ };
101
+ }
102
+ return error;
103
+ };
31
104
  var FetchPromise = (params) => {
32
105
  const controller = new AbortController();
33
106
  const signal = controller.signal;
107
+ let timeoutId;
108
+ let timedOut = false;
109
+ let cancelled = false;
34
110
  const promise = new Promise(async function(resolve, reject) {
35
- const headers = {
36
- Accept: params.respType === "raw" ? "blob" : "application/json",
37
- "Content-Type": "application/json"
38
- };
39
- await fetch(params.url, {
40
- method: params.method,
41
- signal,
42
- headers,
43
- body: JSON.stringify(params.body)
44
- }).then((response) => {
111
+ try {
112
+ const method = params.method.toUpperCase();
113
+ const parseAs = mapParseMode(params.parseAs, params.respType);
114
+ const headers = mergeHeaders(void 0, params.headers);
115
+ if (!headers.has("Accept")) {
116
+ if (parseAs === "raw") {
117
+ headers.set("Accept", "blob");
118
+ } else if (parseAs === "json") {
119
+ headers.set("Accept", "application/json");
120
+ } else {
121
+ headers.set("Accept", "*/*");
122
+ }
123
+ }
124
+ if (!headers.has("Authorization") && params.getAuthToken) {
125
+ const token = await params.getAuthToken();
126
+ if (token) {
127
+ headers.set("Authorization", `Bearer ${token}`);
128
+ }
129
+ }
130
+ const hasBodyValue = params.body !== void 0 && params.body !== null;
131
+ const allowsBody = !METHODS_WITHOUT_BODY.has(method) || params.allowBodyForGetHead === true;
132
+ const shouldSendBody = hasBodyValue && allowsBody;
133
+ let body;
134
+ let isJsonBody = false;
135
+ if (shouldSendBody) {
136
+ if (isBodyInitLike(params.body)) {
137
+ body = params.body;
138
+ } else {
139
+ body = JSON.stringify(params.body);
140
+ isJsonBody = true;
141
+ }
142
+ }
143
+ if (shouldSendBody && isJsonBody && params.includeContentType !== false && !headers.has("Content-Type") && METHODS_WITH_DEFAULT_BODY.has(method)) {
144
+ headers.set("Content-Type", "application/json");
145
+ }
146
+ const requestConfig = {
147
+ url: resolveUrl(params.url, params.baseUrl),
148
+ init: {
149
+ method,
150
+ signal,
151
+ headers,
152
+ body
153
+ },
154
+ headers
155
+ };
156
+ if (params.onRequest) {
157
+ const maybeUpdatedConfig = await params.onRequest(requestConfig);
158
+ if (maybeUpdatedConfig) {
159
+ requestConfig.url = maybeUpdatedConfig.url;
160
+ requestConfig.init = maybeUpdatedConfig.init;
161
+ requestConfig.headers = maybeUpdatedConfig.headers;
162
+ }
163
+ }
164
+ if (typeof params.timeoutMs === "number" && params.timeoutMs > 0) {
165
+ timeoutId = setTimeout(() => {
166
+ timedOut = true;
167
+ controller.abort();
168
+ }, params.timeoutMs);
169
+ }
170
+ const response = await fetch(requestConfig.url, __spreadProps(__spreadValues({}, requestConfig.init), {
171
+ signal,
172
+ headers: requestConfig.headers
173
+ }));
45
174
  if (response.status === 401) {
46
- reject({ reason: "Unauthorized", details: response });
175
+ reject({ reason: "Unauthorized", details: response, status: 401, response });
176
+ return;
47
177
  }
48
- if (!response.ok) {
49
- throw new Error(response.statusText);
178
+ const isValid = params.validateStatus ? params.validateStatus(response.status, response) : response.ok;
179
+ if (!isValid) {
180
+ reject({
181
+ reason: "Unknown",
182
+ details: {
183
+ status: response.status,
184
+ statusText: response.statusText,
185
+ response
186
+ },
187
+ status: response.status,
188
+ response
189
+ });
190
+ return;
50
191
  }
51
- if (params.respType === "raw") {
52
- return response.blob();
192
+ if (parseAs === "response") {
193
+ resolve(response);
194
+ return;
53
195
  }
54
- return response.json();
55
- }).then((data) => {
56
- if (data !== void 0) resolve(data);
57
- }).catch((error) => {
58
- reject({ reason: "Unknown", details: error });
59
- });
196
+ if (parseAs === "raw") {
197
+ resolve(await response.blob());
198
+ return;
199
+ }
200
+ if (parseAs === "text") {
201
+ resolve(await response.text());
202
+ return;
203
+ }
204
+ resolve(await parseJsonSafely(response));
205
+ } catch (error) {
206
+ if (timedOut && (error == null ? void 0 : error.name) === "AbortError") {
207
+ reject({
208
+ reason: "Timeout",
209
+ details: {
210
+ timeoutMs: params.timeoutMs,
211
+ abortError: normalizeErrorDetails(error)
212
+ },
213
+ originalError: error
214
+ });
215
+ return;
216
+ }
217
+ reject({
218
+ reason: "Unknown",
219
+ details: normalizeErrorDetails(error),
220
+ originalError: error
221
+ });
222
+ } finally {
223
+ if (timeoutId) {
224
+ clearTimeout(timeoutId);
225
+ }
226
+ }
60
227
  });
61
- promise.cancel = () => controller.abort();
228
+ promise.cancel = () => {
229
+ if (cancelled) return;
230
+ cancelled = true;
231
+ controller.abort();
232
+ };
62
233
  return promise;
63
234
  };
235
+ var createFetchClient = (defaults = {}) => {
236
+ return (params) => {
237
+ var _a, _b, _c, _d;
238
+ const runDefaultOnRequest = defaults.onRequest;
239
+ const runParamOnRequest = params.onRequest;
240
+ return FetchPromise(__spreadProps(__spreadValues(__spreadValues({}, defaults), params), {
241
+ method: (_b = (_a = params.method) != null ? _a : defaults.method) != null ? _b : "GET",
242
+ headers: mergeHeaders(defaults.headers, params.headers),
243
+ onRequest: async (request) => {
244
+ let nextRequest = request;
245
+ if (runDefaultOnRequest) {
246
+ const maybeDefaultUpdated = await runDefaultOnRequest(nextRequest);
247
+ if (maybeDefaultUpdated) {
248
+ nextRequest = maybeDefaultUpdated;
249
+ }
250
+ }
251
+ if (runParamOnRequest) {
252
+ const maybeParamUpdated = await runParamOnRequest(nextRequest);
253
+ if (maybeParamUpdated) {
254
+ nextRequest = maybeParamUpdated;
255
+ }
256
+ }
257
+ return nextRequest;
258
+ },
259
+ validateStatus: (_c = params.validateStatus) != null ? _c : defaults.validateStatus,
260
+ getAuthToken: (_d = params.getAuthToken) != null ? _d : defaults.getAuthToken
261
+ }));
262
+ };
263
+ };
64
264
  var FetchPromise_default = FetchPromise;
65
265
 
66
266
  // src/useQueries.ts
@@ -121,6 +321,7 @@ var useRequest = (fetchPromise, disableCache = false) => {
121
321
  // Annotate the CommonJS export names for ESM import in node:
122
322
  0 && (module.exports = {
123
323
  FetchPromise,
324
+ createFetchClient,
124
325
  statusEnum,
125
326
  useQueries,
126
327
  useRequest
package/dist/index.mjs CHANGED
@@ -1,37 +1,239 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __spreadValues = (a, b) => {
9
+ for (var prop in b || (b = {}))
10
+ if (__hasOwnProp.call(b, prop))
11
+ __defNormalProp(a, prop, b[prop]);
12
+ if (__getOwnPropSymbols)
13
+ for (var prop of __getOwnPropSymbols(b)) {
14
+ if (__propIsEnum.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ }
17
+ return a;
18
+ };
19
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
+
1
21
  // src/FetchPromise.ts
22
+ var METHODS_WITH_DEFAULT_BODY = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
23
+ var METHODS_WITHOUT_BODY = /* @__PURE__ */ new Set(["GET", "HEAD"]);
24
+ var isBodyInitLike = (value) => {
25
+ if (typeof value === "string") return true;
26
+ if (typeof Blob !== "undefined" && value instanceof Blob) return true;
27
+ if (typeof FormData !== "undefined" && value instanceof FormData) return true;
28
+ if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) return true;
29
+ if (typeof ReadableStream !== "undefined" && value instanceof ReadableStream) return true;
30
+ if (typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer) return true;
31
+ if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(value)) return true;
32
+ return false;
33
+ };
34
+ var mapParseMode = (parseAs, respType) => {
35
+ if (parseAs) return parseAs;
36
+ if (respType === "raw") return "raw";
37
+ return "json";
38
+ };
39
+ var mergeHeaders = (defaults, overrides) => {
40
+ const headers = new Headers(defaults != null ? defaults : {});
41
+ if (overrides) {
42
+ new Headers(overrides).forEach((value, key) => {
43
+ headers.set(key, value);
44
+ });
45
+ }
46
+ return headers;
47
+ };
48
+ var resolveUrl = (url, baseUrl) => {
49
+ if (!baseUrl) return url;
50
+ if (/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url) || url.startsWith("//")) {
51
+ return url;
52
+ }
53
+ const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
54
+ const normalizedPath = url.startsWith("/") ? url : `/${url}`;
55
+ return `${normalizedBase}${normalizedPath}`;
56
+ };
57
+ var parseJsonSafely = async (response) => {
58
+ if (response.status === 204 || response.status === 205) {
59
+ return void 0;
60
+ }
61
+ const text = await response.text();
62
+ if (!text.trim()) {
63
+ return void 0;
64
+ }
65
+ return JSON.parse(text);
66
+ };
67
+ var normalizeErrorDetails = (error) => {
68
+ if (error instanceof Error) {
69
+ return {
70
+ name: error.name,
71
+ message: error.message,
72
+ stack: error.stack
73
+ };
74
+ }
75
+ return error;
76
+ };
2
77
  var FetchPromise = (params) => {
3
78
  const controller = new AbortController();
4
79
  const signal = controller.signal;
80
+ let timeoutId;
81
+ let timedOut = false;
82
+ let cancelled = false;
5
83
  const promise = new Promise(async function(resolve, reject) {
6
- const headers = {
7
- Accept: params.respType === "raw" ? "blob" : "application/json",
8
- "Content-Type": "application/json"
9
- };
10
- await fetch(params.url, {
11
- method: params.method,
12
- signal,
13
- headers,
14
- body: JSON.stringify(params.body)
15
- }).then((response) => {
84
+ try {
85
+ const method = params.method.toUpperCase();
86
+ const parseAs = mapParseMode(params.parseAs, params.respType);
87
+ const headers = mergeHeaders(void 0, params.headers);
88
+ if (!headers.has("Accept")) {
89
+ if (parseAs === "raw") {
90
+ headers.set("Accept", "blob");
91
+ } else if (parseAs === "json") {
92
+ headers.set("Accept", "application/json");
93
+ } else {
94
+ headers.set("Accept", "*/*");
95
+ }
96
+ }
97
+ if (!headers.has("Authorization") && params.getAuthToken) {
98
+ const token = await params.getAuthToken();
99
+ if (token) {
100
+ headers.set("Authorization", `Bearer ${token}`);
101
+ }
102
+ }
103
+ const hasBodyValue = params.body !== void 0 && params.body !== null;
104
+ const allowsBody = !METHODS_WITHOUT_BODY.has(method) || params.allowBodyForGetHead === true;
105
+ const shouldSendBody = hasBodyValue && allowsBody;
106
+ let body;
107
+ let isJsonBody = false;
108
+ if (shouldSendBody) {
109
+ if (isBodyInitLike(params.body)) {
110
+ body = params.body;
111
+ } else {
112
+ body = JSON.stringify(params.body);
113
+ isJsonBody = true;
114
+ }
115
+ }
116
+ if (shouldSendBody && isJsonBody && params.includeContentType !== false && !headers.has("Content-Type") && METHODS_WITH_DEFAULT_BODY.has(method)) {
117
+ headers.set("Content-Type", "application/json");
118
+ }
119
+ const requestConfig = {
120
+ url: resolveUrl(params.url, params.baseUrl),
121
+ init: {
122
+ method,
123
+ signal,
124
+ headers,
125
+ body
126
+ },
127
+ headers
128
+ };
129
+ if (params.onRequest) {
130
+ const maybeUpdatedConfig = await params.onRequest(requestConfig);
131
+ if (maybeUpdatedConfig) {
132
+ requestConfig.url = maybeUpdatedConfig.url;
133
+ requestConfig.init = maybeUpdatedConfig.init;
134
+ requestConfig.headers = maybeUpdatedConfig.headers;
135
+ }
136
+ }
137
+ if (typeof params.timeoutMs === "number" && params.timeoutMs > 0) {
138
+ timeoutId = setTimeout(() => {
139
+ timedOut = true;
140
+ controller.abort();
141
+ }, params.timeoutMs);
142
+ }
143
+ const response = await fetch(requestConfig.url, __spreadProps(__spreadValues({}, requestConfig.init), {
144
+ signal,
145
+ headers: requestConfig.headers
146
+ }));
16
147
  if (response.status === 401) {
17
- reject({ reason: "Unauthorized", details: response });
148
+ reject({ reason: "Unauthorized", details: response, status: 401, response });
149
+ return;
18
150
  }
19
- if (!response.ok) {
20
- throw new Error(response.statusText);
151
+ const isValid = params.validateStatus ? params.validateStatus(response.status, response) : response.ok;
152
+ if (!isValid) {
153
+ reject({
154
+ reason: "Unknown",
155
+ details: {
156
+ status: response.status,
157
+ statusText: response.statusText,
158
+ response
159
+ },
160
+ status: response.status,
161
+ response
162
+ });
163
+ return;
21
164
  }
22
- if (params.respType === "raw") {
23
- return response.blob();
165
+ if (parseAs === "response") {
166
+ resolve(response);
167
+ return;
24
168
  }
25
- return response.json();
26
- }).then((data) => {
27
- if (data !== void 0) resolve(data);
28
- }).catch((error) => {
29
- reject({ reason: "Unknown", details: error });
30
- });
169
+ if (parseAs === "raw") {
170
+ resolve(await response.blob());
171
+ return;
172
+ }
173
+ if (parseAs === "text") {
174
+ resolve(await response.text());
175
+ return;
176
+ }
177
+ resolve(await parseJsonSafely(response));
178
+ } catch (error) {
179
+ if (timedOut && (error == null ? void 0 : error.name) === "AbortError") {
180
+ reject({
181
+ reason: "Timeout",
182
+ details: {
183
+ timeoutMs: params.timeoutMs,
184
+ abortError: normalizeErrorDetails(error)
185
+ },
186
+ originalError: error
187
+ });
188
+ return;
189
+ }
190
+ reject({
191
+ reason: "Unknown",
192
+ details: normalizeErrorDetails(error),
193
+ originalError: error
194
+ });
195
+ } finally {
196
+ if (timeoutId) {
197
+ clearTimeout(timeoutId);
198
+ }
199
+ }
31
200
  });
32
- promise.cancel = () => controller.abort();
201
+ promise.cancel = () => {
202
+ if (cancelled) return;
203
+ cancelled = true;
204
+ controller.abort();
205
+ };
33
206
  return promise;
34
207
  };
208
+ var createFetchClient = (defaults = {}) => {
209
+ return (params) => {
210
+ var _a, _b, _c, _d;
211
+ const runDefaultOnRequest = defaults.onRequest;
212
+ const runParamOnRequest = params.onRequest;
213
+ return FetchPromise(__spreadProps(__spreadValues(__spreadValues({}, defaults), params), {
214
+ method: (_b = (_a = params.method) != null ? _a : defaults.method) != null ? _b : "GET",
215
+ headers: mergeHeaders(defaults.headers, params.headers),
216
+ onRequest: async (request) => {
217
+ let nextRequest = request;
218
+ if (runDefaultOnRequest) {
219
+ const maybeDefaultUpdated = await runDefaultOnRequest(nextRequest);
220
+ if (maybeDefaultUpdated) {
221
+ nextRequest = maybeDefaultUpdated;
222
+ }
223
+ }
224
+ if (runParamOnRequest) {
225
+ const maybeParamUpdated = await runParamOnRequest(nextRequest);
226
+ if (maybeParamUpdated) {
227
+ nextRequest = maybeParamUpdated;
228
+ }
229
+ }
230
+ return nextRequest;
231
+ },
232
+ validateStatus: (_c = params.validateStatus) != null ? _c : defaults.validateStatus,
233
+ getAuthToken: (_d = params.getAuthToken) != null ? _d : defaults.getAuthToken
234
+ }));
235
+ };
236
+ };
35
237
  var FetchPromise_default = FetchPromise;
36
238
 
37
239
  // src/useQueries.ts
@@ -91,6 +293,7 @@ var useRequest = (fetchPromise, disableCache = false) => {
91
293
  };
92
294
  export {
93
295
  FetchPromise_default as FetchPromise,
296
+ createFetchClient,
94
297
  statusEnum,
95
298
  useQueries,
96
299
  useRequest
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-fetch-utils",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Lightweight React hooks and utilities for fetch requests with cancellation support",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {