@bereasoftware/nexa 1.0.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,8 +22,31 @@ export interface HttpRequest {
22
22
  body?: unknown;
23
23
  query?: Record<string, string | number | boolean>;
24
24
  params?: Record<string, string | number>;
25
- timeout?: number;
25
+ timeout?: HttpTimeout;
26
26
  signal?: AbortSignal;
27
+ /**
28
+ * Controls cookie/credential policy for CORS requests. Same as fetch API.
29
+ * 'omit' | 'same-origin' | 'include'
30
+ */
31
+ credentials?: RequestCredentials;
32
+ /**
33
+ * Adapter personalizado para esta request (firma igual a fetch).
34
+ */
35
+ adapter?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
36
+ /**
37
+ * Si es true (default), convierte automáticamente el body a FormData si detecta archivos.
38
+ */
39
+ autoFormData?: boolean;
40
+ /**
41
+ * Transport layer to use for this request.
42
+ * Overrides global transport setting.
43
+ */
44
+ transport?: 'fetch' | 'node' | 'http2' | 'deno' | 'bun' | 'cloudflare';
45
+ /**
46
+ * Node.js specific transport options for this request.
47
+ * Overrides global nodeOptions.
48
+ */
49
+ nodeOptions?: NodeTransportOptions;
27
50
  }
28
51
  export interface HttpResponse<T = unknown> {
29
52
  status: number;
@@ -39,6 +62,18 @@ export interface HttpErrorDetails {
39
62
  statusText?: string;
40
63
  code?: string;
41
64
  originalError?: unknown;
65
+ /**
66
+ * The HTTP request that caused the error (available for both network and HTTP errors)
67
+ */
68
+ request?: HttpRequest;
69
+ /**
70
+ * The HTTP response if the request reached the server and received a response (HTTP errors only)
71
+ */
72
+ response?: HttpResponse<unknown>;
73
+ /**
74
+ * The configuration used for the request
75
+ */
76
+ config?: HttpRequestConfig;
42
77
  }
43
78
  export interface ProgressEvent {
44
79
  loaded: number;
@@ -60,10 +95,49 @@ export interface ResponseInterceptor {
60
95
  onResponse<T = unknown>(response: HttpResponse<T>): HttpResponse<T> | Promise<HttpResponse<T>>;
61
96
  onError?(error: HttpErrorDetails): HttpErrorDetails | Promise<HttpErrorDetails>;
62
97
  }
98
+ export type RetryCondition = (error: HttpErrorDetails, attempt: number) => boolean;
63
99
  export interface RetryStrategy {
64
100
  shouldRetry(attempt: number, error: HttpErrorDetails): boolean;
65
101
  delayMs(attempt: number): number;
66
102
  }
103
+ export interface InlineRetryConfig {
104
+ maxAttempts?: number;
105
+ backoffMs?: number;
106
+ on?: RetryCondition;
107
+ }
108
+ /**
109
+ * Node.js specific transport configuration for HTTP/1.1 and HTTP/2.
110
+ */
111
+ export interface NodeTransportOptions {
112
+ /**
113
+ * Enable keep-alive connections. Default: true
114
+ */
115
+ keepAlive?: boolean;
116
+ /**
117
+ * Maximum number of sockets to allow per host. Default: 50
118
+ */
119
+ maxSockets?: number;
120
+ /**
121
+ * Maximum number of sockets to leave open in a free state. Default: 10
122
+ */
123
+ maxFreeSockets?: number;
124
+ /**
125
+ * Maximum number of requests per socket. Default: 0 (unlimited)
126
+ */
127
+ maxRequestsPerSocket?: number;
128
+ /**
129
+ * Socket timeout in milliseconds. Default: 60000 (60 seconds)
130
+ */
131
+ timeout?: number;
132
+ /**
133
+ * Enable HTTP/2 protocol (only when transport is 'http2').
134
+ */
135
+ http2?: boolean;
136
+ /**
137
+ * HTTP/2 specific settings.
138
+ */
139
+ http2Settings?: Record<string, unknown>;
140
+ }
67
141
  export interface CacheStrategy {
68
142
  get(key: string): unknown | null;
69
143
  set(key: string, value: unknown, ttlMs?: number): void;
@@ -77,12 +151,54 @@ export interface Transformer {
77
151
  transform(data: unknown): unknown;
78
152
  }
79
153
  export type ResponseType = 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData' | 'stream' | 'auto';
154
+ /**
155
+ * Timeout configuration for HTTP requests.
156
+ * - number: total timeout for the entire request (connection + response)
157
+ * - object: differentiated timeouts for connection and response phases
158
+ */
159
+ export type HttpTimeout = number | {
160
+ /**
161
+ * Maximum time to establish connection (TCP/TLS handshake) in milliseconds.
162
+ * If not specified, no connection timeout is applied.
163
+ */
164
+ connection?: number;
165
+ /**
166
+ * Maximum time to receive complete response (after connection is established) in milliseconds.
167
+ * If not specified, no response timeout is applied.
168
+ */
169
+ response?: number;
170
+ /**
171
+ * Total timeout for the entire request (connection + response) in milliseconds.
172
+ * If specified, overrides both connection and response timeouts.
173
+ * Provided for backward compatibility and convenience.
174
+ */
175
+ total?: number;
176
+ };
177
+ /**
178
+ * Configuración extendida para solicitudes HTTP.
179
+ * - credentials: controla el envío de cookies/credenciales ('omit' | 'same-origin' | 'include').
180
+ */
80
181
  export interface HttpRequestConfig extends HttpRequest {
81
- retry?: RetryStrategy | {
82
- maxAttempts: number;
83
- backoffMs: number;
84
- };
85
- timeout?: number;
182
+ /**
183
+ * Si es true (default), convierte automáticamente el body a FormData si detecta archivos.
184
+ */
185
+ autoFormData?: boolean;
186
+ /**
187
+ * Compatibilidad con axios: si es true, establece credentials: 'include'; si es false, credentials: 'same-origin'.
188
+ * Si también se especifica credentials, este campo es ignorado.
189
+ */
190
+ withCredentials?: boolean;
191
+ /**
192
+ * Permite usar un adapter personalizado para la request (firma igual a fetch).
193
+ * Útil para mocks, tests, o entornos especiales.
194
+ */
195
+ adapter?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
196
+ /**
197
+ * Permite transformar el body antes de serializarlo y enviarlo. Similar a axios.transformRequest.
198
+ * Puede ser una función o un array de funciones.
199
+ */
200
+ transformRequest?: ((data: unknown, headers: Record<string, string>) => unknown) | Array<(data: unknown, headers: Record<string, string>) => unknown>;
201
+ retry?: RetryStrategy | InlineRetryConfig;
86
202
  validate?: Validator;
87
203
  transform?: Transformer;
88
204
  cache?: {
@@ -93,6 +209,24 @@ export interface HttpRequestConfig extends HttpRequest {
93
209
  hooks?: RequestHooks;
94
210
  onUploadProgress?: (event: ProgressEvent) => void;
95
211
  onDownloadProgress?: (event: ProgressEvent) => void;
212
+ /**
213
+ * Enable debug logging for this request. Overrides global debug setting.
214
+ */
215
+ debug?: boolean | 'verbose';
216
+ /**
217
+ * Custom logger function for this request. Overrides global logger.
218
+ */
219
+ logger?: (message: string, data?: unknown) => void;
220
+ /**
221
+ * Transport layer to use for this request.
222
+ * Overrides global transport setting.
223
+ */
224
+ transport?: 'fetch' | 'node' | 'http2' | 'deno' | 'bun' | 'cloudflare';
225
+ /**
226
+ * Node.js specific transport options for this request.
227
+ * Overrides global nodeOptions.
228
+ */
229
+ nodeOptions?: NodeTransportOptions;
96
230
  }
97
231
  export interface PaginateOptions<T> {
98
232
  /** Extract items from a response page */
@@ -131,12 +265,219 @@ export interface IHttpClient {
131
265
  /** Function that removes a previously added interceptor */
132
266
  export type Disposer = () => void;
133
267
  export interface HttpClientConfig {
268
+ /**
269
+ * Adapter global para todas las requests (firma igual a fetch).
270
+ */
271
+ adapter?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
134
272
  baseURL?: string;
135
273
  defaultHeaders?: Record<string, string>;
136
- defaultTimeout?: number;
274
+ /**
275
+ * Controls cookie/credential policy for CORS requests. Same as fetch API.
276
+ * 'omit' | 'same-origin' | 'include'
277
+ */
278
+ credentials?: RequestCredentials;
279
+ /**
280
+ * Compatibilidad con axios: si es true, establece credentials: 'include'; si es false, credentials: 'same-origin'.
281
+ * Si también se especifica credentials, este campo es ignorado.
282
+ */
283
+ withCredentials?: boolean;
284
+ defaultTimeout?: HttpTimeout;
137
285
  cacheStrategy?: CacheStrategy;
138
286
  validateStatus?: (status: number) => boolean;
139
287
  maxConcurrent?: number;
140
288
  defaultResponseType?: ResponseType;
141
289
  defaultHooks?: RequestHooks;
290
+ /**
291
+ * Permite transformar el body antes de serializarlo y enviarlo por defecto en todas las requests.
292
+ */
293
+ transformRequest?: ((data: unknown, headers: Record<string, string>) => unknown) | Array<(data: unknown, headers: Record<string, string>) => unknown>;
294
+ /**
295
+ * Si es true (default), convierte automáticamente el body a FormData si detecta archivos.
296
+ */
297
+ autoFormData?: boolean;
298
+ /**
299
+ * Enable debug logging for requests/responses. true for basic logs, 'verbose' for detailed logs.
300
+ */
301
+ debug?: boolean | 'verbose';
302
+ /**
303
+ * Custom logger function. If provided, replaces the default console.log with custom logging.
304
+ */
305
+ logger?: (message: string, data?: unknown) => void;
306
+ /**
307
+ * Transport layer to use for HTTP requests.
308
+ * - 'fetch': Uses global fetch API (default)
309
+ * - 'node': Uses Node.js http/https modules with HTTP/1.1
310
+ * - 'http2': Uses Node.js http2 module (HTTP/2)
311
+ * - 'deno': Uses Deno's fetch API (Deno environment)
312
+ * - 'bun': Uses Bun's fetch API (Bun environment)
313
+ * - 'cloudflare': Uses Cloudflare Workers fetch API
314
+ *
315
+ * When 'node' or 'http2' is specified, nodeOptions can be used to configure
316
+ * keep-alive, connection pooling, and other Node-specific settings.
317
+ *
318
+ * Note: Node transports are only available in Node.js environments.
319
+ * Deno, Bun, and Cloudflare transports use their respective fetch implementations.
320
+ */
321
+ transport?: 'fetch' | 'node' | 'http2' | 'deno' | 'bun' | 'cloudflare';
322
+ /**
323
+ * Node.js specific transport options.
324
+ * Only applies when transport is 'node' or 'http2'.
325
+ */
326
+ nodeOptions?: NodeTransportOptions;
327
+ }
328
+ /**
329
+ * WebSocket connection options
330
+ */
331
+ export interface WebSocketOptions {
332
+ /** WebSocket protocols (subprotocols) */
333
+ protocols?: string | string[];
334
+ /** Headers to send during handshake */
335
+ headers?: Record<string, string>;
336
+ /** Automatic reconnection settings */
337
+ reconnect?: {
338
+ /** Enable automatic reconnection (default: true) */
339
+ enabled?: boolean;
340
+ /** Base delay in ms for exponential backoff (default: 1000) */
341
+ baseDelay?: number;
342
+ /** Maximum delay in ms (default: 30000) */
343
+ maxDelay?: number;
344
+ /** Maximum number of reconnect attempts (default: Infinity) */
345
+ maxAttempts?: number;
346
+ /** Called before each reconnection attempt */
347
+ onReconnecting?: (attempt: number, delay: number) => void;
348
+ };
349
+ /** Timeout for connection establishment in ms (default: 10000) */
350
+ timeout?: number;
351
+ /** Callback for connection open */
352
+ onOpen?: (event: Event) => void;
353
+ /** Callback for connection close */
354
+ onClose?: (event: CloseEvent) => void;
355
+ /** Callback for connection error */
356
+ onError?: (event: Event) => void;
357
+ /** Enable heartbeat/ping-pong to keep connection alive */
358
+ heartbeat?: {
359
+ /** Interval in ms to send ping (default: 30000) */
360
+ interval?: number;
361
+ /** Timeout in ms to wait for pong before closing (default: 5000) */
362
+ timeout?: number;
363
+ /** Custom ping message (default: 'ping') */
364
+ pingMessage?: string | ArrayBuffer | Blob;
365
+ /** Custom pong message (default: 'pong') */
366
+ pongMessage?: string | ArrayBuffer | Blob;
367
+ };
368
+ }
369
+ /**
370
+ * Server-Sent Events (SSE) connection options
371
+ */
372
+ export interface SSEOptions {
373
+ /** Headers to send with the request */
374
+ headers?: Record<string, string>;
375
+ /** Request method (default: GET) */
376
+ method?: string;
377
+ /** Request body (for POST requests) */
378
+ body?: unknown;
379
+ /** Whether to send credentials (cookies) (default: same-origin) */
380
+ credentials?: RequestCredentials;
381
+ /** Timeout for connection establishment in ms (default: 10000) */
382
+ timeout?: number;
383
+ /** Automatic reconnection settings */
384
+ reconnect?: {
385
+ /** Enable automatic reconnection (default: true) */
386
+ enabled?: boolean;
387
+ /** Base delay in ms for exponential backoff (default: 1000) */
388
+ baseDelay?: number;
389
+ /** Maximum delay in ms (default: 30000) */
390
+ maxDelay?: number;
391
+ /** Maximum number of reconnect attempts (default: Infinity) */
392
+ maxAttempts?: number;
393
+ /** Called before each reconnection attempt */
394
+ onReconnecting?: (attempt: number, delay: number) => void;
395
+ };
396
+ /** Callback for connection open */
397
+ onOpen?: (event: Event) => void;
398
+ /** Callback for connection error */
399
+ onError?: (event: Event) => void;
400
+ /** Callback for connection close */
401
+ onClose?: () => void;
402
+ }
403
+ /**
404
+ * Real-time message event
405
+ */
406
+ export interface RealtimeMessageEvent<T = unknown> {
407
+ /** Message data (parsed if possible) */
408
+ data: T;
409
+ /** Raw message data */
410
+ raw: string | ArrayBuffer | Blob;
411
+ /** Message type (for WebSocket: 'message', for SSE: event type) */
412
+ type: string;
413
+ /** Timestamp when message was received */
414
+ timestamp: number;
415
+ }
416
+ /**
417
+ * Real-time client interface
418
+ */
419
+ export interface IRealtimeClient {
420
+ /** Connect to the server */
421
+ connect(): Promise<void>;
422
+ /** Disconnect from the server */
423
+ disconnect(): void;
424
+ /** Send a message */
425
+ send(data: string | ArrayBuffer | Blob): void;
426
+ /** Subscribe to messages */
427
+ onMessage<T = unknown>(callback: (event: RealtimeMessageEvent<T>) => void): () => void;
428
+ /** Subscribe to connection open events */
429
+ onOpen(callback: (event: Event) => void): () => void;
430
+ /** Subscribe to connection close events */
431
+ onClose(callback: (event?: CloseEvent) => void): () => void;
432
+ /** Subscribe to connection error events */
433
+ onError(callback: (event: Event) => void): () => void;
434
+ /** Get connection status */
435
+ getStatus(): 'connecting' | 'open' | 'closing' | 'closed';
436
+ /** Get connection statistics */
437
+ getStats(): {
438
+ messagesSent: number;
439
+ messagesReceived: number;
440
+ connectionTime: number;
441
+ reconnectAttempts: number;
442
+ };
443
+ }
444
+ /**
445
+ * WebSocket client interface (extends IRealtimeClient)
446
+ */
447
+ export interface IWebSocketClient extends IRealtimeClient {
448
+ /** WebSocket instance */
449
+ readonly socket: WebSocket | null;
450
+ /** Send JSON data (automatically serialized) */
451
+ sendJson(data: unknown): void;
452
+ /** Subscribe to specific message types */
453
+ onMessageType<T = unknown>(type: string, callback: (data: T) => void): () => void;
454
+ }
455
+ /**
456
+ * SSE client interface (extends IRealtimeClient)
457
+ */
458
+ export interface ISSEClient extends IRealtimeClient {
459
+ /** EventSource instance */
460
+ readonly source: EventSource | null;
461
+ /** Subscribe to specific event types */
462
+ onEvent<T = unknown>(event: string, callback: (data: T) => void): () => void;
463
+ /** Last event ID */
464
+ readonly lastEventId: string | null;
465
+ }
466
+ declare global {
467
+ interface Deno {
468
+ readonly version: {
469
+ deno: string;
470
+ };
471
+ }
472
+ const Deno: Deno | undefined;
473
+ interface Bun {
474
+ readonly version: string;
475
+ }
476
+ const Bun: Bun | undefined;
477
+ const WebSocketPair: {
478
+ new (): {
479
+ 0: WebSocket;
480
+ 1: WebSocket;
481
+ };
482
+ } | undefined;
142
483
  }
@@ -129,6 +129,43 @@ export declare function createDedupeMiddleware(options?: {
129
129
  * Pre-configured deduplication middleware for GET requests
130
130
  */
131
131
  export declare const dedupeMiddleware: Middleware<HttpContext>;
132
+ /**
133
+ * Rate limiting middleware factory - limits requests per time window
134
+ */
135
+ export declare function createRateLimitMiddleware(options?: {
136
+ /** Maximum number of requests per window */
137
+ maxRequests: number;
138
+ /** Time window in milliseconds (default: 60000 = 1 minute) */
139
+ windowMs?: number;
140
+ /** Function to generate rate limit key (default: uses request URL) */
141
+ keyGenerator?: (ctx: HttpContext) => string;
142
+ /** Custom error response when limit is exceeded (default: 429 Too Many Requests) */
143
+ errorResponse?: {
144
+ status: number;
145
+ body: unknown;
146
+ };
147
+ }): Middleware<HttpContext>;
148
+ /**
149
+ * Pre-configured rate limit middleware: 100 requests per minute per endpoint
150
+ */
151
+ export declare const rateLimitMiddleware: Middleware<HttpContext>;
152
+ /**
153
+ * Circuit breaker middleware factory - prevents cascading failures
154
+ */
155
+ export declare function createCircuitBreakerMiddleware(options?: {
156
+ /** Failure threshold to open circuit (default: 5) */
157
+ failureThreshold?: number;
158
+ /** Time in ms to wait before attempting again (default: 30000) */
159
+ resetTimeout?: number;
160
+ /** Function to determine if a response is a failure (default: status >= 500) */
161
+ isFailure?: (ctx: HttpContext) => boolean;
162
+ /** Function to generate circuit key (default: uses request URL) */
163
+ keyGenerator?: (ctx: HttpContext) => string;
164
+ }): Middleware<HttpContext>;
165
+ /**
166
+ * Pre-configured circuit breaker middleware
167
+ */
168
+ export declare const circuitBreakerMiddleware: Middleware<HttpContext>;
132
169
  /**
133
170
  * HTTP Context passed through middleware chain
134
171
  */
@@ -389,4 +426,35 @@ export declare class DedupePlugin implements Plugin {
389
426
  name: string;
390
427
  setup(manager: PluginManager): void;
391
428
  }
429
+ /**
430
+ * Rate limiting plugin - adds rate limiting middleware
431
+ */
432
+ export declare class RateLimitPlugin implements Plugin {
433
+ name: string;
434
+ private options;
435
+ constructor(options?: {
436
+ maxRequests?: number;
437
+ windowMs?: number;
438
+ keyGenerator?: (ctx: HttpContext) => string;
439
+ errorResponse?: {
440
+ status: number;
441
+ body: unknown;
442
+ };
443
+ });
444
+ setup(manager: PluginManager): void;
445
+ }
446
+ /**
447
+ * Circuit breaker plugin - adds circuit breaker middleware
448
+ */
449
+ export declare class CircuitBreakerPlugin implements Plugin {
450
+ name: string;
451
+ private options;
452
+ constructor(options?: {
453
+ failureThreshold?: number;
454
+ resetTimeout?: number;
455
+ isFailure?: (ctx: HttpContext) => boolean;
456
+ keyGenerator?: (ctx: HttpContext) => string;
457
+ });
458
+ setup(manager: PluginManager): void;
459
+ }
392
460
  export type { HttpErrorDetails };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bereasoftware/nexa",
3
- "version": "1.0.5",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/nexa.cjs.js",
6
6
  "module": "./dist/nexa.es.js",
@@ -16,6 +16,12 @@
16
16
  "import": "./dist/nexa.es.js",
17
17
  "require": "./dist/nexa.cjs.js",
18
18
  "default": "./dist/nexa.umd.js"
19
+ },
20
+ "./testing": {
21
+ "types": "./dist/types/index.d.ts",
22
+ "import": "./dist/nexa.es.js",
23
+ "require": "./dist/nexa.cjs.js",
24
+ "default": "./dist/nexa.umd.js"
19
25
  }
20
26
  },
21
27
  "files": [
@@ -47,6 +53,8 @@
47
53
  "author": "John Andrade <johnandrade@bereasoft.com>",
48
54
  "license": "MIT",
49
55
  "description": "Nexa is a TypeScript HTTP client library that combines the power of fetch with the convenience of axios, while adhering to SOLID principles. It provides a flexible and extensible API for making HTTP requests, handling retries, caching, and more.",
56
+ "about": "Nexa is designed to be a modern, type-safe HTTP client that simplifies the process of making API requests in TypeScript. It offers features such as request and response interceptors, automatic retries with exponential backoff, caching mechanisms, and a result type for better error handling. With support for both ESM and CommonJS, Nexa can be easily integrated into any TypeScript project.",
57
+ "homepage": "https://github.com/Berea-Soft/nexa",
50
58
  "maintainers": [
51
59
  {
52
60
  "name": "John Andrade",
@@ -81,18 +89,22 @@
81
89
  "prepublishOnly": "npm run build"
82
90
  },
83
91
  "devDependencies": {
84
- "@microsoft/api-extractor": "^7.58.0",
92
+ "@microsoft/api-extractor": "^7.58.5",
85
93
  "@semantic-release/changelog": "^6.0.3",
86
94
  "@semantic-release/git": "^10.0.1",
87
95
  "@semantic-release/github": "^12.0.6",
88
96
  "@semantic-release/npm": "^13.1.5",
89
- "@types/node": "^25.5.0",
90
- "@vitest/coverage-v8": "^4.1.2",
91
- "@vitest/ui": "^4.1.2",
97
+ "@types/cookie-parser": "^1.4.10",
98
+ "@types/node": "^25.6.0",
99
+ "@vitest/coverage-v8": "^4.1.4",
100
+ "@vitest/ui": "^4.1.4",
92
101
  "semantic-release": "^25.0.3",
93
- "typescript": "~6.0.2",
94
- "vite": "^8.0.3",
102
+ "typescript": "~6.0.3",
103
+ "vite": "^8.0.8",
95
104
  "vite-plugin-dts": "^4.5.4",
96
- "vitest": "^4.1.2"
105
+ "vitest": "^4.1.4"
106
+ },
107
+ "dependencies": {
108
+ "npm": "^11.12.1"
97
109
  }
98
110
  }