react-native-fetch-client 0.1.1 → 0.1.2-Beta
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 +238 -430
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# react-native-fetch-client
|
|
2
2
|
|
|
3
3
|
> **A high-performance, Fetch-native HTTP client for TypeScript & JavaScript (React Native, Expo, modern browsers, and Node.js).**
|
|
4
|
-
> Zero runtime dependencies • Dual ESM & CJS • In-flight Request Deduplication • Response Caching • Auth Mutex • Offline Queue • Streaming Progress
|
|
4
|
+
> Zero runtime dependencies • Dual ESM & CJS • In-flight Request Deduplication • Response Caching & SWR • Auth Mutex • Offline Queue • Streaming Progress
|
|
5
5
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
[](package.json)
|
|
9
9
|
[](test)
|
|
10
|
+
[](https://reactnative.dev)
|
|
10
11
|
|
|
11
12
|
---
|
|
12
13
|
|
|
@@ -28,37 +29,38 @@ For in-depth explanations, execution pipeline flows, and implementation guides,
|
|
|
28
29
|
## Table of Contents
|
|
29
30
|
|
|
30
31
|
- [Why react-native-fetch-client?](#why-react-native-fetch-client)
|
|
32
|
+
- [Feature Comparison](#feature-comparison)
|
|
31
33
|
- [Installation](#installation)
|
|
32
|
-
- [Developer Documentation Guides](#-developer-documentation-guides)
|
|
33
34
|
- [Quick Start (TypeScript & JavaScript)](#quick-start)
|
|
34
35
|
- [TypeScript (ESM)](#1-typescript-esm)
|
|
35
36
|
- [JavaScript (ES Modules - `import`)](#2-javascript-es-modules---import)
|
|
36
37
|
- [JavaScript (CommonJS - `require`)](#3-javascript-commonjs---require)
|
|
37
38
|
- [HTTP Methods & Shorthands](#http-methods--shorthands)
|
|
39
|
+
- [Go/Rust-Style Safe Requests (`api.safe.*`)](#gorust-style-safe-requests-apisafe)
|
|
38
40
|
- [Standard Schema Validation (`schema`)](#standard-schema-validation-schema)
|
|
39
41
|
- [Auto-Pagination Helper (`api.paginate`)](#auto-pagination-helper-apipaginate)
|
|
40
42
|
- [Native AI & SSE Streaming (`api.stream`)](#native-ai--sse-streaming-apistream)
|
|
41
|
-
- [
|
|
43
|
+
- [Declarative Testing Mock Adapter (`createMockAdapter`)](#declarative-testing-mock-adapter-createmockadapter)
|
|
44
|
+
- [Response Caching, SWR & Persistent Storage](#response-caching-swr--persistent-storage)
|
|
45
|
+
- [In-Memory LRU Cache](#1-in-memory-lru-cache)
|
|
46
|
+
- [Persistent Storage (MMKV / AsyncStorage)](#2-persistent-storage-adapter-storage)
|
|
47
|
+
- [Stale-While-Revalidate (SWR)](#3-stale-while-revalidate-swr)
|
|
48
|
+
- [RFC 7234 Conditional Validation (ETag & 304)](#4-rfc-7234-conditional-validation-etag--304)
|
|
42
49
|
- [Client Concurrency Throttler (`maxConcurrentRequests`)](#client-concurrency-throttler-maxconcurrentrequests)
|
|
43
50
|
- [Circuit Breaker Pattern (`circuitBreaker`)](#circuit-breaker-pattern-circuitbreaker)
|
|
44
|
-
- [
|
|
45
|
-
- [
|
|
46
|
-
- [
|
|
47
|
-
- [Declarative Testing Mock Adapter (`createMockAdapter`)](#declarative-testing-mock-adapter-createmockadapter)
|
|
48
|
-
- [Path Parameter Interpolation (`pathParams`)](#path-parameter-interpolation-pathparams)
|
|
51
|
+
- [In-Flight Request Deduplication (`dedup`)](#in-flight-request-deduplication-dedup)
|
|
52
|
+
- [Resilient Retry Engine (`retry`)](#resilient-retry-engine-retry)
|
|
53
|
+
- [Auth & Token Refresh Mutex (`auth`)](#auth--token-refresh-mutex-auth)
|
|
49
54
|
- [Bearer Token Preset (`bearerToken`)](#bearer-token-preset-bearertoken)
|
|
55
|
+
- [Path Parameter Interpolation (`pathParams`)](#path-parameter-interpolation-pathparams)
|
|
50
56
|
- [File Upload Helper (`api.upload`)](#file-upload-helper-apiupload)
|
|
57
|
+
- [Upload & Download Progress](#upload--download-progress)
|
|
58
|
+
- [Offline Request Queue (`createOfflineQueue`)](#offline-request-queue-createofflinequeue)
|
|
51
59
|
- [Response Unwrapping (`unwrap: true`)](#response-unwrapping-unwrap-true)
|
|
52
60
|
- [Diagnostic Debug Logger (`debug: true`)](#diagnostic-debug-logger-debug-true)
|
|
53
|
-
- [In-Flight Request Deduplication](#in-flight-request-deduplication)
|
|
54
|
-
- [Response Caching (LRU)](#response-caching-lru)
|
|
55
|
-
- [Resilient Retry Engine](#resilient-retry-engine)
|
|
56
|
-
- [Auth & Token Refresh Mutex](#auth--token-refresh-mutex)
|
|
57
|
-
- [Upload & Download Progress](#upload--download-progress)
|
|
58
|
-
- [Offline Request Queue](#offline-request-queue)
|
|
59
61
|
- [Node.js Enterprise Proxies & Dispatcher](#nodejs-enterprise-proxies--dispatcher)
|
|
60
62
|
- [Migration Guide (From Axios)](#migration-guide-from-axios)
|
|
61
|
-
- [
|
|
63
|
+
- [License](#license)
|
|
62
64
|
|
|
63
65
|
---
|
|
64
66
|
|
|
@@ -70,14 +72,33 @@ Traditional HTTP clients like Axios were built over a decade ago around browser
|
|
|
70
72
|
|
|
71
73
|
1. **Zero Production Dependencies (`dependencies: {}`)**: Eliminates supply-chain risks, CVE vulnerabilities, and `node_modules` bloat.
|
|
72
74
|
2. **First-Class TypeScript & Pure JavaScript**: Strict TypeScript generics plus full CommonJS (`require`) and ES Module (`import`) support.
|
|
73
|
-
3. **Native Multi-Runtime**: 100% compliant with WHATWG standards (`fetch`, `ReadableStream`, `AbortController`). Identical behavior across React Native, Expo, iOS, Android, modern browsers, Bun, and Node.js 18+.
|
|
75
|
+
3. **Native Multi-Runtime**: 100% compliant with WHATWG standards (`fetch`, `ReadableStream`, `AbortController`). Identical behavior across React Native (New Architecture & Bridgeless), Expo, iOS, Android, modern browsers, Bun, and Node.js 18+.
|
|
74
76
|
4. **Single-Flight Auth Mutex**: Solves the classic race condition where parallel requests fail on 401s and trigger multiple refresh token calls.
|
|
75
77
|
5. **In-Flight Deduplication**: Coalesces concurrent identical `GET` requests into a single network flight with partial cancellation safety.
|
|
76
|
-
6. **Mutation-Aware LRU Cache**:
|
|
78
|
+
6. **Mutation-Aware LRU Cache & SWR**: Delivers 0ms instant cached data with background revalidation, and automatically invalidates cached GET routes whenever a mutation succeeds.
|
|
77
79
|
7. **Persistent Offline Queue**: Queues mutations offline and replays them sequentially with RFC-standard `Idempotency-Key` headers when reconnected.
|
|
78
80
|
|
|
79
81
|
---
|
|
80
82
|
|
|
83
|
+
## Feature Comparison
|
|
84
|
+
|
|
85
|
+
| Feature | `react-native-fetch-client` | Axios (`axios`) | Ky (`ky`) | Native Fetch |
|
|
86
|
+
| :--- | :---: | :---: | :---: | :---: |
|
|
87
|
+
| **Runtime Dependencies** | **0 (Zero)** | ~4-6 | **0** | **0** |
|
|
88
|
+
| **React Native New Architecture** | ✅ Native Bridgeless | ⚠️ Legacy XHR | ⚠️ Needs Polyfills | ✅ Native |
|
|
89
|
+
| **Single-Flight 401 Auth Mutex** | ✅ Built-in | ❌ Manual Code | ❌ None | ❌ None |
|
|
90
|
+
| **SWR Caching Strategy (0ms)** | ✅ Built-in | ❌ Needs Plugin | ❌ None | ❌ None |
|
|
91
|
+
| **Persistent Storage (MMKV / Storage)** | ✅ Built-in | ❌ None | ❌ None | ❌ None |
|
|
92
|
+
| **In-Flight Request Deduplication** | ✅ Built-in | ❌ Needs Plugin | ❌ None | ❌ None |
|
|
93
|
+
| **Circuit Breaker** | ✅ Built-in | ❌ None | ❌ None | ❌ None |
|
|
94
|
+
| **Auto-Pagination Helper** | ✅ Built-in | ❌ None | ❌ None | ❌ None |
|
|
95
|
+
| **Declarative Mock Adapter** | ✅ Built-in | ❌ Separate Pkg | ❌ None | ❌ None |
|
|
96
|
+
| **Go/Rust Safe Requests (`api.safe`)** | ✅ Built-in | ❌ None | ❌ None | ❌ None |
|
|
97
|
+
| **Standard Schema / Zod Validation** | ✅ Built-in | ❌ None | ❌ None | ❌ None |
|
|
98
|
+
| **Offline Mutation Queue** | ✅ Built-in | ❌ None | ❌ None | ❌ None |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
81
102
|
## Installation
|
|
82
103
|
|
|
83
104
|
```bash
|
|
@@ -214,156 +235,27 @@ const res = await api.request({
|
|
|
214
235
|
|
|
215
236
|
---
|
|
216
237
|
|
|
217
|
-
##
|
|
218
|
-
|
|
219
|
-
Options can be specified globally in `createClient(config)` or overridden per-request:
|
|
220
|
-
|
|
221
|
-
```typescript
|
|
222
|
-
const config = {
|
|
223
|
-
baseURL: 'https://api.example.com',
|
|
224
|
-
timeout: 10000, // Timeout in ms (0 = disabled)
|
|
225
|
-
headers: { 'Accept': 'application/json' },
|
|
226
|
-
params: { page: 1, limit: 20 }, // Query parameters: ?page=1&limit=20
|
|
227
|
-
responseType: 'json', // 'json' | 'text' | 'blob' | 'arrayBuffer' | 'none'
|
|
228
|
-
credentials: 'same-origin', // 'same-origin' | 'include' | 'omit'
|
|
229
|
-
redirect: 'follow', // 'follow' | 'manual' | 'error'
|
|
230
|
-
formUrlEncoded: false, // Serializes plain objects to URL-encoded form
|
|
231
|
-
signal: controller.signal, // Caller cancellation signal
|
|
232
|
-
onDownloadProgress: (event) => {}, // Streaming download callback
|
|
233
|
-
onUploadProgress: (event) => {}, // Outgoing upload progress callback
|
|
234
|
-
retry: { attempts: 3 }, // Retry policy
|
|
235
|
-
cache: { enabled: true, ttl: 30000 }, // Response cache
|
|
236
|
-
dedup: { enabled: true }, // In-flight request deduplication
|
|
237
|
-
auth: { skip: true }, // Bypass token injection for public routes
|
|
238
|
-
dispatcher: customAgent, // Undici ProxyAgent (Node.js proxies)
|
|
239
|
-
};
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
---
|
|
243
|
-
|
|
244
|
-
## Response Structure
|
|
245
|
-
|
|
246
|
-
Every successful call resolves to an `HttpResponse` object:
|
|
247
|
-
|
|
248
|
-
```javascript
|
|
249
|
-
const response = await api.get('/users');
|
|
250
|
-
|
|
251
|
-
console.log(response.data); // Parsed response body (JSON, text, or Blob)
|
|
252
|
-
console.log(response.status); // HTTP status code (e.g. 200)
|
|
253
|
-
console.log(response.statusText); // HTTP status text (e.g. "OK")
|
|
254
|
-
console.log(response.headers); // Lowercased response headers object
|
|
255
|
-
console.log(response.cached); // true if returned from local LRU cache
|
|
256
|
-
console.log(response.fromDedup); // true if shared via concurrent in-flight flight
|
|
257
|
-
console.log(response.config); // Resolved configuration used for request
|
|
258
|
-
console.log(response.request); // Native WHATWG Request object
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
---
|
|
262
|
-
|
|
263
|
-
## Error Handling
|
|
238
|
+
## Go/Rust-Style Safe Requests (`api.safe.*`)
|
|
264
239
|
|
|
265
|
-
|
|
240
|
+
Eliminate `try/catch` boilerplate. The `api.safe.*` namespace returns clean `[data, error]` result tuples where exactly one value is populated:
|
|
266
241
|
|
|
267
|
-
#### TypeScript:
|
|
268
242
|
```typescript
|
|
269
|
-
|
|
270
|
-
isHttpError,
|
|
271
|
-
isNetworkError,
|
|
272
|
-
isTimeoutError,
|
|
273
|
-
isCancelError,
|
|
274
|
-
isRetryExhaustedError,
|
|
275
|
-
} from 'react-native-fetch-client';
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
await api.get('/orders');
|
|
279
|
-
} catch (err: unknown) {
|
|
280
|
-
if (isHttpError(err)) {
|
|
281
|
-
// Non-2xx response from the server (e.g. 400, 404, 500)
|
|
282
|
-
console.error(`HTTP ${err.status}:`, err.data);
|
|
283
|
-
console.error('Headers:', err.headers);
|
|
284
|
-
} else if (isTimeoutError(err)) {
|
|
285
|
-
// Request timed out past config.timeout
|
|
286
|
-
console.error(`Timed out after ${err.timeoutMs}ms`);
|
|
287
|
-
} else if (isCancelError(err)) {
|
|
288
|
-
// Request was aborted by caller via AbortController
|
|
289
|
-
console.warn('Request canceled by caller');
|
|
290
|
-
} else if (isRetryExhaustedError(err)) {
|
|
291
|
-
// Retried N times and all attempts failed
|
|
292
|
-
console.error(`Failed after ${err.attempts} attempts. Last error:`, err.lastError);
|
|
293
|
-
} else if (isNetworkError(err)) {
|
|
294
|
-
// Offline, DNS resolution failure, or TCP connection reset
|
|
295
|
-
console.error('Network connection error:', err.message);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
```
|
|
243
|
+
const [user, error] = await api.safe.get<User>('/users/123');
|
|
299
244
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
await api.get('/orders');
|
|
306
|
-
} catch (err) {
|
|
307
|
-
if (isHttpError(err)) {
|
|
308
|
-
console.error('Server returned HTTP', err.status, err.data);
|
|
309
|
-
} else if (isTimeoutError(err)) {
|
|
310
|
-
console.error('Request timed out!');
|
|
311
|
-
} else if (isCancelError(err)) {
|
|
312
|
-
console.warn('Request was aborted.');
|
|
313
|
-
} else {
|
|
314
|
-
console.error('Network or system error:', err.message);
|
|
315
|
-
}
|
|
245
|
+
if (error) {
|
|
246
|
+
console.error(`Request failed (${error.code}):`, error.message);
|
|
247
|
+
return;
|
|
316
248
|
}
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
---
|
|
320
|
-
|
|
321
|
-
## Interceptors
|
|
322
249
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
```javascript
|
|
326
|
-
// 1. Add Request Interceptor
|
|
327
|
-
const reqId = api.interceptors.request.use(
|
|
328
|
-
(config) => {
|
|
329
|
-
config.headers = {
|
|
330
|
-
...config.headers,
|
|
331
|
-
'X-Client-Platform': 'mobile',
|
|
332
|
-
};
|
|
333
|
-
return config;
|
|
334
|
-
},
|
|
335
|
-
(error) => Promise.reject(error)
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
// 2. Add Response Interceptor
|
|
339
|
-
const resId = api.interceptors.response.use(
|
|
340
|
-
(response) => {
|
|
341
|
-
return response;
|
|
342
|
-
},
|
|
343
|
-
async (error) => {
|
|
344
|
-
if (error.status === 403) {
|
|
345
|
-
console.warn('Forbidden! Redirecting to permissions page...');
|
|
346
|
-
}
|
|
347
|
-
return Promise.reject(error);
|
|
348
|
-
}
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
// 3. Eject Interceptors
|
|
352
|
-
api.interceptors.request.eject(reqId);
|
|
353
|
-
api.interceptors.response.eject(resId);
|
|
354
|
-
|
|
355
|
-
// 4. Clear all interceptors
|
|
356
|
-
api.interceptors.request.clear();
|
|
250
|
+
// Guaranteed non-null and fully typed!
|
|
251
|
+
console.log('User loaded:', user.name);
|
|
357
252
|
```
|
|
358
253
|
|
|
359
254
|
---
|
|
360
255
|
|
|
361
256
|
## Standard Schema Validation (`schema`)
|
|
362
257
|
|
|
363
|
-
Validate
|
|
364
|
-
- [Standard Schema v1 (`~standard`)](https://standardschema.dev/) (Zod 3.24+, Valibot, ArkType)
|
|
365
|
-
- Zod-like schemas (`.parse()`, `.safeParse()`)
|
|
366
|
-
- Plain validation functions `(data) => T`
|
|
258
|
+
Validate response payloads at runtime using the **Standard Schema v1** specification, Zod, Valibot, or custom validation functions:
|
|
367
259
|
|
|
368
260
|
```typescript
|
|
369
261
|
import { z } from 'zod';
|
|
@@ -374,121 +266,117 @@ const UserSchema = z.object({
|
|
|
374
266
|
email: z.string().email(),
|
|
375
267
|
});
|
|
376
268
|
|
|
377
|
-
// Throws ValidationError
|
|
378
|
-
const
|
|
269
|
+
// Throws ValidationError with detailed issue paths if schema fails:
|
|
270
|
+
const res = await api.get('/users/1', {
|
|
379
271
|
schema: UserSchema,
|
|
380
272
|
});
|
|
381
273
|
|
|
382
|
-
console.log(
|
|
274
|
+
console.log(res.data.email);
|
|
383
275
|
```
|
|
384
276
|
|
|
385
277
|
---
|
|
386
278
|
|
|
387
|
-
##
|
|
279
|
+
## Auto-Pagination Helper (`api.paginate`)
|
|
388
280
|
|
|
389
|
-
|
|
281
|
+
Traverse multi-page APIs seamlessly using async generators (`for await`) or collect all pages into an array in one call:
|
|
390
282
|
|
|
391
283
|
```typescript
|
|
392
|
-
//
|
|
393
|
-
for await (const
|
|
394
|
-
|
|
395
|
-
body: { model: 'gpt-4o', stream: true, messages: [{ role: 'user', content: 'Hello!' }] },
|
|
284
|
+
// 1. Stream items individually with for-await (Memory efficient!)
|
|
285
|
+
for await (const user of api.paginate<User>('/users', {
|
|
286
|
+
pagination: { type: 'page', pageParam: 'page', pageSize: 20 },
|
|
396
287
|
})) {
|
|
397
|
-
console.log(
|
|
288
|
+
console.log(user.name);
|
|
398
289
|
}
|
|
290
|
+
|
|
291
|
+
// 2. Fetch all pages and flatten into an array:
|
|
292
|
+
const allPosts = await api.paginate.all<Post>('/posts', {
|
|
293
|
+
pagination: { type: 'page', pageParam: '_page', limitParam: '_limit', limit: 10 },
|
|
294
|
+
});
|
|
295
|
+
console.log(`Fetched ${allPosts.length} posts across all pages!`);
|
|
399
296
|
```
|
|
400
297
|
|
|
298
|
+
Supports 4 pagination strategies: `page`, `offset`, `cursor`, and RFC 5988/8288 standard `Link` headers.
|
|
299
|
+
|
|
401
300
|
---
|
|
402
301
|
|
|
403
|
-
##
|
|
302
|
+
## Native AI & SSE Streaming (`api.stream`)
|
|
404
303
|
|
|
405
|
-
|
|
304
|
+
Consume Server-Sent Events (SSE) and AI LLM token streams (OpenAI, Anthropic, Gemini, Ollama) using native async generators:
|
|
406
305
|
|
|
407
306
|
```typescript
|
|
408
|
-
const
|
|
307
|
+
const eventStream = api.stream('/chat/completions', {
|
|
308
|
+
method: 'POST',
|
|
309
|
+
body: {
|
|
310
|
+
model: 'gpt-4o',
|
|
311
|
+
stream: true,
|
|
312
|
+
messages: [{ role: 'user', content: 'Hello!' }],
|
|
313
|
+
},
|
|
314
|
+
});
|
|
409
315
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
316
|
+
for await (const chunk of eventStream) {
|
|
317
|
+
if (chunk.data === '[DONE]') break;
|
|
318
|
+
const parsed = JSON.parse(chunk.data);
|
|
319
|
+
process.stdout.write(parsed.choices[0]?.delta?.content ?? '');
|
|
413
320
|
}
|
|
414
|
-
|
|
415
|
-
console.log('User loaded:', user.name);
|
|
416
321
|
```
|
|
417
322
|
|
|
418
323
|
---
|
|
419
324
|
|
|
420
|
-
##
|
|
325
|
+
## Declarative Testing Mock Adapter (`createMockAdapter`)
|
|
421
326
|
|
|
422
|
-
|
|
327
|
+
Zero-dependency unit testing for your network calls. Stub routes, simulate network timeouts or offline errors, and assert call history:
|
|
423
328
|
|
|
424
329
|
```typescript
|
|
425
|
-
|
|
426
|
-
baseURL: 'https://api.example.com',
|
|
427
|
-
maxConcurrentRequests: 4, // Max 4 concurrent sockets; excess wait in FIFO queue
|
|
428
|
-
});
|
|
429
|
-
```
|
|
330
|
+
import { createClient, createMockAdapter } from 'react-native-fetch-client';
|
|
430
331
|
|
|
431
|
-
|
|
332
|
+
const api = createClient({ baseURL: 'https://api.example.com' });
|
|
333
|
+
const mock = createMockAdapter(api);
|
|
432
334
|
|
|
433
|
-
|
|
335
|
+
// 1. Mock a route
|
|
336
|
+
mock.onGet('/users/active').reply(200, [{ id: 1, name: 'Alice' }]);
|
|
434
337
|
|
|
435
|
-
|
|
338
|
+
// 2. Simulate errors
|
|
339
|
+
mock.onPost('/billing/charge').replyWithTimeout();
|
|
436
340
|
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
},
|
|
445
|
-
});
|
|
341
|
+
// 3. Test & assert history
|
|
342
|
+
const res = await api.get('/users/active');
|
|
343
|
+
expect(res.data).toEqual([{ id: 1, name: 'Alice' }]);
|
|
344
|
+
expect(mock.history.get).toHaveLength(1);
|
|
345
|
+
|
|
346
|
+
// Clean up
|
|
347
|
+
mock.restore();
|
|
446
348
|
```
|
|
447
349
|
|
|
448
350
|
---
|
|
449
351
|
|
|
450
|
-
##
|
|
352
|
+
## Response Caching, SWR & Persistent Storage
|
|
451
353
|
|
|
452
|
-
|
|
354
|
+
A complete multi-tier caching engine with in-memory LRU, persistent disk storage, Stale-While-Revalidate (SWR), and RFC 7234 conditional validation.
|
|
453
355
|
|
|
356
|
+
### 1. In-Memory LRU Cache
|
|
454
357
|
```typescript
|
|
455
358
|
const api = createClient({
|
|
456
359
|
baseURL: 'https://api.example.com',
|
|
457
360
|
cache: {
|
|
458
361
|
enabled: true,
|
|
459
|
-
|
|
362
|
+
defaultTtl: 60000, // Cache responses for 60 seconds
|
|
363
|
+
maxSize: 100, // Maximum 100 items in memory (LRU eviction)
|
|
364
|
+
allowAuthenticated: false, // Prevents leaking user-specific data across accounts
|
|
460
365
|
},
|
|
461
366
|
});
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
---
|
|
465
|
-
|
|
466
|
-
## Auto-Pagination Helper (`api.paginate`)
|
|
467
367
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
for await (const user of api.paginate<User>('/users', {
|
|
473
|
-
pagination: { type: 'page', pageParam: 'page', pageSize: 20 },
|
|
474
|
-
})) {
|
|
475
|
-
console.log(user.name);
|
|
476
|
-
}
|
|
368
|
+
// Initial request hits network; second request returns in ~0ms:
|
|
369
|
+
const r1 = await api.get('/products');
|
|
370
|
+
const r2 = await api.get('/products');
|
|
371
|
+
console.log(r2.cached); // true
|
|
477
372
|
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
});
|
|
482
|
-
console.log(`Fetched ${allPosts.length} posts across all pages!`);
|
|
373
|
+
// Automatic Invalidation on Mutation:
|
|
374
|
+
// Executing POST, PUT, PATCH, or DELETE automatically purges matching cached paths!
|
|
375
|
+
await api.post('/products', { name: 'New Item' });
|
|
483
376
|
```
|
|
484
377
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
---
|
|
488
|
-
|
|
489
|
-
## Persistent Storage Response Cache (`storage`)
|
|
490
|
-
|
|
491
|
-
Cache responses directly to mobile disk (MMKV, AsyncStorage, or localStorage) so data survives app restarts:
|
|
378
|
+
### 2. Persistent Storage Adapter (`storage`)
|
|
379
|
+
Plug directly into high-speed mobile storage (MMKV or AsyncStorage) so cached responses survive app restarts:
|
|
492
380
|
|
|
493
381
|
```typescript
|
|
494
382
|
import { MMKV } from 'react-native-mmkv';
|
|
@@ -508,16 +396,12 @@ const api = createClient({
|
|
|
508
396
|
},
|
|
509
397
|
});
|
|
510
398
|
|
|
511
|
-
//
|
|
512
|
-
// Immediate cache hit on cold app boots (0ms latency):
|
|
399
|
+
// Instant cache hit on cold app boots (0ms latency):
|
|
513
400
|
const res = await api.get('/user/profile');
|
|
514
401
|
```
|
|
515
402
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
## Stale-While-Revalidate (`strategy: 'stale-while-revalidate'`)
|
|
519
|
-
|
|
520
|
-
Return cached data instantly (**0ms latency**, `cached: true, stale: true`) to keep the UI buttery smooth, while quietly fetching the latest data from the network in the background:
|
|
403
|
+
### 3. Stale-While-Revalidate (SWR)
|
|
404
|
+
Return cached data instantly (**0ms latency**, `cached: true, stale: true`) to keep the UI responsive, while quietly fetching the latest data from the network in the background:
|
|
521
405
|
|
|
522
406
|
```typescript
|
|
523
407
|
const res = await api.get('/dashboard/stats', {
|
|
@@ -533,128 +417,65 @@ const res = await api.get('/dashboard/stats', {
|
|
|
533
417
|
console.log(res.stale); // true (Served immediately without network lag!)
|
|
534
418
|
```
|
|
535
419
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
## Declarative Testing Mock Adapter (`createMockAdapter`)
|
|
539
|
-
|
|
540
|
-
Zero-dependency unit testing for your network calls. Stub routes, simulate network timeouts or offline errors, and assert call history:
|
|
420
|
+
### 4. RFC 7234 Conditional Validation (`ETag` & `304`)
|
|
421
|
+
Automatically stores `ETag` and `Last-Modified` headers. On subsequent requests, sends `If-None-Match`. When the server returns `304 Not Modified`, reuses the cached response body while updating headers:
|
|
541
422
|
|
|
542
423
|
```typescript
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
// 1. Mock a route
|
|
549
|
-
mock.onGet('/users/active').reply(200, [{ id: 1, name: 'Alice' }]);
|
|
550
|
-
|
|
551
|
-
// 2. Simulate errors
|
|
552
|
-
mock.onPost('/billing/charge').replyWithTimeout();
|
|
553
|
-
|
|
554
|
-
// 3. Test & assert history
|
|
555
|
-
const res = await api.get('/users/active');
|
|
556
|
-
expect(res.data).toEqual([{ id: 1, name: 'Alice' }]);
|
|
557
|
-
expect(mock.history.get).toHaveLength(1);
|
|
558
|
-
|
|
559
|
-
// Clean up
|
|
560
|
-
mock.restore();
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
---
|
|
564
|
-
|
|
565
|
-
## Path Parameter Interpolation (`pathParams`)
|
|
566
|
-
|
|
567
|
-
Interpolate Express-style (`:param`) and OpenAPI-style (`{param}`) route variables with automated URI encoding:
|
|
568
|
-
|
|
569
|
-
```typescript
|
|
570
|
-
await api.get('/orgs/:orgId/projects/{projectId}/items/:itemId', {
|
|
571
|
-
pathParams: {
|
|
572
|
-
orgId: 'team-acme',
|
|
573
|
-
projectId: 'proj-123',
|
|
574
|
-
itemId: 'item#456', // Auto-encoded to item%23456
|
|
424
|
+
const api = createClient({
|
|
425
|
+
baseURL: 'https://api.example.com',
|
|
426
|
+
cache: {
|
|
427
|
+
enabled: true,
|
|
428
|
+
etag: true, // Enables conditional 304 validation
|
|
575
429
|
},
|
|
576
430
|
});
|
|
577
431
|
```
|
|
578
432
|
|
|
579
433
|
---
|
|
580
434
|
|
|
581
|
-
##
|
|
435
|
+
## Client Concurrency Throttler (`maxConcurrentRequests`)
|
|
582
436
|
|
|
583
|
-
|
|
437
|
+
Cap the maximum number of simultaneous in-flight sockets across your application to avoid mobile network congestion or connection bottlenecks:
|
|
584
438
|
|
|
585
439
|
```typescript
|
|
586
440
|
const api = createClient({
|
|
587
441
|
baseURL: 'https://api.example.com',
|
|
588
|
-
|
|
589
|
-
});
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
---
|
|
593
|
-
|
|
594
|
-
## File Upload Helper (`api.upload`)
|
|
595
|
-
|
|
596
|
-
Upload files, Blobs, ArrayBuffers, or FormDatas with automatic boundary resolution and upload progress:
|
|
597
|
-
|
|
598
|
-
```typescript
|
|
599
|
-
const avatarFile = fileInput.files[0];
|
|
600
|
-
|
|
601
|
-
const response = await api.upload('/users/avatar', avatarFile, {
|
|
602
|
-
fieldName: 'avatar',
|
|
603
|
-
fileName: 'photo.jpg',
|
|
604
|
-
extraFields: { userId: 123 },
|
|
605
|
-
onUploadProgress: (evt) => {
|
|
606
|
-
console.log(`Upload progress: ${Math.round((evt.progress ?? 0) * 100)}%`);
|
|
607
|
-
},
|
|
442
|
+
maxConcurrentRequests: 4, // Max 4 concurrent sockets; excess wait in FIFO queue
|
|
608
443
|
});
|
|
609
444
|
```
|
|
610
445
|
|
|
611
446
|
---
|
|
612
447
|
|
|
613
|
-
##
|
|
614
|
-
|
|
615
|
-
Directly return `response.data` instead of the full envelope:
|
|
616
|
-
|
|
617
|
-
```typescript
|
|
618
|
-
// Per-request unwrap:
|
|
619
|
-
const users = await api.get('/users', { unwrap: true });
|
|
620
|
-
|
|
621
|
-
// Or configure client-level unwrap:
|
|
622
|
-
const api = createClient({ unwrap: true });
|
|
623
|
-
const users = await api.get('/users'); // directly returns parsed data
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
---
|
|
627
|
-
|
|
628
|
-
## Diagnostic Debug Logger (`debug: true`)
|
|
448
|
+
## Circuit Breaker Pattern (`circuitBreaker`)
|
|
629
449
|
|
|
630
|
-
|
|
450
|
+
Prevent cascading outages when upstream services experience downtime. Automatically fails fast locally when consecutive errors exceed a configured threshold:
|
|
631
451
|
|
|
632
452
|
```typescript
|
|
633
453
|
const api = createClient({
|
|
634
454
|
baseURL: 'https://api.example.com',
|
|
635
|
-
|
|
455
|
+
circuitBreaker: {
|
|
456
|
+
enabled: true,
|
|
457
|
+
failureThreshold: 5, // Trip to OPEN after 5 consecutive failures
|
|
458
|
+
resetTimeoutMs: 30000, // Probe in HALF_OPEN state after 30 seconds
|
|
459
|
+
},
|
|
636
460
|
});
|
|
637
461
|
```
|
|
638
462
|
|
|
639
463
|
---
|
|
640
464
|
|
|
641
|
-
## In-Flight Request Deduplication
|
|
642
|
-
|
|
465
|
+
## In-Flight Request Deduplication (`dedup`)
|
|
643
466
|
|
|
644
|
-
|
|
467
|
+
Coalesces concurrent identical `GET` requests into **one single flight**, preventing duplicate calls when multiple UI components mount simultaneously:
|
|
645
468
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
```javascript
|
|
469
|
+
```typescript
|
|
649
470
|
const api = createClient({
|
|
650
471
|
baseURL: 'https://api.example.com',
|
|
651
472
|
dedup: {
|
|
652
|
-
enabled: true,
|
|
653
|
-
methods: ['GET', 'HEAD'],
|
|
473
|
+
enabled: true,
|
|
474
|
+
methods: ['GET', 'HEAD'],
|
|
654
475
|
},
|
|
655
476
|
});
|
|
656
477
|
|
|
657
|
-
// Fire 3 simultaneous calls
|
|
478
|
+
// Fire 3 simultaneous calls: exactly 1 network socket is opened!
|
|
658
479
|
const [user1, user2, user3] = await Promise.all([
|
|
659
480
|
api.get('/users/1'),
|
|
660
481
|
api.get('/users/1'),
|
|
@@ -666,80 +487,41 @@ console.log(user2.fromDedup); // true (Subscribed to in-flight flight)
|
|
|
666
487
|
console.log(user3.fromDedup); // true (Subscribed to in-flight flight)
|
|
667
488
|
```
|
|
668
489
|
|
|
669
|
-
> **Partial Cancellation Safety**: If `user2` calls `controller.abort()`, the underlying socket is **not** aborted; `user1` and `user3` still resolve normally.
|
|
670
|
-
|
|
671
490
|
---
|
|
672
491
|
|
|
673
|
-
##
|
|
674
|
-
|
|
675
|
-
Built-in in-memory LRU cache with automatic mutation invalidation and deep-clone immutability:
|
|
676
|
-
|
|
677
|
-
```javascript
|
|
678
|
-
const api = createClient({
|
|
679
|
-
baseURL: 'https://api.example.com',
|
|
680
|
-
cache: {
|
|
681
|
-
enabled: true,
|
|
682
|
-
defaultTtl: 60000, // Cache responses for 60 seconds
|
|
683
|
-
maxSize: 100, // Maximum 100 items in memory (LRU eviction)
|
|
684
|
-
allowAuthenticated: false, // Prevents leaking user-specific data across accounts
|
|
685
|
-
},
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
// 1. Initial request (hits live network)
|
|
689
|
-
const r1 = await api.get('/products');
|
|
690
|
-
console.log(r1.cached); // false
|
|
691
|
-
|
|
692
|
-
// 2. Subsequent request within 60s (returns from RAM in ~0ms)
|
|
693
|
-
const r2 = await api.get('/products');
|
|
694
|
-
console.log(r2.cached); // true
|
|
695
|
-
|
|
696
|
-
// 3. Automatic Invalidation on Mutation:
|
|
697
|
-
// Executing POST, PUT, PATCH, or DELETE automatically purges matching cached paths!
|
|
698
|
-
await api.post('/products', { name: 'New Item' });
|
|
699
|
-
|
|
700
|
-
// 4. Next GET fetches fresh data from server
|
|
701
|
-
const r3 = await api.get('/products');
|
|
702
|
-
console.log(r3.cached); // false
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
---
|
|
706
|
-
|
|
707
|
-
## Resilient Retry Engine
|
|
492
|
+
## Resilient Retry Engine (`retry`)
|
|
708
493
|
|
|
709
494
|
Automatically retries transient network errors and 5xx gateway errors with exponential backoff, full jitter, and RFC `Retry-After` header parsing:
|
|
710
495
|
|
|
711
|
-
```
|
|
496
|
+
```typescript
|
|
712
497
|
const api = createClient({
|
|
713
498
|
baseURL: 'https://api.example.com',
|
|
714
499
|
retry: {
|
|
715
500
|
attempts: 3, // Maximum 3 retry attempts
|
|
716
501
|
delay: 500, // Initial backoff delay: 500ms
|
|
717
502
|
maxDelay: 10000, // Cap delay at 10 seconds
|
|
718
|
-
backoff: 'exponential', // 'exponential' | 'linear'
|
|
503
|
+
backoff: 'exponential', // 'exponential' | 'linear'
|
|
719
504
|
jitter: true, // Applies full jitter to prevent thundering herds
|
|
720
|
-
|
|
505
|
+
retryOn: [408, 429, 500, 502, 503, 504],
|
|
506
|
+
respectRetryAfter: true, // Respects Retry-After response header
|
|
721
507
|
},
|
|
722
508
|
});
|
|
723
509
|
```
|
|
724
510
|
|
|
725
|
-
> **RFC Retry-After**: If the server responds with `429 Too Many Requests` and a header `Retry-After: 5` or `Retry-After: Wed, 21 Oct 2026 07:28:00 GMT`, the client automatically pauses for the specified duration before retrying.
|
|
726
|
-
|
|
727
511
|
---
|
|
728
512
|
|
|
729
|
-
## Auth & Token Refresh Mutex
|
|
513
|
+
## Auth & Token Refresh Mutex (`auth`)
|
|
730
514
|
|
|
731
|
-
Solves race
|
|
515
|
+
Solves the race condition where parallel requests fail on 401s and trigger multiple refresh token calls simultaneously:
|
|
732
516
|
|
|
733
|
-
```
|
|
517
|
+
```typescript
|
|
734
518
|
let currentAccessToken = 'initial_token';
|
|
735
519
|
|
|
736
520
|
const api = createClient({
|
|
737
521
|
baseURL: 'https://api.example.com',
|
|
522
|
+
bearerToken: () => currentAccessToken,
|
|
738
523
|
auth: {
|
|
739
|
-
//
|
|
740
|
-
getToken: () => currentAccessToken,
|
|
741
|
-
|
|
742
|
-
// 2. Single-flight mutex refresh callback:
|
|
524
|
+
// Single-flight mutex refresh callback:
|
|
743
525
|
// If 10 requests fail with 401 simultaneously, refreshToken is called EXACTLY ONCE.
|
|
744
526
|
// The other 9 requests wait, then replay with the new token.
|
|
745
527
|
refreshToken: async () => {
|
|
@@ -748,68 +530,107 @@ const api = createClient({
|
|
|
748
530
|
currentAccessToken = data.access_token;
|
|
749
531
|
return currentAccessToken;
|
|
750
532
|
},
|
|
751
|
-
|
|
533
|
+
refreshOn: [401],
|
|
534
|
+
refreshExcludeUrls: ['/oauth/refresh', '/auth/login'],
|
|
752
535
|
headerName: 'Authorization',
|
|
753
|
-
|
|
536
|
+
tokenPrefix: 'Bearer ',
|
|
754
537
|
},
|
|
755
538
|
});
|
|
756
539
|
```
|
|
757
540
|
|
|
758
541
|
---
|
|
759
542
|
|
|
760
|
-
##
|
|
543
|
+
## Bearer Token Preset (`bearerToken`)
|
|
761
544
|
|
|
762
|
-
|
|
545
|
+
Avoid manual interceptors for token attachment. Pass a static string or an asynchronous token getter:
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
const api = createClient({
|
|
549
|
+
baseURL: 'https://api.example.com',
|
|
550
|
+
bearerToken: async () => await secureStorage.getItem('access_token'),
|
|
551
|
+
});
|
|
552
|
+
```
|
|
763
553
|
|
|
764
|
-
|
|
554
|
+
---
|
|
765
555
|
|
|
766
|
-
|
|
767
|
-
const fileBlob = new Blob(['sample data'], { type: 'text/plain' });
|
|
556
|
+
## Path Parameter Interpolation (`pathParams`)
|
|
768
557
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
558
|
+
Interpolate Express-style (`:param`) and OpenAPI-style (`{param}`) route variables with automated URI encoding:
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
await api.get('/orgs/:orgId/projects/{projectId}/items/:itemId', {
|
|
562
|
+
pathParams: {
|
|
563
|
+
orgId: 'team-acme',
|
|
564
|
+
projectId: 'proj-123',
|
|
565
|
+
itemId: 'item#456', // Auto-encoded to item%23456
|
|
773
566
|
},
|
|
774
567
|
});
|
|
775
568
|
```
|
|
776
569
|
|
|
777
|
-
|
|
570
|
+
---
|
|
778
571
|
|
|
779
|
-
|
|
572
|
+
## File Upload Helper (`api.upload`)
|
|
780
573
|
|
|
781
|
-
|
|
574
|
+
Upload files, Blobs, ArrayBuffers, or FormDatas with automatic boundary resolution:
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
const avatarFile = fileInput.files[0];
|
|
578
|
+
|
|
579
|
+
const response = await api.upload('/users/avatar', avatarFile, {
|
|
580
|
+
fieldName: 'avatar',
|
|
581
|
+
fileName: 'photo.jpg',
|
|
582
|
+
additionalFields: { userId: 123 },
|
|
583
|
+
onUploadProgress: (evt) => {
|
|
584
|
+
console.log(`Upload progress: ${evt.percent}%`);
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
## Upload & Download Progress
|
|
592
|
+
|
|
593
|
+
Monitor real-time progress for both uploads and downloads:
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
// 1. Upload Progress
|
|
597
|
+
await api.post('/upload', fileBlob, {
|
|
598
|
+
onUploadProgress: (event) => {
|
|
599
|
+
console.log(`Uploaded ${event.loaded} / ${event.total} bytes (${event.percent}%)`);
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// 2. Download Progress
|
|
782
604
|
await api.get('/downloads/big-file.zip', {
|
|
783
605
|
responseType: 'blob',
|
|
784
606
|
onDownloadProgress: (event) => {
|
|
785
|
-
console.log(`
|
|
607
|
+
console.log(`Downloaded ${event.loaded} / ${event.total} bytes (${event.percent}%)`);
|
|
786
608
|
},
|
|
787
609
|
});
|
|
788
610
|
```
|
|
789
611
|
|
|
790
612
|
---
|
|
791
613
|
|
|
792
|
-
## Offline Request Queue
|
|
614
|
+
## Offline Request Queue (`createOfflineQueue`)
|
|
793
615
|
|
|
794
616
|
Queue mutations when the device is offline and replay them deterministically when connectivity returns:
|
|
795
617
|
|
|
796
|
-
```
|
|
797
|
-
|
|
618
|
+
```typescript
|
|
619
|
+
import { createClient, createOfflineQueue, MemoryStorageAdapter } from 'react-native-fetch-client';
|
|
798
620
|
|
|
799
621
|
const api = createClient({ baseURL: 'https://api.example.com' });
|
|
800
622
|
|
|
801
|
-
// Initialize queue with any storage engine (AsyncStorage, localStorage, IndexedDB)
|
|
802
623
|
const queue = createOfflineQueue({
|
|
803
624
|
client: api,
|
|
804
|
-
storage: new MemoryStorageAdapter(), // Or
|
|
805
|
-
maxQueueSize: 100,
|
|
806
|
-
maxAttempts: 5,
|
|
807
|
-
idempotencyHeader: 'Idempotency-Key'
|
|
625
|
+
storage: new MemoryStorageAdapter(), // Or MMKV / AsyncStorage adapter
|
|
626
|
+
maxQueueSize: 100,
|
|
627
|
+
maxAttempts: 5,
|
|
628
|
+
idempotencyHeader: 'Idempotency-Key', // Automatically attaches UUID idempotency key
|
|
808
629
|
onReplaySuccess: (req) => {
|
|
809
630
|
console.log(`Synced offline mutation: ${req.method} ${req.url}`);
|
|
810
631
|
},
|
|
811
632
|
onPermanentFailure: (req, err) => {
|
|
812
|
-
console.error(`Mutation failed permanently: ${req.url}`, err
|
|
633
|
+
console.error(`Mutation failed permanently: ${req.url}`, err);
|
|
813
634
|
},
|
|
814
635
|
});
|
|
815
636
|
|
|
@@ -825,13 +646,39 @@ const summary = await queue.process();
|
|
|
825
646
|
console.log(`Replay complete: ${summary.succeeded} succeeded, ${summary.remaining} remaining.`);
|
|
826
647
|
```
|
|
827
648
|
|
|
828
|
-
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## Response Unwrapping (`unwrap: true`)
|
|
652
|
+
|
|
653
|
+
Directly return `response.data` instead of the full envelope:
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
// Per-request unwrap:
|
|
657
|
+
const users = await api.get('/users', { unwrap: true });
|
|
658
|
+
|
|
659
|
+
// Or configure client-level unwrap:
|
|
660
|
+
const api = createClient({ unwrap: true });
|
|
661
|
+
const users = await api.get('/users'); // directly returns parsed data
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## Diagnostic Debug Logger (`debug: true`)
|
|
667
|
+
|
|
668
|
+
Log outgoing requests with execution durations, status codes, and automatically redacted authentication tokens:
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
const api = createClient({
|
|
672
|
+
baseURL: 'https://api.example.com',
|
|
673
|
+
debug: true, // or custom logger callback: (entry) => sendToTelemetry(entry)
|
|
674
|
+
});
|
|
675
|
+
```
|
|
829
676
|
|
|
830
677
|
---
|
|
831
678
|
|
|
832
679
|
## Node.js Enterprise Proxies & Dispatcher
|
|
833
680
|
|
|
834
|
-
In Node.js 18+ environments, pass an Undici `ProxyAgent` to route requests through corporate forward proxies
|
|
681
|
+
In Node.js 18+ environments, pass an Undici `ProxyAgent` to route requests through corporate forward proxies:
|
|
835
682
|
|
|
836
683
|
```javascript
|
|
837
684
|
const { ProxyAgent } = require('undici');
|
|
@@ -858,49 +705,10 @@ Switching from Axios takes only a few minutes. Most method signatures are identi
|
|
|
858
705
|
| `axios.isAxiosError(err)` | `isHttpError(err)` or `isFetchNativeError(err)` | Type-safe guards |
|
|
859
706
|
| `axios-retry` (plugin) | `client = createClient({ retry: {...} })` | Built-in |
|
|
860
707
|
| `axios-cache-interceptor` (plugin) | `client = createClient({ cache: {...} })` | Built-in |
|
|
861
|
-
|
|
862
|
-
---
|
|
863
|
-
|
|
864
|
-
## Testing Locally with Your Team
|
|
865
|
-
|
|
866
|
-
To test this library within your team before publishing to public npm:
|
|
867
|
-
|
|
868
|
-
### Option 1: Pack as a Local `.tgz` Tarball
|
|
869
|
-
```bash
|
|
870
|
-
# In react-native-fetch-client:
|
|
871
|
-
npm run build
|
|
872
|
-
npm pack
|
|
873
|
-
# Generates react-native-fetch-client-0.1.0.tgz
|
|
874
|
-
```
|
|
875
|
-
Share `react-native-fetch-client-0.1.0.tgz` with your teammate. In their project, they install:
|
|
876
|
-
```bash
|
|
877
|
-
npm install ./path/to/react-native-fetch-client-0.1.0.tgz
|
|
878
|
-
```
|
|
879
|
-
|
|
880
|
-
### Option 2: Local `npm link`
|
|
881
|
-
```bash
|
|
882
|
-
# In react-native-fetch-client:
|
|
883
|
-
npm link
|
|
884
|
-
|
|
885
|
-
# In teammate's project:
|
|
886
|
-
npm link react-native-fetch-client
|
|
887
|
-
```
|
|
888
|
-
|
|
889
|
-
---
|
|
890
|
-
|
|
891
|
-
## Verification & Quality Gates
|
|
892
|
-
|
|
893
|
-
The codebase enforces strict quality checks:
|
|
894
|
-
|
|
895
|
-
```bash
|
|
896
|
-
npm run typecheck # Strict TypeScript check (0 errors)
|
|
897
|
-
npm test # 13 Vitest suites, 143 unit/integration tests (0 failures)
|
|
898
|
-
npm run lint # ESLint 10 verification
|
|
899
|
-
npm run build # Dual ESM & CJS build via tsup
|
|
900
|
-
```
|
|
708
|
+
| `axios-mock-adapter` (package) | `mock = createMockAdapter(client)` | Built-in |
|
|
901
709
|
|
|
902
710
|
---
|
|
903
711
|
|
|
904
712
|
## License
|
|
905
713
|
|
|
906
|
-
[MIT](LICENSE) © 2026
|
|
714
|
+
[MIT](LICENSE) © 2026 Arunkumar Arjunan
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-fetch-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2-Beta",
|
|
4
4
|
"description": "A lightweight, Fetch-native HTTP client for TypeScript, React Native, Expo, browsers, and Node.js",
|
|
5
|
-
"author": "",
|
|
5
|
+
"author": "Arunkumar Arjunan",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"sideEffects": false,
|
|
7
8
|
"keywords": [
|
|
8
9
|
"fetch",
|
|
9
10
|
"http",
|