@zayne-labs/callapi 0.0.6 → 0.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 +536 -0
- package/dist/createFetchClient.cjs +3 -2
- package/dist/createFetchClient.cjs.map +1 -1
- package/dist/createFetchClient.d.cts +5 -17
- package/dist/createFetchClient.d.ts +5 -17
- package/dist/createFetchClient.js +4 -3
- package/dist/createFetchClient.js.map +1 -1
- package/dist/index.cjs +8 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -2
- package/dist/index.d.ts +14 -2
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/types-CGXvcfe9.d.cts +241 -0
- package/dist/types-CGXvcfe9.d.ts +241 -0
- package/dist/utils.cjs +14 -14
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +4 -2
- package/dist/utils.js.map +1 -1
- package/package.json +2 -3
- package/dist/types-B8arQl4O.d.cts +0 -147
- package/dist/types-B8arQl4O.d.ts +0 -147
package/README.md
CHANGED
|
@@ -1 +1,537 @@
|
|
|
1
1
|
# CallApi
|
|
2
|
+
|
|
3
|
+
[](https://bundlephobia.com/result?p=@zayne-labs/callapi)[](https://www.npmjs.com/package/@zayne-labs/callapi)
|
|
4
|
+
|
|
5
|
+
CallApi Fetch is an extra-lightweight wrapper over fetch that provides convenient options for making HTTP requests, while keeping the API familiar to the fetch api.
|
|
6
|
+
|
|
7
|
+
It takes in a url and an request options object, just like fetch, but with some additional options to make your life easier. Check out the [API Reference](#api-reference) for more details.
|
|
8
|
+
|
|
9
|
+
## Installing CallApi
|
|
10
|
+
|
|
11
|
+
### Through npm (recommended)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# npm
|
|
15
|
+
npm install @zayne-labs/callapi
|
|
16
|
+
|
|
17
|
+
# pnpm
|
|
18
|
+
pnpm add @zayne-labs/callapi
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then you can use it by importing it in your JavaScript file.
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { callApi } from "@zayne-labs/callapi";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Using `callApi` without `npm`
|
|
28
|
+
|
|
29
|
+
You can import callApi directly into JavaScript through a CDN.
|
|
30
|
+
|
|
31
|
+
To do this, you first need to set your `script`'s type to `module`, then import `callApi`.
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<script type="module">
|
|
35
|
+
import { callApi } from "https://cdn.jsdelivr.net/npm/@zayne-labs/callapi/dist/index.js";
|
|
36
|
+
</script>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
You can use callApi just like a normal `fetch` function. The only difference is you don't have to write a `response.json` or `response.text`, you could just destructure the data and data directly.
|
|
42
|
+
|
|
43
|
+
This also means that all options for the native fetch function are supported, and you can use the same syntax to send requests.
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
const { data, error } = await callApi("url", fetchOptions);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
You also have access to the response object itself via destructuring:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
const { data, error, response } = await callApi("url", fetchOptions);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
To see how to use callApi with typescript for extra autocomplete convenience, visit the [Typescript section](#usage-with-typescript)
|
|
56
|
+
|
|
57
|
+
## Supported response types
|
|
58
|
+
|
|
59
|
+
CallApi supports all response types offered by the fetch api like `json`, `text`,`blob` etc, so you don't have to write `response.json()`, `response.text()` or `response.blob()`.
|
|
60
|
+
|
|
61
|
+
It can configure the response type by passing in the `responseType` option and setting it to the appropriate type. By default it's set to `json`.
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const { data, error } = await callApi("url", { responseType: "json" });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Easy error handling when using `async`/`await`
|
|
68
|
+
|
|
69
|
+
CallApi lets you access all errors, both http errors and javascript errors, in an `error` object. This object contains the `errorName` (eg: TypeError, SyntaxError etx) and the error message as well.
|
|
70
|
+
|
|
71
|
+
If the error is an http error, the `errorName` property will be set to "HTTPError" and the `error` object will also contain a property `errorData`.
|
|
72
|
+
|
|
73
|
+
This property contains the error response data coming from the api. If the error is not an http error but some other error, the `errorData` property will be set to `null`.
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
const { data, error } = await callApi("some-url");
|
|
77
|
+
|
|
78
|
+
console.log(error.errorName);
|
|
79
|
+
console.log(error.message);
|
|
80
|
+
console.log(error.errorData);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For extra convenience with typescript, visit the [Typescript section](#usage-with-typescript)
|
|
84
|
+
|
|
85
|
+
## Helpful Features
|
|
86
|
+
|
|
87
|
+
## Auto cancellation of redundant requests
|
|
88
|
+
|
|
89
|
+
`CallApi` automatically cancels the previous requests if the same url is called again before the previous request is resolved. This essentially only lets the last request through, hence preventing dreaded race conditions.
|
|
90
|
+
|
|
91
|
+
What this implies is that you can use `callApi` in a `useEffect` hook for instance and it will automatically cancel the previous request if the url is called again before the previous request is resolved 🤩.
|
|
92
|
+
|
|
93
|
+
This behavior can be disabled if you don't like it, by passing in `{ cancelPreviousRequest: false }` to the fetch options.
|
|
94
|
+
|
|
95
|
+
You can also cancel a request to a particular url by passing the url as a parameter to the cancel property attached to callApi.
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
callApi("some-url");
|
|
99
|
+
|
|
100
|
+
callApi.cancel("some-url");
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
You can also pass a signal to callApi as an option and cancel it when you want to.
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
|
|
108
|
+
callApi("some-url", { signal: controller.signal });
|
|
109
|
+
|
|
110
|
+
controller.abort();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## ✔️ Query search params
|
|
114
|
+
|
|
115
|
+
You can add `query` object as an option and callApi will create a query string for you automatically.
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
callApi("some-url", {
|
|
119
|
+
query: {
|
|
120
|
+
param1: "value1",
|
|
121
|
+
param2: "to encode",
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// The above request can be written in Fetch like this:
|
|
126
|
+
fetch("url?param1=value1¶m2=to%20encode");
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## ✔️ `Content-Type` generation based on `body` content
|
|
130
|
+
|
|
131
|
+
`CallApi` sets `Content-Type` automatically depending on your `body` data. Supported data types for this automatic setting include:
|
|
132
|
+
|
|
133
|
+
- Object
|
|
134
|
+
- Query Strings
|
|
135
|
+
- FormData
|
|
136
|
+
|
|
137
|
+
If you pass in an `object`, callApi will set `Content-Type` to `application/json`. It will also `JSON.stringify` your body so you don't have to do it yourself.
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
callApi.post("some-url", {
|
|
141
|
+
body: { message: "Good game" },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// The above request is equivalent to this
|
|
145
|
+
fetch("some-url", {
|
|
146
|
+
method: "post",
|
|
147
|
+
headers: { "Content-Type": "application/json" },
|
|
148
|
+
body: JSON.stringify({ message: "Good game" }),
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
If you pass in a string, callApi will set `Content-Type` to `application/x-www-form-urlencoded`.
|
|
153
|
+
|
|
154
|
+
`CallApi` also contains a `toQueryString` method that can help you convert objects to query strings so you can use this option easily.
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
import { toQueryString } from "@zayne-labs/callapi";
|
|
158
|
+
|
|
159
|
+
callApi("some-url", {
|
|
160
|
+
method: "POST",
|
|
161
|
+
body: toQueryString({ message: "Good game" }),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// The above request is equivalent to this
|
|
165
|
+
fetch("some-url", {
|
|
166
|
+
method: "post",
|
|
167
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
168
|
+
body: "message=Good%20game",
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
If you pass in a FormData, callApi will let the native `fetch` function handle the `Content-Type`. Generally, this will use `multipart/form-data` with the default options.
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
const data = new FormData(form.elements);
|
|
176
|
+
|
|
177
|
+
callApi("some-url", { body: data });
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## ✔️ Authorization header helpers
|
|
181
|
+
|
|
182
|
+
If you provide callApi with an `auth` property, it will generate an Authorization Header for you.
|
|
183
|
+
|
|
184
|
+
If you pass in a `string` (commonly for tokens) , it will generate a Bearer Auth.
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
callApi("some-url", { auth: "token12345" });
|
|
188
|
+
|
|
189
|
+
// The above request can be written in Fetch like this:
|
|
190
|
+
fetch("some-url", {
|
|
191
|
+
headers: { Authorization: `Bearer token12345` },
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## ✔️ Creating a callApi Instance
|
|
196
|
+
|
|
197
|
+
You can create an instance of `callApi` with predefined options. This is super helpful if you need to send requests with similar `options`.
|
|
198
|
+
|
|
199
|
+
**Things to note:**
|
|
200
|
+
|
|
201
|
+
- All `options` that can be passed to `callApi` can also be passed to `callApi.create`.
|
|
202
|
+
- Any options passed to `callApi.create` will be applied to all requests made with the instance.
|
|
203
|
+
- If you pass a similar `options` property to the instance, the instance's options will take precedence.
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
import { callApi } from "@zayne-labs/callapi";
|
|
207
|
+
|
|
208
|
+
// Creating the instance, with some base options
|
|
209
|
+
const callAnotherApi = callApi.create({ timeout: 5000, baseURL: "https://api.example.com" });
|
|
210
|
+
|
|
211
|
+
// Using the instance
|
|
212
|
+
const { data, error } = await callAnotherApi("some-url");
|
|
213
|
+
|
|
214
|
+
// Overriding the timeout option (all base options can be overridden via the instance)
|
|
215
|
+
const { data, error } = await callAnotherApi("some-url", {
|
|
216
|
+
timeout: 10000,
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## ✔️ Custom response handler and custom body serializer
|
|
221
|
+
|
|
222
|
+
By default callApi supports all response types offered by the fetch api like `json`, `text`,`blob` etc, so you don't have to write `response.json()`, `response.text()` or `response.blob()`.
|
|
223
|
+
|
|
224
|
+
But if you want to handle a response not supported by fetch, you can pass a custom handler function to the `responseParser` option.
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
const { data, error } = await callApi("url", {
|
|
228
|
+
responseParser: customResponseParser,
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Or even better, provide it as a callApi base option.
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
const callAnotherApi = callApi.create({
|
|
236
|
+
responseParser: customResponseParser,
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
You could also provide a custom serializer/stringifier for objects passed to the body of the request via the `bodySerializer` option.
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
const callAnotherApi = callApi.create({
|
|
244
|
+
bodySerializer: customBodySerializer,
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## ✔️ Interceptors (just like axios)
|
|
249
|
+
|
|
250
|
+
Providing interceptors to hook into lifecycle events of a `callApi` call is possible.
|
|
251
|
+
|
|
252
|
+
These interceptors can be either asynchronous or synchronous.
|
|
253
|
+
|
|
254
|
+
You might want to use `callApi.create` to set shared interceptors.
|
|
255
|
+
|
|
256
|
+
### `onRequest({ request, options })`
|
|
257
|
+
|
|
258
|
+
`onRequest` is called function that is called just before the request is made, allowing you to modify the request or perform additional operations.
|
|
259
|
+
|
|
260
|
+
```js
|
|
261
|
+
await callApi("/api", {
|
|
262
|
+
onRequest: ({ request, options }) => {
|
|
263
|
+
// Log request
|
|
264
|
+
console.log(request, options);
|
|
265
|
+
|
|
266
|
+
// Do other stuff
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### `onRequestError({ error, request, options,})`
|
|
272
|
+
|
|
273
|
+
`onRequestError` when an error occurs during the fetch request and it fails, providing access to the error object, request details and fetch options.
|
|
274
|
+
|
|
275
|
+
```js
|
|
276
|
+
await callApi("/api", {
|
|
277
|
+
onRequestError: ({ request, options, error }) => {
|
|
278
|
+
// Log error
|
|
279
|
+
console.log("[fetch request error]", request, error);
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### `onResponse({ response, request, options })`
|
|
285
|
+
|
|
286
|
+
`onResponse` will be called when a successful response is received, providing access to the response, request details and fetch options.
|
|
287
|
+
|
|
288
|
+
The response object here contains all regular fetch response properties, plus a `data` property, which contains the parsed response body.
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
await callApi("/api", {
|
|
292
|
+
onResponse: ({ request, response, options }) => {
|
|
293
|
+
// Log response
|
|
294
|
+
console.log(request, response.status, response.data);
|
|
295
|
+
|
|
296
|
+
// Do other stuff
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### `onResponseError({ request, options, response })`
|
|
302
|
+
|
|
303
|
+
`onResponseError` is called when an error response (status code >= 400) is received from the api, providing access to the response object, request details, and fetch options used.
|
|
304
|
+
|
|
305
|
+
The response object here contains all regular fetch response properties, plus an `errorData` property, which contains the parsed response error json response, if the server returns one.
|
|
306
|
+
|
|
307
|
+
**This to note for this interceptor to be triggered:**
|
|
308
|
+
|
|
309
|
+
- The `response.ok` property will be `false`.
|
|
310
|
+
- The `response.status` property will be >= 400.
|
|
311
|
+
- Essentially only error http responses return by the api will trigger this interceptor.
|
|
312
|
+
- It won't trigger for error responses not from the api, like network errors, syntax errors etc. Handle those in `onRequestError` interceptor.
|
|
313
|
+
|
|
314
|
+
The response object here contains all regular fetch response properties, plus an `errorData` property, which contains the parsed response error json response, if the server returns one.
|
|
315
|
+
|
|
316
|
+
This example uses a shared interceptor for all requests made with the instance.
|
|
317
|
+
|
|
318
|
+
```js
|
|
319
|
+
const callAnotherApi = callApi.create({
|
|
320
|
+
onResponseError: ({ response, request, options }) => {
|
|
321
|
+
// Log error response
|
|
322
|
+
console.log(request, response.status, response.errorData);
|
|
323
|
+
|
|
324
|
+
// Perform action on various error conditions
|
|
325
|
+
if (response.status === 401) {
|
|
326
|
+
actions.clearSession();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (response.status === 429) {
|
|
330
|
+
toast.error("Too may requests!");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (response.status === 403 && response.errorData?.message === "2FA is required") {
|
|
334
|
+
toast.error(response.errorData?.message, {
|
|
335
|
+
description: "Please authenticate to continue",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (response.status === 500) {
|
|
340
|
+
toast.error("Internal server Error!");
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## ✔️ Retries
|
|
347
|
+
|
|
348
|
+
`CallApi` support retries for requests if an error happens and if the response status code is included in `retryStatusCodes` list:
|
|
349
|
+
|
|
350
|
+
**Default Retry status codes:**
|
|
351
|
+
|
|
352
|
+
- `408` - Request Timeout
|
|
353
|
+
- `409` - Conflict
|
|
354
|
+
- `425` - Too Early
|
|
355
|
+
- `429` - Too Many Requests
|
|
356
|
+
- `500` - Internal Server Error
|
|
357
|
+
- `502` - Bad Gateway
|
|
358
|
+
- `503` - Service Unavailable
|
|
359
|
+
- `504` - Gateway Timeout
|
|
360
|
+
|
|
361
|
+
You can specify the amount of retries and delay between them using `retry` and `retryDelay` options and also pass a custom array of codes using `retryStatusCodes` option.
|
|
362
|
+
|
|
363
|
+
You can also specify which methods should be retried by passing in a custom `retryMethods` array.
|
|
364
|
+
|
|
365
|
+
The default for `retry` is `0` retries.
|
|
366
|
+
The default for `retryDelay` is `0` ms.
|
|
367
|
+
The default for `retryMethods` is `["GET", "POST"]`.
|
|
368
|
+
|
|
369
|
+
```js
|
|
370
|
+
await callApi("http://google.com/404", {
|
|
371
|
+
retry: 3,
|
|
372
|
+
retryDelay: 500, // ms
|
|
373
|
+
retryStatusCodes: [404, 502, 503, 504], // custom status codes for retries
|
|
374
|
+
retryMethods: ["POST", "PUT", "PATCH", "DELETE"], // custom methods for retries
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## ✔️ Timeout
|
|
379
|
+
|
|
380
|
+
You can specify `timeout` in milliseconds to automatically abort a request after a timeout (default is disabled).
|
|
381
|
+
|
|
382
|
+
```js
|
|
383
|
+
await callApi("http://google.com/404", {
|
|
384
|
+
timeout: 3000, // Timeout after 3 seconds
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## ✔️ Throw on all errors
|
|
389
|
+
|
|
390
|
+
You can throw an error on all errors (including http errors) by passing `throwOnError` option. This makes callApi play nice with other libraries that expect a promise to resolve to a value, for example `React Query`.
|
|
391
|
+
|
|
392
|
+
```js
|
|
393
|
+
const callMainApi = callApi.create({
|
|
394
|
+
throwOnError: true,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const { data, error } = useQuery({
|
|
398
|
+
queryKey: ["todos"],
|
|
399
|
+
queryFn: async () => {
|
|
400
|
+
// CallApi will throw an error if the request fails or there is an error response, which react query would handle
|
|
401
|
+
const { data } = await callMainApi("todos");
|
|
402
|
+
|
|
403
|
+
return data;
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Doing this with regular fetch would imply the following extra steps:
|
|
409
|
+
|
|
410
|
+
```js
|
|
411
|
+
const { data, error } = useQuery({
|
|
412
|
+
queryKey: ["todos"],
|
|
413
|
+
queryFn: async () => {
|
|
414
|
+
const response = await fetch("todos");
|
|
415
|
+
|
|
416
|
+
if (!response.ok) {
|
|
417
|
+
throw new Error("Failed to fetch");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return response.json();
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
For even more convenience, you can specify a resultMode for callApi in addition with the throwOnError option. Use this if you feel to lazy to make a tiny wrapper over callApi for something like react query:
|
|
426
|
+
|
|
427
|
+
```js
|
|
428
|
+
const callMainApi = callApi.create({
|
|
429
|
+
throwOnError: true,
|
|
430
|
+
resultMode: "onlySuccess",
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const { data, error } = useQuery({
|
|
434
|
+
queryKey: ["todos"],
|
|
435
|
+
// CallApi will throw on errors here, and also return only data, which react query is interested in
|
|
436
|
+
queryFn: () => callMainApi("todos"),
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
## Usage with Typescript
|
|
441
|
+
|
|
442
|
+
- You can provide types for the success and error data via generics, to enable autocomplete and type checking in your codebase.
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
const callMainApi = callApi.create<FormResponseDataType, FormErrorResponseType>({
|
|
446
|
+
baseURL: BASE_AUTH_URL,
|
|
447
|
+
|
|
448
|
+
method: "POST",
|
|
449
|
+
|
|
450
|
+
retries: 3,
|
|
451
|
+
|
|
452
|
+
credentials: "same-origin",
|
|
453
|
+
});
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
- Since the data and error properties destructured from callApi are a discriminated union, simply checking for and handling the `error` property will narrow down the type of the data.
|
|
457
|
+
|
|
458
|
+
This simply means that if data is available error will be null, and if error is available data will be null. Both cannot exist at the same time.
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
// As is, both data and error could be null
|
|
462
|
+
const { data, error } = await callMainApi("some-url", {
|
|
463
|
+
body: { message: "Good game" },
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
if (error) {
|
|
467
|
+
console.error(error);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Now, data is no longer null
|
|
472
|
+
console.log(data);
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
- The types for the object passed to onResponse and onResponseError could be augmented with type helpers provided by `@zayne-labs/callapi`.
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
const callAnotherApi = callApi.create({
|
|
479
|
+
onResponseError: ({ response, request, options }: ResponseErrorContext<{ message?: string }>) => {
|
|
480
|
+
// Log error response
|
|
481
|
+
console.log(
|
|
482
|
+
request,
|
|
483
|
+
response.status,
|
|
484
|
+
// error data, coming back from api
|
|
485
|
+
response.errorData,
|
|
486
|
+
// Typescript will then understand the errorData might contains a message property
|
|
487
|
+
response.errorData?.message
|
|
488
|
+
);
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## Api Reference
|
|
494
|
+
|
|
495
|
+
### Fetch Options
|
|
496
|
+
|
|
497
|
+
- All Regular fetch options are supported as is, with only body extended to support more types.
|
|
498
|
+
- `body`: Optional body of the request, can be an object or any other supported body type.
|
|
499
|
+
- `query`: Query parameters to append to the URL.
|
|
500
|
+
- `auth`: Authorization header value.
|
|
501
|
+
- `bodySerializer`: Custom function to serialize the body object into a string.
|
|
502
|
+
- `responseParser`: Custom function to parse the response string into an object.
|
|
503
|
+
- `resultMode`: Mode of the result, can influence how results are handled or returned. (default: "all")
|
|
504
|
+
- `cancelRedundantRequests`: If true, cancels previous unfinished requests to the same URL. (default: true)
|
|
505
|
+
- `baseURL`: Base URL to be prepended to all request URLs.
|
|
506
|
+
- `timeout`: Request timeout in milliseconds.
|
|
507
|
+
- `defaultErrorMessage`: Default error message to use if none is provided from a response. (default: "Failed to fetch data from server!")
|
|
508
|
+
- `throwOnError`: If true or the function returns true, throws errors instead of returning them.
|
|
509
|
+
|
|
510
|
+
- `responseType`: Expected response type, affects how response is parsed. (default: "json")
|
|
511
|
+
- `retries`: Number of retry attempts for failed requests. (default: 0)
|
|
512
|
+
- `retryDelay`: Delay between retries in milliseconds. (default: 500)
|
|
513
|
+
- `retryCodes`: HTTP status codes that trigger a retry. (default: [409, 425, 429, 500, 502, 503, 504])
|
|
514
|
+
- `retryMethods`: HTTP methods that are allowed to retry. (default: ["GET", "POST"])
|
|
515
|
+
- `meta`: An optional field for additional information, typically used for logging or tracing.
|
|
516
|
+
- `onRequest`: Interceptor called just before the request is made, allowing for modifications or additional operations.
|
|
517
|
+
- `onRequestError`: Interceptor called when an error occurs during the fetch request.
|
|
518
|
+
- `onResponse`: Interceptor called when a successful response is received from the API.
|
|
519
|
+
- `onResponseError`: Interceptor called when an error response is received from the API.
|
|
520
|
+
|
|
521
|
+
### Methods
|
|
522
|
+
|
|
523
|
+
- `callApi.create(options)`: Creates an instance of `callApi` with shared base configurations.
|
|
524
|
+
- `callApi.cancel(url: string)`: Cancels an ongoing request to the specified URL.
|
|
525
|
+
|
|
526
|
+
### Utility Functions
|
|
527
|
+
|
|
528
|
+
- `isHTTPError`: Type guard for if an error is an HTTPError
|
|
529
|
+
|
|
530
|
+
- `isHTTPErrorInstance`: Type guard for if an error is an instance of HTTPError. Useful for when `throwAllErrors` option is set to `true`
|
|
531
|
+
|
|
532
|
+
- `toQueryString`: Converts an object to a URL query string
|
|
533
|
+
|
|
534
|
+
## Acknowledgements
|
|
535
|
+
|
|
536
|
+
- Credits to `ofetch` by unjs for some of the ideas for the features in the library like the function-based interceptors, retries etc
|
|
537
|
+
- Credits to `zl-fetch` fetch wrapper as well for the inspiration behind a few features in this library as well
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var _typeof = require('./typeof');
|
|
3
4
|
var utils = require('./utils');
|
|
4
5
|
|
|
5
|
-
const m
|
|
6
|
+
const b=m=>{const i=new Map,[C,D]=utils.splitConfig(m??{}),{headers:p,body:w,signal:A,...M}=C,l=async(o,f)=>{const[q,B]=utils.splitConfig(f??{}),e={bodySerializer:JSON.stringify,responseType:"json",baseURL:"",retries:0,retryDelay:0,retryCodes:utils.defaultRetryCodes,retryMethods:utils.defaultRetryMethods,defaultErrorMessage:"Failed to fetch data from server!",cancelRedundantRequests:!0,...D,...B},{signal:O=A,body:s=w,headers:g,...x}=q,R=i.get(o);if(R&&e.cancelRedundantRequests){const t=new DOMException("Cancelled the previous unfinished request","AbortError");R.abort(t);}const c=new AbortController;i.set(o,c);const S=e.timeout?AbortSignal.timeout(e.timeout):null,T=AbortSignal.any([c.signal,S??c.signal,O??c.signal]),a={signal:T,method:"GET",body:_typeof.isObject(s)?e.bodySerializer(s):s,headers:p||g||e.auth||_typeof.isObject(s)?{..._typeof.isObject(s)&&{"Content-Type":"application/json",Accept:"application/json"},..._typeof.isFormData(s)&&{"Content-Type":"multipart/form-data"},..._typeof.isString(s)&&{"Content-Type":"application/x-www-form-urlencoded"},...!!e.auth&&{Authorization:`Bearer ${e.auth}`},...utils.objectifyHeaders(p),...utils.objectifyHeaders(g)}:void 0,...M,...x};try{await e.onRequest?.({request:a,options:e});const t=await fetch(`${e.baseURL}${utils.mergeUrlWithParams(o,e.query)}`,a);if(!t.ok&&!T.aborted&&e.retries>0&&e.retryCodes.includes(t.status)&&e.retryMethods.includes(a.method))return await utils.waitUntil(e.retryDelay),await l(o,{...f,retries:e.retries-1});if(!t.ok){const u=await utils.getResponseData(t,e.responseType,e.responseParser);throw new utils.HTTPError({response:{...t,errorData:u},defaultErrorMessage:e.defaultErrorMessage})}const r=await utils.getResponseData(t,e.responseType,e.responseParser);return await e.onResponse?.({response:{...t,data:r},request:a,options:e}),utils.resolveSuccessResult({successData:r,response:t,options:e})}catch(t){const n=utils.$resolveErrorResult({error:t,options:e});if(t instanceof DOMException&&t.name==="TimeoutError"){const r=`Request timed out after ${e.timeout}ms`;return console.info(`%cTimeoutError: ${r}`,"color: red; font-weight: 500; font-size: 14px;"),console.trace("TimeoutError"),n({message:r})}if(t instanceof DOMException&&t.name==="AbortError"){const r="Request was cancelled";return console.info(`%AbortError: ${r}`,"color: red; font-weight: 500; font-size: 14px;"),console.trace("AbortError"),n({message:r})}if(utils.isHTTPErrorInstance(t)){const{errorData:r,...u}=t.response;return await e.onResponseError?.({response:{...u,errorData:r},request:a,options:e}),n({errorData:r,response:u,message:r?.message})}return await e.onRequestError?.({request:a,error:t,options:e}),n()}finally{i.delete(o);}};return l.create=b,l.cancel=o=>i.get(o)?.abort(),l};
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
exports.createFetchClient = b;
|
|
8
9
|
//# sourceMappingURL=out.js.map
|
|
9
10
|
//# sourceMappingURL=createFetchClient.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/createFetchClient.ts"],"names":["$resolveErrorResult","HTTPError","defaultRetryCodes","defaultRetryMethods","getResponseData","isFormData","isHTTPErrorInfo","isHTTPErrorInstance","isObject","mergeUrlWithParams","objectifyHeaders","resolveSuccessResult","splitConfig","wait","createFetchClient","baseConfig","abortControllerStore","baseFetchConfig","baseExtraOptions","baseHeaders","baseSignal","restOfBaseFetchConfig","callApi","url","config","fetchConfig","extraOptions","options","signal","body","headers","restOfFetchConfig","prevFetchController","reason","newFetchController","timeoutSignal","combinedSignal","requestInit","response","errorData","successData","error","resolveErrorResult","message","createFetchClient_default"],"mappings":"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","sourcesContent":["import type {\n\t$RequestConfig,\n\tAbortSignalWithAny,\n\tBaseConfig,\n\tExtraOptions,\n\tFetchConfig,\n\tGetCallApiResult,\n\tPossibleErrorObject,\n\tResultStyleUnion,\n} from \"./types\";\nimport {\n\t$resolveErrorResult,\n\tHTTPError,\n\tdefaultRetryCodes,\n\tdefaultRetryMethods,\n\tgetResponseData,\n\tisFormData,\n\tisHTTPErrorInfo,\n\tisHTTPErrorInstance,\n\tisObject,\n\tmergeUrlWithParams,\n\tobjectifyHeaders,\n\tresolveSuccessResult,\n\tsplitConfig,\n\twait,\n} from \"./utils\";\n\nconst createFetchClient = <\n\tTBaseData,\n\tTBaseErrorData,\n\tTBaseResultMode extends ResultStyleUnion = undefined,\n>(\n\tbaseConfig?: BaseConfig<TBaseData, TBaseErrorData, TBaseResultMode>\n) => {\n\tconst abortControllerStore = new Map<string, AbortController>();\n\n\tconst [baseFetchConfig, baseExtraOptions] = splitConfig(baseConfig ?? {});\n\n\tconst { headers: baseHeaders, signal: baseSignal, ...restOfBaseFetchConfig } = baseFetchConfig;\n\n\tconst callApi = async <\n\t\tTData = TBaseData,\n\t\tTErrorData = TBaseErrorData,\n\t\tTResultMode extends ResultStyleUnion = TBaseResultMode,\n\t>(\n\t\turl: string,\n\t\tconfig?: FetchConfig<TData, TErrorData, TResultMode>\n\t): Promise<GetCallApiResult<TData, TErrorData, TResultMode>> => {\n\t\ttype CallApiResult = GetCallApiResult<TData, TErrorData, TResultMode>;\n\n\t\tconst [fetchConfig, extraOptions] = splitConfig(config ?? {});\n\n\t\tconst options = {\n\t\t\tbodySerializer: JSON.stringify,\n\t\t\tresponseType: \"json\",\n\t\t\tbaseURL: \"\",\n\t\t\tretries: 0,\n\t\t\tretryDelay: 0,\n\t\t\tretryCodes: defaultRetryCodes,\n\t\t\tretryMethods: defaultRetryMethods,\n\t\t\tdefaultErrorMessage: \"Failed to fetch data from server!\",\n\t\t\tcancelRedundantRequests: true,\n\t\t\t...baseExtraOptions,\n\t\t\t...extraOptions,\n\t\t} satisfies ExtraOptions;\n\n\t\tconst { signal = baseSignal, body, headers, ...restOfFetchConfig } = fetchConfig;\n\n\t\tconst prevFetchController = abortControllerStore.get(url);\n\n\t\tif (prevFetchController && options.cancelRedundantRequests) {\n\t\t\tconst reason = new DOMException(\"Cancelled the previous unfinished request\", \"AbortError\");\n\t\t\tprevFetchController.abort(reason);\n\t\t}\n\n\t\tconst newFetchController = new AbortController();\n\n\t\tabortControllerStore.set(url, newFetchController);\n\n\t\tconst timeoutSignal = options.timeout ? AbortSignal.timeout(options.timeout) : null;\n\n\t\t// FIXME - Remove this type cast once TS updates its lib-dom types for AbortSignal to include the any() method\n\t\tconst combinedSignal = (AbortSignal as AbortSignalWithAny).any([\n\t\t\tnewFetchController.signal,\n\t\t\ttimeoutSignal ?? newFetchController.signal,\n\t\t\tsignal ?? newFetchController.signal,\n\t\t]);\n\n\t\tconst requestInit = {\n\t\t\tsignal: combinedSignal,\n\n\t\t\tmethod: \"GET\",\n\n\t\t\tbody: isObject(body) ? options.bodySerializer(body) : body,\n\n\t\t\t// == Return undefined if there are no headers provided or if the body is not an object\n\t\t\theaders:\n\t\t\t\tbaseHeaders || headers || isObject(body)\n\t\t\t\t\t? {\n\t\t\t\t\t\t\t...(isObject(body) && {\n\t\t\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t...(isFormData(body) && {\n\t\t\t\t\t\t\t\t\"Content-Type\": \"multipart/form-data\",\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t...objectifyHeaders(baseHeaders),\n\t\t\t\t\t\t\t...objectifyHeaders(headers),\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\n\t\t\t...restOfBaseFetchConfig,\n\t\t\t...restOfFetchConfig,\n\t\t} satisfies $RequestConfig;\n\n\t\ttry {\n\t\t\tawait options.onRequest?.({ request: requestInit, options });\n\n\t\t\tconst response = await fetch(\n\t\t\t\t`${options.baseURL}${mergeUrlWithParams(url, options.query)}`,\n\t\t\t\trequestInit\n\t\t\t);\n\n\t\t\tconst shouldRetry =\n\t\t\t\t!combinedSignal.aborted &&\n\t\t\t\toptions.retries !== 0 &&\n\t\t\t\t!response.ok &&\n\t\t\t\toptions.retryCodes.includes(response.status) &&\n\t\t\t\toptions.retryMethods.includes(requestInit.method);\n\n\t\t\tif (shouldRetry) {\n\t\t\t\tawait wait(options.retryDelay);\n\n\t\t\t\treturn await callApi(url, { ...config, retries: options.retries - 1 });\n\t\t\t}\n\n\t\t\tif (!response.ok) {\n\t\t\t\tconst errorData = await getResponseData<TErrorData>(\n\t\t\t\t\tresponse,\n\t\t\t\t\toptions.responseType,\n\t\t\t\t\toptions.responseParser\n\t\t\t\t);\n\n\t\t\t\t// == Pushing all error handling responsibilities to the catch block\n\t\t\t\tthrow new HTTPError({\n\t\t\t\t\tresponse: { ...response, errorData },\n\t\t\t\t\tdefaultErrorMessage: options.defaultErrorMessage,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst successData = await getResponseData<TData>(\n\t\t\t\tresponse,\n\t\t\t\toptions.responseType,\n\t\t\t\toptions.responseParser\n\t\t\t);\n\n\t\t\tawait options.onResponse?.({\n\t\t\t\tresponse: { ...response, data: successData },\n\t\t\t\trequest: requestInit,\n\t\t\t\toptions,\n\t\t\t});\n\n\t\t\treturn resolveSuccessResult<CallApiResult>({ successData, response, options });\n\n\t\t\t// == Exhaustive Error handling\n\t\t} catch (error) {\n\t\t\tconst resolveErrorResult = $resolveErrorResult<CallApiResult>({ error, options });\n\n\t\t\tif (error instanceof DOMException && error.name === \"TimeoutError\") {\n\t\t\t\tconst message = `Request timed out after ${options.timeout}ms`;\n\n\t\t\t\tconsole.info(`%cTimeoutError: ${message}`, \"color: red; font-weight: 500; font-size: 14px;\");\n\t\t\t\tconsole.trace(\"TimeoutError\");\n\n\t\t\t\treturn resolveErrorResult({ message });\n\t\t\t}\n\n\t\t\tif (error instanceof DOMException && error.name === \"AbortError\") {\n\t\t\t\tconst message = `Request was cancelled`;\n\n\t\t\t\tconsole.info(`%AbortError: ${message}`, \"color: red; font-weight: 500; font-size: 14px;\");\n\t\t\t\tconsole.trace(\"AbortError\");\n\n\t\t\t\treturn resolveErrorResult({ message });\n\t\t\t}\n\n\t\t\tif (isHTTPErrorInstance<TErrorData>(error)) {\n\t\t\t\tconst { errorData, ...response } = error.response;\n\n\t\t\t\tawait options.onResponseError?.({\n\t\t\t\t\tresponse: { ...response, errorData },\n\t\t\t\t\trequest: requestInit,\n\t\t\t\t\toptions,\n\t\t\t\t});\n\n\t\t\t\treturn resolveErrorResult({\n\t\t\t\t\terrorData,\n\t\t\t\t\tresponse,\n\t\t\t\t\tmessage: (errorData as PossibleErrorObject)?.message,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// == At this point only the request errors exist, so the request error interceptor is called\n\t\t\tawait options.onRequestError?.({ request: requestInit, error: error as Error, options });\n\n\t\t\treturn resolveErrorResult();\n\n\t\t\t// == Removing the now unneeded AbortController from store\n\t\t} finally {\n\t\t\tabortControllerStore.delete(url);\n\t\t}\n\t};\n\n\tcallApi.create = createFetchClient;\n\tcallApi.isHTTPErrorInfo = isHTTPErrorInfo;\n\tcallApi.isHTTPErrorInstance = isHTTPErrorInstance;\n\tcallApi.cancel = (url: string) => abortControllerStore.get(url)?.abort();\n\n\treturn callApi;\n};\n\nconst callApi = createFetchClient();\n\nexport default callApi;\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/createFetchClient.ts"],"names":["isFormData","isObject","isString","$resolveErrorResult","HTTPError","defaultRetryCodes","defaultRetryMethods","getResponseData","isHTTPErrorInstance","mergeUrlWithParams","objectifyHeaders","resolveSuccessResult","splitConfig","waitUntil","createFetchClient","baseConfig","abortControllerStore","baseFetchConfig","baseExtraOptions","baseHeaders","baseBody","baseSignal","restOfBaseFetchConfig","callApi","url","config","fetchConfig","extraOptions","options","signal","body","headers","restOfFetchConfig","prevFetchController","reason","newFetchController","timeoutSignal","combinedSignal","requestInit","response","errorData","successData","error","resolveErrorResult","message"],"mappings":"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","sourcesContent":["import { isFormData, isObject, isString } from \"./typeof\";\nimport type {\n\t$RequestOptions,\n\tAbortSignalWithAny,\n\tBaseConfig,\n\tExtraOptions,\n\tFetchConfig,\n\tGetCallApiResult,\n\tPossibleErrorObject,\n\tResultModeUnion,\n} from \"./types\";\nimport {\n\t$resolveErrorResult,\n\tHTTPError,\n\tdefaultRetryCodes,\n\tdefaultRetryMethods,\n\tgetResponseData,\n\tisHTTPErrorInstance,\n\tmergeUrlWithParams,\n\tobjectifyHeaders,\n\tresolveSuccessResult,\n\tsplitConfig,\n\twaitUntil,\n} from \"./utils\";\n\nconst createFetchClient = <TBaseData, TBaseErrorData, TBaseResultMode extends ResultModeUnion = undefined>(\n\tbaseConfig?: BaseConfig<TBaseData, TBaseErrorData, TBaseResultMode>\n) => {\n\tconst abortControllerStore = new Map<string, AbortController>();\n\n\tconst [baseFetchConfig, baseExtraOptions] = splitConfig(baseConfig ?? {});\n\n\tconst {\n\t\theaders: baseHeaders,\n\t\tbody: baseBody,\n\t\tsignal: baseSignal,\n\t\t...restOfBaseFetchConfig\n\t} = baseFetchConfig;\n\n\tconst callApi = async <\n\t\tTData = TBaseData,\n\t\tTErrorData = TBaseErrorData,\n\t\tTResultMode extends ResultModeUnion = TBaseResultMode,\n\t>(\n\t\turl: string,\n\t\tconfig?: FetchConfig<TData, TErrorData, TResultMode>\n\t): Promise<GetCallApiResult<TData, TErrorData, TResultMode>> => {\n\t\ttype CallApiResult = GetCallApiResult<TData, TErrorData, TResultMode>;\n\n\t\tconst [fetchConfig, extraOptions] = splitConfig(config ?? {});\n\n\t\tconst options = {\n\t\t\tbodySerializer: JSON.stringify,\n\t\t\tresponseType: \"json\",\n\t\t\tbaseURL: \"\",\n\t\t\tretries: 0,\n\t\t\tretryDelay: 0,\n\t\t\tretryCodes: defaultRetryCodes,\n\t\t\tretryMethods: defaultRetryMethods,\n\t\t\tdefaultErrorMessage: \"Failed to fetch data from server!\",\n\t\t\tcancelRedundantRequests: true,\n\t\t\t...baseExtraOptions,\n\t\t\t...extraOptions,\n\t\t} satisfies ExtraOptions;\n\n\t\tconst { signal = baseSignal, body = baseBody, headers, ...restOfFetchConfig } = fetchConfig;\n\n\t\tconst prevFetchController = abortControllerStore.get(url);\n\n\t\tif (prevFetchController && options.cancelRedundantRequests) {\n\t\t\tconst reason = new DOMException(\"Cancelled the previous unfinished request\", \"AbortError\");\n\t\t\tprevFetchController.abort(reason);\n\t\t}\n\n\t\tconst newFetchController = new AbortController();\n\n\t\tabortControllerStore.set(url, newFetchController);\n\n\t\tconst timeoutSignal = options.timeout ? AbortSignal.timeout(options.timeout) : null;\n\n\t\t// FIXME - Remove this type cast once TS updates its lib-dom types for AbortSignal to include the any() method\n\t\tconst combinedSignal = (AbortSignal as AbortSignalWithAny).any([\n\t\t\tnewFetchController.signal,\n\t\t\ttimeoutSignal ?? newFetchController.signal,\n\t\t\tsignal ?? newFetchController.signal,\n\t\t]);\n\n\t\tconst requestInit = {\n\t\t\tsignal: combinedSignal,\n\n\t\t\tmethod: \"GET\",\n\n\t\t\tbody: isObject(body) ? options.bodySerializer(body) : body,\n\n\t\t\t// == Return undefined if the following conditions are not met (so that native fetch would auto set the correct headers):\n\t\t\t// - headers are provided\n\t\t\t// - The body is an object\n\t\t\t// - The auth option is provided\n\t\t\theaders:\n\t\t\t\tbaseHeaders || headers || options.auth || isObject(body)\n\t\t\t\t\t? {\n\t\t\t\t\t\t\t...(isObject(body) && {\n\t\t\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t...(isFormData(body) && {\n\t\t\t\t\t\t\t\t\"Content-Type\": \"multipart/form-data\",\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t...(isString(body) && {\n\t\t\t\t\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t...(Boolean(options.auth) && {\n\t\t\t\t\t\t\t\tAuthorization: `Bearer ${options.auth}`,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t...objectifyHeaders(baseHeaders),\n\t\t\t\t\t\t\t...objectifyHeaders(headers),\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\n\t\t\t...restOfBaseFetchConfig,\n\t\t\t...restOfFetchConfig,\n\t\t} satisfies $RequestOptions;\n\n\t\ttry {\n\t\t\tawait options.onRequest?.({ request: requestInit, options });\n\n\t\t\tconst response = await fetch(\n\t\t\t\t`${options.baseURL}${mergeUrlWithParams(url, options.query)}`,\n\t\t\t\trequestInit\n\t\t\t);\n\n\t\t\tconst shouldRetry =\n\t\t\t\t!response.ok &&\n\t\t\t\t!combinedSignal.aborted &&\n\t\t\t\toptions.retries > 0 &&\n\t\t\t\toptions.retryCodes.includes(response.status) &&\n\t\t\t\toptions.retryMethods.includes(requestInit.method);\n\n\t\t\tif (shouldRetry) {\n\t\t\t\tawait waitUntil(options.retryDelay);\n\n\t\t\t\treturn await callApi(url, { ...config, retries: options.retries - 1 });\n\t\t\t}\n\n\t\t\tif (!response.ok) {\n\t\t\t\tconst errorData = await getResponseData<TErrorData>(\n\t\t\t\t\tresponse,\n\t\t\t\t\toptions.responseType,\n\t\t\t\t\toptions.responseParser\n\t\t\t\t);\n\n\t\t\t\t// == Pushing all error handling responsibilities to the catch block\n\t\t\t\tthrow new HTTPError({\n\t\t\t\t\tresponse: { ...response, errorData },\n\t\t\t\t\tdefaultErrorMessage: options.defaultErrorMessage,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst successData = await getResponseData<TData>(\n\t\t\t\tresponse,\n\t\t\t\toptions.responseType,\n\t\t\t\toptions.responseParser\n\t\t\t);\n\n\t\t\tawait options.onResponse?.({\n\t\t\t\tresponse: { ...response, data: successData },\n\t\t\t\trequest: requestInit,\n\t\t\t\toptions,\n\t\t\t});\n\n\t\t\treturn resolveSuccessResult<CallApiResult>({ successData, response, options });\n\n\t\t\t// == Exhaustive Error handling\n\t\t} catch (error) {\n\t\t\tconst resolveErrorResult = $resolveErrorResult<CallApiResult>({ error, options });\n\n\t\t\tif (error instanceof DOMException && error.name === \"TimeoutError\") {\n\t\t\t\tconst message = `Request timed out after ${options.timeout}ms`;\n\n\t\t\t\tconsole.info(`%cTimeoutError: ${message}`, \"color: red; font-weight: 500; font-size: 14px;\");\n\t\t\t\tconsole.trace(\"TimeoutError\");\n\n\t\t\t\treturn resolveErrorResult({ message });\n\t\t\t}\n\n\t\t\tif (error instanceof DOMException && error.name === \"AbortError\") {\n\t\t\t\tconst message = `Request was cancelled`;\n\n\t\t\t\tconsole.info(`%AbortError: ${message}`, \"color: red; font-weight: 500; font-size: 14px;\");\n\t\t\t\tconsole.trace(\"AbortError\");\n\n\t\t\t\treturn resolveErrorResult({ message });\n\t\t\t}\n\n\t\t\tif (isHTTPErrorInstance<TErrorData>(error)) {\n\t\t\t\tconst { errorData, ...response } = error.response;\n\n\t\t\t\tawait options.onResponseError?.({\n\t\t\t\t\tresponse: { ...response, errorData },\n\t\t\t\t\trequest: requestInit,\n\t\t\t\t\toptions,\n\t\t\t\t});\n\n\t\t\t\treturn resolveErrorResult({\n\t\t\t\t\terrorData,\n\t\t\t\t\tresponse,\n\t\t\t\t\tmessage: (errorData as PossibleErrorObject)?.message,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// == At this point only the request errors exist, so the request error interceptor is called\n\t\t\tawait options.onRequestError?.({ request: requestInit, error: error as Error, options });\n\n\t\t\treturn resolveErrorResult();\n\n\t\t\t// == Removing the now unneeded AbortController from store\n\t\t} finally {\n\t\t\tabortControllerStore.delete(url);\n\t\t}\n\t};\n\n\tcallApi.create = createFetchClient;\n\n\tcallApi.cancel = (url: string) => abortControllerStore.get(url)?.abort();\n\n\treturn callApi;\n};\n\nexport { createFetchClient };\n"]}
|
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import { R as
|
|
1
|
+
import { R as ResultModeUnion, B as BaseConfig, F as FetchConfig, G as GetCallApiResult } from './types-CGXvcfe9.cjs';
|
|
2
2
|
|
|
3
|
-
declare const
|
|
4
|
-
<TData =
|
|
5
|
-
create:
|
|
6
|
-
<TData = TBaseData, TErrorData = TBaseErrorData, TResultMode extends ResultStyleUnion = TBaseResultMode>(url: string, config?: FetchConfig<TData, TErrorData, TResultMode>): Promise<GetCallApiResult<TData, TErrorData, TResultMode>>;
|
|
7
|
-
create: any;
|
|
8
|
-
isHTTPErrorInfo: (errorInfo: Record<string, unknown> | null) => errorInfo is {
|
|
9
|
-
errorName: "HTTPError";
|
|
10
|
-
};
|
|
11
|
-
isHTTPErrorInstance: <TErrorResponse>(error: unknown) => error is HTTPError<TErrorResponse>;
|
|
12
|
-
cancel(url: string): void | undefined;
|
|
13
|
-
};
|
|
14
|
-
isHTTPErrorInfo: (errorInfo: Record<string, unknown> | null) => errorInfo is {
|
|
15
|
-
errorName: "HTTPError";
|
|
16
|
-
};
|
|
17
|
-
isHTTPErrorInstance: <TErrorResponse>(error: unknown) => error is HTTPError<TErrorResponse>;
|
|
3
|
+
declare const createFetchClient: <TBaseData, TBaseErrorData, TBaseResultMode extends ResultModeUnion = undefined>(baseConfig?: BaseConfig<TBaseData, TBaseErrorData, TBaseResultMode>) => {
|
|
4
|
+
<TData = TBaseData, TErrorData = TBaseErrorData, TResultMode extends ResultModeUnion = TBaseResultMode>(url: string, config?: FetchConfig<TData, TErrorData, TResultMode>): Promise<GetCallApiResult<TData, TErrorData, TResultMode>>;
|
|
5
|
+
create: any;
|
|
18
6
|
cancel(url: string): void | undefined;
|
|
19
7
|
};
|
|
20
8
|
|
|
21
|
-
export {
|
|
9
|
+
export { createFetchClient };
|
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import { R as
|
|
1
|
+
import { R as ResultModeUnion, B as BaseConfig, F as FetchConfig, G as GetCallApiResult } from './types-CGXvcfe9.js';
|
|
2
2
|
|
|
3
|
-
declare const
|
|
4
|
-
<TData =
|
|
5
|
-
create:
|
|
6
|
-
<TData = TBaseData, TErrorData = TBaseErrorData, TResultMode extends ResultStyleUnion = TBaseResultMode>(url: string, config?: FetchConfig<TData, TErrorData, TResultMode>): Promise<GetCallApiResult<TData, TErrorData, TResultMode>>;
|
|
7
|
-
create: any;
|
|
8
|
-
isHTTPErrorInfo: (errorInfo: Record<string, unknown> | null) => errorInfo is {
|
|
9
|
-
errorName: "HTTPError";
|
|
10
|
-
};
|
|
11
|
-
isHTTPErrorInstance: <TErrorResponse>(error: unknown) => error is HTTPError<TErrorResponse>;
|
|
12
|
-
cancel(url: string): void | undefined;
|
|
13
|
-
};
|
|
14
|
-
isHTTPErrorInfo: (errorInfo: Record<string, unknown> | null) => errorInfo is {
|
|
15
|
-
errorName: "HTTPError";
|
|
16
|
-
};
|
|
17
|
-
isHTTPErrorInstance: <TErrorResponse>(error: unknown) => error is HTTPError<TErrorResponse>;
|
|
3
|
+
declare const createFetchClient: <TBaseData, TBaseErrorData, TBaseResultMode extends ResultModeUnion = undefined>(baseConfig?: BaseConfig<TBaseData, TBaseErrorData, TBaseResultMode>) => {
|
|
4
|
+
<TData = TBaseData, TErrorData = TBaseErrorData, TResultMode extends ResultModeUnion = TBaseResultMode>(url: string, config?: FetchConfig<TData, TErrorData, TResultMode>): Promise<GetCallApiResult<TData, TErrorData, TResultMode>>;
|
|
5
|
+
create: any;
|
|
18
6
|
cancel(url: string): void | undefined;
|
|
19
7
|
};
|
|
20
8
|
|
|
21
|
-
export {
|
|
9
|
+
export { createFetchClient };
|