react-native-fetch-client 0.1.0 → 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 +274 -367
- package/package.json +6 -5
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
|
|
|
@@ -14,51 +15,52 @@
|
|
|
14
15
|
|
|
15
16
|
For in-depth explanations, execution pipeline flows, and implementation guides, explore the dedicated documentation suite:
|
|
16
17
|
|
|
17
|
-
* 🏛️ **[Architecture & Execution Pipeline Flow](
|
|
18
|
-
* 🛠️ **[Core Features & Usage Guide](
|
|
19
|
-
* ⚡ **[Caching & Stale-While-Revalidate](
|
|
20
|
-
* 🛡️ **[Resilience, Retries & Offline Queue](
|
|
21
|
-
* 🤖 **[Streaming, AI & File Uploads](
|
|
22
|
-
* 🔑 **[Authentication & Security](
|
|
23
|
-
* 🔄 **[Auto-Pagination Helper](
|
|
24
|
-
* 🧪 **[Testing & Mock Adapter Guide](
|
|
18
|
+
* 🏛️ **[Architecture & Execution Pipeline Flow](https://github.com/arunkumarArjun/react-native-fetch-client/blob/main/docs/architecture-flow.md)** — Step-by-step trace through the 16-stage request pipeline.
|
|
19
|
+
* 🛠️ **[Core Features & Usage Guide](https://github.com/arunkumarArjun/react-native-fetch-client/blob/main/docs/core-features-guide.md)** — Client setup, HTTP methods, path params, form encoding, safe requests, and unwrap.
|
|
20
|
+
* ⚡ **[Caching & Stale-While-Revalidate](https://github.com/arunkumarArjun/react-native-fetch-client/blob/main/docs/caching-and-swr.md)** — In-memory LRU, persistent storage adapters (MMKV/AsyncStorage), ETag/304, and SWR.
|
|
21
|
+
* 🛡️ **[Resilience, Retries & Offline Queue](https://github.com/arunkumarArjun/react-native-fetch-client/blob/main/docs/resilience-and-offline.md)** — Exponential backoff, jitter, circuit breakers, concurrency throttling, and offline replay.
|
|
22
|
+
* 🤖 **[Streaming, AI & File Uploads](https://github.com/arunkumarArjun/react-native-fetch-client/blob/main/docs/streaming-and-uploads.md)** — SSE & AI LLM token streams, WHATWG streams, file uploads, and upload/download progress.
|
|
23
|
+
* 🔑 **[Authentication & Security](https://github.com/arunkumarArjun/react-native-fetch-client/blob/main/docs/auth-and-security.md)** — Bearer presets, single-flight 401 refresh mutex, token rotation, and header redaction.
|
|
24
|
+
* 🔄 **[Auto-Pagination Helper](https://github.com/arunkumarArjun/react-native-fetch-client/blob/main/docs/auto-pagination.md)** — Traversal strategies (`page`, `offset`, `cursor`, `linkHeader`) and async generator streaming.
|
|
25
|
+
* 🧪 **[Testing & Mock Adapter Guide](https://github.com/arunkumarArjun/react-native-fetch-client/blob/main/docs/testing-and-mocking.md)** — Declarative unit testing (`createMockAdapter`), route stubbing, and call assertions.
|
|
25
26
|
|
|
26
27
|
---
|
|
27
28
|
|
|
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
|
-
##
|
|
238
|
+
## Go/Rust-Style Safe Requests (`api.safe.*`)
|
|
218
239
|
|
|
219
|
-
|
|
240
|
+
Eliminate `try/catch` boilerplate. The `api.safe.*` namespace returns clean `[data, error]` result tuples where exactly one value is populated:
|
|
220
241
|
|
|
221
242
|
```typescript
|
|
222
|
-
const
|
|
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
|
|
264
|
-
|
|
265
|
-
Errors thrown by `react-native-fetch-client` are typed subclasses of `FetchNativeError`. Type guard functions are exported for safe discrimination:
|
|
243
|
+
const [user, error] = await api.safe.get<User>('/users/123');
|
|
266
244
|
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
}
|
|
245
|
+
if (error) {
|
|
246
|
+
console.error(`Request failed (${error.code}):`, error.message);
|
|
247
|
+
return;
|
|
297
248
|
}
|
|
298
|
-
```
|
|
299
249
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const { isHttpError, isTimeoutError, isCancelError } = require('react-native-fetch-client');
|
|
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
|
-
}
|
|
316
|
-
}
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
---
|
|
320
|
-
|
|
321
|
-
## Interceptors
|
|
322
|
-
|
|
323
|
-
Interceptors allow you to transform outgoing requests or inspect/recover from responses:
|
|
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,188 +266,216 @@ 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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
330
|
+
import { createClient, createMockAdapter } from 'react-native-fetch-client';
|
|
331
|
+
|
|
332
|
+
const api = createClient({ baseURL: 'https://api.example.com' });
|
|
333
|
+
const mock = createMockAdapter(api);
|
|
334
|
+
|
|
335
|
+
// 1. Mock a route
|
|
336
|
+
mock.onGet('/users/active').reply(200, [{ id: 1, name: 'Alice' }]);
|
|
337
|
+
|
|
338
|
+
// 2. Simulate errors
|
|
339
|
+
mock.onPost('/billing/charge').replyWithTimeout();
|
|
340
|
+
|
|
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();
|
|
429
348
|
```
|
|
430
349
|
|
|
431
350
|
---
|
|
432
351
|
|
|
433
|
-
##
|
|
352
|
+
## Response Caching, SWR & Persistent Storage
|
|
434
353
|
|
|
435
|
-
|
|
354
|
+
A complete multi-tier caching engine with in-memory LRU, persistent disk storage, Stale-While-Revalidate (SWR), and RFC 7234 conditional validation.
|
|
436
355
|
|
|
356
|
+
### 1. In-Memory LRU Cache
|
|
437
357
|
```typescript
|
|
438
358
|
const api = createClient({
|
|
439
359
|
baseURL: 'https://api.example.com',
|
|
440
|
-
|
|
360
|
+
cache: {
|
|
441
361
|
enabled: true,
|
|
442
|
-
|
|
443
|
-
|
|
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
|
|
444
365
|
},
|
|
445
366
|
});
|
|
446
|
-
```
|
|
447
367
|
|
|
448
|
-
|
|
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
|
|
449
372
|
|
|
450
|
-
|
|
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' });
|
|
376
|
+
```
|
|
451
377
|
|
|
452
|
-
|
|
378
|
+
### 2. Persistent Storage Adapter (`storage`)
|
|
379
|
+
Plug directly into high-speed mobile storage (MMKV or AsyncStorage) so cached responses survive app restarts:
|
|
453
380
|
|
|
454
381
|
```typescript
|
|
382
|
+
import { MMKV } from 'react-native-mmkv';
|
|
383
|
+
|
|
384
|
+
const mmkv = new MMKV({ id: 'http-cache' });
|
|
385
|
+
|
|
455
386
|
const api = createClient({
|
|
456
387
|
baseURL: 'https://api.example.com',
|
|
457
388
|
cache: {
|
|
458
389
|
enabled: true,
|
|
459
|
-
|
|
390
|
+
storage: {
|
|
391
|
+
getItem: (key) => mmkv.getString(key) ?? null,
|
|
392
|
+
setItem: (key, value) => mmkv.set(key, value),
|
|
393
|
+
removeItem: (key) => mmkv.delete(key),
|
|
394
|
+
},
|
|
395
|
+
defaultTtl: 300_000, // 5 minutes
|
|
460
396
|
},
|
|
461
397
|
});
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
---
|
|
465
398
|
|
|
466
|
-
|
|
399
|
+
// Instant cache hit on cold app boots (0ms latency):
|
|
400
|
+
const res = await api.get('/user/profile');
|
|
401
|
+
```
|
|
467
402
|
|
|
468
|
-
|
|
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:
|
|
469
405
|
|
|
470
406
|
```typescript
|
|
471
|
-
await api.get('/
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
407
|
+
const res = await api.get('/dashboard/stats', {
|
|
408
|
+
cache: {
|
|
409
|
+
strategy: 'stale-while-revalidate',
|
|
410
|
+
onRevalidated: (freshResponse) => {
|
|
411
|
+
console.log('Background sync complete! Updated data:', freshResponse.data);
|
|
412
|
+
updateUI(freshResponse.data);
|
|
413
|
+
},
|
|
476
414
|
},
|
|
477
415
|
});
|
|
416
|
+
|
|
417
|
+
console.log(res.stale); // true (Served immediately without network lag!)
|
|
478
418
|
```
|
|
479
419
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
## Bearer Token Preset (`bearerToken`)
|
|
483
|
-
|
|
484
|
-
Avoid manual interceptors for token attachment. Pass a static string or an asynchronous token getter:
|
|
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:
|
|
485
422
|
|
|
486
423
|
```typescript
|
|
487
424
|
const api = createClient({
|
|
488
425
|
baseURL: 'https://api.example.com',
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
---
|
|
494
|
-
|
|
495
|
-
## File Upload Helper (`api.upload`)
|
|
496
|
-
|
|
497
|
-
Upload files, Blobs, ArrayBuffers, or FormDatas with automatic boundary resolution and upload progress:
|
|
498
|
-
|
|
499
|
-
```typescript
|
|
500
|
-
const avatarFile = fileInput.files[0];
|
|
501
|
-
|
|
502
|
-
const response = await api.upload('/users/avatar', avatarFile, {
|
|
503
|
-
fieldName: 'avatar',
|
|
504
|
-
fileName: 'photo.jpg',
|
|
505
|
-
extraFields: { userId: 123 },
|
|
506
|
-
onUploadProgress: (evt) => {
|
|
507
|
-
console.log(`Upload progress: ${Math.round((evt.progress ?? 0) * 100)}%`);
|
|
426
|
+
cache: {
|
|
427
|
+
enabled: true,
|
|
428
|
+
etag: true, // Enables conditional 304 validation
|
|
508
429
|
},
|
|
509
430
|
});
|
|
510
431
|
```
|
|
511
432
|
|
|
512
433
|
---
|
|
513
434
|
|
|
514
|
-
##
|
|
435
|
+
## Client Concurrency Throttler (`maxConcurrentRequests`)
|
|
515
436
|
|
|
516
|
-
|
|
437
|
+
Cap the maximum number of simultaneous in-flight sockets across your application to avoid mobile network congestion or connection bottlenecks:
|
|
517
438
|
|
|
518
439
|
```typescript
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const api = createClient({ unwrap: true });
|
|
524
|
-
const users = await api.get('/users'); // directly returns parsed data
|
|
440
|
+
const api = createClient({
|
|
441
|
+
baseURL: 'https://api.example.com',
|
|
442
|
+
maxConcurrentRequests: 4, // Max 4 concurrent sockets; excess wait in FIFO queue
|
|
443
|
+
});
|
|
525
444
|
```
|
|
526
445
|
|
|
527
446
|
---
|
|
528
447
|
|
|
529
|
-
##
|
|
448
|
+
## Circuit Breaker Pattern (`circuitBreaker`)
|
|
530
449
|
|
|
531
|
-
|
|
450
|
+
Prevent cascading outages when upstream services experience downtime. Automatically fails fast locally when consecutive errors exceed a configured threshold:
|
|
532
451
|
|
|
533
452
|
```typescript
|
|
534
453
|
const api = createClient({
|
|
535
454
|
baseURL: 'https://api.example.com',
|
|
536
|
-
|
|
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
|
+
},
|
|
537
460
|
});
|
|
538
461
|
```
|
|
539
462
|
|
|
540
463
|
---
|
|
541
464
|
|
|
542
|
-
## In-Flight Request Deduplication
|
|
543
|
-
|
|
465
|
+
## In-Flight Request Deduplication (`dedup`)
|
|
544
466
|
|
|
545
|
-
|
|
467
|
+
Coalesces concurrent identical `GET` requests into **one single flight**, preventing duplicate calls when multiple UI components mount simultaneously:
|
|
546
468
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
```javascript
|
|
469
|
+
```typescript
|
|
550
470
|
const api = createClient({
|
|
551
471
|
baseURL: 'https://api.example.com',
|
|
552
472
|
dedup: {
|
|
553
|
-
enabled: true,
|
|
554
|
-
methods: ['GET', 'HEAD'],
|
|
473
|
+
enabled: true,
|
|
474
|
+
methods: ['GET', 'HEAD'],
|
|
555
475
|
},
|
|
556
476
|
});
|
|
557
477
|
|
|
558
|
-
// Fire 3 simultaneous calls
|
|
478
|
+
// Fire 3 simultaneous calls: exactly 1 network socket is opened!
|
|
559
479
|
const [user1, user2, user3] = await Promise.all([
|
|
560
480
|
api.get('/users/1'),
|
|
561
481
|
api.get('/users/1'),
|
|
@@ -567,80 +487,41 @@ console.log(user2.fromDedup); // true (Subscribed to in-flight flight)
|
|
|
567
487
|
console.log(user3.fromDedup); // true (Subscribed to in-flight flight)
|
|
568
488
|
```
|
|
569
489
|
|
|
570
|
-
> **Partial Cancellation Safety**: If `user2` calls `controller.abort()`, the underlying socket is **not** aborted; `user1` and `user3` still resolve normally.
|
|
571
|
-
|
|
572
490
|
---
|
|
573
491
|
|
|
574
|
-
##
|
|
575
|
-
|
|
576
|
-
Built-in in-memory LRU cache with automatic mutation invalidation and deep-clone immutability:
|
|
577
|
-
|
|
578
|
-
```javascript
|
|
579
|
-
const api = createClient({
|
|
580
|
-
baseURL: 'https://api.example.com',
|
|
581
|
-
cache: {
|
|
582
|
-
enabled: true,
|
|
583
|
-
defaultTtl: 60000, // Cache responses for 60 seconds
|
|
584
|
-
maxSize: 100, // Maximum 100 items in memory (LRU eviction)
|
|
585
|
-
allowAuthenticated: false, // Prevents leaking user-specific data across accounts
|
|
586
|
-
},
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
// 1. Initial request (hits live network)
|
|
590
|
-
const r1 = await api.get('/products');
|
|
591
|
-
console.log(r1.cached); // false
|
|
592
|
-
|
|
593
|
-
// 2. Subsequent request within 60s (returns from RAM in ~0ms)
|
|
594
|
-
const r2 = await api.get('/products');
|
|
595
|
-
console.log(r2.cached); // true
|
|
596
|
-
|
|
597
|
-
// 3. Automatic Invalidation on Mutation:
|
|
598
|
-
// Executing POST, PUT, PATCH, or DELETE automatically purges matching cached paths!
|
|
599
|
-
await api.post('/products', { name: 'New Item' });
|
|
600
|
-
|
|
601
|
-
// 4. Next GET fetches fresh data from server
|
|
602
|
-
const r3 = await api.get('/products');
|
|
603
|
-
console.log(r3.cached); // false
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
---
|
|
607
|
-
|
|
608
|
-
## Resilient Retry Engine
|
|
492
|
+
## Resilient Retry Engine (`retry`)
|
|
609
493
|
|
|
610
494
|
Automatically retries transient network errors and 5xx gateway errors with exponential backoff, full jitter, and RFC `Retry-After` header parsing:
|
|
611
495
|
|
|
612
|
-
```
|
|
496
|
+
```typescript
|
|
613
497
|
const api = createClient({
|
|
614
498
|
baseURL: 'https://api.example.com',
|
|
615
499
|
retry: {
|
|
616
500
|
attempts: 3, // Maximum 3 retry attempts
|
|
617
501
|
delay: 500, // Initial backoff delay: 500ms
|
|
618
502
|
maxDelay: 10000, // Cap delay at 10 seconds
|
|
619
|
-
backoff: 'exponential', // 'exponential' | 'linear'
|
|
503
|
+
backoff: 'exponential', // 'exponential' | 'linear'
|
|
620
504
|
jitter: true, // Applies full jitter to prevent thundering herds
|
|
621
|
-
|
|
505
|
+
retryOn: [408, 429, 500, 502, 503, 504],
|
|
506
|
+
respectRetryAfter: true, // Respects Retry-After response header
|
|
622
507
|
},
|
|
623
508
|
});
|
|
624
509
|
```
|
|
625
510
|
|
|
626
|
-
> **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.
|
|
627
|
-
|
|
628
511
|
---
|
|
629
512
|
|
|
630
|
-
## Auth & Token Refresh Mutex
|
|
513
|
+
## Auth & Token Refresh Mutex (`auth`)
|
|
631
514
|
|
|
632
|
-
Solves race
|
|
515
|
+
Solves the race condition where parallel requests fail on 401s and trigger multiple refresh token calls simultaneously:
|
|
633
516
|
|
|
634
|
-
```
|
|
517
|
+
```typescript
|
|
635
518
|
let currentAccessToken = 'initial_token';
|
|
636
519
|
|
|
637
520
|
const api = createClient({
|
|
638
521
|
baseURL: 'https://api.example.com',
|
|
522
|
+
bearerToken: () => currentAccessToken,
|
|
639
523
|
auth: {
|
|
640
|
-
//
|
|
641
|
-
getToken: () => currentAccessToken,
|
|
642
|
-
|
|
643
|
-
// 2. Single-flight mutex refresh callback:
|
|
524
|
+
// Single-flight mutex refresh callback:
|
|
644
525
|
// If 10 requests fail with 401 simultaneously, refreshToken is called EXACTLY ONCE.
|
|
645
526
|
// The other 9 requests wait, then replay with the new token.
|
|
646
527
|
refreshToken: async () => {
|
|
@@ -649,68 +530,107 @@ const api = createClient({
|
|
|
649
530
|
currentAccessToken = data.access_token;
|
|
650
531
|
return currentAccessToken;
|
|
651
532
|
},
|
|
652
|
-
|
|
533
|
+
refreshOn: [401],
|
|
534
|
+
refreshExcludeUrls: ['/oauth/refresh', '/auth/login'],
|
|
653
535
|
headerName: 'Authorization',
|
|
654
|
-
|
|
536
|
+
tokenPrefix: 'Bearer ',
|
|
655
537
|
},
|
|
656
538
|
});
|
|
657
539
|
```
|
|
658
540
|
|
|
659
541
|
---
|
|
660
542
|
|
|
661
|
-
##
|
|
543
|
+
## Bearer Token Preset (`bearerToken`)
|
|
662
544
|
|
|
663
|
-
|
|
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
|
+
```
|
|
664
553
|
|
|
665
|
-
|
|
554
|
+
---
|
|
666
555
|
|
|
667
|
-
|
|
668
|
-
const fileBlob = new Blob(['sample data'], { type: 'text/plain' });
|
|
556
|
+
## Path Parameter Interpolation (`pathParams`)
|
|
669
557
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## File Upload Helper (`api.upload`)
|
|
573
|
+
|
|
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}%`);
|
|
674
585
|
},
|
|
675
586
|
});
|
|
676
587
|
```
|
|
677
588
|
|
|
678
|
-
|
|
589
|
+
---
|
|
679
590
|
|
|
680
|
-
|
|
591
|
+
## Upload & Download Progress
|
|
681
592
|
|
|
682
|
-
|
|
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
|
|
683
604
|
await api.get('/downloads/big-file.zip', {
|
|
684
605
|
responseType: 'blob',
|
|
685
606
|
onDownloadProgress: (event) => {
|
|
686
|
-
console.log(`
|
|
607
|
+
console.log(`Downloaded ${event.loaded} / ${event.total} bytes (${event.percent}%)`);
|
|
687
608
|
},
|
|
688
609
|
});
|
|
689
610
|
```
|
|
690
611
|
|
|
691
612
|
---
|
|
692
613
|
|
|
693
|
-
## Offline Request Queue
|
|
614
|
+
## Offline Request Queue (`createOfflineQueue`)
|
|
694
615
|
|
|
695
616
|
Queue mutations when the device is offline and replay them deterministically when connectivity returns:
|
|
696
617
|
|
|
697
|
-
```
|
|
698
|
-
|
|
618
|
+
```typescript
|
|
619
|
+
import { createClient, createOfflineQueue, MemoryStorageAdapter } from 'react-native-fetch-client';
|
|
699
620
|
|
|
700
621
|
const api = createClient({ baseURL: 'https://api.example.com' });
|
|
701
622
|
|
|
702
|
-
// Initialize queue with any storage engine (AsyncStorage, localStorage, IndexedDB)
|
|
703
623
|
const queue = createOfflineQueue({
|
|
704
624
|
client: api,
|
|
705
|
-
storage: new MemoryStorageAdapter(), // Or
|
|
706
|
-
maxQueueSize: 100,
|
|
707
|
-
maxAttempts: 5,
|
|
708
|
-
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
|
|
709
629
|
onReplaySuccess: (req) => {
|
|
710
630
|
console.log(`Synced offline mutation: ${req.method} ${req.url}`);
|
|
711
631
|
},
|
|
712
632
|
onPermanentFailure: (req, err) => {
|
|
713
|
-
console.error(`Mutation failed permanently: ${req.url}`, err
|
|
633
|
+
console.error(`Mutation failed permanently: ${req.url}`, err);
|
|
714
634
|
},
|
|
715
635
|
});
|
|
716
636
|
|
|
@@ -726,13 +646,39 @@ const summary = await queue.process();
|
|
|
726
646
|
console.log(`Replay complete: ${summary.succeeded} succeeded, ${summary.remaining} remaining.`);
|
|
727
647
|
```
|
|
728
648
|
|
|
729
|
-
|
|
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
|
+
```
|
|
730
676
|
|
|
731
677
|
---
|
|
732
678
|
|
|
733
679
|
## Node.js Enterprise Proxies & Dispatcher
|
|
734
680
|
|
|
735
|
-
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:
|
|
736
682
|
|
|
737
683
|
```javascript
|
|
738
684
|
const { ProxyAgent } = require('undici');
|
|
@@ -759,49 +705,10 @@ Switching from Axios takes only a few minutes. Most method signatures are identi
|
|
|
759
705
|
| `axios.isAxiosError(err)` | `isHttpError(err)` or `isFetchNativeError(err)` | Type-safe guards |
|
|
760
706
|
| `axios-retry` (plugin) | `client = createClient({ retry: {...} })` | Built-in |
|
|
761
707
|
| `axios-cache-interceptor` (plugin) | `client = createClient({ cache: {...} })` | Built-in |
|
|
762
|
-
|
|
763
|
-
---
|
|
764
|
-
|
|
765
|
-
## Testing Locally with Your Team
|
|
766
|
-
|
|
767
|
-
To test this library within your team before publishing to public npm:
|
|
768
|
-
|
|
769
|
-
### Option 1: Pack as a Local `.tgz` Tarball
|
|
770
|
-
```bash
|
|
771
|
-
# In react-native-fetch-client:
|
|
772
|
-
npm run build
|
|
773
|
-
npm pack
|
|
774
|
-
# Generates react-native-fetch-client-0.1.0.tgz
|
|
775
|
-
```
|
|
776
|
-
Share `react-native-fetch-client-0.1.0.tgz` with your teammate. In their project, they install:
|
|
777
|
-
```bash
|
|
778
|
-
npm install ./path/to/react-native-fetch-client-0.1.0.tgz
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
### Option 2: Local `npm link`
|
|
782
|
-
```bash
|
|
783
|
-
# In react-native-fetch-client:
|
|
784
|
-
npm link
|
|
785
|
-
|
|
786
|
-
# In teammate's project:
|
|
787
|
-
npm link react-native-fetch-client
|
|
788
|
-
```
|
|
789
|
-
|
|
790
|
-
---
|
|
791
|
-
|
|
792
|
-
## Verification & Quality Gates
|
|
793
|
-
|
|
794
|
-
The codebase enforces strict quality checks:
|
|
795
|
-
|
|
796
|
-
```bash
|
|
797
|
-
npm run typecheck # Strict TypeScript check (0 errors)
|
|
798
|
-
npm test # 13 Vitest suites, 143 unit/integration tests (0 failures)
|
|
799
|
-
npm run lint # ESLint 10 verification
|
|
800
|
-
npm run build # Dual ESM & CJS build via tsup
|
|
801
|
-
```
|
|
708
|
+
| `axios-mock-adapter` (package) | `mock = createMockAdapter(client)` | Built-in |
|
|
802
709
|
|
|
803
710
|
---
|
|
804
711
|
|
|
805
712
|
## License
|
|
806
713
|
|
|
807
|
-
[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",
|
|
@@ -16,11 +17,11 @@
|
|
|
16
17
|
],
|
|
17
18
|
"repository": {
|
|
18
19
|
"type": "git",
|
|
19
|
-
"url": "https://github.com/
|
|
20
|
+
"url": "git+https://github.com/arunkumarArjun/react-native-fetch-client.git"
|
|
20
21
|
},
|
|
21
|
-
"homepage": "https://github.com/
|
|
22
|
+
"homepage": "https://github.com/arunkumarArjun/react-native-fetch-client#readme",
|
|
22
23
|
"bugs": {
|
|
23
|
-
"url": "https://github.com/
|
|
24
|
+
"url": "https://github.com/arunkumarArjun/react-native-fetch-client/issues"
|
|
24
25
|
},
|
|
25
26
|
|
|
26
27
|
"engines": {
|