react-native-fetch-client 0.0.0-stage → 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 CHANGED
@@ -1,3 +1,807 @@
1
- # Temporary Holding Version
1
+ # react-native-fetch-client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
5
+
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+ [![TypeScript & JavaScript](https://img.shields.io/badge/Languages-TypeScript%20%7C%20JavaScript-blue)](https://www.typescriptlang.org/)
8
+ [![Runtime Dependencies](https://img.shields.io/badge/Dependencies-0-success.svg)](package.json)
9
+ [![Tests: 186 passing](https://img.shields.io/badge/Tests-186%20Passing-brightgreen.svg)](test)
10
+
11
+ ---
12
+
13
+ ## 📖 Developer Documentation Guides
14
+
15
+ For in-depth explanations, execution pipeline flows, and implementation guides, explore the dedicated documentation suite:
16
+
17
+ * 🏛️ **[Architecture & Execution Pipeline Flow](./docs/architecture-flow.md)** — Step-by-step trace through the 16-stage request pipeline.
18
+ * 🛠️ **[Core Features & Usage Guide](./docs/core-features-guide.md)** — Client setup, HTTP methods, path params, form encoding, safe requests, and unwrap.
19
+ * ⚡ **[Caching & Stale-While-Revalidate](./docs/caching-and-swr.md)** — In-memory LRU, persistent storage adapters (MMKV/AsyncStorage), ETag/304, and SWR.
20
+ * 🛡️ **[Resilience, Retries & Offline Queue](./docs/resilience-and-offline.md)** — Exponential backoff, jitter, circuit breakers, concurrency throttling, and offline replay.
21
+ * 🤖 **[Streaming, AI & File Uploads](./docs/streaming-and-uploads.md)** — SSE & AI LLM token streams, WHATWG streams, file uploads, and upload/download progress.
22
+ * 🔑 **[Authentication & Security](./docs/auth-and-security.md)** — Bearer presets, single-flight 401 refresh mutex, token rotation, and header redaction.
23
+ * 🔄 **[Auto-Pagination Helper](./docs/auto-pagination.md)** — Traversal strategies (`page`, `offset`, `cursor`, `linkHeader`) and async generator streaming.
24
+ * 🧪 **[Testing & Mock Adapter Guide](./docs/testing-and-mocking.md)** — Declarative unit testing (`createMockAdapter`), route stubbing, and call assertions.
25
+
26
+ ---
27
+
28
+ ## Table of Contents
29
+
30
+ - [Why react-native-fetch-client?](#why-react-native-fetch-client)
31
+ - [Installation](#installation)
32
+ - [Developer Documentation Guides](#-developer-documentation-guides)
33
+ - [Quick Start (TypeScript & JavaScript)](#quick-start)
34
+ - [TypeScript (ESM)](#1-typescript-esm)
35
+ - [JavaScript (ES Modules - `import`)](#2-javascript-es-modules---import)
36
+ - [JavaScript (CommonJS - `require`)](#3-javascript-commonjs---require)
37
+ - [HTTP Methods & Shorthands](#http-methods--shorthands)
38
+ - [Standard Schema Validation (`schema`)](#standard-schema-validation-schema)
39
+ - [Auto-Pagination Helper (`api.paginate`)](#auto-pagination-helper-apipaginate)
40
+ - [Native AI & SSE Streaming (`api.stream`)](#native-ai--sse-streaming-apistream)
41
+ - [Go/Rust-Style Safe Requests (`api.safe.*`)](#gorust-style-safe-requests-apisafe)
42
+ - [Client Concurrency Throttler (`maxConcurrentRequests`)](#client-concurrency-throttler-maxconcurrentrequests)
43
+ - [Circuit Breaker Pattern (`circuitBreaker`)](#circuit-breaker-pattern-circuitbreaker)
44
+ - [RFC 7234 Conditional Caching (`ETag` & `304 Not Modified`)](#rfc-7234-conditional-caching-etag--304-not-modified)
45
+ - [Persistent Storage Response Cache (`storage`)](#persistent-storage-response-cache-storage)
46
+ - [Stale-While-Revalidate (`strategy: 'stale-while-revalidate'`)](#stale-while-revalidate-strategy-stale-while-revalidate)
47
+ - [Declarative Testing Mock Adapter (`createMockAdapter`)](#declarative-testing-mock-adapter-createmockadapter)
48
+ - [Path Parameter Interpolation (`pathParams`)](#path-parameter-interpolation-pathparams)
49
+ - [Bearer Token Preset (`bearerToken`)](#bearer-token-preset-bearertoken)
50
+ - [File Upload Helper (`api.upload`)](#file-upload-helper-apiupload)
51
+ - [Response Unwrapping (`unwrap: true`)](#response-unwrapping-unwrap-true)
52
+ - [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
+ - [Node.js Enterprise Proxies & Dispatcher](#nodejs-enterprise-proxies--dispatcher)
60
+ - [Migration Guide (From Axios)](#migration-guide-from-axios)
61
+ - [Testing Locally with Your Team](#testing-locally-with-your-team)
62
+
63
+ ---
64
+
65
+ ## Why react-native-fetch-client?
66
+
67
+ Traditional HTTP clients like Axios were built over a decade ago around browser `XMLHttpRequest` (XHR), requiring polyfills and bulky dependencies on React Native, Expo, and modern Edge runtimes. Moreover, adding retries, token refresh mutexes, and caching requires stitching together 4 to 6 separate third-party packages.
68
+
69
+ `react-native-fetch-client` provides the complete modern networking stack in **one single, zero-dependency package**:
70
+
71
+ 1. **Zero Production Dependencies (`dependencies: {}`)**: Eliminates supply-chain risks, CVE vulnerabilities, and `node_modules` bloat.
72
+ 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+.
74
+ 4. **Single-Flight Auth Mutex**: Solves the classic race condition where parallel requests fail on 401s and trigger multiple refresh token calls.
75
+ 5. **In-Flight Deduplication**: Coalesces concurrent identical `GET` requests into a single network flight with partial cancellation safety.
76
+ 6. **Mutation-Aware LRU Cache**: Automatically invalidates cached GET routes whenever a POST/PUT/PATCH/DELETE mutation succeeds.
77
+ 7. **Persistent Offline Queue**: Queues mutations offline and replays them sequentially with RFC-standard `Idempotency-Key` headers when reconnected.
78
+
79
+ ---
80
+
81
+ ## Installation
82
+
83
+ ```bash
84
+ npm install react-native-fetch-client
85
+ ```
86
+
87
+ Or using other package managers:
88
+ ```bash
89
+ yarn add react-native-fetch-client
90
+ pnpm add react-native-fetch-client
91
+ bun add react-native-fetch-client
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Quick Start
97
+
98
+ ### 1. TypeScript (ESM)
99
+
100
+ ```typescript
101
+ import { createClient } from 'react-native-fetch-client';
102
+
103
+ interface User {
104
+ id: number;
105
+ name: string;
106
+ email: string;
107
+ }
108
+
109
+ const api = createClient({
110
+ baseURL: 'https://api.example.com',
111
+ timeout: 10_000,
112
+ });
113
+
114
+ // Strongly typed GET request
115
+ const response = await api.get<User[]>('/users');
116
+ console.log(response.status); // 200
117
+ console.log(response.data); // User[]
118
+
119
+ // Strongly typed POST request
120
+ const created = await api.post<User>('/users', {
121
+ name: 'Jane Doe',
122
+ email: 'jane@example.com',
123
+ });
124
+ console.log('Created ID:', created.data.id);
125
+ ```
126
+
127
+ ### 2. JavaScript (ES Modules - `import`)
128
+
129
+ Works in modern frontend projects (React, Next.js, Vue, Vite) and Node.js (`"type": "module"`):
130
+
131
+ ```javascript
132
+ import { createClient } from 'react-native-fetch-client';
133
+
134
+ const api = createClient({
135
+ baseURL: 'https://api.example.com',
136
+ timeout: 10000,
137
+ });
138
+
139
+ async function main() {
140
+ const res = await api.get('/users');
141
+ console.log(res.data);
142
+
143
+ const newUser = await api.post('/users', {
144
+ name: 'Jane Doe',
145
+ email: 'jane@example.com',
146
+ });
147
+ console.log('Created user:', newUser.data);
148
+ }
149
+
150
+ main();
151
+ ```
152
+
153
+ ### 3. JavaScript (CommonJS - `require`)
154
+
155
+ Works in standard Node.js scripts, Express backends, and CommonJS projects:
156
+
157
+ ```javascript
158
+ const { createClient, isHttpError } = require('react-native-fetch-client');
159
+
160
+ const api = createClient({
161
+ baseURL: 'https://api.example.com',
162
+ timeout: 10000,
163
+ });
164
+
165
+ async function main() {
166
+ try {
167
+ const res = await api.get('/users');
168
+ console.log(res.data);
169
+ } catch (err) {
170
+ if (isHttpError(err)) {
171
+ console.error('HTTP Error:', err.status, err.data);
172
+ }
173
+ }
174
+ }
175
+
176
+ main();
177
+ ```
178
+
179
+ ---
180
+
181
+ ## HTTP Methods & Shorthands
182
+
183
+ Available in both TypeScript and JavaScript:
184
+
185
+ ```typescript
186
+ // GET
187
+ const res = await api.get(url, config);
188
+
189
+ // POST
190
+ const res = await api.post(url, body, config);
191
+
192
+ // PUT
193
+ const res = await api.put(url, body, config);
194
+
195
+ // PATCH
196
+ const res = await api.patch(url, body, config);
197
+
198
+ // DELETE
199
+ const res = await api.delete(url, config);
200
+
201
+ // HEAD
202
+ const res = await api.head(url, config);
203
+
204
+ // OPTIONS
205
+ const res = await api.options(url, config);
206
+
207
+ // Generic Request
208
+ const res = await api.request({
209
+ url: '/custom',
210
+ method: 'POST',
211
+ body: { foo: 'bar' },
212
+ });
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Request Configuration
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
264
+
265
+ Errors thrown by `react-native-fetch-client` are typed subclasses of `FetchNativeError`. Type guard functions are exported for safe discrimination:
266
+
267
+ #### TypeScript:
268
+ ```typescript
269
+ import {
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
+ ```
299
+
300
+ #### JavaScript:
301
+ ```javascript
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();
357
+ ```
358
+
359
+ ---
360
+
361
+ ## Standard Schema Validation (`schema`)
362
+
363
+ Validate and parse incoming response payloads directly at runtime with **zero dependencies**. `react-native-fetch-client` uses duck-typing to seamlessly support:
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`
367
+
368
+ ```typescript
369
+ import { z } from 'zod';
370
+
371
+ const UserSchema = z.object({
372
+ id: z.number(),
373
+ name: z.string(),
374
+ email: z.string().email(),
375
+ });
376
+
377
+ // Throws ValidationError if response payload does not conform to the schema:
378
+ const response = await api.get('/user/me', {
379
+ schema: UserSchema,
380
+ });
381
+
382
+ console.log(response.data.email); // Fully typed and runtime-validated!
383
+ ```
384
+
385
+ ---
386
+
387
+ ## Native AI & SSE Streaming (`api.stream`)
388
+
389
+ Stream Server-Sent Events (SSE) or chunked LLM completions line-by-line using standard WHATWG `ReadableStream` and `AsyncGenerator`:
390
+
391
+ ```typescript
392
+ // Connects to SSE endpoint with Accept: text/event-stream
393
+ for await (const chunk of api.stream('/v1/chat/completions', {
394
+ method: 'POST',
395
+ body: { model: 'gpt-4o', stream: true, messages: [{ role: 'user', content: 'Hello!' }] },
396
+ })) {
397
+ console.log(`Event: ${chunk.event}, Data: ${chunk.data}`);
398
+ }
399
+ ```
400
+
401
+ ---
402
+
403
+ ## Go/Rust-Style Safe Requests (`api.safe.*`)
404
+
405
+ Eliminate `try/catch` boilerplate. The `api.safe.*` namespace returns clean `[data, error]` result tuples:
406
+
407
+ ```typescript
408
+ const [user, error] = await api.safe.get<User>('/users/123');
409
+
410
+ if (error) {
411
+ console.error(`Request failed (${error.code}):`, error.message);
412
+ return;
413
+ }
414
+
415
+ console.log('User loaded:', user.name);
416
+ ```
417
+
418
+ ---
419
+
420
+ ## Client Concurrency Throttler (`maxConcurrentRequests`)
421
+
422
+ Cap the maximum number of simultaneous in-flight sockets across your entire application to avoid browser connection limits (HTTP/1.1 6-socket bottleneck) or mobile network congestion:
423
+
424
+ ```typescript
425
+ const api = createClient({
426
+ baseURL: 'https://api.example.com',
427
+ maxConcurrentRequests: 4, // Max 4 concurrent sockets; excess wait in FIFO queue
428
+ });
429
+ ```
430
+
431
+ ---
432
+
433
+ ## Circuit Breaker Pattern (`circuitBreaker`)
434
+
435
+ Prevent cascading outages when upstream microservices experience downtime. Automatically fails fast locally when consecutive errors exceed a configured threshold:
436
+
437
+ ```typescript
438
+ const api = createClient({
439
+ baseURL: 'https://api.example.com',
440
+ circuitBreaker: {
441
+ enabled: true,
442
+ failureThreshold: 5, // Trip to OPEN after 5 consecutive 5xx/network failures
443
+ resetTimeoutMs: 30000, // Probe in HALF_OPEN state after 30 seconds
444
+ },
445
+ });
446
+ ```
447
+
448
+ ---
449
+
450
+ ## RFC 7234 Conditional Caching (`ETag` & `304 Not Modified`)
451
+
452
+ Automatically stores `ETag` and `Last-Modified` headers. On subsequent requests, sends `If-None-Match` and `If-Modified-Since`. When the server returns `304 Not Modified`, reuses the cached response body while updating response headers:
453
+
454
+ ```typescript
455
+ const api = createClient({
456
+ baseURL: 'https://api.example.com',
457
+ cache: {
458
+ enabled: true,
459
+ etag: true, // Enables conditional 304 validation
460
+ },
461
+ });
462
+ ```
463
+
464
+ ---
465
+
466
+ ## Path Parameter Interpolation (`pathParams`)
467
+
468
+ Interpolate Express-style (`:param`) and OpenAPI-style (`{param}`) route variables with automated URI encoding:
469
+
470
+ ```typescript
471
+ await api.get('/orgs/:orgId/projects/{projectId}/items/:itemId', {
472
+ pathParams: {
473
+ orgId: 'team-acme',
474
+ projectId: 'proj-123',
475
+ itemId: 'item#456', // Auto-encoded to item%23456
476
+ },
477
+ });
478
+ ```
479
+
480
+ ---
481
+
482
+ ## Bearer Token Preset (`bearerToken`)
483
+
484
+ Avoid manual interceptors for token attachment. Pass a static string or an asynchronous token getter:
485
+
486
+ ```typescript
487
+ const api = createClient({
488
+ baseURL: 'https://api.example.com',
489
+ bearerToken: async () => await secureStorage.getItem('access_token'),
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)}%`);
508
+ },
509
+ });
510
+ ```
511
+
512
+ ---
513
+
514
+ ## Response Unwrapping (`unwrap: true`)
515
+
516
+ Directly return `response.data` instead of the full envelope:
517
+
518
+ ```typescript
519
+ // Per-request unwrap:
520
+ const users = await api.get('/users', { unwrap: true });
521
+
522
+ // Or configure client-level unwrap:
523
+ const api = createClient({ unwrap: true });
524
+ const users = await api.get('/users'); // directly returns parsed data
525
+ ```
526
+
527
+ ---
528
+
529
+ ## Diagnostic Debug Logger (`debug: true`)
530
+
531
+ Log outgoing requests with execution durations, status codes, and automatically redacted authentication tokens:
532
+
533
+ ```typescript
534
+ const api = createClient({
535
+ baseURL: 'https://api.example.com',
536
+ debug: true, // or custom logger callback: (entry) => sendToDatadog(entry)
537
+ });
538
+ ```
539
+
540
+ ---
541
+
542
+ ## In-Flight Request Deduplication
543
+
544
+
545
+ When multiple UI components mount simultaneously and request the exact same endpoint (e.g. `GET /api/me`), standard clients fire multiple HTTP requests over the wire.
546
+
547
+ `react-native-fetch-client` coalesces them into **one single flight**:
548
+
549
+ ```javascript
550
+ const api = createClient({
551
+ baseURL: 'https://api.example.com',
552
+ dedup: {
553
+ enabled: true, // Enable in-flight deduplication
554
+ methods: ['GET', 'HEAD'], // Methods eligible for deduplication
555
+ },
556
+ });
557
+
558
+ // Fire 3 simultaneous calls
559
+ const [user1, user2, user3] = await Promise.all([
560
+ api.get('/users/1'),
561
+ api.get('/users/1'),
562
+ api.get('/users/1'),
563
+ ]);
564
+
565
+ console.log(user1.fromDedup); // false (Primary network socket)
566
+ console.log(user2.fromDedup); // true (Subscribed to in-flight flight)
567
+ console.log(user3.fromDedup); // true (Subscribed to in-flight flight)
568
+ ```
569
+
570
+ > **Partial Cancellation Safety**: If `user2` calls `controller.abort()`, the underlying socket is **not** aborted; `user1` and `user3` still resolve normally.
571
+
572
+ ---
573
+
574
+ ## Response Caching (LRU)
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
609
+
610
+ Automatically retries transient network errors and 5xx gateway errors with exponential backoff, full jitter, and RFC `Retry-After` header parsing:
611
+
612
+ ```javascript
613
+ const api = createClient({
614
+ baseURL: 'https://api.example.com',
615
+ retry: {
616
+ attempts: 3, // Maximum 3 retry attempts
617
+ delay: 500, // Initial backoff delay: 500ms
618
+ maxDelay: 10000, // Cap delay at 10 seconds
619
+ backoff: 'exponential', // 'exponential' | 'linear' | 'constant'
620
+ jitter: true, // Applies full jitter to prevent thundering herds
621
+ statusCodes: [408, 429, 500, 502, 503, 504],
622
+ },
623
+ });
624
+ ```
625
+
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
+ ---
629
+
630
+ ## Auth & Token Refresh Mutex
631
+
632
+ Solves race conditions when multiple parallel requests receive 401s on expired access tokens:
633
+
634
+ ```javascript
635
+ let currentAccessToken = 'initial_token';
636
+
637
+ const api = createClient({
638
+ baseURL: 'https://api.example.com',
639
+ auth: {
640
+ // 1. Returns current token (called before each request)
641
+ getToken: () => currentAccessToken,
642
+
643
+ // 2. Single-flight mutex refresh callback:
644
+ // If 10 requests fail with 401 simultaneously, refreshToken is called EXACTLY ONCE.
645
+ // The other 9 requests wait, then replay with the new token.
646
+ refreshToken: async () => {
647
+ const res = await fetch('https://api.example.com/oauth/refresh', { method: 'POST' });
648
+ const data = await res.json();
649
+ currentAccessToken = data.access_token;
650
+ return currentAccessToken;
651
+ },
652
+
653
+ headerName: 'Authorization',
654
+ tokenType: 'Bearer',
655
+ },
656
+ });
657
+ ```
658
+
659
+ ---
660
+
661
+ ## Upload & Download Progress
662
+
663
+ Monitor real-time progress for both uploads and downloads:
664
+
665
+ ### 1. Upload Progress (`onUploadProgress`)
666
+
667
+ ```javascript
668
+ const fileBlob = new Blob(['sample data'], { type: 'text/plain' });
669
+
670
+ await api.post('/upload', fileBlob, {
671
+ onUploadProgress: (event) => {
672
+ const percent = Math.round((event.loaded / event.total) * 100);
673
+ console.log(`Uploaded ${event.loaded} / ${event.total} bytes (${percent}%)`);
674
+ },
675
+ });
676
+ ```
677
+
678
+ ### 2. Streaming Download Progress (`onDownloadProgress`)
679
+
680
+ Uses WHATWG `ReadableStream` to stream chunks without buffering the whole response in memory:
681
+
682
+ ```javascript
683
+ await api.get('/downloads/big-file.zip', {
684
+ responseType: 'blob',
685
+ onDownloadProgress: (event) => {
686
+ console.log(`Received chunk: ${event.bytes} bytes. Total loaded: ${event.loaded} bytes`);
687
+ },
688
+ });
689
+ ```
690
+
691
+ ---
692
+
693
+ ## Offline Request Queue
694
+
695
+ Queue mutations when the device is offline and replay them deterministically when connectivity returns:
696
+
697
+ ```javascript
698
+ const { createClient, createOfflineQueue, MemoryStorageAdapter } = require('react-native-fetch-client');
699
+
700
+ const api = createClient({ baseURL: 'https://api.example.com' });
701
+
702
+ // Initialize queue with any storage engine (AsyncStorage, localStorage, IndexedDB)
703
+ const queue = createOfflineQueue({
704
+ client: api,
705
+ storage: new MemoryStorageAdapter(), // Or custom AsyncStorage adapter
706
+ maxQueueSize: 100, // Cap at 100 queued items
707
+ maxAttempts: 5, // Drop poison-pills after 5 attempts
708
+ idempotencyHeader: 'Idempotency-Key',// Automatically attaches UUID idempotency key
709
+ onReplaySuccess: (req) => {
710
+ console.log(`Synced offline mutation: ${req.method} ${req.url}`);
711
+ },
712
+ onPermanentFailure: (req, err) => {
713
+ console.error(`Mutation failed permanently: ${req.url}`, err.message);
714
+ },
715
+ });
716
+
717
+ // 1. Enqueue action when network is lost:
718
+ await queue.enqueue({
719
+ url: '/notes',
720
+ method: 'POST',
721
+ body: { title: 'Drafted while offline' },
722
+ });
723
+
724
+ // 2. Replay queue sequentially when back online:
725
+ const summary = await queue.process();
726
+ console.log(`Replay complete: ${summary.succeeded} succeeded, ${summary.remaining} remaining.`);
727
+ ```
728
+
729
+ > **Financial Endpoint Guardrail**: Requests targeting `/pay`, `/checkout`, `/orders`, `/charge`, or `/billing` are strictly rejected from offline queuing by default to prevent unexpected delayed charges.
730
+
731
+ ---
732
+
733
+ ## Node.js Enterprise Proxies & Dispatcher
734
+
735
+ In Node.js 18+ environments, pass an Undici `ProxyAgent` to route requests through corporate forward proxies or custom mTLS agents:
736
+
737
+ ```javascript
738
+ const { ProxyAgent } = require('undici');
739
+ const { createClient } = require('react-native-fetch-client');
740
+
741
+ const proxyClient = createClient({
742
+ baseURL: 'https://internal.corp',
743
+ dispatcher: new ProxyAgent('http://corporate-proxy.internal:8080'),
744
+ });
745
+ ```
746
+
747
+ ---
748
+
749
+ ## Migration Guide (From Axios)
750
+
751
+ Switching from Axios takes only a few minutes. Most method signatures are identical:
752
+
753
+ | Axios | `react-native-fetch-client` | Notes |
754
+ | :--- | :--- | :--- |
755
+ | `axios.get(url, config)` | `client.get(url, config)` | Identical |
756
+ | `axios.post(url, data, config)` | `client.post(url, data, config)` | Identical |
757
+ | `res.data` | `res.data` | Identical |
758
+ | `axios.interceptors.request.use()` | `client.interceptors.request.use()` | Identical |
759
+ | `axios.isAxiosError(err)` | `isHttpError(err)` or `isFetchNativeError(err)` | Type-safe guards |
760
+ | `axios-retry` (plugin) | `client = createClient({ retry: {...} })` | Built-in |
761
+ | `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
+ ```
802
+
803
+ ---
804
+
805
+ ## License
806
+
807
+ [MIT](LICENSE) © 2026