@bereasoftware/nexa 0.0.1

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.en.md ADDED
@@ -0,0 +1,1288 @@
1
+ <p align="center">
2
+ <h1 align="center">@bereasoftware/nexa</h1>
3
+ <p align="center">
4
+ A modern, type-safe HTTP client that combines the power of <code>fetch</code> with the convenience of <code>axios</code> β€” built on SOLID principles.
5
+ </p>
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="#tests"><img src="https://img.shields.io/badge/Tests-157_passing-brightgreen?style=for-the-badge" alt="Tests" /></a>
10
+ <a href="#test-coverage"><img src="https://img.shields.io/badge/Coverage-75.73%25-orange?style=for-the-badge" alt="Coverage" /></a>
11
+ <a href="https://www.npmjs.com/package/@bereasoftware/nexa"><img src="https://img.shields.io/npm/v/@bereasoftware/nexa?style=for-the-badge" alt="NPM Version" /></a>
12
+ <a href="https://bundlephobia.com/package/@bereasoftware/nexa"><img src="https://img.shields.io/bundlephobia/minzip/@bereasoftware/nexa?label=Bundle&style=for-the-badge" alt="Bundle Size" /></a>
13
+ <a href="https://www.npmjs.com/package/@bereasoftware/nexa"><img src="https://img.shields.io/npm/dm/@bereasoftware/nexa?style=for-the-badge" alt="NPM Downloads" /></a>
14
+ <img src="https://img.shields.io/badge/Node-18%2B-success?style=for-the-badge" alt="Node" />
15
+ <img src="https://img.shields.io/badge/TypeScript-5.x-3178C6?style=for-the-badge" alt="TypeScript" />
16
+ <img src="https://img.shields.io/badge/Dependencies-Zero-brightgreen?style=for-the-badge" alt="Dependencies" />
17
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge" alt="License" /></a>
18
+ <a href="https://github.com/Berea-Soft/nexa"><img src="https://img.shields.io/badge/github-Repository-blue?logo=github&style=for-the-badge" alt="Repository" /></a>
19
+ </p>
20
+
21
+ > πŸ“š **Documentation available in other languages:**
22
+ >
23
+ > - πŸ‡ͺπŸ‡Έ [EspaΓ±ol](README.md)
24
+ > - πŸ‡¬πŸ‡§ **English** (this file - README.en.md)
25
+
26
+ ---
27
+
28
+ ## Why Nexa?
29
+
30
+ | Feature | `fetch` | `axios` | **Nexa** |
31
+ | ------------------------------- | :-----: | :-----: | :------: |
32
+ | Zero dependencies | βœ… | ❌ | βœ… |
33
+ | Type-safe errors (Result monad) | ❌ | ❌ | βœ… |
34
+ | Auto body serialization | ❌ | βœ… | βœ… |
35
+ | Path parameter interpolation | ❌ | ❌ | βœ… |
36
+ | Retry strategies (pluggable) | ❌ | ❌ | βœ… |
37
+ | Built-in caching | ❌ | ❌ | βœ… |
38
+ | Request deduplication | ❌ | ❌ | βœ… |
39
+ | Download progress | ❌ | βœ… | βœ… |
40
+ | Lifecycle hooks | ❌ | ❌ | βœ… |
41
+ | Concurrent request limiting | ❌ | ❌ | βœ… |
42
+ | Auto-pagination | ❌ | ❌ | βœ… |
43
+ | Smart polling | ❌ | ❌ | βœ… |
44
+ | Client extension (`.extend()`) | ❌ | βœ… | βœ… |
45
+ | Interceptor disposal | ❌ | ❌ | βœ… |
46
+ | Middleware pipeline | ❌ | ❌ | βœ… |
47
+ | Plugin system | ❌ | ❌ | βœ… |
48
+ | Validators & transformers | ❌ | ❌ | βœ… |
49
+ | Response duration tracking | ❌ | ❌ | βœ… |
50
+ | Smart response type detection | ❌ | βœ… | βœ… |
51
+ | Tree-shakeable | βœ… | ❌ | βœ… |
52
+
53
+ ---
54
+
55
+ ## Table of Contents
56
+
57
+ - [Installation](#installation)
58
+ - [Quick Start](#quick-start)
59
+ - [Core Concepts](#core-concepts)
60
+ - [Result Monad (No try/catch)](#result-monad)
61
+ - [Creating a Client](#creating-a-client)
62
+ - [HTTP Methods](#http-methods)
63
+ - [Request Configuration](#request-configuration)
64
+ - [Path Parameters](#path-parameters)
65
+ - [Query Parameters](#query-parameters)
66
+ - [Auto Body Serialization](#auto-body-serialization)
67
+ - [Response Types](#response-types)
68
+ - [Timeout](#timeout)
69
+ - [Retry Strategies](#retry-strategies)
70
+ - [Inline Config](#inline-retry-config)
71
+ - [AggressiveRetry](#aggressiveretry)
72
+ - [ConservativeRetry](#conservativeretry)
73
+ - [CircuitBreakerRetry](#circuitbreakerretry)
74
+ - [Custom Strategy](#custom-retry-strategy)
75
+ - [Interceptors](#interceptors)
76
+ - [Request Interceptors](#request-interceptors)
77
+ - [Response Interceptors](#response-interceptors)
78
+ - [Interceptor Disposal](#interceptor-disposal)
79
+ - [Caching](#caching)
80
+ - [Lifecycle Hooks](#lifecycle-hooks)
81
+ - [Download Progress](#download-progress)
82
+ - [Concurrent Request Limiting](#concurrent-request-limiting)
83
+ - [Client Extension](#client-extension)
84
+ - [Auto-Pagination](#auto-pagination)
85
+ - [Smart Polling](#smart-polling)
86
+ - [Request Cancellation](#request-cancellation)
87
+ - [Validators](#validators)
88
+ - [Transformers](#transformers)
89
+ - [Middleware Pipeline](#middleware-pipeline)
90
+ - [Plugin System](#plugin-system)
91
+ - [Streaming](#streaming)
92
+ - [Typed Generics](#typed-generics)
93
+ - [Error Handling](#error-handling)
94
+ - [API Reference](#api-reference)
95
+ - [Build Formats](#build-formats)
96
+ - [Development](#development)
97
+ - [License](#license)
98
+
99
+ ---
100
+
101
+ ## Installation
102
+
103
+ ```bash
104
+ npm install @bereasoftware/nexa
105
+ ```
106
+
107
+ ```bash
108
+ yarn add @bereasoftware/nexa
109
+ ```
110
+
111
+ ```bash
112
+ pnpm add @bereasoftware/nexa
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Quick Start
118
+
119
+ ```typescript
120
+ import { createHttpClient } from "@bereasoftware/nexa";
121
+
122
+ const client = createHttpClient({
123
+ baseURL: "https://api.example.com",
124
+ });
125
+
126
+ // Type-safe, no try/catch needed
127
+ const result = await client.get<User>("/users/1");
128
+
129
+ if (result.ok) {
130
+ console.log(result.value.data); // User
131
+ console.log(result.value.status); // 200
132
+ console.log(result.value.duration); // 42 (ms)
133
+ } else {
134
+ console.log(result.error.message); // "Request failed with status 404"
135
+ console.log(result.error.code); // "HTTP_ERROR"
136
+ }
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Core Concepts
142
+
143
+ ### Result Monad
144
+
145
+ Nexa returns a `Result<T, E>` type instead of throwing exceptions. This eliminates the need for `try/catch` blocks and gives you full type safety on both success and error paths.
146
+
147
+ ```typescript
148
+ type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
149
+ ```
150
+
151
+ You can also construct results manually:
152
+
153
+ ```typescript
154
+ import { Ok, Err } from "@bereasoftware/nexa";
155
+
156
+ const success = Ok({ name: "John" }); // { ok: true, value: { name: 'John' } }
157
+ const failure = Err({ message: "Not found", code: "HTTP_ERROR" });
158
+ ```
159
+
160
+ Every client method returns `Promise<Result<HttpResponse<T>, HttpErrorDetails>>`:
161
+
162
+ ```typescript
163
+ const result = await client.get<User[]>('/users');
164
+
165
+ if (result.ok) {
166
+ // result.value is HttpResponse<User[]>
167
+ const users: User[] = result.value.data;
168
+ const status: number = result.value.status;
169
+ const duration: number = result.value.duration;
170
+ const headers: Headers = result.value.headers;
171
+ } else {
172
+ // result.error is HttpErrorDetails
173
+ const message: string = result.error.message;
174
+ const code: string = result.error.code; // 'HTTP_ERROR' | 'TIMEOUT' | 'NETWORK_ERROR' | 'ABORTED' | ...
175
+ const status?: number = result.error.status;
176
+ }
177
+ ```
178
+
179
+ ### Creating a Client
180
+
181
+ ```typescript
182
+ import { createHttpClient } from "@bereasoftware/nexa";
183
+
184
+ const client = createHttpClient({
185
+ baseURL: "https://api.example.com",
186
+ defaultHeaders: { Authorization: "Bearer token123" },
187
+ defaultTimeout: 10000, // 10s (default: 30s)
188
+ validateStatus: (status) => status < 400, // Custom status validation
189
+ maxConcurrent: 5, // Max 5 simultaneous requests
190
+ defaultResponseType: "json", // 'json' | 'text' | 'blob' | 'auto' | ...
191
+ defaultHooks: {
192
+ onStart: (req) => console.log("Starting:", req.url),
193
+ onFinally: () => console.log("Done"),
194
+ },
195
+ });
196
+ ```
197
+
198
+ **Full `HttpClientConfig` options:**
199
+
200
+ | Option | Type | Default | Description |
201
+ | --------------------- | ----------------------------- | ---------------------------------------- | ------------------------------------------ |
202
+ | `baseURL` | `string` | `''` | Base URL prepended to all requests |
203
+ | `defaultHeaders` | `Record<string, string>` | `{ 'Content-Type': 'application/json' }` | Default headers for every request |
204
+ | `defaultTimeout` | `number` | `30000` | Default timeout in ms |
205
+ | `validateStatus` | `(status: number) => boolean` | `status >= 200 && status < 300` | Which HTTP statuses are considered success |
206
+ | `cacheStrategy` | `CacheStrategy` | `MemoryCache` | Custom cache implementation |
207
+ | `maxConcurrent` | `number` | `0` (unlimited) | Max concurrent requests |
208
+ | `defaultResponseType` | `ResponseType` | `'auto'` | Default response parsing strategy |
209
+ | `defaultHooks` | `RequestHooks` | `{}` | Default lifecycle hooks for all requests |
210
+
211
+ ---
212
+
213
+ ## HTTP Methods
214
+
215
+ ```typescript
216
+ // GET
217
+ const result = await client.get<User>("/users/1");
218
+
219
+ // POST
220
+ const result = await client.post<User>("/users", {
221
+ name: "John",
222
+ email: "john@example.com",
223
+ });
224
+
225
+ // PUT
226
+ const result = await client.put<User>("/users/1", { name: "John Updated" });
227
+
228
+ // PATCH
229
+ const result = await client.patch<User>("/users/1", {
230
+ email: "new@example.com",
231
+ });
232
+
233
+ // DELETE
234
+ const result = await client.delete<void>("/users/1");
235
+
236
+ // HEAD (check resource existence)
237
+ const result = await client.head("/users/1");
238
+
239
+ // OPTIONS (CORS preflight, available methods)
240
+ const result = await client.options("/users");
241
+ ```
242
+
243
+ All methods accept an optional config object as the last parameter:
244
+
245
+ ```typescript
246
+ const result = await client.get<User>("/users/1", {
247
+ timeout: 5000,
248
+ headers: { "X-Custom": "value" },
249
+ cache: { enabled: true, ttlMs: 60000 },
250
+ retry: { maxAttempts: 3, backoffMs: 1000 },
251
+ });
252
+ ```
253
+
254
+ ---
255
+
256
+ ## Request Configuration
257
+
258
+ ### Path Parameters
259
+
260
+ Nexa supports `:param` style path interpolation with automatic URI encoding:
261
+
262
+ ```typescript
263
+ const result = await client.get<User>("/users/:id/posts/:postId", {
264
+ params: { id: 42, postId: "hello world" },
265
+ });
266
+ // β†’ GET /users/42/posts/hello%20world
267
+ ```
268
+
269
+ ### Query Parameters
270
+
271
+ ```typescript
272
+ const result = await client.get<User[]>("/users", {
273
+ query: { page: 1, limit: 20, active: true },
274
+ });
275
+ // β†’ GET /users?page=1&limit=20&active=true
276
+ ```
277
+
278
+ ### Auto Body Serialization
279
+
280
+ Nexa automatically detects and serializes the request body:
281
+
282
+ | Body Type | Serialization | Content-Type |
283
+ | ------------------ | ------------------ | ----------------------------------- |
284
+ | `object` / `array` | `JSON.stringify()` | `application/json` |
285
+ | `string` | Passed as-is | `text/plain` |
286
+ | `FormData` | Passed as-is | Auto (multipart boundary) |
287
+ | `URLSearchParams` | Passed as-is | `application/x-www-form-urlencoded` |
288
+ | `Blob` | Passed as-is | Blob's type |
289
+ | `ArrayBuffer` | Passed as-is | `application/octet-stream` |
290
+ | `ReadableStream` | Passed as-is | `application/octet-stream` |
291
+
292
+ ```typescript
293
+ // JSON (automatic)
294
+ await client.post("/users", { name: "John" });
295
+
296
+ // FormData (automatic content-type with boundary)
297
+ const form = new FormData();
298
+ form.append("file", fileBlob);
299
+ await client.post("/upload", form);
300
+
301
+ // URL-encoded
302
+ await client.post(
303
+ "/login",
304
+ new URLSearchParams({ user: "john", pass: "secret" }),
305
+ );
306
+ ```
307
+
308
+ ### Response Types
309
+
310
+ Control how the response body is parsed:
311
+
312
+ ```typescript
313
+ // Auto-detect based on Content-Type header (default)
314
+ const result = await client.get("/data", { responseType: "auto" });
315
+
316
+ // Force JSON parsing
317
+ const result = await client.get<User>("/user", { responseType: "json" });
318
+
319
+ // Get raw text
320
+ const result = await client.get<string>("/page", { responseType: "text" });
321
+
322
+ // Download as Blob
323
+ const result = await client.get<Blob>("/file.pdf", { responseType: "blob" });
324
+
325
+ // Get ArrayBuffer
326
+ const result = await client.get<ArrayBuffer>("/binary", {
327
+ responseType: "arrayBuffer",
328
+ });
329
+
330
+ // Get FormData
331
+ const result = await client.get<FormData>("/form", {
332
+ responseType: "formData",
333
+ });
334
+
335
+ // Get ReadableStream (for manual streaming)
336
+ const result = await client.get<ReadableStream>("/stream", {
337
+ responseType: "stream",
338
+ });
339
+ ```
340
+
341
+ **Auto-detection logic:** `application/json` β†’ JSON, `text/*` β†’ text, `multipart/form-data` β†’ FormData, `application/octet-stream` / `image/*` / `audio/*` / `video/*` β†’ Blob, fallback β†’ try JSON then text.
342
+
343
+ ### Timeout
344
+
345
+ ```typescript
346
+ // Per-request timeout
347
+ const result = await client.get("/slow-endpoint", { timeout: 5000 });
348
+
349
+ // Timeout produces a specific error code
350
+ if (!result.ok && result.error.code === "TIMEOUT") {
351
+ console.log("Request timed out");
352
+ }
353
+ ```
354
+
355
+ ---
356
+
357
+ ## Retry Strategies
358
+
359
+ ### Inline Retry Config
360
+
361
+ Simple retry with exponential backoff:
362
+
363
+ ```typescript
364
+ const result = await client.get("/unstable-api", {
365
+ retry: { maxAttempts: 3, backoffMs: 1000 },
366
+ });
367
+ // Retries up to 3 times with exponential backoff + jitter
368
+ ```
369
+
370
+ ### AggressiveRetry
371
+
372
+ Retries all errors up to max attempts with minimal delay:
373
+
374
+ ```typescript
375
+ import { AggressiveRetry } from "@bereasoftware/nexa";
376
+
377
+ const result = await client.get("/api", {
378
+ retry: new AggressiveRetry(5), // 5 attempts, 50ms * attempt delay
379
+ });
380
+ ```
381
+
382
+ ### ConservativeRetry
383
+
384
+ Only retries on specific HTTP status codes (408, 429, 500, 502, 503, 504) and timeouts:
385
+
386
+ ```typescript
387
+ import { ConservativeRetry } from "@bereasoftware/nexa";
388
+
389
+ const result = await client.get("/api", {
390
+ retry: new ConservativeRetry(3), // 3 attempts, exponential backoff capped at 10s
391
+ });
392
+ ```
393
+
394
+ ### CircuitBreakerRetry
395
+
396
+ Fail-fast pattern β€” stops retrying after a threshold of failures:
397
+
398
+ ```typescript
399
+ import { CircuitBreakerRetry } from "@bereasoftware/nexa";
400
+
401
+ const breaker = new CircuitBreakerRetry(
402
+ 3, // maxAttempts per request
403
+ 5, // failureThreshold before circuit opens
404
+ 60000, // resetTimeMs β€” circuit resets after 60s
405
+ );
406
+
407
+ const result = await client.get("/api", { retry: breaker });
408
+
409
+ // Reset the circuit manually
410
+ breaker.reset();
411
+ ```
412
+
413
+ ### Custom Retry Strategy
414
+
415
+ Implement the `RetryStrategy` interface:
416
+
417
+ ```typescript
418
+ import type { RetryStrategy, HttpErrorDetails } from "@bereasoftware/nexa";
419
+
420
+ const customRetry: RetryStrategy = {
421
+ shouldRetry(attempt: number, error: HttpErrorDetails): boolean {
422
+ // Only retry network errors and 503
423
+ return (
424
+ (error.code === "NETWORK_ERROR" || error.status === 503) && attempt < 5
425
+ );
426
+ },
427
+ delayMs(attempt: number): number {
428
+ // Linear backoff: 500ms, 1000ms, 1500ms...
429
+ return attempt * 500;
430
+ },
431
+ };
432
+
433
+ const result = await client.get("/api", { retry: customRetry });
434
+ ```
435
+
436
+ ---
437
+
438
+ ## Interceptors
439
+
440
+ ### Request Interceptors
441
+
442
+ Modify requests before they are sent:
443
+
444
+ ```typescript
445
+ client.addRequestInterceptor({
446
+ onRequest(request) {
447
+ // Add auth token to every request
448
+ return {
449
+ ...request,
450
+ headers: {
451
+ ...request.headers,
452
+ Authorization: `Bearer ${getToken()}`,
453
+ },
454
+ };
455
+ },
456
+ });
457
+ ```
458
+
459
+ ### Response Interceptors
460
+
461
+ Transform responses or handle errors globally:
462
+
463
+ ```typescript
464
+ client.addResponseInterceptor({
465
+ onResponse(response) {
466
+ // Log all successful responses
467
+ console.log(
468
+ `[${response.status}] ${response.request.url} (${response.duration}ms)`,
469
+ );
470
+ return response;
471
+ },
472
+ onError(error) {
473
+ // Handle 401 globally
474
+ if (error.status === 401) {
475
+ redirectToLogin();
476
+ }
477
+ return error;
478
+ },
479
+ });
480
+ ```
481
+
482
+ ### Interceptor Disposal
483
+
484
+ Both `addRequestInterceptor` and `addResponseInterceptor` return a disposer function to remove the interceptor:
485
+
486
+ ```typescript
487
+ const dispose = client.addRequestInterceptor({
488
+ onRequest(request) {
489
+ return { ...request, headers: { ...request.headers, "X-Temp": "value" } };
490
+ },
491
+ });
492
+
493
+ // Later: remove the interceptor
494
+ dispose();
495
+
496
+ // Or clear all interceptors
497
+ client.clearInterceptors();
498
+ ```
499
+
500
+ ---
501
+
502
+ ## Caching
503
+
504
+ Built-in in-memory cache with TTL support. Only caches GET requests:
505
+
506
+ ```typescript
507
+ const result = await client.get<User>("/users/1", {
508
+ cache: { enabled: true, ttlMs: 60000 }, // Cache for 1 minute
509
+ });
510
+
511
+ // Second call returns cached response instantly
512
+ const cached = await client.get<User>("/users/1", {
513
+ cache: { enabled: true, ttlMs: 60000 },
514
+ });
515
+ ```
516
+
517
+ **Custom cache implementation:**
518
+
519
+ ```typescript
520
+ import type { CacheStrategy } from "@bereasoftware/nexa";
521
+
522
+ const redisCache: CacheStrategy = {
523
+ get(key: string) {
524
+ return redis.get(key);
525
+ },
526
+ set(key: string, value: unknown, ttlMs?: number) {
527
+ redis.set(key, value, "PX", ttlMs);
528
+ },
529
+ has(key: string) {
530
+ return redis.exists(key);
531
+ },
532
+ clear() {
533
+ redis.flushdb();
534
+ },
535
+ };
536
+
537
+ const client = createHttpClient({ cacheStrategy: redisCache });
538
+ ```
539
+
540
+ ---
541
+
542
+ ## Lifecycle Hooks
543
+
544
+ Monitor the full request lifecycle:
545
+
546
+ ```typescript
547
+ const result = await client.get<User>("/users/1", {
548
+ hooks: {
549
+ onStart(request) {
550
+ console.log("Starting request to:", request.url);
551
+ },
552
+ onSuccess(response) {
553
+ console.log("Success:", response.status, `(${response.duration}ms)`);
554
+ },
555
+ onError(error) {
556
+ console.error("Failed:", error.message, error.code);
557
+ },
558
+ onRetry(attempt, error) {
559
+ console.warn(`Retry #${attempt}:`, error.message);
560
+ },
561
+ onFinally() {
562
+ console.log("Request complete (success or failure)");
563
+ },
564
+ },
565
+ });
566
+ ```
567
+
568
+ Default hooks can be set at the client level:
569
+
570
+ ```typescript
571
+ const client = createHttpClient({
572
+ defaultHooks: {
573
+ onError: (error) => reportToSentry(error),
574
+ onFinally: () => hideLoadingSpinner(),
575
+ },
576
+ });
577
+ ```
578
+
579
+ ---
580
+
581
+ ## Download Progress
582
+
583
+ Track download progress with a callback:
584
+
585
+ ```typescript
586
+ const result = await client.get<Blob>("/large-file.zip", {
587
+ responseType: "blob",
588
+ onDownloadProgress(event) {
589
+ console.log(
590
+ `Downloaded: ${event.percent}% (${event.loaded}/${event.total} bytes)`,
591
+ );
592
+ updateProgressBar(event.percent);
593
+ },
594
+ });
595
+ ```
596
+
597
+ The `ProgressEvent` interface:
598
+
599
+ ```typescript
600
+ interface ProgressEvent {
601
+ loaded: number; // Bytes downloaded so far
602
+ total: number; // Total bytes (from Content-Length header)
603
+ percent: number; // 0-100
604
+ }
605
+ ```
606
+
607
+ ---
608
+
609
+ ## Concurrent Request Limiting
610
+
611
+ Limit the number of simultaneous requests to avoid overwhelming a server:
612
+
613
+ ```typescript
614
+ const client = createHttpClient({
615
+ baseURL: "https://api.example.com",
616
+ maxConcurrent: 3, // Only 3 requests at a time
617
+ });
618
+
619
+ // Fire 10 requests β€” only 3 run simultaneously, rest queue automatically
620
+ const results = await Promise.all(urls.map((url) => client.get(url)));
621
+ ```
622
+
623
+ Check queue status:
624
+
625
+ ```typescript
626
+ console.log(client.queueStats);
627
+ // { active: 3, pending: 7 }
628
+
629
+ console.log(client.activeRequests);
630
+ // 3
631
+ ```
632
+
633
+ ---
634
+
635
+ ## Client Extension
636
+
637
+ Create child clients that inherit configuration and interceptors:
638
+
639
+ ```typescript
640
+ const baseClient = createHttpClient({
641
+ baseURL: "https://api.example.com",
642
+ defaultHeaders: { "X-App": "MyApp" },
643
+ });
644
+
645
+ baseClient.addRequestInterceptor({
646
+ onRequest(req) {
647
+ return {
648
+ ...req,
649
+ headers: { ...req.headers, Authorization: "Bearer token" },
650
+ };
651
+ },
652
+ });
653
+
654
+ // Child inherits baseURL, headers, interceptors β€” and adds version header
655
+ const v2Client = baseClient.extend({
656
+ defaultHeaders: { "X-API-Version": "2" },
657
+ });
658
+
659
+ // v2Client has headers: { 'X-App': 'MyApp', 'X-API-Version': '2' }
660
+ // v2Client also has the auth interceptor from baseClient
661
+ ```
662
+
663
+ ---
664
+
665
+ ## Auto-Pagination
666
+
667
+ Iterate through paginated APIs with async generators:
668
+
669
+ ```typescript
670
+ interface PageResponse {
671
+ items: User[];
672
+ nextCursor: string | null;
673
+ }
674
+
675
+ for await (const users of client.paginate<PageResponse>("/users", {
676
+ getItems: (data) => data.items,
677
+ getNextPage: (data, config) =>
678
+ data.nextCursor
679
+ ? {
680
+ ...config,
681
+ query: { ...(config.query as any), cursor: data.nextCursor },
682
+ }
683
+ : null,
684
+ })) {
685
+ console.log("Page with", users.length, "users");
686
+ // Process each page of users
687
+ }
688
+ ```
689
+
690
+ Pagination stops automatically when `getNextPage` returns `null` or a request fails.
691
+
692
+ ---
693
+
694
+ ## Smart Polling
695
+
696
+ Poll an endpoint until a condition is met:
697
+
698
+ ```typescript
699
+ interface Job {
700
+ id: string;
701
+ status: "pending" | "running" | "completed" | "failed";
702
+ result?: string;
703
+ }
704
+
705
+ const result = await client.poll<Job>("/jobs/abc123", {
706
+ intervalMs: 2000, // Poll every 2 seconds
707
+ maxAttempts: 30, // Give up after 30 attempts (0 = unlimited)
708
+ until: (job) => job.status === "completed" || job.status === "failed",
709
+ onPoll: (job, attempt) => {
710
+ console.log(`Attempt ${attempt}: ${job.status}`);
711
+ },
712
+ });
713
+
714
+ if (result.ok) {
715
+ console.log("Job finished:", result.value.data.result);
716
+ } else if (result.error.code === "POLL_EXHAUSTED") {
717
+ console.log("Polling timed out");
718
+ }
719
+ ```
720
+
721
+ ---
722
+
723
+ ## Request Cancellation
724
+
725
+ Cancel all pending requests:
726
+
727
+ ```typescript
728
+ // Start several requests
729
+ const promise1 = client.get("/slow-1");
730
+ const promise2 = client.get("/slow-2");
731
+
732
+ // Cancel everything
733
+ client.cancelAll();
734
+
735
+ // Or use AbortSignal for individual requests
736
+ const controller = new AbortController();
737
+ const result = client.get("/data", { signal: controller.signal });
738
+ controller.abort();
739
+ ```
740
+
741
+ ---
742
+
743
+ ## Validators
744
+
745
+ Validate response data before it reaches your code:
746
+
747
+ ```typescript
748
+ import {
749
+ createSchemaValidator,
750
+ createRequiredFieldsValidator,
751
+ validatorIsArray,
752
+ validatorIsObject,
753
+ } from "@bereasoftware/nexa";
754
+
755
+ // Schema validator
756
+ const userValidator = createSchemaValidator<User>({
757
+ id: (v) => typeof v === "number",
758
+ name: (v) => typeof v === "string" && (v as string).length > 0,
759
+ email: (v) => typeof v === "string" && (v as string).includes("@"),
760
+ });
761
+
762
+ const result = await client.get<User>("/users/1", {
763
+ validate: userValidator,
764
+ });
765
+ // If validation fails: result.error.code === 'VALIDATION_ERROR'
766
+
767
+ // Required fields validator
768
+ const result = await client.get("/api/data", {
769
+ validate: createRequiredFieldsValidator(["id", "name", "createdAt"]),
770
+ });
771
+
772
+ // Array validator
773
+ const result = await client.get("/users", {
774
+ validate: validatorIsArray,
775
+ });
776
+
777
+ // Object validator
778
+ const result = await client.get("/user/1", {
779
+ validate: validatorIsObject,
780
+ });
781
+ ```
782
+
783
+ ---
784
+
785
+ ## Transformers
786
+
787
+ Transform response data after parsing:
788
+
789
+ ```typescript
790
+ import {
791
+ transformSnakeToCamel,
792
+ transformCamelToSnake,
793
+ transformFlatten,
794
+ createProjectionTransformer,
795
+ createWrapperTransformer,
796
+ } from "@bereasoftware/nexa";
797
+
798
+ // Convert snake_case API responses to camelCase
799
+ const result = await client.get("/users/1", {
800
+ transform: transformSnakeToCamel,
801
+ });
802
+ // { first_name: 'John' } β†’ { firstName: 'John' }
803
+
804
+ // Convert camelCase to snake_case (for sending data)
805
+ const result = await client.get("/data", {
806
+ transform: transformCamelToSnake,
807
+ });
808
+
809
+ // Flatten nested objects
810
+ const result = await client.get("/nested", {
811
+ transform: transformFlatten,
812
+ });
813
+ // { user: { name: 'John' } } β†’ { 'user.name': 'John' }
814
+
815
+ // Pick specific fields
816
+ const result = await client.get("/users/1", {
817
+ transform: createProjectionTransformer(["id", "name"]),
818
+ });
819
+ // Only keeps { id, name } from the response
820
+
821
+ // Wrap data in a container
822
+ const result = await client.get("/items", {
823
+ transform: createWrapperTransformer("data"),
824
+ });
825
+ // [1, 2, 3] β†’ { data: [1, 2, 3] }
826
+ ```
827
+
828
+ ---
829
+
830
+ ## Middleware Pipeline
831
+
832
+ Express/Koa-style middleware pipeline for advanced request processing:
833
+
834
+ ```typescript
835
+ import {
836
+ createPipeline,
837
+ createCacheMiddleware,
838
+ createDedupeMiddleware,
839
+ createStreamingMiddleware,
840
+ type HttpContext,
841
+ type Middleware,
842
+ } from "@bereasoftware/nexa";
843
+
844
+ // Create custom middleware
845
+ const loggingMiddleware: Middleware<HttpContext> = async (ctx, next) => {
846
+ console.log(`β†’ ${ctx.request.method} ${ctx.request.url}`);
847
+ const start = Date.now();
848
+ await next();
849
+ console.log(`← ${ctx.response.status} (${Date.now() - start}ms)`);
850
+ };
851
+
852
+ const authMiddleware: Middleware<HttpContext> = async (ctx, next) => {
853
+ ctx.request.headers["Authorization"] = `Bearer ${getToken()}`;
854
+ await next();
855
+ };
856
+
857
+ // Build and execute pipeline
858
+ const pipeline = createPipeline([
859
+ loggingMiddleware,
860
+ authMiddleware,
861
+ createCacheMiddleware({ ttlMs: 30000 }),
862
+ createDedupeMiddleware(),
863
+ ]);
864
+
865
+ const ctx: HttpContext = {
866
+ request: { method: "GET", url: "/users", headers: {} },
867
+ response: { status: 0, headers: {} },
868
+ state: {},
869
+ };
870
+
871
+ await pipeline(ctx);
872
+ ```
873
+
874
+ **Pre-built middleware:**
875
+
876
+ | Middleware | Description |
877
+ | ------------------------------------- | -------------------------------------------- |
878
+ | `createCacheMiddleware(options?)` | Caches GET responses with TTL |
879
+ | `cacheMiddleware` | Pre-configured cache (60s TTL) |
880
+ | `createDedupeMiddleware(options?)` | Deduplicates concurrent identical requests |
881
+ | `dedupeMiddleware` | Pre-configured deduplication for GET |
882
+ | `createStreamingMiddleware(options?)` | Handles streaming responses with progress |
883
+ | `streamingMiddleware` | Pre-configured streaming with console output |
884
+
885
+ ---
886
+
887
+ ## Plugin System
888
+
889
+ Extend Nexa with a plugin architecture:
890
+
891
+ ```typescript
892
+ import {
893
+ PluginManager,
894
+ LoggerPlugin,
895
+ MetricsPlugin,
896
+ CachePlugin,
897
+ DedupePlugin,
898
+ } from "@bereasoftware/nexa";
899
+
900
+ const manager = new PluginManager();
901
+
902
+ // Register plugins
903
+ manager
904
+ .register(LoggerPlugin)
905
+ .register(new MetricsPlugin())
906
+ .register(new CachePlugin(30000)) // 30s TTL
907
+ .register(new DedupePlugin());
908
+
909
+ // Listen to events
910
+ manager.on("request:start", (url) => console.log("Request to:", url));
911
+ manager.on("request:success", (url, status) =>
912
+ console.log("Success:", url, status),
913
+ );
914
+
915
+ // Get metrics
916
+ const metrics = (
917
+ manager.getPlugins().find((p) => p.name === "metrics") as MetricsPlugin
918
+ ).getMetrics();
919
+ console.log(metrics); // { requests: 10, errors: 1, totalTime: 4200, avgTime: 420 }
920
+ ```
921
+
922
+ **Create custom plugins:**
923
+
924
+ ```typescript
925
+ import type { Plugin } from "@bereasoftware/nexa";
926
+
927
+ const rateLimitPlugin: Plugin = {
928
+ name: "rate-limit",
929
+ setup(client) {
930
+ // Add rate limiting middleware, event listeners, etc.
931
+ const manager = client as PluginManager;
932
+ manager.on("request:start", () => {
933
+ // Custom rate limiting logic
934
+ });
935
+ },
936
+ };
937
+
938
+ manager.register(rateLimitPlugin);
939
+ ```
940
+
941
+ ---
942
+
943
+ ## Streaming
944
+
945
+ Handle large files and streaming responses:
946
+
947
+ ```typescript
948
+ import { handleStream, streamToFile } from "@bereasoftware/nexa";
949
+
950
+ // Manual stream processing
951
+ const response = await fetch("https://example.com/large-file");
952
+ const data = await handleStream(response, {
953
+ onChunk(chunk) {
954
+ console.log("Received chunk:", chunk.length, "bytes");
955
+ },
956
+ onProgress(loaded, total) {
957
+ console.log(`Progress: ${Math.round((loaded / total) * 100)}%`);
958
+ },
959
+ });
960
+
961
+ // Download stream to file
962
+ const response = await fetch("https://example.com/data.csv");
963
+ await streamToFile(response, "output.csv");
964
+ // Works in both Node.js (fs.writeFile) and browser (Blob download)
965
+ ```
966
+
967
+ ---
968
+
969
+ ## Typed Generics
970
+
971
+ Advanced type-safe utilities for API client design:
972
+
973
+ ### Typed API Client
974
+
975
+ ```typescript
976
+ import { createTypedApiClient, type ApiEndpoint } from "@bereasoftware/nexa";
977
+
978
+ // Define your API schema with full types
979
+ interface UserApi {
980
+ getUser: ApiEndpoint<void, User>;
981
+ createUser: ApiEndpoint<CreateUserDto, User>;
982
+ listUsers: ApiEndpoint<void, User[]>;
983
+ }
984
+
985
+ const api = createTypedApiClient<UserApi>({
986
+ getUser: { method: "GET", path: "/users/1", response: {} as User },
987
+ createUser: { method: "POST", path: "/users", response: {} as User },
988
+ listUsers: { method: "GET", path: "/users", response: [] as User[] },
989
+ });
990
+
991
+ // Fully typed request β€” knows input and output types
992
+ const user = await api.request(client, "getUser");
993
+ const newUser = await api.request(client, "createUser", {
994
+ name: "Ella",
995
+ email: "ella@example.com",
996
+ });
997
+ ```
998
+
999
+ ### Branded Types
1000
+
1001
+ ```typescript
1002
+ import {
1003
+ createUrl,
1004
+ createApiUrl,
1005
+ type Url,
1006
+ type ApiUrl,
1007
+ type FileUrl,
1008
+ } from "@bereasoftware/nexa";
1009
+
1010
+ // Branded URLs prevent mixing different URL types at compile time
1011
+ const apiUrl: ApiUrl = createApiUrl("/users/1");
1012
+ const genericUrl: Url = createUrl("https://example.com");
1013
+ ```
1014
+
1015
+ ### Type Guards
1016
+
1017
+ ```typescript
1018
+ import { createTypeGuard } from "@bereasoftware/nexa";
1019
+
1020
+ interface User {
1021
+ id: number;
1022
+ name: string;
1023
+ }
1024
+
1025
+ const ensureUser = createTypeGuard<User>(
1026
+ (value): value is User =>
1027
+ typeof value === "object" &&
1028
+ value !== null &&
1029
+ "id" in value &&
1030
+ "name" in value,
1031
+ );
1032
+
1033
+ const user = ensureUser(unknownData); // throws TypeError if invalid
1034
+ ```
1035
+
1036
+ ### Observable Pattern
1037
+
1038
+ ```typescript
1039
+ import { TypedObservable } from "@bereasoftware/nexa";
1040
+
1041
+ const stream = new TypedObservable<User>();
1042
+
1043
+ const sub = stream.subscribe(
1044
+ (user) => console.log("User:", user.name),
1045
+ (err) => console.error("Error:", err),
1046
+ () => console.log("Complete"),
1047
+ );
1048
+
1049
+ // Chainable operators
1050
+ const names = stream.filter((user) => user.active).map((user) => user.name);
1051
+
1052
+ stream.next({ id: 1, name: "John", active: true });
1053
+ stream.complete();
1054
+
1055
+ sub.unsubscribe();
1056
+ ```
1057
+
1058
+ ### Defer (Lazy Promise)
1059
+
1060
+ ```typescript
1061
+ import { Defer } from "@bereasoftware/nexa";
1062
+
1063
+ const deferred = new Defer<string>();
1064
+
1065
+ // Pass the promise to consumers
1066
+ someConsumer(deferred.promise_());
1067
+
1068
+ // Resolve later
1069
+ deferred.resolve("done");
1070
+ // Or reject
1071
+ deferred.reject(new Error("failed"));
1072
+ ```
1073
+
1074
+ ---
1075
+
1076
+ ## Error Handling
1077
+
1078
+ ### Error Codes
1079
+
1080
+ | Code | Description |
1081
+ | ------------------ | ------------------------------------------------------- |
1082
+ | `HTTP_ERROR` | Non-2xx HTTP status (configurable via `validateStatus`) |
1083
+ | `TIMEOUT` | Request exceeded timeout duration |
1084
+ | `NETWORK_ERROR` | Network failure (DNS, connection refused, etc.) |
1085
+ | `ABORTED` | Request was manually cancelled |
1086
+ | `VALIDATION_ERROR` | Response data failed validation |
1087
+ | `POLL_EXHAUSTED` | Polling reached max attempts without condition met |
1088
+ | `MAX_RETRIES` | All retry attempts exhausted |
1089
+ | `UNKNOWN_ERROR` | Unclassified error |
1090
+
1091
+ ### HttpError Class
1092
+
1093
+ ```typescript
1094
+ import { HttpError, isHttpError } from "@bereasoftware/nexa";
1095
+
1096
+ // Check if an error is an HttpError
1097
+ if (isHttpError(error)) {
1098
+ console.log(error.status); // HTTP status code
1099
+ console.log(error.code); // Error code string
1100
+ }
1101
+ ```
1102
+
1103
+ ### Pattern: Handle Different Error Types
1104
+
1105
+ ```typescript
1106
+ const result = await client.get<User>("/users/1");
1107
+
1108
+ if (!result.ok) {
1109
+ switch (result.error.code) {
1110
+ case "TIMEOUT":
1111
+ showNotification("Request timed out, please try again");
1112
+ break;
1113
+ case "NETWORK_ERROR":
1114
+ showNotification("No internet connection");
1115
+ break;
1116
+ case "HTTP_ERROR":
1117
+ if (result.error.status === 404) showNotification("User not found");
1118
+ else if (result.error.status === 403) redirectToLogin();
1119
+ break;
1120
+ case "VALIDATION_ERROR":
1121
+ reportBug("API returned unexpected data format");
1122
+ break;
1123
+ default:
1124
+ reportError(result.error);
1125
+ }
1126
+ }
1127
+ ```
1128
+
1129
+ ---
1130
+
1131
+ ## API Reference
1132
+
1133
+ ### `createHttpClient(config?: HttpClientConfig): HttpClient`
1134
+
1135
+ Factory function to create a new HTTP client instance.
1136
+
1137
+ ### `HttpClient` Methods
1138
+
1139
+ | Method | Signature | Description |
1140
+ | ------------------------ | ------------------------------------------------------------------------------------- | ---------------------------- |
1141
+ | `request` | `<T>(config: HttpRequestConfig) β†’ Promise<Result<HttpResponse<T>, HttpErrorDetails>>` | Core request method |
1142
+ | `get` | `<T>(url, config?) β†’ Promise<Result<...>>` | GET request |
1143
+ | `post` | `<T>(url, body?, config?) β†’ Promise<Result<...>>` | POST request |
1144
+ | `put` | `<T>(url, body?, config?) β†’ Promise<Result<...>>` | PUT request |
1145
+ | `patch` | `<T>(url, body?, config?) β†’ Promise<Result<...>>` | PATCH request |
1146
+ | `delete` | `<T>(url, config?) β†’ Promise<Result<...>>` | DELETE request |
1147
+ | `head` | `(url, config?) β†’ Promise<Result<...>>` | HEAD request |
1148
+ | `options` | `(url, config?) β†’ Promise<Result<...>>` | OPTIONS request |
1149
+ | `extend` | `(overrides?: HttpClientConfig) β†’ HttpClient` | Create child client |
1150
+ | `paginate` | `<T>(url, options, config?) β†’ AsyncGenerator<T[]>` | Auto-pagination |
1151
+ | `poll` | `<T>(url, options, config?) β†’ Promise<Result<...>>` | Smart polling |
1152
+ | `addRequestInterceptor` | `(interceptor) β†’ Disposer` | Add request interceptor |
1153
+ | `addResponseInterceptor` | `(interceptor) β†’ Disposer` | Add response interceptor |
1154
+ | `clearInterceptors` | `() β†’ void` | Remove all interceptors |
1155
+ | `cancelAll` | `() β†’ void` | Cancel all pending requests |
1156
+ | `activeRequests` | `number` (getter) | Number of in-flight requests |
1157
+ | `queueStats` | `{ active, pending }` (getter) | Queue statistics |
1158
+
1159
+ ### Types
1160
+
1161
+ | Type | Description |
1162
+ | --------------------- | --------------------------------------------------------------------------------- |
1163
+ | `Result<T, E>` | Success/failure discriminated union |
1164
+ | `HttpRequest` | Request configuration (url, method, headers, body, query, params) |
1165
+ | `HttpResponse<T>` | Response with data, status, headers, duration |
1166
+ | `HttpErrorDetails` | Error with message, code, status, originalError |
1167
+ | `HttpRequestConfig` | Full request config (extends HttpRequest + retry, cache, hooks, etc.) |
1168
+ | `HttpClientConfig` | Client-level configuration |
1169
+ | `RequestInterceptor` | Intercept outgoing requests |
1170
+ | `ResponseInterceptor` | Intercept incoming responses |
1171
+ | `RetryStrategy` | Custom retry logic interface |
1172
+ | `CacheStrategy` | Custom cache implementation interface |
1173
+ | `Validator` | Response validation interface |
1174
+ | `Transformer` | Response transformation interface |
1175
+ | `PaginateOptions<T>` | Pagination configuration |
1176
+ | `PollOptions<T>` | Polling configuration |
1177
+ | `RequestHooks<T>` | Lifecycle hook callbacks |
1178
+ | `ProgressEvent` | Download progress data |
1179
+ | `ResponseType` | `'json' \| 'text' \| 'blob' \| 'arrayBuffer' \| 'formData' \| 'stream' \| 'auto'` |
1180
+ | `Disposer` | Function that removes an interceptor |
1181
+
1182
+ ---
1183
+
1184
+ ## Build Formats
1185
+
1186
+ Nexa ships in multiple module formats:
1187
+
1188
+ | Format | File | Use Case |
1189
+ | --------- | ----------------------- | --------------------------------------- |
1190
+ | **ESM** | `dist/nexa.es.js` | Modern bundlers (Vite, Rollup, esbuild) |
1191
+ | **CJS** | `dist/nexa.cjs.js` | Node.js `require()` |
1192
+ | **UMD** | `dist/nexa.umd.js` | Universal (AMD, CJS, global) |
1193
+ | **IIFE** | `dist/nexa.iife.js` | Script tags (`<script>`) |
1194
+ | **Types** | `dist/types/index.d.ts` | TypeScript type declarations |
1195
+
1196
+ ---
1197
+
1198
+ ## Development
1199
+
1200
+ ### Tests
1201
+
1202
+ **157 tests in total**: 88 HTTP Client tests + 69 utilities tests
1203
+
1204
+ ```bash
1205
+ # Run all tests
1206
+ npm test
1207
+
1208
+ # Watch mode
1209
+ npm run test:watch
1210
+
1211
+ # Coverage report
1212
+ npm run test:coverage
1213
+ ```
1214
+
1215
+ Tests use **Vitest** (globals mode) with BDD style (`describe`/`it`/`expect`).
1216
+
1217
+ ### Build
1218
+
1219
+ ```bash
1220
+ # Generate distribution
1221
+ npm run build
1222
+ ```
1223
+
1224
+ **Build configuration:**
1225
+
1226
+ - **Formats**: ES, CommonJS, UMD, IIFE
1227
+ - **Minification**: OXC (ultra-fast)
1228
+ - **Type Definitions**: Bundled in `/dist/types`
1229
+ - **Tree-shakeable**: Only imports what you use
1230
+ - **Externals**: `fs` (Node.js only for `streamToFile`)
1231
+
1232
+ **Output:**
1233
+
1234
+ ```
1235
+ dist/
1236
+ β”œβ”€β”€ nexa.es.js (24.9 KB, gzip: 7.53 KB)
1237
+ β”œβ”€β”€ nexa.cjs.js (19.9 KB, gzip: 6.68 KB)
1238
+ β”œβ”€β”€ nexa.umd.js (19.8 KB, gzip: 6.75 KB)
1239
+ β”œβ”€β”€ nexa.iife.js (19.6 KB, gzip: 6.68 KB)
1240
+ └── types/ (TypeScript .d.ts declarations)
1241
+ ```
1242
+
1243
+ ### Test Coverage
1244
+
1245
+ **Overall Coverage: 75.73%** β€” solid unit test coverage with mock-based HTTP testing
1246
+
1247
+ | Component | Coverage | Details |
1248
+ | ----------- | ---------- | --------------------------------- |
1249
+ | HTTP Client | **80.85%** | 81.25% branches, 73.43% functions |
1250
+ | Types | **100%** | Perfect type coverage |
1251
+ | Utils | **71.79%** | 66.66% branches, 81.14% functions |
1252
+
1253
+ **HTTP Client** (`test/http-client.test.ts`) β€” **88 tests**:
1254
+
1255
+ - βœ“ Core methods: create, GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS (7 tests)
1256
+ - βœ“ Retry strategies & timeouts (3 tests)
1257
+ - βœ“ Interceptors & disposal (5 tests)
1258
+ - βœ“ Caching & validation (4 tests)
1259
+ - βœ“ Type safety & extensions (3 tests)
1260
+ - βœ“ Pagination & polling (5 tests)
1261
+ - βœ“ Response type handling: all 8+ types + auto-detection (13 tests)
1262
+ - βœ“ Binary content-type detection: image/_, audio/_, video/\*, octet-stream (5 tests)
1263
+ - βœ“ Body serialization: JSON, null, strings, Blob, URLSearchParams, ArrayBuffer, TypedArray, FormData, ReadableStream (7 tests)
1264
+ - βœ“ Error normalization: TimeoutError, AbortError, TypeError, unknown, NETWORK_ERROR (5+ tests)
1265
+ - βœ“ Request management: activeRequests, cancelAll, clearCache (2 tests)
1266
+ - βœ“ Module exports verification: all 8 export categories (8 tests)
1267
+ - βœ“ Plugin integration: LoggerPlugin, MetricsPlugin event handlers (7 tests)
1268
+ - βœ“ Advanced configuration: null body, direct Blob, abort messages (5+ tests)
1269
+
1270
+ **Utilities** (`test/utils.test.ts`) \u2014 **69 tests**:
1271
+
1272
+ - \u2713 Validators: schema, required fields, type checks (4 tests)
1273
+ - \u2713 Transformers: snake↔camel case, flatten, projection, wrapper (5 tests)
1274
+ - \u2713 Retry Strategies: Aggressive, Conservative, Circuit Breaker (10 tests)
1275
+ - \u2713 Timeout & Retry: withTimeout, retry function (6 tests)
1276
+ - \u2713 Cache: CacheStore CRUD, TTL expiry (5 tests)
1277
+ - \u2713 Deduplication: RequestDeduplicator sharing, cleanup (3 tests)
1278
+ - \u2713 Middleware Pipeline: ordering, next() guard, legacy pipeline (3 tests)
1279
+ - \u2713 Cache Middleware: GET caching, POST bypass (2 tests)
1280
+ - \u2713 Dedup Middleware: GET dedup, POST bypass (2 tests)
1281
+ - \u2713 Typed Generics: TypedResponse, TypedObservable (map/filter), Defer, type guards, branded types (9 tests)
1282
+ - \u2713 Plugins: PluginManager, LoggerPlugin, MetricsPlugin, CachePlugin, DedupePlugin (5 tests)\n\n### Coverage Limitations & Realistic Ceiling\n\nUnit test coverage plateaus around **75-80%** due to inherent mock-based testing limitations:\n\n**Why not 95%?**\n- **Streaming features** (~3-5% gap): Download progress tracking uses `ReadableStream.getReader()` which requires real HTTP streamsβ€”not mockable with `fetch-mock`\n- **Utility examples** (~5-10% gap): Middleware patterns and reference code are intentionally not exercised in production \n- **Export-only files** (~2-3% gap): `http-client/index.ts` verified via import validation, not unit testable\n\n**Realistic maximums:**\n- Unit tests + mocks: **~80-85%** ceiling (current: 75.73%)\n- Integration tests required: Would reach 90%+ but beyond this project\u2019s scope\n\nThe 75.73% coverage represents comprehensive testing of all **production code paths** that can be reached via HTTP mocks.
1283
+
1284
+ ---
1285
+
1286
+ ## License
1287
+
1288
+ MIT Β© [John Andrade](mailto:johnandrade@bereasoft.com) β€” [@bereasoftware](https://github.com/Berea-Soft)