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.
Files changed (2) hide show
  1. package/README.md +238 -430
  2. 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: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
7
  [![TypeScript & JavaScript](https://img.shields.io/badge/Languages-TypeScript%20%7C%20JavaScript-blue)](https://www.typescriptlang.org/)
8
8
  [![Runtime Dependencies](https://img.shields.io/badge/Dependencies-0-success.svg)](package.json)
9
9
  [![Tests: 186 passing](https://img.shields.io/badge/Tests-186%20Passing-brightgreen.svg)](test)
10
+ [![Expo & React Native](https://img.shields.io/badge/React%20Native-Bridgeless%20%26%20New%20Arch%20Ready-success.svg)](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
- - [Go/Rust-Style Safe Requests (`api.safe.*`)](#gorust-style-safe-requests-apisafe)
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
- - [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)
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
- - [Testing Locally with Your Team](#testing-locally-with-your-team)
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**: Automatically invalidates cached GET routes whenever a POST/PUT/PATCH/DELETE mutation succeeds.
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
- ## 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
238
+ ## Go/Rust-Style Safe Requests (`api.safe.*`)
264
239
 
265
- Errors thrown by `react-native-fetch-client` are typed subclasses of `FetchNativeError`. Type guard functions are exported for safe discrimination:
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
- 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
- ```
243
+ const [user, error] = await api.safe.get<User>('/users/123');
299
244
 
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
- }
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
- 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 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`
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 if response payload does not conform to the schema:
378
- const response = await api.get('/user/me', {
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(response.data.email); // Fully typed and runtime-validated!
274
+ console.log(res.data.email);
383
275
  ```
384
276
 
385
277
  ---
386
278
 
387
- ## Native AI & SSE Streaming (`api.stream`)
279
+ ## Auto-Pagination Helper (`api.paginate`)
388
280
 
389
- Stream Server-Sent Events (SSE) or chunked LLM completions line-by-line using standard WHATWG `ReadableStream` and `AsyncGenerator`:
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
- // 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!' }] },
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(`Event: ${chunk.event}, Data: ${chunk.data}`);
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
- ## Go/Rust-Style Safe Requests (`api.safe.*`)
302
+ ## Native AI & SSE Streaming (`api.stream`)
404
303
 
405
- Eliminate `try/catch` boilerplate. The `api.safe.*` namespace returns clean `[data, error]` result tuples:
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 [user, error] = await api.safe.get<User>('/users/123');
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
- if (error) {
411
- console.error(`Request failed (${error.code}):`, error.message);
412
- return;
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
- ## Client Concurrency Throttler (`maxConcurrentRequests`)
325
+ ## Declarative Testing Mock Adapter (`createMockAdapter`)
421
326
 
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:
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
- const api = createClient({
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
- ## Circuit Breaker Pattern (`circuitBreaker`)
335
+ // 1. Mock a route
336
+ mock.onGet('/users/active').reply(200, [{ id: 1, name: 'Alice' }]);
434
337
 
435
- Prevent cascading outages when upstream microservices experience downtime. Automatically fails fast locally when consecutive errors exceed a configured threshold:
338
+ // 2. Simulate errors
339
+ mock.onPost('/billing/charge').replyWithTimeout();
436
340
 
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
- });
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
- ## RFC 7234 Conditional Caching (`ETag` & `304 Not Modified`)
352
+ ## Response Caching, SWR & Persistent Storage
451
353
 
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:
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
- etag: true, // Enables conditional 304 validation
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
- Traverse multi-page APIs seamlessly using async generators (`for await`) or collect all pages directly:
469
-
470
- ```typescript
471
- // 1. Stream items individually with for-await (Memory efficient!)
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
- // 2. Fetch all pages and flatten into an array in one call:
479
- const allPosts = await api.paginate.all<Post>('/posts', {
480
- pagination: { type: 'page', pageParam: '_page', limitParam: '_limit', limit: 10 },
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
- Supports 4 pagination strategies: `page`, `offset`, `cursor`, and RFC 5988/8288 standard `Link` headers.
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
- // Automatically hydrates from MMKV on startup!
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
- import { createClient, createMockAdapter } from 'react-native-fetch-client';
544
-
545
- const api = createClient({ baseURL: 'https://api.example.com' });
546
- const mock = createMockAdapter(api);
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
- ## Bearer Token Preset (`bearerToken`)
435
+ ## Client Concurrency Throttler (`maxConcurrentRequests`)
582
436
 
583
- Avoid manual interceptors for token attachment. Pass a static string or an asynchronous token getter:
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
- bearerToken: async () => await secureStorage.getItem('access_token'),
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
- ## Response Unwrapping (`unwrap: true`)
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
- Log outgoing requests with execution durations, status codes, and automatically redacted authentication tokens:
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
- debug: true, // or custom logger callback: (entry) => sendToDatadog(entry)
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
- 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.
467
+ Coalesces concurrent identical `GET` requests into **one single flight**, preventing duplicate calls when multiple UI components mount simultaneously:
645
468
 
646
- `react-native-fetch-client` coalesces them into **one single flight**:
647
-
648
- ```javascript
469
+ ```typescript
649
470
  const api = createClient({
650
471
  baseURL: 'https://api.example.com',
651
472
  dedup: {
652
- enabled: true, // Enable in-flight deduplication
653
- methods: ['GET', 'HEAD'], // Methods eligible for deduplication
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
- ## Response Caching (LRU)
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
- ```javascript
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' | 'constant'
503
+ backoff: 'exponential', // 'exponential' | 'linear'
719
504
  jitter: true, // Applies full jitter to prevent thundering herds
720
- statusCodes: [408, 429, 500, 502, 503, 504],
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 conditions when multiple parallel requests receive 401s on expired access tokens:
515
+ Solves the race condition where parallel requests fail on 401s and trigger multiple refresh token calls simultaneously:
732
516
 
733
- ```javascript
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
- // 1. Returns current token (called before each request)
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
- tokenType: 'Bearer',
536
+ tokenPrefix: 'Bearer ',
754
537
  },
755
538
  });
756
539
  ```
757
540
 
758
541
  ---
759
542
 
760
- ## Upload & Download Progress
543
+ ## Bearer Token Preset (`bearerToken`)
761
544
 
762
- Monitor real-time progress for both uploads and downloads:
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
- ### 1. Upload Progress (`onUploadProgress`)
554
+ ---
765
555
 
766
- ```javascript
767
- const fileBlob = new Blob(['sample data'], { type: 'text/plain' });
556
+ ## Path Parameter Interpolation (`pathParams`)
768
557
 
769
- await api.post('/upload', fileBlob, {
770
- onUploadProgress: (event) => {
771
- const percent = Math.round((event.loaded / event.total) * 100);
772
- console.log(`Uploaded ${event.loaded} / ${event.total} bytes (${percent}%)`);
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
- ### 2. Streaming Download Progress (`onDownloadProgress`)
570
+ ---
778
571
 
779
- Uses WHATWG `ReadableStream` to stream chunks without buffering the whole response in memory:
572
+ ## File Upload Helper (`api.upload`)
780
573
 
781
- ```javascript
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(`Received chunk: ${event.bytes} bytes. Total loaded: ${event.loaded} bytes`);
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
- ```javascript
797
- const { createClient, createOfflineQueue, MemoryStorageAdapter } = require('react-native-fetch-client');
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 custom AsyncStorage adapter
805
- maxQueueSize: 100, // Cap at 100 queued items
806
- maxAttempts: 5, // Drop poison-pills after 5 attempts
807
- idempotencyHeader: 'Idempotency-Key',// Automatically attaches UUID 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.message);
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
- > **Financial Endpoint Guardrail**: Requests targeting `/pay`, `/checkout`, `/orders`, `/charge`, or `/billing` are strictly rejected from offline queuing by default to prevent unexpected delayed charges.
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 or custom mTLS agents:
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.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",