@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.md ADDED
@@ -0,0 +1,1304 @@
1
+ <p align="center">
2
+ <h1 align="center">@bereasoftware/nexa</h1>
3
+ <p align="center">
4
+ Un cliente HTTP moderno y type-safe que combina el poder de <code>fetch</code> con la comodidad de <code>axios</code> — construido sobre principios SOLID.
5
+ </p>
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="#tests"><img src="https://img.shields.io/badge/Tests-157_pasando-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-20%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/Dependencias-Cero-brightgreen?style=for-the-badge" alt="Dependencies" />
17
+ <a href="LICENSE"><img src="https://img.shields.io/badge/Licencia-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-Repositorio-blue?logo=github&style=for-the-badge" alt="Repository" /></a>
19
+ </p>
20
+
21
+ > 📚 **Documentación disponible en otros idiomas:**
22
+ >
23
+ > - 🇪🇸 **Español** (este archivo - README.md)
24
+ > - 🇬🇧 **English** (README.en.md)
25
+
26
+ ---
27
+
28
+ ## ¿Por qué Nexa?
29
+
30
+ | Característica | `fetch` | `axios` | **Nexa** |
31
+ | ------------------------------------------ | :-----: | :-----: | :------: |
32
+ | Cero dependencias | ✅ | ❌ | ✅ |
33
+ | Errores type-safe (Result monad) | ❌ | ❌ | ✅ |
34
+ | Serialización automática del body | ❌ | ✅ | ✅ |
35
+ | Interpolación de parámetros en ruta | ❌ | ❌ | ✅ |
36
+ | Estrategias de reintentos (pluggable) | ❌ | ❌ | ✅ |
37
+ | Caché integrado | ❌ | ❌ | ✅ |
38
+ | Deduplicación de peticiones | ❌ | ❌ | ✅ |
39
+ | Progreso de descarga | ❌ | ✅ | ✅ |
40
+ | Hooks de ciclo de vida | ❌ | ❌ | ✅ |
41
+ | Limitación de peticiones concurrentes | ❌ | ❌ | ✅ |
42
+ | Auto-paginación | ❌ | ❌ | ✅ |
43
+ | Polling inteligente | ❌ | ❌ | ✅ |
44
+ | Extensión de cliente (`.extend()`) | ❌ | ✅ | ✅ |
45
+ | Disposal de interceptores | ❌ | ❌ | ✅ |
46
+ | Pipeline de middleware | ❌ | ❌ | ✅ |
47
+ | Sistema de plugins | ❌ | ❌ | ✅ |
48
+ | Validadores y transformadores | ❌ | ❌ | ✅ |
49
+ | Tracking de duración de respuesta | ❌ | ❌ | ✅ |
50
+ | Detección inteligente de tipo de respuesta | ❌ | ✅ | ✅ |
51
+ | Tree-shakeable | ✅ | ❌ | ✅ |
52
+
53
+ ---
54
+
55
+ ## Tabla de Contenidos
56
+
57
+ - [Instalación](#instalación)
58
+ - [Inicio Rápido](#inicio-rápido)
59
+ - [Conceptos Fundamentales](#conceptos-fundamentales)
60
+ - [Result Monad (Sin try/catch)](#result-monad)
61
+ - [Creando un Cliente](#creando-un-cliente)
62
+ - [Métodos HTTP](#métodos-http)
63
+ - [Configuración de Peticiones](#configuración-de-peticiones)
64
+ - [Parámetros de Ruta](#parámetros-de-ruta)
65
+ - [Parámetros de Query](#parámetros-de-query)
66
+ - [Serialización Automática del Body](#serialización-automática-del-body)
67
+ - [Tipos de Respuesta](#tipos-de-respuesta)
68
+ - [Timeout](#timeout)
69
+ - [Estrategias de Reintentos](#estrategias-de-reintentos)
70
+ - [Configuración Inline](#configuración-inline)
71
+ - [AggressiveRetry](#aggressiveretry)
72
+ - [ConservativeRetry](#conservativeretry)
73
+ - [CircuitBreakerRetry](#circuitbreakerretry)
74
+ - [Estrategia Personalizada](#estrategia-personalizada)
75
+ - [Interceptores](#interceptores)
76
+ - [Interceptores de Petición](#interceptores-de-petición)
77
+ - [Interceptores de Respuesta](#interceptores-de-respuesta)
78
+ - [Disposal de Interceptores](#disposal-de-interceptores)
79
+ - [Caché](#caché)
80
+ - [Hooks de Ciclo de Vida](#hooks-de-ciclo-de-vida)
81
+ - [Progreso de Descarga](#progreso-de-descarga)
82
+ - [Limitación de Peticiones Concurrentes](#limitación-de-peticiones-concurrentes)
83
+ - [Extensión de Cliente](#extensión-de-cliente)
84
+ - [Auto-Paginación](#auto-paginación)
85
+ - [Polling Inteligente](#polling-inteligente)
86
+ - [Cancelación de Peticiones](#cancelación-de-peticiones)
87
+ - [Validadores](#validadores)
88
+ - [Transformadores](#transformadores)
89
+ - [Pipeline de Middleware](#pipeline-de-middleware)
90
+ - [Sistema de Plugins](#sistema-de-plugins)
91
+ - [Streaming](#streaming)
92
+ - [Generics Tipados](#generics-tipados)
93
+ - [Manejo de Errores](#manejo-de-errores)
94
+ - [Referencia de API](#referencia-de-api)
95
+ - [Formatos de Build](#formatos-de-build)
96
+ - [Desarrollo](#desarrollo)
97
+ - [Licencia](#licencia)
98
+
99
+ ---
100
+
101
+ ## Instalación
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
+ ## Inicio Rápido
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, sin necesidad de try/catch
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
+ ## Conceptos Fundamentales
142
+
143
+ ### Result Monad
144
+
145
+ Nexa retorna un tipo `Result<T, E>` en lugar de lanzar excepciones. Esto elimina la necesidad de bloques `try/catch` y te da seguridad de tipos completa tanto en el camino de éxito como en el de error.
146
+
147
+ ```typescript
148
+ type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
149
+ ```
150
+
151
+ También puedes construir resultados manualmente:
152
+
153
+ ```typescript
154
+ import { Ok, Err } from "@bereasoftware/nexa";
155
+
156
+ const exito = Ok({ name: "John" }); // { ok: true, value: { name: 'John' } }
157
+ const fallo = Err({ message: "No encontrado", code: "HTTP_ERROR" });
158
+ ```
159
+
160
+ Cada método del cliente retorna `Promise<Result<HttpResponse<T>, HttpErrorDetails>>`:
161
+
162
+ ```typescript
163
+ const result = await client.get<User[]>('/users');
164
+
165
+ if (result.ok) {
166
+ // result.value es 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 es 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
+ ### Creando un Cliente
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 (por defecto: 30s)
188
+ validateStatus: (status) => status < 400, // Validación de status personalizada
189
+ maxConcurrent: 5, // Máximo 5 peticiones simultáneas
190
+ defaultResponseType: "json", // 'json' | 'text' | 'blob' | 'auto' | ...
191
+ defaultHooks: {
192
+ onStart: (req) => console.log("Iniciando:", req.url),
193
+ onFinally: () => console.log("Listo"),
194
+ },
195
+ });
196
+ ```
197
+
198
+ **Opciones completas de `HttpClientConfig`:**
199
+
200
+ | Opción | Tipo | Por defecto | Descripción |
201
+ | --------------------- | ----------------------------- | ---------------------------------------- | ------------------------------------------------------------ |
202
+ | `baseURL` | `string` | `''` | URL base que se antepone a todas las peticiones |
203
+ | `defaultHeaders` | `Record<string, string>` | `{ 'Content-Type': 'application/json' }` | Headers por defecto para cada petición |
204
+ | `defaultTimeout` | `number` | `30000` | Timeout por defecto en ms |
205
+ | `validateStatus` | `(status: number) => boolean` | `status >= 200 && status < 300` | Qué códigos HTTP se consideran exitosos |
206
+ | `cacheStrategy` | `CacheStrategy` | `MemoryCache` | Implementación de caché personalizada |
207
+ | `maxConcurrent` | `number` | `0` (ilimitado) | Máximo de peticiones concurrentes |
208
+ | `defaultResponseType` | `ResponseType` | `'auto'` | Estrategia de parseo de respuesta por defecto |
209
+ | `defaultHooks` | `RequestHooks` | `{}` | Hooks de ciclo de vida por defecto para todas las peticiones |
210
+
211
+ ---
212
+
213
+ ## Métodos HTTP
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 Actualizado" });
227
+
228
+ // PATCH
229
+ const result = await client.patch<User>("/users/1", {
230
+ email: "nuevo@example.com",
231
+ });
232
+
233
+ // DELETE
234
+ const result = await client.delete<void>("/users/1");
235
+
236
+ // HEAD (verificar existencia de recurso)
237
+ const result = await client.head("/users/1");
238
+
239
+ // OPTIONS (preflight CORS, métodos disponibles)
240
+ const result = await client.options("/users");
241
+ ```
242
+
243
+ Todos los métodos aceptan un objeto de configuración opcional como último parámetro:
244
+
245
+ ```typescript
246
+ const result = await client.get<User>("/users/1", {
247
+ timeout: 5000,
248
+ headers: { "X-Custom": "valor" },
249
+ cache: { enabled: true, ttlMs: 60000 },
250
+ retry: { maxAttempts: 3, backoffMs: 1000 },
251
+ });
252
+ ```
253
+
254
+ ---
255
+
256
+ ## Configuración de Peticiones
257
+
258
+ ### Parámetros de Ruta
259
+
260
+ Nexa soporta interpolación de rutas estilo `:param` con codificación URI automática:
261
+
262
+ ```typescript
263
+ const result = await client.get<User>("/users/:id/posts/:postId", {
264
+ params: { id: 42, postId: "hola mundo" },
265
+ });
266
+ // → GET /users/42/posts/hola%20mundo
267
+ ```
268
+
269
+ ### Parámetros de Query
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
+ ### Serialización Automática del Body
279
+
280
+ Nexa detecta y serializa automáticamente el cuerpo de la petición:
281
+
282
+ | Tipo de Body | Serialización | Content-Type |
283
+ | ------------------ | ------------------ | ----------------------------------- |
284
+ | `object` / `array` | `JSON.stringify()` | `application/json` |
285
+ | `string` | Se envía tal cual | `text/plain` |
286
+ | `FormData` | Se envía tal cual | Auto (boundary multipart) |
287
+ | `URLSearchParams` | Se envía tal cual | `application/x-www-form-urlencoded` |
288
+ | `Blob` | Se envía tal cual | Tipo del Blob |
289
+ | `ArrayBuffer` | Se envía tal cual | `application/octet-stream` |
290
+ | `ReadableStream` | Se envía tal cual | `application/octet-stream` |
291
+
292
+ ```typescript
293
+ // JSON (automático)
294
+ await client.post("/users", { name: "John" });
295
+
296
+ // FormData (content-type automático con 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: "secreto" }),
305
+ );
306
+ ```
307
+
308
+ ### Tipos de Respuesta
309
+
310
+ Controla cómo se parsea el cuerpo de la respuesta:
311
+
312
+ ```typescript
313
+ // Auto-detección basada en el header Content-Type (por defecto)
314
+ const result = await client.get("/data", { responseType: "auto" });
315
+
316
+ // Forzar parseo JSON
317
+ const result = await client.get<User>("/user", { responseType: "json" });
318
+
319
+ // Obtener texto crudo
320
+ const result = await client.get<string>("/page", { responseType: "text" });
321
+
322
+ // Descargar como Blob
323
+ const result = await client.get<Blob>("/file.pdf", { responseType: "blob" });
324
+
325
+ // Obtener ArrayBuffer
326
+ const result = await client.get<ArrayBuffer>("/binary", {
327
+ responseType: "arrayBuffer",
328
+ });
329
+
330
+ // Obtener FormData
331
+ const result = await client.get<FormData>("/form", {
332
+ responseType: "formData",
333
+ });
334
+
335
+ // Obtener ReadableStream (para streaming manual)
336
+ const result = await client.get<ReadableStream>("/stream", {
337
+ responseType: "stream",
338
+ });
339
+ ```
340
+
341
+ **Lógica de auto-detección:** `application/json` → JSON, `text/*` → texto, `multipart/form-data` → FormData, `application/octet-stream` / `image/*` / `audio/*` / `video/*` → Blob, fallback → intenta JSON luego texto.
342
+
343
+ ### Timeout
344
+
345
+ ```typescript
346
+ // Timeout por petición
347
+ const result = await client.get("/endpoint-lento", { timeout: 5000 });
348
+
349
+ // El timeout produce un código de error específico
350
+ if (!result.ok && result.error.code === "TIMEOUT") {
351
+ console.log("La petición expiró");
352
+ }
353
+ ```
354
+
355
+ ---
356
+
357
+ ## Estrategias de Reintentos
358
+
359
+ ### Configuración Inline
360
+
361
+ Reintento simple con backoff exponencial:
362
+
363
+ ```typescript
364
+ const result = await client.get("/api-inestable", {
365
+ retry: { maxAttempts: 3, backoffMs: 1000 },
366
+ });
367
+ // Reintenta hasta 3 veces con backoff exponencial + jitter
368
+ ```
369
+
370
+ ### AggressiveRetry
371
+
372
+ Reintenta todos los errores hasta el máximo de intentos con delay mínimo:
373
+
374
+ ```typescript
375
+ import { AggressiveRetry } from "@bereasoftware/nexa";
376
+
377
+ const result = await client.get("/api", {
378
+ retry: new AggressiveRetry(5), // 5 intentos, delay de 50ms * intento
379
+ });
380
+ ```
381
+
382
+ ### ConservativeRetry
383
+
384
+ Solo reintenta en códigos HTTP específicos (408, 429, 500, 502, 503, 504) y timeouts:
385
+
386
+ ```typescript
387
+ import { ConservativeRetry } from "@bereasoftware/nexa";
388
+
389
+ const result = await client.get("/api", {
390
+ retry: new ConservativeRetry(3), // 3 intentos, backoff exponencial con tope de 10s
391
+ });
392
+ ```
393
+
394
+ ### CircuitBreakerRetry
395
+
396
+ Patrón fail-fast — deja de reintentar después de un umbral de fallos:
397
+
398
+ ```typescript
399
+ import { CircuitBreakerRetry } from "@bereasoftware/nexa";
400
+
401
+ const breaker = new CircuitBreakerRetry(
402
+ 3, // maxAttempts por petición
403
+ 5, // failureThreshold antes de abrir el circuito
404
+ 60000, // resetTimeMs — el circuito se resetea después de 60s
405
+ );
406
+
407
+ const result = await client.get("/api", { retry: breaker });
408
+
409
+ // Resetear el circuito manualmente
410
+ breaker.reset();
411
+ ```
412
+
413
+ ### Estrategia Personalizada
414
+
415
+ Implementa la interfaz `RetryStrategy`:
416
+
417
+ ```typescript
418
+ import type { RetryStrategy, HttpErrorDetails } from "@bereasoftware/nexa";
419
+
420
+ const reintentoCustom: RetryStrategy = {
421
+ shouldRetry(attempt: number, error: HttpErrorDetails): boolean {
422
+ // Solo reintentar errores de red y 503
423
+ return (
424
+ (error.code === "NETWORK_ERROR" || error.status === 503) && attempt < 5
425
+ );
426
+ },
427
+ delayMs(attempt: number): number {
428
+ // Backoff lineal: 500ms, 1000ms, 1500ms...
429
+ return attempt * 500;
430
+ },
431
+ };
432
+
433
+ const result = await client.get("/api", { retry: reintentoCustom });
434
+ ```
435
+
436
+ ---
437
+
438
+ ## Interceptores
439
+
440
+ ### Interceptores de Petición
441
+
442
+ Modifica las peticiones antes de que se envíen:
443
+
444
+ ```typescript
445
+ client.addRequestInterceptor({
446
+ onRequest(request) {
447
+ // Agregar token de auth a cada petición
448
+ return {
449
+ ...request,
450
+ headers: {
451
+ ...request.headers,
452
+ Authorization: `Bearer ${getToken()}`,
453
+ },
454
+ };
455
+ },
456
+ });
457
+ ```
458
+
459
+ ### Interceptores de Respuesta
460
+
461
+ Transforma respuestas o maneja errores globalmente:
462
+
463
+ ```typescript
464
+ client.addResponseInterceptor({
465
+ onResponse(response) {
466
+ // Loguear todas las respuestas exitosas
467
+ console.log(
468
+ `[${response.status}] ${response.request.url} (${response.duration}ms)`,
469
+ );
470
+ return response;
471
+ },
472
+ onError(error) {
473
+ // Manejar 401 globalmente
474
+ if (error.status === 401) {
475
+ redirigirAlLogin();
476
+ }
477
+ return error;
478
+ },
479
+ });
480
+ ```
481
+
482
+ ### Disposal de Interceptores
483
+
484
+ Tanto `addRequestInterceptor` como `addResponseInterceptor` retornan una función disposer para remover el interceptor:
485
+
486
+ ```typescript
487
+ const dispose = client.addRequestInterceptor({
488
+ onRequest(request) {
489
+ return { ...request, headers: { ...request.headers, "X-Temp": "valor" } };
490
+ },
491
+ });
492
+
493
+ // Después: remover el interceptor
494
+ dispose();
495
+
496
+ // O limpiar todos los interceptores
497
+ client.clearInterceptors();
498
+ ```
499
+
500
+ ---
501
+
502
+ ## Caché
503
+
504
+ Caché en memoria integrado con soporte TTL. Solo cachea peticiones GET:
505
+
506
+ ```typescript
507
+ const result = await client.get<User>("/users/1", {
508
+ cache: { enabled: true, ttlMs: 60000 }, // Cachear por 1 minuto
509
+ });
510
+
511
+ // La segunda llamada retorna la respuesta cacheada instantáneamente
512
+ const cached = await client.get<User>("/users/1", {
513
+ cache: { enabled: true, ttlMs: 60000 },
514
+ });
515
+ ```
516
+
517
+ **Implementación de caché personalizada:**
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
+ ## Hooks de Ciclo de Vida
543
+
544
+ Monitorea el ciclo de vida completo de la petición:
545
+
546
+ ```typescript
547
+ const result = await client.get<User>("/users/1", {
548
+ hooks: {
549
+ onStart(request) {
550
+ console.log("Iniciando petición a:", request.url);
551
+ },
552
+ onSuccess(response) {
553
+ console.log("Éxito:", response.status, `(${response.duration}ms)`);
554
+ },
555
+ onError(error) {
556
+ console.error("Falló:", error.message, error.code);
557
+ },
558
+ onRetry(attempt, error) {
559
+ console.warn(`Reintento #${attempt}:`, error.message);
560
+ },
561
+ onFinally() {
562
+ console.log("Petición completada (éxito o fallo)");
563
+ },
564
+ },
565
+ });
566
+ ```
567
+
568
+ Los hooks por defecto se pueden configurar a nivel de cliente:
569
+
570
+ ```typescript
571
+ const client = createHttpClient({
572
+ defaultHooks: {
573
+ onError: (error) => reportarASentry(error),
574
+ onFinally: () => ocultarSpinnerDeCarga(),
575
+ },
576
+ });
577
+ ```
578
+
579
+ ---
580
+
581
+ ## Progreso de Descarga
582
+
583
+ Trackea el progreso de descarga con un callback:
584
+
585
+ ```typescript
586
+ const result = await client.get<Blob>("/archivo-grande.zip", {
587
+ responseType: "blob",
588
+ onDownloadProgress(event) {
589
+ console.log(
590
+ `Descargado: ${event.percent}% (${event.loaded}/${event.total} bytes)`,
591
+ );
592
+ actualizarBarraDeProgreso(event.percent);
593
+ },
594
+ });
595
+ ```
596
+
597
+ La interfaz `ProgressEvent`:
598
+
599
+ ```typescript
600
+ interface ProgressEvent {
601
+ loaded: number; // Bytes descargados hasta ahora
602
+ total: number; // Total de bytes (del header Content-Length)
603
+ percent: number; // 0-100
604
+ }
605
+ ```
606
+
607
+ ---
608
+
609
+ ## Limitación de Peticiones Concurrentes
610
+
611
+ Limita el número de peticiones simultáneas para no sobrecargar el servidor:
612
+
613
+ ```typescript
614
+ const client = createHttpClient({
615
+ baseURL: "https://api.example.com",
616
+ maxConcurrent: 3, // Solo 3 peticiones a la vez
617
+ });
618
+
619
+ // Lanza 10 peticiones — solo 3 corren simultáneamente, el resto se encola automáticamente
620
+ const results = await Promise.all(urls.map((url) => client.get(url)));
621
+ ```
622
+
623
+ Consultar el estado de la cola:
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
+ ## Extensión de Cliente
636
+
637
+ Crea clientes hijo que heredan configuración e interceptores:
638
+
639
+ ```typescript
640
+ const clienteBase = createHttpClient({
641
+ baseURL: "https://api.example.com",
642
+ defaultHeaders: { "X-App": "MiApp" },
643
+ });
644
+
645
+ clienteBase.addRequestInterceptor({
646
+ onRequest(req) {
647
+ return {
648
+ ...req,
649
+ headers: { ...req.headers, Authorization: "Bearer token" },
650
+ };
651
+ },
652
+ });
653
+
654
+ // El hijo hereda baseURL, headers, interceptores — y agrega header de versión
655
+ const clienteV2 = clienteBase.extend({
656
+ defaultHeaders: { "X-API-Version": "2" },
657
+ });
658
+
659
+ // clienteV2 tiene headers: { 'X-App': 'MiApp', 'X-API-Version': '2' }
660
+ // clienteV2 también tiene el interceptor de auth del clienteBase
661
+ ```
662
+
663
+ ---
664
+
665
+ ## Auto-Paginación
666
+
667
+ Itera a través de APIs paginadas con generadores asíncronos:
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("Página con", users.length, "usuarios");
686
+ // Procesar cada página de usuarios
687
+ }
688
+ ```
689
+
690
+ La paginación se detiene automáticamente cuando `getNextPage` retorna `null` o una petición falla.
691
+
692
+ ---
693
+
694
+ ## Polling Inteligente
695
+
696
+ Consulta un endpoint repetidamente hasta que se cumpla una condición:
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, // Consultar cada 2 segundos
707
+ maxAttempts: 30, // Rendirse después de 30 intentos (0 = ilimitado)
708
+ until: (job) => job.status === "completed" || job.status === "failed",
709
+ onPoll: (job, attempt) => {
710
+ console.log(`Intento ${attempt}: ${job.status}`);
711
+ },
712
+ });
713
+
714
+ if (result.ok) {
715
+ console.log("Job terminado:", result.value.data.result);
716
+ } else if (result.error.code === "POLL_EXHAUSTED") {
717
+ console.log("El polling expiró");
718
+ }
719
+ ```
720
+
721
+ ---
722
+
723
+ ## Cancelación de Peticiones
724
+
725
+ Cancela todas las peticiones pendientes:
726
+
727
+ ```typescript
728
+ // Iniciar varias peticiones
729
+ const promise1 = client.get("/lento-1");
730
+ const promise2 = client.get("/lento-2");
731
+
732
+ // Cancelar todo
733
+ client.cancelAll();
734
+
735
+ // O usar AbortSignal para peticiones individuales
736
+ const controller = new AbortController();
737
+ const result = client.get("/data", { signal: controller.signal });
738
+ controller.abort();
739
+ ```
740
+
741
+ ---
742
+
743
+ ## Validadores
744
+
745
+ Valida los datos de respuesta antes de que lleguen a tu código:
746
+
747
+ ```typescript
748
+ import {
749
+ createSchemaValidator,
750
+ createRequiredFieldsValidator,
751
+ validatorIsArray,
752
+ validatorIsObject,
753
+ } from "@bereasoftware/nexa";
754
+
755
+ // Validador de esquema
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
+ // Si la validación falla: result.error.code === 'VALIDATION_ERROR'
766
+
767
+ // Validador de campos requeridos
768
+ const result = await client.get("/api/data", {
769
+ validate: createRequiredFieldsValidator(["id", "name", "createdAt"]),
770
+ });
771
+
772
+ // Validador de array
773
+ const result = await client.get("/users", {
774
+ validate: validatorIsArray,
775
+ });
776
+
777
+ // Validador de objeto
778
+ const result = await client.get("/user/1", {
779
+ validate: validatorIsObject,
780
+ });
781
+ ```
782
+
783
+ ---
784
+
785
+ ## Transformadores
786
+
787
+ Transforma los datos de respuesta después del parseo:
788
+
789
+ ```typescript
790
+ import {
791
+ transformSnakeToCamel,
792
+ transformCamelToSnake,
793
+ transformFlatten,
794
+ createProjectionTransformer,
795
+ createWrapperTransformer,
796
+ } from "@bereasoftware/nexa";
797
+
798
+ // Convertir respuestas snake_case de la API a camelCase
799
+ const result = await client.get("/users/1", {
800
+ transform: transformSnakeToCamel,
801
+ });
802
+ // { first_name: 'John' } → { firstName: 'John' }
803
+
804
+ // Convertir camelCase a snake_case (para enviar datos)
805
+ const result = await client.get("/data", {
806
+ transform: transformCamelToSnake,
807
+ });
808
+
809
+ // Aplanar objetos anidados
810
+ const result = await client.get("/nested", {
811
+ transform: transformFlatten,
812
+ });
813
+ // { user: { name: 'John' } } → { 'user.name': 'John' }
814
+
815
+ // Seleccionar campos específicos
816
+ const result = await client.get("/users/1", {
817
+ transform: createProjectionTransformer(["id", "name"]),
818
+ });
819
+ // Solo mantiene { id, name } de la respuesta
820
+
821
+ // Envolver datos en un contenedor
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
+ ## Pipeline de Middleware
831
+
832
+ Pipeline de middleware estilo Express/Koa para procesamiento avanzado de peticiones:
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
+ // Crear middleware personalizado
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
+ // Construir y ejecutar 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
+ **Middleware pre-construidos:**
875
+
876
+ | Middleware | Descripción |
877
+ | ------------------------------------- | ---------------------------------------------- |
878
+ | `createCacheMiddleware(options?)` | Cachea respuestas GET con TTL |
879
+ | `cacheMiddleware` | Caché pre-configurado (60s TTL) |
880
+ | `createDedupeMiddleware(options?)` | Deduplica peticiones concurrentes idénticas |
881
+ | `dedupeMiddleware` | Deduplicación pre-configurada para GET |
882
+ | `createStreamingMiddleware(options?)` | Maneja respuestas streaming con progreso |
883
+ | `streamingMiddleware` | Streaming pre-configurado con salida a consola |
884
+
885
+ ---
886
+
887
+ ## Sistema de Plugins
888
+
889
+ Extiende Nexa con una arquitectura de plugins:
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
+ // Registrar plugins
903
+ manager
904
+ .register(LoggerPlugin)
905
+ .register(new MetricsPlugin())
906
+ .register(new CachePlugin(30000)) // 30s TTL
907
+ .register(new DedupePlugin());
908
+
909
+ // Escuchar eventos
910
+ manager.on("request:start", (url) => console.log("Petición a:", url));
911
+ manager.on("request:success", (url, status) =>
912
+ console.log("Éxito:", url, status),
913
+ );
914
+
915
+ // Obtener métricas
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
+ **Crear plugins personalizados:**
923
+
924
+ ```typescript
925
+ import type { Plugin } from "@bereasoftware/nexa";
926
+
927
+ const rateLimitPlugin: Plugin = {
928
+ name: "rate-limit",
929
+ setup(client) {
930
+ // Agregar middleware de rate limiting, event listeners, etc.
931
+ const manager = client as PluginManager;
932
+ manager.on("request:start", () => {
933
+ // Lógica de rate limiting personalizada
934
+ });
935
+ },
936
+ };
937
+
938
+ manager.register(rateLimitPlugin);
939
+ ```
940
+
941
+ ---
942
+
943
+ ## Streaming
944
+
945
+ Maneja archivos grandes y respuestas streaming:
946
+
947
+ ```typescript
948
+ import { handleStream, streamToFile } from "@bereasoftware/nexa";
949
+
950
+ // Procesamiento de stream manual
951
+ const response = await fetch("https://example.com/archivo-grande");
952
+ const data = await handleStream(response, {
953
+ onChunk(chunk) {
954
+ console.log("Chunk recibido:", chunk.length, "bytes");
955
+ },
956
+ onProgress(loaded, total) {
957
+ console.log(`Progreso: ${Math.round((loaded / total) * 100)}%`);
958
+ },
959
+ });
960
+
961
+ // Descargar stream a archivo
962
+ const response = await fetch("https://example.com/datos.csv");
963
+ await streamToFile(response, "salida.csv");
964
+ // Funciona tanto en Node.js (fs.writeFile) como en navegador (descarga Blob)
965
+ ```
966
+
967
+ ---
968
+
969
+ ## Generics Tipados
970
+
971
+ Utilidades avanzadas type-safe para diseño de clientes API:
972
+
973
+ ### Cliente API Tipado
974
+
975
+ ```typescript
976
+ import { createTypedApiClient, type ApiEndpoint } from "@bereasoftware/nexa";
977
+
978
+ // Define tu esquema API con tipos completos
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
+ // Petición totalmente tipada — conoce los tipos de entrada y salida
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
+ ### Tipos Branded
1000
+
1001
+ ```typescript
1002
+ import {
1003
+ createUrl,
1004
+ createApiUrl,
1005
+ type Url,
1006
+ type ApiUrl,
1007
+ type FileUrl,
1008
+ } from "@bereasoftware/nexa";
1009
+
1010
+ // Las URLs branded previenen mezclar diferentes tipos de URL en tiempo de compilación
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 asegurarUser = 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 = asegurarUser(datosDesconocidos); // lanza TypeError si es inválido
1034
+ ```
1035
+
1036
+ ### Patrón Observable
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("Usuario:", user.name),
1045
+ (err) => console.error("Error:", err),
1046
+ () => console.log("Completado"),
1047
+ );
1048
+
1049
+ // Operadores encadenables
1050
+ const nombres = 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 (Promesa Lazy)
1059
+
1060
+ ```typescript
1061
+ import { Defer } from "@bereasoftware/nexa";
1062
+
1063
+ const deferred = new Defer<string>();
1064
+
1065
+ // Pasar la promesa a los consumidores
1066
+ algunConsumidor(deferred.promise_());
1067
+
1068
+ // Resolver después
1069
+ deferred.resolve("listo");
1070
+ // O rechazar
1071
+ deferred.reject(new Error("falló"));
1072
+ ```
1073
+
1074
+ ---
1075
+
1076
+ ## Manejo de Errores
1077
+
1078
+ ### Códigos de Error
1079
+
1080
+ | Código | Descripción |
1081
+ | ------------------ | ----------------------------------------------------------------- |
1082
+ | `HTTP_ERROR` | Status HTTP no-2xx (configurable vía `validateStatus`) |
1083
+ | `TIMEOUT` | La petición excedió la duración del timeout |
1084
+ | `NETWORK_ERROR` | Fallo de red (DNS, conexión rechazada, etc.) |
1085
+ | `ABORTED` | La petición fue cancelada manualmente |
1086
+ | `VALIDATION_ERROR` | Los datos de respuesta no pasaron la validación |
1087
+ | `POLL_EXHAUSTED` | El polling alcanzó el máximo de intentos sin cumplir la condición |
1088
+ | `MAX_RETRIES` | Todos los intentos de reintento agotados |
1089
+ | `UNKNOWN_ERROR` | Error no clasificado |
1090
+
1091
+ ### Clase HttpError
1092
+
1093
+ ```typescript
1094
+ import { HttpError, isHttpError } from "@bereasoftware/nexa";
1095
+
1096
+ // Verificar si un error es un HttpError
1097
+ if (isHttpError(error)) {
1098
+ console.log(error.status); // Código de status HTTP
1099
+ console.log(error.code); // Cadena de código de error
1100
+ }
1101
+ ```
1102
+
1103
+ ### Patrón: Manejar Diferentes Tipos de Error
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
+ mostrarNotificacion("La petición expiró, intenta de nuevo");
1112
+ break;
1113
+ case "NETWORK_ERROR":
1114
+ mostrarNotificacion("Sin conexión a internet");
1115
+ break;
1116
+ case "HTTP_ERROR":
1117
+ if (result.error.status === 404)
1118
+ mostrarNotificacion("Usuario no encontrado");
1119
+ else if (result.error.status === 403) redirigirAlLogin();
1120
+ break;
1121
+ case "VALIDATION_ERROR":
1122
+ reportarBug("La API retornó un formato de datos inesperado");
1123
+ break;
1124
+ default:
1125
+ reportarError(result.error);
1126
+ }
1127
+ }
1128
+ ```
1129
+
1130
+ ---
1131
+
1132
+ ## Referencia de API
1133
+
1134
+ ### `createHttpClient(config?: HttpClientConfig): HttpClient`
1135
+
1136
+ Función factory para crear una nueva instancia del cliente HTTP.
1137
+
1138
+ ### Métodos de `HttpClient`
1139
+
1140
+ | Método | Firma | Descripción |
1141
+ | ------------------------ | ------------------------------------------------------------------------------------- | ---------------------------------------- |
1142
+ | `request` | `<T>(config: HttpRequestConfig) → Promise<Result<HttpResponse<T>, HttpErrorDetails>>` | Método core de petición |
1143
+ | `get` | `<T>(url, config?) → Promise<Result<...>>` | Petición GET |
1144
+ | `post` | `<T>(url, body?, config?) → Promise<Result<...>>` | Petición POST |
1145
+ | `put` | `<T>(url, body?, config?) → Promise<Result<...>>` | Petición PUT |
1146
+ | `patch` | `<T>(url, body?, config?) → Promise<Result<...>>` | Petición PATCH |
1147
+ | `delete` | `<T>(url, config?) → Promise<Result<...>>` | Petición DELETE |
1148
+ | `head` | `(url, config?) → Promise<Result<...>>` | Petición HEAD |
1149
+ | `options` | `(url, config?) → Promise<Result<...>>` | Petición OPTIONS |
1150
+ | `extend` | `(overrides?: HttpClientConfig) → HttpClient` | Crear cliente hijo |
1151
+ | `paginate` | `<T>(url, options, config?) → AsyncGenerator<T[]>` | Auto-paginación |
1152
+ | `poll` | `<T>(url, options, config?) → Promise<Result<...>>` | Polling inteligente |
1153
+ | `addRequestInterceptor` | `(interceptor) → Disposer` | Agregar interceptor de petición |
1154
+ | `addResponseInterceptor` | `(interceptor) → Disposer` | Agregar interceptor de respuesta |
1155
+ | `clearInterceptors` | `() → void` | Remover todos los interceptores |
1156
+ | `cancelAll` | `() → void` | Cancelar todas las peticiones pendientes |
1157
+ | `activeRequests` | `number` (getter) | Número de peticiones en vuelo |
1158
+ | `queueStats` | `{ active, pending }` (getter) | Estadísticas de la cola |
1159
+
1160
+ ### Tipos
1161
+
1162
+ | Tipo | Descripción |
1163
+ | --------------------- | --------------------------------------------------------------------------------- |
1164
+ | `Result<T, E>` | Unión discriminada éxito/fallo |
1165
+ | `HttpRequest` | Configuración de petición (url, method, headers, body, query, params) |
1166
+ | `HttpResponse<T>` | Respuesta con data, status, headers, duration |
1167
+ | `HttpErrorDetails` | Error con message, code, status, originalError |
1168
+ | `HttpRequestConfig` | Config completa de petición (extiende HttpRequest + retry, cache, hooks, etc.) |
1169
+ | `HttpClientConfig` | Configuración a nivel de cliente |
1170
+ | `RequestInterceptor` | Interceptar peticiones salientes |
1171
+ | `ResponseInterceptor` | Interceptar respuestas entrantes |
1172
+ | `RetryStrategy` | Interfaz de lógica de reintento personalizada |
1173
+ | `CacheStrategy` | Interfaz de implementación de caché personalizada |
1174
+ | `Validator` | Interfaz de validación de respuesta |
1175
+ | `Transformer` | Interfaz de transformación de respuesta |
1176
+ | `PaginateOptions<T>` | Configuración de paginación |
1177
+ | `PollOptions<T>` | Configuración de polling |
1178
+ | `RequestHooks<T>` | Callbacks de hooks de ciclo de vida |
1179
+ | `ProgressEvent` | Datos de progreso de descarga |
1180
+ | `ResponseType` | `'json' \| 'text' \| 'blob' \| 'arrayBuffer' \| 'formData' \| 'stream' \| 'auto'` |
1181
+ | `Disposer` | Función que remueve un interceptor |
1182
+
1183
+ ---
1184
+
1185
+ ## Formatos de Build
1186
+
1187
+ Nexa se distribuye en múltiples formatos de módulo:
1188
+
1189
+ | Formato | Archivo | Caso de Uso |
1190
+ | --------- | ----------------------- | ----------------------------------------- |
1191
+ | **ESM** | `dist/nexa.es.js` | Bundlers modernos (Vite, Rollup, esbuild) |
1192
+ | **CJS** | `dist/nexa.cjs.js` | Node.js `require()` |
1193
+ | **UMD** | `dist/nexa.umd.js` | Universal (AMD, CJS, global) |
1194
+ | **IIFE** | `dist/nexa.iife.js` | Tags de script (`<script>`) |
1195
+ | **Types** | `dist/types/index.d.ts` | Declaraciones de tipos TypeScript |
1196
+
1197
+ ---
1198
+
1199
+ ## Desarrollo
1200
+
1201
+ ### Pruebas
1202
+
1203
+ **157 tests en total**: 88 tests de HTTP Client + 69 tests de utilities
1204
+
1205
+ ```bash
1206
+ # Ejecutar todos los tests
1207
+ npm test
1208
+
1209
+ # Watch mode
1210
+ npm run test:watch
1211
+
1212
+ # Test coverage
1213
+ npm run test:coverage
1214
+ ```
1215
+
1216
+ Los tests usan **Vitest** (globals mode) con BDD style (`describe`/`it`/`expect`).
1217
+
1218
+ ### Build
1219
+
1220
+ ```bash
1221
+ # Generar distribución
1222
+ npm run build
1223
+ ```
1224
+
1225
+ **Configuración de build:**
1226
+
1227
+ - **Formatos**: ES, CommonJS, UMD, IIFE
1228
+ - **Minificación**: OXC (ultra-rápido)
1229
+ - **Type Definitions**: Bundled en `/dist/types`
1230
+ - **Tree-shakeable**: Solo importa lo que usas
1231
+ - **Externas**: `fs` (Node.js only para `streamToFile`)
1232
+
1233
+ **Output:**
1234
+
1235
+ ```
1236
+ dist/
1237
+ ├── nexa.es.js (24.9 KB, gzip: 7.53 KB)
1238
+ ├── nexa.cjs.js (19.9 KB, gzip: 6.68 KB)
1239
+ ├── nexa.umd.js (19.8 KB, gzip: 6.75 KB)
1240
+ ├── nexa.iife.js (19.6 KB, gzip: 6.68 KB)
1241
+ └── types/ (Tipo definitivo .d.ts)
1242
+ ```
1243
+
1244
+ ### Cobertura de Tests
1245
+
1246
+ **Cobertura General: 75.73%** — sólida cobertura de tests unitarios con mocking HTTP
1247
+
1248
+ | Componente | Cobertura | Detalles |
1249
+ | ----------- | ---------- | --------------------------------- |
1250
+ | HTTP Client | **80.85%** | 81.25% ramas, 73.43% funciones |
1251
+ | Types | **100%** | Cobertura perfecta de tipos |
1252
+ | Utils | **71.79%** | 66.66% ramas, 81.14% funciones |
1253
+
1254
+ **HTTP Client** (`test/http-client.test.ts`) — **88 tests**:
1255
+
1256
+ - ✓ Métodos core: create, GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS (7 tests)
1257
+ - ✓ Estrategias de reintentos & timeouts (3 tests)
1258
+ - ✓ Interceptores & disposal (5 tests)
1259
+ - ✓ Caché & validación (4 tests)
1260
+ - ✓ Type safety & extensiones (3 tests)
1261
+ - ✓ Paginación & polling (5 tests)
1262
+ - ✓ Manejo de tipos de respuesta: 8+ tipos + auto-detección (13 tests)
1263
+ - ✓ Detección de content-type binario: image/*, audio/*, video/*, octet-stream (5 tests)
1264
+ - ✓ Serialización de body: JSON, null, strings, Blob, URLSearchParams, ArrayBuffer, TypedArray, FormData, ReadableStream (7 tests)
1265
+ - ✓ Normalización de errores: TimeoutError, AbortError, TypeError, unknown, NETWORK_ERROR (5+ tests)
1266
+ - ✓ Gestión de peticiones: activeRequests, cancelAll, clearCache (2 tests)
1267
+ - ✓ Verificación de exports: todas las 8 categorías de exports (8 tests)
1268
+ - ✓ Integración de plugins: LoggerPlugin, MetricsPlugin event handlers (7 tests)
1269
+ - ✓ Configuración avanzada: null body, direct Blob, abort messages (5+ tests)
1270
+
1271
+ **Utilities** (`test/utils.test.ts`) — **69 tests**:
1272
+
1273
+ - ✓ Validadores: schema, required fields, type checks (4 tests)
1274
+ - ✓ Transformadores: snake↔camel case, flatten, projection, wrapper (5 tests)
1275
+ - ✓ Estrategias de Reintentos: Aggressive, Conservative, Circuit Breaker (10 tests)
1276
+ - ✓ Timeout & Retry: withTimeout, retry function (6 tests)
1277
+ - ✓ Caché: CacheStore CRUD, TTL expiry (5 tests)
1278
+ - ✓ Deduplicación: RequestDeduplicator sharing, cleanup (3 tests)
1279
+ - ✓ Pipeline de Middleware: ordering, next() guard, legacy pipeline (3 tests)
1280
+ - ✓ Cache Middleware: GET caching, POST bypass (2 tests)
1281
+ - ✓ Dedup Middleware: GET dedup, POST bypass (2 tests)
1282
+ - ✓ Generics Tipados: TypedResponse, TypedObservable (map/filter), Defer, type guards, branded types (9 tests)
1283
+ - ✓ Plugins: PluginManager, LoggerPlugin, MetricsPlugin, CachePlugin, DedupePlugin (5 tests)
1284
+
1285
+ ### Limitaciones de Cobertura & Techo Realista
1286
+
1287
+ La cobertura de tests unitarios se estabiliza alrededor del **75-80%** debido a limitaciones inherentes del mocking:
1288
+
1289
+ **¿Por qué no 95%?**
1290
+ - **Características de streaming** (~3-5% gap): Download progress tracking usa `ReadableStream.getReader()` que requiere HTTP real — no mockeable con `fetch-mock`
1291
+ - **Ejemplos de utilities** (~5-10% gap): Patrones de middleware y código de referencia no son ejercitados activamente en producción
1292
+ - **Archivos solo-export** (~2-3% gap): `http-client/index.ts` verificado vía validación de imports, no testeable por unidad
1293
+
1294
+ **Máximos realistas:**
1295
+ - Unit tests + mocks: **~80-85%** techo (actual: 75.73%)
1296
+ - Tests de integración requeridos: llegaría a 90%+ pero fuera del alcance del proyecto
1297
+
1298
+ El 75.73% de cobertura representa testing exhaustivo de todas las **rutas de código de producción** alcanzables vía mocks HTTP.
1299
+
1300
+ ---
1301
+
1302
+ ## Licencia
1303
+
1304
+ MIT © [John Andrade](mailto:johnandrade@bereasoft.com) — [@bereasoftware](https://github.com/Berea-Soft)