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.
Files changed (2) hide show
  1. package/README.md +274 -367
  2. 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: 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
 
@@ -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](./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.
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
- - [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
238
+ ## Go/Rust-Style Safe Requests (`api.safe.*`)
218
239
 
219
- Options can be specified globally in `createClient(config)` or overridden per-request:
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 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:
243
+ const [user, error] = await api.safe.get<User>('/users/123');
266
244
 
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
- }
245
+ if (error) {
246
+ console.error(`Request failed (${error.code}):`, error.message);
247
+ return;
297
248
  }
298
- ```
299
249
 
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();
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,188 +266,216 @@ 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
- });
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
- ## Circuit Breaker Pattern (`circuitBreaker`)
352
+ ## Response Caching, SWR & Persistent Storage
434
353
 
435
- Prevent cascading outages when upstream microservices experience downtime. Automatically fails fast locally when consecutive errors exceed a configured threshold:
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
- circuitBreaker: {
360
+ cache: {
441
361
  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
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
- ## RFC 7234 Conditional Caching (`ETag` & `304 Not Modified`)
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
- 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:
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
- etag: true, // Enables conditional 304 validation
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
- ## Path Parameter Interpolation (`pathParams`)
399
+ // Instant cache hit on cold app boots (0ms latency):
400
+ const res = await api.get('/user/profile');
401
+ ```
467
402
 
468
- Interpolate Express-style (`:param`) and OpenAPI-style (`{param}`) route variables with automated URI encoding:
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('/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
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
- 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)}%`);
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
- ## Response Unwrapping (`unwrap: true`)
435
+ ## Client Concurrency Throttler (`maxConcurrentRequests`)
515
436
 
516
- Directly return `response.data` instead of the full envelope:
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
- // 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
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
- ## Diagnostic Debug Logger (`debug: true`)
448
+ ## Circuit Breaker Pattern (`circuitBreaker`)
530
449
 
531
- 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:
532
451
 
533
452
  ```typescript
534
453
  const api = createClient({
535
454
  baseURL: 'https://api.example.com',
536
- 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
+ },
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
- 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:
546
468
 
547
- `react-native-fetch-client` coalesces them into **one single flight**:
548
-
549
- ```javascript
469
+ ```typescript
550
470
  const api = createClient({
551
471
  baseURL: 'https://api.example.com',
552
472
  dedup: {
553
- enabled: true, // Enable in-flight deduplication
554
- methods: ['GET', 'HEAD'], // Methods eligible for deduplication
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
- ## 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
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
- ```javascript
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' | 'constant'
503
+ backoff: 'exponential', // 'exponential' | 'linear'
620
504
  jitter: true, // Applies full jitter to prevent thundering herds
621
- statusCodes: [408, 429, 500, 502, 503, 504],
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 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:
633
516
 
634
- ```javascript
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
- // 1. Returns current token (called before each request)
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
- tokenType: 'Bearer',
536
+ tokenPrefix: 'Bearer ',
655
537
  },
656
538
  });
657
539
  ```
658
540
 
659
541
  ---
660
542
 
661
- ## Upload & Download Progress
543
+ ## Bearer Token Preset (`bearerToken`)
662
544
 
663
- 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
+ ```
664
553
 
665
- ### 1. Upload Progress (`onUploadProgress`)
554
+ ---
666
555
 
667
- ```javascript
668
- const fileBlob = new Blob(['sample data'], { type: 'text/plain' });
556
+ ## Path Parameter Interpolation (`pathParams`)
669
557
 
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}%)`);
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
- ### 2. Streaming Download Progress (`onDownloadProgress`)
589
+ ---
679
590
 
680
- Uses WHATWG `ReadableStream` to stream chunks without buffering the whole response in memory:
591
+ ## Upload & Download Progress
681
592
 
682
- ```javascript
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(`Received chunk: ${event.bytes} bytes. Total loaded: ${event.loaded} bytes`);
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
- ```javascript
698
- const { createClient, createOfflineQueue, MemoryStorageAdapter } = require('react-native-fetch-client');
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 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
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.message);
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
- > **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
+ ```
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 or custom mTLS agents:
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.0",
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/OWNER/react-native-fetch-client.git"
20
+ "url": "git+https://github.com/arunkumarArjun/react-native-fetch-client.git"
20
21
  },
21
- "homepage": "https://github.com/OWNER/react-native-fetch-client#readme",
22
+ "homepage": "https://github.com/arunkumarArjun/react-native-fetch-client#readme",
22
23
  "bugs": {
23
- "url": "https://github.com/OWNER/react-native-fetch-client/issues"
24
+ "url": "https://github.com/arunkumarArjun/react-native-fetch-client/issues"
24
25
  },
25
26
 
26
27
  "engines": {