stelar-time-real 2.0.4 → 3.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.
package/README.md CHANGED
@@ -1,37 +1,106 @@
1
- # stelar-time-real
1
+ # stelar-time-real v3
2
2
 
3
- Your own custom real-time system. A lightweight, dependency-free library for real-time communication via WebSockets.
3
+ **Librería en tiempo real para producción.** Zero dependencias. Protocolo binario TCP custom + WebSocket manual (RFC 6455 implementado desde cero). Sin paquetes externos.
4
4
 
5
5
  ![npm](https://img.shields.io/npm/v/stelar-time-real)
6
6
  ![license](https://img.shields.io/npm/l/stelar-time-real)
7
- ![size](https://img.shields.io/bundlephobia/min/stelar-time-real)
7
+ ![zero-deps](https://img.shields.io/badge/dependencies-0-green)
8
+ ![production](https://img.shields.io/badge/status-production_ready-blue)
8
9
 
9
- ## Why stelar-time-real?
10
+ ---
10
11
 
11
- - **Ultra lightweight** - Only ~13MB of heap
12
- - 🚀 **No dependencies** - Uses only native `ws` (WebSocket)
13
- - 🎯 **Fully customizable** - You control everything, no one else's code
14
- - 🔌 **Compatible** - Works with Express, Fastify, native HTTP, etc.
15
- - 💓 **Heartbeat included** - Automatically detects disconnections
16
- - 🌐 **Namespaces** - Multiple independent channels (`/chat`, `/game`, etc.)
17
- - ⚡ **Ultra fast ACK** - Request-response with Promises, no overhead
18
- - 📦 **Binaries** - Send images, files, audio, video without base64 overhead
12
+ ## Que es stelar-time-real?
19
13
 
20
- ## Installation
14
+ stelar-time-real es una librería de comunicación en tiempo real diseñada desde cero para producción. No envuelve ni depende de ninguna librería externa — implementa su propio protocolo binario TCP y su propio WebSocket (RFC 6455) usando exclusivamente los módulos built-in de Node.js (`http`, `net`, `crypto`, `tls`).
21
15
 
22
- ```bash
23
- npm install stelar-time-real
16
+ Esto significa control total: sin dependencias que se rompan, sin vulnerabilidades de terceros, sin bloat, y sin sorpresas. Cada byte que viaja por la red es controlado por la librería.
17
+
18
+ ### Para que sirve?
19
+
20
+ - Chat en tiempo real (mensajería, notificaciones, typing indicators)
21
+ - Aplicaciones colaborativas (editores, pizarras, documentos compartidos)
22
+ - Streaming de datos binarios (imágenes, archivos, audio, video)
23
+ - Microservicios internos con comunicación ultra rápida via TCP
24
+ - Dashboards en vivo (métricas, monitoreo, trading)
25
+ - Juegos multijugador en tiempo real
26
+ - Redes sociales, plataformas tipo Discord
27
+ - IoT y dispositivos conectados
28
+
29
+ ---
30
+
31
+ ## Características Principales
32
+
33
+ ### Protocolo Dual
34
+
35
+ El servidor soporta **dos protocolos simultáneamente** en puertos diferentes:
36
+
37
+ - **WebSocket** — Para navegadores y clientes web. Implementación manual completa de RFC 6455: handshake, framing, masking, close codes, validación de RSV bits, max frame size enforcement.
38
+ - **TCP Custom** — Para comunicación entre servidores y microservicios Node.js. Protocolo binario propio con overhead mínimo (7 bytes de header). Latencia ultra baja.
39
+
40
+ Ambos protocolos comparten la misma API del servidor. Un cliente WebSocket y un cliente TCP pueden estar en el mismo room, recibir los mismos broadcasts, e interactuar como si fueran el mismo tipo de conexión.
41
+
42
+ ### Zero Dependencias
43
+
44
+ ```
45
+ dependencies: {}
24
46
  ```
25
47
 
26
- ## Quick Start
48
+ No hay `ws`, no hay `engine.io`, no hay nada. Solo Node.js puro. Esto significa:
49
+
50
+ - Sin vulnerabilidades en dependencias de terceros
51
+ - Sin breaking changes por actualizaciones ajenas
52
+ - Sin supply chain attacks
53
+ - Tamaño mínimo en node_modules
54
+ - Instalación instantánea
55
+
56
+ ### Preparada para Producción
57
+
58
+ Cada feature fue diseñada pensando en un entorno real con usuarios, ataques, y errores:
59
+
60
+ | Feature | Descripción |
61
+ |---------|-------------|
62
+ | **Rate Limiting** | Token bucket por cliente. Limita cuántos mensajes puede enviar cada cliente por ventana de tiempo. Previene spam y abuso. |
63
+ | **Per-IP Throttling** | Limita conexiones simultáneas desde la misma dirección IP. Previene ataques de fuerza bruta y bots. |
64
+ | **Max Connections** | Límite global de conexiones concurrentes. El servidor rechaza nuevas conexiones cuando se alcanza el límite. |
65
+ | **Max Rooms** | Límite global de rooms y límite por cliente. Previene que un solo cliente cree miles de rooms y consuma memoria. |
66
+ | **Graceful Shutdown** | Captura SIGINT/SIGTERM, deja de aceptar conexiones nuevas, espera a que las existentes se cierren (con timeout configurable), y limpia todos los recursos. |
67
+ | **Health Check** | Endpoint HTTP `/health` con estadísticas del servidor en vivo. Compatible con Kubernetes, Docker, y load balancers. |
68
+ | **Server Metrics** | Método `getStats()` con: conexiones activas, mensajes enviados/recibidos, rooms, uptime, uso de memoria, entradas del rate limiter. |
69
+ | **TLS/SSL** | Soporte nativo para `wss://` y TCP sobre TLS. Configuración simple con key y cert. |
70
+ | **Origin Checking** | Whitelist de orígenes permitidos para conexiones WebSocket. Previene CSRF y cross-origin abuse. |
71
+ | **CORS** | Headers CORS automáticos en el health endpoint con soporte para OPTIONS preflight. |
72
+ | **Input Validation** | Validación de nombres de eventos (strings no vacíos), tamaño máximo de payload, tamaño máximo de frames. |
73
+ | **Backpressure Handling** | Manejo del evento `drain` del socket. No se pierden datos cuando el buffer de red está lleno. |
74
+ | **Message Queue** | En el cliente: cola de mensajes cuando está desconectado. Se envían automáticamente al reconectar. Tamaño configurable, descarta los más viejos si se llena. |
75
+ | **Exponential Backoff** | Reconexión inteligente con backoff exponencial y jitter. Evita thundering herd cuando el servidor se reinicia. |
76
+ | **O(1) Client Lookup** | Búsqueda de cliente por ID en tiempo constante usando un Map indexado. Escalable a decenas de miles de clientes. |
77
+ | **No Signal Handler Leaks** | Los handlers de SIGINT/SIGTERM se limpian correctamente al hacer `stop()`. Múltiples instancias no causan `MaxListenersExceeded`. |
78
+ | **Timer unref** | Todos los timers internos usan `.unref()`. No impiden que el proceso de Node.js termine naturalmente. |
79
+ | **Custom Rate Limiter** | Interfaz `IRateLimiter` para reemplazar el rate limiter built-in con tu propia implementación (Redis, MongoDB, etc). |
80
+ | **Custom IP Tracker** | Interfaz `IIPTracker` para reemplazar el tracker de IP built-in con tu propia lógica. |
81
+ | **Custom Client ID** | Función `generateClientId` para generar IDs con tu propio formato. |
82
+ | **Event Rate Limits** | Rate limits por evento individual. Cada evento puede tener su propio límite de mensajes. |
83
+ | **Per-Client Rate Limits** | Rate limits por cliente individual con `setClientRateLimit()`. Override del límite global para clientes específicos. |
84
+ | **Hook System** | Callbacks para cada evento del servidor: rate limit excedido, max connections, payload too large, join/leave room, etc. |
85
+ | **Custom Health Handler** | Función `customHealthHandler` para reemplazar el health check built-in con tu propia lógica. |
86
+ | **Runtime Config** | Método `updateConfig()` para cambiar la configuración del servidor en caliente, sin reiniciar. |
87
+ | **Client Hooks** | Hooks en el cliente: `onBeforeEmit`, `onMessage`, `onStateChange`, `onReconnectDelay`, `onMessageQueued`, `onQueueDrained`, `onError`. |
88
+ | **Custom Reconnect** | Función `customReconnectDelay` o hook `onReconnectDelay` para controlar la lógica de reconexión del cliente. |
89
+ | **Client Runtime Config** | Método `updateOptions()` para cambiar la configuración del cliente en caliente. |
27
90
 
28
- ### One import for everything
91
+ ---
29
92
 
30
- ```javascript
31
- import StelarServer, { StelarClient } from 'stelar-time-real';
93
+ ## Instalación
94
+
95
+ ```bash
96
+ npm install stelar-time-real
32
97
  ```
33
98
 
34
- ### Server
99
+ ---
100
+
101
+ ## Quick Start
102
+
103
+ ### Servidor basico
35
104
 
36
105
  ```javascript
37
106
  import express from 'express';
@@ -43,523 +112,1131 @@ const server = app.listen(3000);
43
112
  const stelar = new StelarServer({ server });
44
113
 
45
114
  stelar.onConnection((client) => {
46
- console.log('New client:', client.id);
47
- client.emit('welcome', 'Hello! Welcome to stelar-time-real');
115
+ console.log('Conectado:', client.id);
116
+ client.emit('welcome', { message: 'Bienvenido al servidor!' });
48
117
  });
49
118
 
50
- stelar.on('message', (ctx) => {
51
- ctx.broadcast('message', ctx.data);
119
+ stelar.on('chat', (ctx) => {
120
+ ctx.broadcast('chat', ctx.data, ctx.id);
52
121
  });
53
122
 
54
- stelar.start();
123
+ await stelar.start();
55
124
  ```
56
125
 
57
- ### Client
126
+ ### Servidor con configuracion de produccion
58
127
 
59
128
  ```javascript
60
- import { StelarClient } from 'stelar-time-real';
129
+ import express from 'express';
130
+ import { StelarServer } from 'stelar-time-real';
61
131
 
62
- const client = new StelarClient('localhost:3000');
132
+ const app = express();
133
+ const server = app.listen(3000);
63
134
 
64
- client.on('connect', () => {
65
- console.log('Connected!');
135
+ const stelar = new StelarServer({
136
+ server,
137
+ maxConnections: 10000,
138
+ maxConnectionsPerIP: 50,
139
+ maxRooms: 10000,
140
+ maxRoomsPerClient: 50,
141
+ maxPayloadSize: 10 * 1024 * 1024,
142
+ rateLimit: { maxPoints: 100, windowMs: 1000 },
143
+ healthEndpoint: '/health',
144
+ heartbeatInterval: 30000,
145
+ heartbeatTimeout: 60000,
146
+ gracefulShutdown: true,
147
+ shutdownTimeout: 10000,
148
+ allowedOrigins: ['https://midominio.com'],
149
+ logger: 'info',
66
150
  });
67
151
 
68
- client.on('welcome', (msg) => {
69
- console.log(msg);
152
+ // Middleware de autenticación
153
+ stelar.use((ctx, next) => {
154
+ const token = ctx.req?.headers?.authorization;
155
+ if (!token) return ctx.ack('error', { message: 'Token requerido' });
156
+ next();
70
157
  });
71
158
 
72
- client.connect();
73
- ```
159
+ stelar.onConnection((client) => {
160
+ console.log(`[${client.protocol}] Cliente conectado: ${client.id} desde ${client.remoteAddress}`);
161
+ client.setMetadata('role', 'user');
162
+ client.emit('welcome', { id: client.id });
163
+ });
74
164
 
75
- ## Full API
165
+ stelar.onDisconnect((client) => {
166
+ console.log('Cliente desconectado:', client.id);
167
+ });
76
168
 
77
- ### StelarServer (Server Side)
169
+ stelar.on('chat', (ctx) => {
170
+ ctx.broadcast('chat', ctx.data, ctx.id);
171
+ });
78
172
 
79
- #### Constructor
173
+ stelar.onAck('getUser', (ctx) => {
174
+ return { id: ctx.data.id, name: 'Juan', role: ctx.getMetadata('role') };
175
+ });
80
176
 
81
- ```javascript
82
- new StelarServer({ server, port, heartbeatInterval })
177
+ await stelar.start();
178
+ console.log('Servidor listo en puerto', stelar.getPort());
83
179
  ```
84
180
 
85
- | Option | Type | Default | Description |
86
- |--------|------|---------|-------------|
87
- | server | http.Server | null | Your existing HTTP server |
88
- | port | number | 3000 | Port if you don't pass server |
89
- | heartbeatInterval | number | 30000 | Ping interval in ms |
90
-
91
- #### Methods
92
-
93
- **`.use(middleware)`**
94
- Add middleware to validate connections.
181
+ ### Cliente (Navegador o Node.js)
95
182
 
96
183
  ```javascript
97
- stelar.use((ctx, next) => {
98
- const token = ctx.req.headers['x-token'];
99
- if (token === 'secret') {
100
- next();
101
- } else {
102
- ctx.socket.close();
103
- }
184
+ import { StelarClient } from 'stelar-time-real';
185
+
186
+ const client = new StelarClient('localhost:3000', {
187
+ reconnection: true,
188
+ reconnectionAttempts: 10,
189
+ reconnectionDelay: 1000,
190
+ maxReconnectionDelay: 30000,
191
+ ackTimeout: 5000,
192
+ messageQueueSize: 100,
104
193
  });
194
+
195
+ client.on('connect', () => console.log('Conectado!'));
196
+ client.on('disconnect', () => console.log('Desconectado'));
197
+ client.on('welcome', (data) => console.log('Bienvenido:', data));
198
+
199
+ client.connect();
200
+
201
+ // Enviar mensaje
202
+ client.emit('chat', { message: 'Hola a todos!' });
203
+
204
+ // Request-response con Promise
205
+ const user = await client.request('getUser', { id: 1 }, 'getUser');
206
+ console.log(user); // { id: 1, name: 'Juan', role: 'user' }
207
+
208
+ // Unirse a rooms
209
+ client.joinRoom('general');
210
+ client.joinRoom('random');
211
+
212
+ // Enviar binario
213
+ const buffer = Buffer.from('datos binarios');
214
+ client.emitBinary('file', buffer);
105
215
  ```
106
216
 
107
- **`.on(event, handler)`**
108
- Listen for client events.
217
+ ### Cliente con modo TCP (Node.js unicamente — maxima eficiencia)
109
218
 
110
219
  ```javascript
111
- stelar.on('chat', (ctx) => {
112
- console.log('Message:', ctx.data);
113
- ctx.broadcast('chat', ctx.data);
220
+ const client = new StelarClient('localhost:3001', {
221
+ mode: 'tcp',
222
+ reconnection: true,
114
223
  });
224
+
225
+ client.on('connect', () => console.log('TCP conectado!'));
226
+ client.connect();
115
227
  ```
116
228
 
117
- **`.onAll(handler)`**
118
- Listen for all events (useful for debug).
229
+ El modo TCP usa el protocolo binario custom en vez de WebSocket. Menos overhead, menor latencia, ideal para comunicación entre servidores.
230
+
231
+ ### Cliente con TLS/WSS (conexiones seguras)
119
232
 
120
233
  ```javascript
121
- stelar.onAll(({ event, data }) => {
122
- console.log(`Event: ${event}`, data);
234
+ // WSS WebSocket seguro
235
+ const client = new StelarClient('wss://secure.midominio.com', {
236
+ tls: true,
237
+ rejectUnauthorized: true,
238
+ });
239
+
240
+ // TCP + TLS
241
+ const client = new StelarClient('secure.midominio.com:3001', {
242
+ mode: 'tcp',
243
+ tls: true,
244
+ rejectUnauthorized: true,
123
245
  });
124
246
  ```
125
247
 
126
- **`.onConnection(handler)`**
127
- Execute when a client connects.
248
+ ### Servidor con TLS
128
249
 
129
250
  ```javascript
130
- stelar.onConnection((client) => {
131
- client.emit('welcome', 'Hello!');
251
+ import { readFileSync } from 'fs';
252
+
253
+ const stelar = new StelarServer({
254
+ port: 3000,
255
+ tls: {
256
+ key: readFileSync('server-key.pem'),
257
+ cert: readFileSync('server-cert.pem'),
258
+ },
259
+ tcpPort: 3001,
132
260
  });
133
261
  ```
134
262
 
135
- **`.broadcast(event, data)`**
136
- Send to all clients.
263
+ ---
137
264
 
138
- ```javascript
139
- stelar.broadcast('chat', { message: 'Hello everyone' });
140
- ```
265
+ ## Arquitectura
141
266
 
142
- **`.to(room, event, data)`**
143
- Send to a specific room.
267
+ ### Protocolo Dual
144
268
 
145
- ```javascript
146
- stelar.to('room-1', 'chat', { message: 'Hello room 1' });
269
+ ```
270
+ Servidor stelar-time-real
271
+ ┌──────────────────────────┐
272
+ │ │
273
+ Navegadores ──────► Puerto 3000 (WebSocket) │
274
+ (ws://) │ │ │
275
+ │ Misma lógica │
276
+ Node.js ──────► Puerto 3001 (TCP Custom) │
277
+ (modo tcp) │ │ │
278
+ │ │
279
+ └──────────────────────────┘
147
280
  ```
148
281
 
149
- **`.toId(id, event, data)`**
150
- Send to a specific client by ID.
282
+ Ambos protocolos comparten:
283
+ - Mismos event handlers
284
+ - Mismos rooms
285
+ - Mismo sistema de broadcast
286
+ - Mismo sistema de ACK
287
+ - Mismo middleware
288
+ - Mismas métricas
151
289
 
152
- ```javascript
153
- stelar.toId('abc123', 'private', 'Just for you');
154
- ```
290
+ Un cliente WebSocket y un cliente TCP pueden estar en el mismo room y comunicarse sin problemas.
155
291
 
156
- **`.getClients(room)`**
157
- Get list of clients.
292
+ ### WebSocket Mode vs TCP Mode
158
293
 
159
- ```javascript
160
- const all = stelar.getClients();
161
- const room = stelar.getClients('my-room');
162
- ```
294
+ | Aspecto | WebSocket | TCP Custom |
295
+ |---------|-----------|------------|
296
+ | Navegador | Si | No |
297
+ | Node.js | Si | Si |
298
+ | Overhead por frame | 2-14 bytes (RFC 6455) | 7 bytes (header custom) |
299
+ | Latencia | Baja | Ultra baja |
300
+ | TLS | wss:// | TLS nativo |
301
+ | Caso de uso | Frontend, apps web | Microservicios, backend |
163
302
 
164
- **`.getPort()`**
165
- Get the port where it's running.
303
+ ### Formato del Protocolo Binario (TCP)
166
304
 
167
- ```javascript
168
- console.log('Port:', stelar.getPort());
305
+ ```
306
+ ┌──────────────┬──────────┬───────────────┬──────────────┬──────────────┐
307
+ │ totalLen (4B) │ type (1B)│ eventLen (2B) │ event (N B) │ payload │
308
+ │ Big Endian │ │ Big Endian │ UTF-8 string │ JSON/Binary │
309
+ └──────────────┴──────────┴───────────────┴──────────────┴──────────────┘
169
310
  ```
170
311
 
171
- **`.start(callback)`**
172
- Start the WebSocket server.
312
+ **11 tipos de frame:**
313
+
314
+ | Tipo | Código | Descripción |
315
+ |------|--------|-------------|
316
+ | JSON | 0 | Evento con payload JSON |
317
+ | Binary | 1 | Datos binarios puros |
318
+ | Ping | 2 | Heartbeat del cliente |
319
+ | Pong | 3 | Respuesta del servidor |
320
+ | ACK Request | 4 | Petición que espera respuesta |
321
+ | ACK Response | 5 | Respuesta a una petición ACK |
322
+ | Connect | 6 | Frame de conexión inicial |
323
+ | Disconnect | 7 | Frame de desconexión |
324
+ | Join Room | 8 | Unirse a un room |
325
+ | Leave Room | 9 | Salir de un room |
326
+ | Error | 10 | Frame de error |
327
+
328
+ ### WebSocket Manual (RFC 6455)
329
+
330
+ stelar-time-real implementa WebSocket desde cero usando solo `http` y `crypto` de Node.js. No usa la librería `ws` ni ninguna otra.
331
+
332
+ La implementación incluye:
333
+ - **Handshake** — Calcula el Sec-WebSocket-Accept con SHA-1 según RFC 6455
334
+ - **Framing** — Parseo y creación de frames (text, binary, ping, pong, close)
335
+ - **Masking** — Aplica/desaplica XOR mask (requerido cliente→servidor)
336
+ - **Fragmentación** — Manejo de frames fragmentados
337
+ - **Close codes** — Todos los códigos de cierre soportados
338
+ - **Validación** — RSV bits, opcode validation, max frame size
339
+ - **PING/PONG** — El servidor responde PONG a PING correctamente
173
340
 
174
- ```javascript
175
- await stelar.start();
176
- console.log('Started!');
177
- ```
341
+ ---
342
+
343
+ ## API Completa
178
344
 
179
- **`.stop()`**
180
- Stop the server.
345
+ ### StelarServer — Opciones
181
346
 
182
347
  ```javascript
183
- stelar.stop();
348
+ new StelarServer({
349
+ // Conexión
350
+ port: 3000, // Puerto HTTP/WebSocket
351
+ server: httpServer, // Servidor HTTP existente (alternativa a port)
352
+ namespace: '/', // Path del namespace
353
+ tcpPort: 3001, // Puerto TCP (false = deshabilitado)
354
+
355
+ // Límites
356
+ maxConnections: 10000, // Máximo de conexiones concurrentes
357
+ maxConnectionsPerIP: 50, // Máximo de conexiones por dirección IP
358
+ maxRooms: 10000, // Máximo de rooms globales
359
+ maxRoomsPerClient: 50, // Máximo de rooms por cliente
360
+ maxPayloadSize: 10 * 1024 * 1024, // Tamaño máximo de payload (10MB)
361
+ maxFrameSize: 10 * 1024 * 1024, // Tamaño máximo de frame WebSocket (10MB)
362
+
363
+ // Rate Limiting
364
+ rateLimit: {
365
+ maxPoints: 100, // Máximo de puntos (mensajes) por ventana
366
+ windowMs: 1000, // Ventana de tiempo en milisegundos
367
+ },
368
+
369
+ // Timeouts
370
+ heartbeatInterval: 30000, // Intervalo de ping (30s)
371
+ heartbeatTimeout: 60000, // Timeout antes de desconectar (60s)
372
+ connectTimeout: 10000, // Timeout de conexión inicial (10s)
373
+
374
+ // Producción
375
+ healthEndpoint: '/health', // URL del health check (false = deshabilitado)
376
+ gracefulShutdown: true, // Capturar SIGINT/SIGTERM
377
+ shutdownTimeout: 10000, // Tiempo máximo de espera al cerrar (10s)
378
+ allowedOrigins: ['https://midominio.com'], // Orígenes permitidos (null = todos)
379
+ tls: { key, cert }, // Opciones TLS para wss:// y TCP TLS
380
+
381
+ // Logging
382
+ logger: 'info', // Nivel: 'debug'|'info'|'warn'|'error'|'silent'
383
+ // También acepta instancia de Logger o false
384
+ });
184
385
  ```
185
386
 
186
- #### Context (ctx) in handlers
387
+ ### StelarServer Métodos
388
+
389
+ #### Eventos
390
+
391
+ | Método | Descripción |
392
+ |--------|-------------|
393
+ | `.on(event, handler)` | Escuchar eventos de clientes |
394
+ | `.onAll(handler)` | Escuchar todos los eventos |
395
+ | `.onConnection(handler)` | Cliente conectado |
396
+ | `.onDisconnect(handler)` | Cliente desconectado |
397
+ | `.onAck(name, handler)` | Registrar handler ACK (retorna valor al cliente) |
398
+
399
+ #### Envío de mensajes
400
+
401
+ | Método | Descripción |
402
+ |--------|-------------|
403
+ | `.broadcast(event, data, excludeId?)` | Enviar a todos los clientes (opcionalmente excluir uno) |
404
+ | `.to(room, event, data, excludeId?)` | Enviar a un room (opcionalmente excluir) |
405
+ | `.toId(id, event, data)` | Enviar a un cliente específico — búsqueda O(1) |
406
+ | `.broadcastBinary(event, buffer)` | Broadcast de datos binarios |
407
+
408
+ #### Información
409
+
410
+ | Método | Descripción |
411
+ |--------|-------------|
412
+ | `.getClients(room?)` | Lista de clientes con sus rooms |
413
+ | `.getRoomMembers(room)` | IDs de clientes en un room |
414
+ | `.getRooms()` | Lista de rooms activos |
415
+ | `.getStats()` | Estadísticas del servidor |
416
+ | `.getPort()` | Puerto en el que corre el servidor |
417
+
418
+ #### Lifecycle
419
+
420
+ | Método | Descripción |
421
+ |--------|-------------|
422
+ | `.use(middleware)` | Agregar middleware de conexión |
423
+ | `.start(callback?)` | Iniciar servidor, retorna `Promise<number>` con el puerto |
424
+ | `.stop()` | Detener servidor, cerrar conexiones, limpiar handlers |
425
+
426
+ ### StelarContext (ctx) — Dentro de los handlers
187
427
 
188
- When you listen to an event, you receive a `ctx` with:
428
+ Cada handler de evento recibe un contexto (`ctx`) con toda la información y acciones disponibles:
189
429
 
190
430
  ```javascript
191
431
  stelar.on('message', (ctx) => {
192
- ctx.id // Unique client ID
193
- ctx.socket // Client's WebSocket
194
- ctx.req // Original HTTP request
195
- ctx.data // Received data
432
+ // Información del cliente
433
+ ctx.id // ID único del cliente
434
+ ctx.socket // net.Socket crudo
435
+ ctx.req // HTTP request (null para TCP)
436
+ ctx.data // Datos recibidos
437
+ ctx.clientInfo // Info del cliente
438
+ ctx.clientInfo.rooms // Set de rooms del cliente
439
+ ctx.clientInfo.metadata // Map de metadata custom
440
+ ctx.clientInfo.remoteAddress // Dirección IP del cliente
441
+ ctx.clientInfo.protocol // 'ws' o 'tcp'
442
+
443
+ // Acciones — Enviar mensajes
444
+ ctx.emit('event', data) // Enviar a este cliente
445
+ ctx.send('response', data) // Responder a ACK
446
+ ctx.emitBinary('event', buffer) // Enviar binario
447
+ ctx.broadcast('event', data) // Enviar a todos (excluyéndose)
448
+ ctx.broadcastBinary('event', buf) // Broadcast binario
449
+ ctx.to('room', 'event', data) // Enviar a un room
450
+ ctx.toId('id', 'event', data) // Enviar a cliente específico (O(1))
451
+
452
+ // Acciones — Rooms
453
+ ctx.joinRoom('room') // Unirse a un room
454
+ ctx.leaveRoom('room') // Salir de un room
455
+ ctx.getClients('room') // Listar clientes del room
456
+
457
+ // Acciones — Metadata
458
+ ctx.setMetadata('role', 'admin') // Guardar dato custom
459
+ ctx.getMetadata('role') // Leer dato custom
460
+
461
+ // Acciones — ACK
462
+ ctx.ack('myAck', data) // Responder a una petición ACK
463
+ });
464
+ ```
196
465
 
197
- // Available methods:
198
- ctx.emit('event', data) // Send to this client only
199
- ctx.send('response', data) // Reply to an ACK
200
- ctx.broadcast('event', data) // Send to everyone
201
- ctx.to('room', 'event', data) // Send to a room
202
- ctx.toId('id', 'event', data) // Send to specific client
203
- ctx.getClients('room') // See clients in room
204
- ctx.joinRoom('room') // Join room
205
- ctx.leaveRoom() // Leave room
206
- ctx.ack('myAck', data) // Reply to a custom ACK
466
+ ### StelarClient — Opciones
467
+
468
+ ```javascript
469
+ new StelarClient(urlOrPort, {
470
+ // Conexión
471
+ reconnection: true, // Auto reconectar
472
+ reconnectionAttempts: 10, // Máximo de intentos
473
+ reconnectionDelay: 1000, // Delay base (ms)
474
+ maxReconnectionDelay: 30000, // Delay máximo (ms)
475
+ heartbeatInterval: 30000, // Intervalo de heartbeat
476
+
477
+ // Protocolo
478
+ mode: 'ws', // 'ws' o 'tcp'
479
+ maxPayloadSize: 10 * 1024 * 1024,
480
+ maxFrameSize: 10 * 1024 * 1024,
481
+
482
+ // ACK
483
+ ackTimeout: 5000, // Timeout de ACK (ms)
484
+
485
+ // Cola de mensajes
486
+ messageQueueSize: 100, // Mensajes en cola cuando está desconectado
487
+
488
+ // Seguridad
489
+ tls: false, // Habilitar TLS para wss:// o TCP TLS
490
+ rejectUnauthorized: true, // Validar certificado TLS
491
+
492
+ // Headers custom
493
+ headers: {}, // Headers para el handshake WebSocket
494
+
495
+ // Logging
496
+ logger: 'warn', // Nivel de log
207
497
  });
208
498
  ```
209
499
 
210
- #### Namespaces
500
+ ### StelarClient — Métodos
211
501
 
212
- Create independent channels:
502
+ #### Eventos
213
503
 
214
- ```javascript
215
- import { StelarServer } from 'stelar-time-real';
504
+ | Método | Descripción |
505
+ |--------|-------------|
506
+ | `.on(event, handler)` | Escuchar evento |
507
+ | `.off(event, handler)` | Remover listener |
508
+ | `.once(event, handler)` | Escuchar una sola vez |
509
+ | `.onAll(handler)` | Escuchar todos los eventos |
510
+ | `.onAck(name, handler)` | Escuchar respuestas ACK |
216
511
 
217
- // Main namespace
218
- const main = new StelarServer({ server, namespace: '/' });
512
+ #### Envío
219
513
 
220
- // Chat namespace
221
- const chat = StelarServer.of('/chat', { server });
222
- chat.on('message', (ctx) => {
223
- ctx.broadcast('message', ctx.data);
224
- });
514
+ | Método | Descripción |
515
+ |--------|-------------|
516
+ | `.emit(event, data, opts?)` | Enviar evento (`opts.ack` para ACK) |
517
+ | `.emitBinary(event, data)` | Enviar datos binarios |
518
+ | `.sendFile(file)` | Enviar archivo |
519
+ | `.sendImage(blob)` | Enviar imagen |
520
+ | `.request(event, data, ackName)` | Request-response con Promise |
225
521
 
226
- // Game namespace
227
- const game = StelarServer.of('/game', { server });
228
- game.on('move', (ctx) => {
229
- ctx.to(ctx.data.room, 'move', ctx.data);
230
- });
231
- ```
522
+ #### Rooms
523
+
524
+ | Método | Descripción |
525
+ |--------|-------------|
526
+ | `.joinRoom(room)` | Unirse a un room |
527
+ | `.leaveRoom(room)` | Salir de un room |
528
+
529
+ #### Lifecycle
530
+
531
+ | Método | Descripción |
532
+ |--------|-------------|
533
+ | `.connect(callback?)` | Conectar al servidor |
534
+ | `.disconnect()` | Desconectar y limpiar todos los recursos |
232
535
 
233
- #### ACK (Request-Response)
536
+ #### Estado y métricas
234
537
 
235
- Ultra efficient system with Promises:
538
+ | Método | Descripción |
539
+ |--------|-------------|
540
+ | `.isConnected()` | Está conectado? |
541
+ | `.getState()` | Estado: `'disconnected'` \| `'connecting'` \| `'connected'` \| `'reconnecting'` |
542
+ | `.getId()` | ID asignado por el servidor |
543
+ | `.getUrl()` | URL del servidor |
544
+ | `.setUrl(url)` | Cambiar URL antes de conectar |
545
+ | `.getMessagesSent()` | Total de mensajes enviados |
546
+ | `.getMessagesReceived()` | Total de mensajes recibidos |
547
+ | `.getLastError()` | Último error |
548
+ | `.getConnectTime()` | Timestamp de la última conexión exitosa |
549
+ | `.getQueueSize()` | Mensajes pendientes en la cola |
550
+ | `.removeAllListeners(event?)` | Limpiar listeners |
236
551
 
237
- **Server:**
552
+ ### Eventos del Cliente
238
553
 
239
554
  ```javascript
240
- // Register an ACK handler
241
- stelar.onAck('getUser', (ctx) => {
242
- return { id: ctx.data.id, name: 'John' };
555
+ client.on('connect', () => {
556
+ // Conexión establecida
243
557
  });
244
558
 
245
- // Or with more complex logic
246
- stelar.onAck('saveData', (ctx) => {
247
- const result = saveToDatabase(ctx.data);
248
- return { success: true, id: result.id };
559
+ client.on('disconnect', (info) => {
560
+ // info = { code, reason } para WebSocket
249
561
  });
250
- ```
251
-
252
- **Client:**
253
562
 
254
- ```javascript
255
- // Using request() - returns Promise
256
- const user = await client.request('getUser', { id: 1 }, 'userData');
257
- console.log(user); // { id: 1, name: 'John' }
563
+ client.on('reconnecting', (attempt) => {
564
+ // Intento número `attempt` de reconexión
565
+ });
258
566
 
259
- // Or emit with callback
260
- client.emit('getUser', { id: 1 }, { ack: 'userData' });
261
- client.on('userData', (data) => {
262
- console.log(data);
567
+ client.on('reconnect_failed', () => {
568
+ // Se agotaron los intentos de reconexión
263
569
  });
264
570
 
265
- // ACK from server to client
266
- client.onAck('serverPush', (data) => {
267
- console.log('Server sent:', data);
571
+ client.on('error', (err) => {
572
+ // Error de conexión o protocolo
268
573
  });
269
574
  ```
270
575
 
271
576
  ---
272
577
 
273
- ### StelarClient (Client Side)
578
+ ## Health Check
274
579
 
275
- #### Constructor
580
+ El endpoint de health check está diseñado para integrarse con orquestadores como Kubernetes, Docker Swarm, o cualquier load balancer.
276
581
 
277
- ```javascript
278
- new StelarClient(urlOrPort, options)
582
+ ```bash
583
+ curl http://localhost:3000/health
279
584
  ```
280
585
 
281
- | Param | Type | Default | Description |
282
- |-------|------|---------|-------------|
283
- | urlOrPort | string/number | localhost:3000 | Server URL or port |
284
- | options.reconnection | boolean | true | Auto reconnect |
285
- | options.reconnectionAttempts | number | 5 | Reconnection attempts |
286
- | options.reconnectionDelay | number | 1000 | Delay between attempts (ms) |
287
- | options.heartbeatInterval | number | 30000 | Ping interval |
586
+ Respuesta:
587
+
588
+ ```json
589
+ {
590
+ "status": "ok",
591
+ "totalConnections": 150,
592
+ "activeConnections": 42,
593
+ "totalMessagesReceived": 5000,
594
+ "totalMessagesSent": 4800,
595
+ "totalRooms": 12,
596
+ "uptime": 3600000,
597
+ "uptimeSeconds": 3600,
598
+ "wsConnections": 38,
599
+ "tcpConnections": 4,
600
+ "memoryMB": 10.54,
601
+ "memoryUsage": {
602
+ "heapUsed": 11062016,
603
+ "heapTotal": 17301504,
604
+ "rss": 24576000,
605
+ "external": 1245184
606
+ },
607
+ "rateLimiterEntries": 42
608
+ }
609
+ ```
610
+
611
+ CORS es automático en el health endpoint. Si `allowedOrigins` está configurado, se agrega el header `Access-Control-Allow-Origin` para los orígenes coincidentes. Las peticiones OPTIONS preflight retornan 204.
612
+
613
+ ---
614
+
615
+ ## Middleware
616
+
617
+ El sistema de middleware permite validar conexiones antes de que un cliente sea aceptado:
288
618
 
289
619
  ```javascript
290
- // Just port
291
- const client = new StelarClient(3000);
620
+ // Autenticación con token
621
+ stelar.use((ctx, next) => {
622
+ const token = ctx.req?.headers?.authorization;
623
+ if (!token) {
624
+ return ctx.ack('error', { message: 'Token requerido' });
625
+ }
626
+ // Validar token...
627
+ ctx.setMetadata('userId', getUserIdFromToken(token));
628
+ next();
629
+ });
292
630
 
293
- // Full URL
294
- const client = new StelarClient('ws://mydomain.com/ws');
631
+ // Rate limiting custom
632
+ stelar.use((ctx, next) => {
633
+ const ip = ctx.req?.headers?.['x-forwarded-for'] || ctx.socket.remoteAddress;
634
+ if (isBlocked(ip)) {
635
+ return ctx.socket.destroy();
636
+ }
637
+ next();
638
+ });
295
639
 
296
- // With options
297
- const client = new StelarClient(3000, {
298
- reconnection: true,
299
- reconnectionAttempts: 10,
300
- reconnectionDelay: 2000
640
+ // Logging
641
+ stelar.use((ctx, next) => {
642
+ console.log(`Nueva conexión desde ${ctx.clientInfo.remoteAddress}`);
643
+ next();
301
644
  });
302
645
  ```
303
646
 
304
- #### Methods
647
+ Múltiples middlewares se ejecutan en orden. Si un middleware no llama a `next()`, la conexión se rechaza.
305
648
 
306
- **`.on(event, handler)`**
307
- Listen for server events.
649
+ ---
308
650
 
309
- ```javascript
310
- client.on('welcome', (data) => {
311
- console.log(data);
312
- });
313
- ```
651
+ ## Rooms
314
652
 
315
- **`.onAll(handler)`**
316
- Listen for all events.
653
+ Los rooms son canales de comunicación. Un cliente puede estar en múltiples rooms simultáneamente:
317
654
 
318
655
  ```javascript
319
- client.onAll(({ event, data }) => {
320
- console.log(`${event}:`, data);
656
+ // Servidor
657
+ stelar.on('joinChannel', (ctx) => {
658
+ ctx.joinRoom(ctx.data.channel);
659
+ ctx.to(ctx.data.channel, 'userJoined', { userId: ctx.id });
321
660
  });
661
+
662
+ stelar.on('channelMessage', (ctx) => {
663
+ const rooms = ctx.clientInfo.rooms;
664
+ for (const room of rooms) {
665
+ ctx.to(room, 'channelMessage', ctx.data, ctx.id);
666
+ }
667
+ });
668
+
669
+ // Cliente
670
+ client.joinRoom('general');
671
+ client.joinRoom('random');
672
+ client.joinRoom('project-alpha');
322
673
  ```
323
674
 
324
- **`.onAck(name, handler)`**
325
- Listen for ACK responses from the server.
675
+ Los rooms se limpian automáticamente cuando el último cliente sale o se desconecta. No hay que liberar recursos manualmente.
676
+
677
+ ---
678
+
679
+ ## ACK (Request-Response)
680
+
681
+ El sistema de ACK permite comunicación request-response confiable sobre el protocolo en tiempo real:
326
682
 
327
683
  ```javascript
328
- client.onAck('userData', (data) => {
329
- console.log('Data received:', data);
684
+ // Servidor Registrar handler ACK
685
+ stelar.onAck('getUsers', (ctx) => {
686
+ return { users: ['Juan', 'Maria', 'Pedro'] };
687
+ });
688
+
689
+ stelar.onAck('validateToken', (ctx) => {
690
+ const valid = validateToken(ctx.data.token);
691
+ if (!valid) throw new Error('Token inválido');
692
+ return { userId: 123 };
330
693
  });
331
- ```
332
694
 
333
- **`.emit(event, data, opts)`**
334
- Send events to the server. Supports `opts.ack` for ACKs.
695
+ // Cliente — Enviar petición y esperar respuesta
696
+ const users = await client.request('getUsers', {}, 'getUsers');
697
+ console.log(users); // { users: ['Juan', 'Maria', 'Pedro'] }
335
698
 
336
- ```javascript
337
- client.emit('chat', { message: 'Hello!' });
338
- client.emit('getUser', { id: 1 }, { ack: 'userData' });
699
+ try {
700
+ const result = await client.request('validateToken', { token: 'abc' }, 'validateToken');
701
+ } catch (err) {
702
+ console.log('Token inválido');
703
+ }
339
704
  ```
340
705
 
341
- **`.request(event, data, ackName)`**
342
- Send and wait for response as Promise.
706
+ Las peticiones ACK tienen timeout configurable (`ackTimeout`). Si el servidor no responde en ese tiempo, la Promise se rechaza.
707
+
708
+ ---
709
+
710
+ ## Datos Binarios
711
+
712
+ Enviar archivos, imágenes, audio, o cualquier dato binario sin overhead de base64:
343
713
 
344
714
  ```javascript
345
- const result = await client.request('getUser', { id: 1 }, 'userData');
346
- console.log(result); // { id: 1, name: 'John' }
715
+ // Servidor Recibir y reenviar binario
716
+ stelar.on('file', (ctx) => {
717
+ ctx.broadcastBinary('file', ctx.data); // ctx.data es un Buffer
718
+ });
347
719
 
348
- // With optional timeout
349
- const client = new StelarClient(3000, { ackTimeout: 10000 });
720
+ // Cliente Enviar binario
721
+ const imageBuffer = await fs.readFile('photo.png');
722
+ client.emitBinary('file', imageBuffer);
723
+
724
+ // Cliente — Recibir binario
725
+ client.on('file', (buffer) => {
726
+ console.log('Archivo recibido:', buffer.length, 'bytes');
727
+ fs.writeFile('received.png', buffer);
728
+ });
350
729
  ```
351
730
 
352
- **`.joinRoom(room)`**
353
- Join a room.
731
+ ---
732
+
733
+ ## Métricas del Servidor
354
734
 
355
735
  ```javascript
356
- client.joinRoom('room-1');
736
+ const stats = stelar.getStats();
737
+ console.log(stats);
738
+
739
+ // {
740
+ // totalConnections: 150,
741
+ // activeConnections: 42,
742
+ // totalMessagesReceived: 5000,
743
+ // totalMessagesSent: 4800,
744
+ // totalRooms: 12,
745
+ // uptime: 3600000,
746
+ // uptimeSeconds: 3600,
747
+ // wsConnections: 38,
748
+ // tcpConnections: 4,
749
+ // memoryMB: 10.54,
750
+ // memoryUsage: { ... },
751
+ // rateLimiterEntries: 42
752
+ // }
357
753
  ```
358
754
 
359
- **`.leaveRoom()`**
360
- Leave current room.
755
+ ---
756
+
757
+ ## Métricas del Cliente
361
758
 
362
759
  ```javascript
363
- client.leaveRoom();
760
+ console.log('Mensajes enviados:', client.getMessagesSent());
761
+ console.log('Mensajes recibidos:', client.getMessagesReceived());
762
+ console.log('Hora de conexión:', client.getConnectTime());
763
+ console.log('Último error:', client.getLastError());
764
+ console.log('Mensajes en cola:', client.getQueueSize());
765
+ console.log('Estado:', client.getState());
766
+ console.log('Conectado?', client.isConnected());
364
767
  ```
365
768
 
366
- **`.connect(callback)`**
367
- Connect to the server.
769
+ ---
770
+
771
+ ## Escalabilidad Horizontal
772
+
773
+ stelar-time-real funciona en un solo servidor por instancia. Para escalar a múltiples instancias, usa Redis Pub/Sub como puente:
368
774
 
369
775
  ```javascript
370
- client.connect(() => {
371
- console.log('Connected!');
776
+ import { StelarServer } from 'stelar-time-real';
777
+ import Redis from 'redis';
778
+
779
+ const redis = Redis.createClient();
780
+ const stelar = new StelarServer({ port: 3000, tcpPort: 3001 });
781
+
782
+ // Cuando un broadcast se hace en esta instancia, publicar en Redis
783
+ stelar.onAll((ctx) => {
784
+ redis.publish('stelar:events', JSON.stringify({
785
+ event: ctx.eventName,
786
+ data: ctx.data,
787
+ excludeId: ctx.id,
788
+ }));
789
+ });
790
+
791
+ // Cuando otra instancia publica, emitir localmente
792
+ redis.subscribe('stelar:events', (message) => {
793
+ const { event, data, excludeId } = JSON.parse(message);
794
+ stelar.broadcast(event, data, excludeId);
372
795
  });
373
796
  ```
374
797
 
375
- **`.disconnect()`**
376
- Manually disconnect.
798
+ ---
377
799
 
378
- ```javascript
379
- client.disconnect();
380
- ```
800
+ ## Performance
381
801
 
382
- **`.isConnected()`**
383
- Check connection status.
802
+ Mediciones con stress test (50 WebSocket + 20 TCP clientes):
803
+
804
+ | Métrica | Valor |
805
+ |---------|-------|
806
+ | Conexiones simultáneas | 70 |
807
+ | RAM por cliente | ~58 KB |
808
+ | Throughput | 3,425 msg/sec |
809
+ | Heap estable | ~10 MB |
810
+ | Memory leaks | No detectados |
811
+ | MaxListeners warnings | 0 |
812
+
813
+ La librería usa ~58KB por cliente conectado. Un servidor con 1GB de RAM puede manejar aproximadamente 17,000 conexiones simultáneas.
814
+
815
+ ---
816
+
817
+ ## Estructura del Proyecto
384
818
 
385
- ```javascript
386
- if (client.isConnected()) {
387
- console.log('Connected');
388
- }
389
819
  ```
820
+ stelar-time-real/
821
+ ├── src/
822
+ │ ├── index.ts # Servidor (StelarServer, RateLimiter, IPConnectionTracker)
823
+ │ ├── client.ts # Cliente (StelarClient, MessageQueue)
824
+ │ ├── protocol.ts # Protocolo binario TCP (encode/decode, FrameParser)
825
+ │ ├── websocket.ts # WebSocket manual RFC 6455 (WSFrameParser, framing)
826
+ │ └── logger.ts # Logger con niveles
827
+ ├── package.json
828
+ ├── tsconfig.json
829
+ └── README.md
830
+ ```
831
+
832
+ ---
390
833
 
391
- **`.getUrl()`**
392
- Get connection URL.
834
+ ## TypeScript
393
835
 
394
- ```javascript
395
- console.log(client.getUrl());
836
+ stelar-time-real está escrita en TypeScript e incluye definiciones de tipos (.d.ts). No necesitas instalar @types separados:
837
+
838
+ ```typescript
839
+ import { StelarServer, StelarClient, StelarStats } from 'stelar-time-real';
840
+
841
+ const server: StelarServer = new StelarServer({ port: 3000 });
842
+ const stats: StelarStats = server.getStats();
396
843
  ```
397
844
 
398
- #### Client Events
845
+ ---
399
846
 
400
- ```javascript
401
- client.on('connect', () => {}); // When connected
402
- client.on('disconnect', () => {}); // When disconnected
403
- client.on('reconnecting', (attempt) => {}); // When trying to reconnect
404
- client.on('error', (err) => {}); // When there's an error
847
+ ## Tests
848
+
849
+ ```bash
850
+ # Tests de producción (54 assertions, 16 suites)
851
+ node test-production.mjs
852
+
853
+ # Stress test (70 clientes, throughput, memoria)
854
+ node test-stress.mjs
405
855
  ```
406
856
 
857
+ Cobertura: server start/stop, health check, CORS, WS connect/emit/broadcast, TCP connect/emit/reply, rooms, ACK, max connections, rate limiting, server stats, max rooms, O(1) lookup, client metrics, binary data, origin checking, middleware.
858
+
407
859
  ---
408
860
 
409
- ## Examples
861
+ ## Configuracion Extensible
410
862
 
411
- ### Basic Chat
863
+ stelar-time-real v3.2 te da control total sobre cada aspecto del servidor y el cliente. Puedes reemplazar componentes enteros, agregar hooks para personalizar el comportamiento, y cambiar la configuración en runtime.
864
+
865
+ ### Custom Rate Limiter
866
+
867
+ Reemplaza el rate limiter built-in (token bucket) con tu propia implementación. Ideal para usar Redis, MongoDB, o cualquier otro store:
412
868
 
413
- **server.js**
414
869
  ```javascript
415
- import express from 'express';
416
- import { StelarServer } from 'stelar-time-real';
870
+ import { StelarServer, IRateLimiter } from 'stelar-time-real';
417
871
 
418
- const app = express();
419
- const server = app.listen(3000);
872
+ // Tu propio rate limiter con Redis
873
+ class RedisRateLimiter implements IRateLimiter {
874
+ private redis; // tu conexión Redis
420
875
 
421
- const stelar = new StelarServer({ server });
876
+ constructor(redisClient) {
877
+ this.redis = redisClient;
878
+ }
422
879
 
423
- stelar.onConnection((client) => {
424
- client.broadcast('system', 'A user joined');
425
- });
880
+ async check(id, cost = 1) {
881
+ const key = `ratelimit:${id}`;
882
+ const current = await this.redis.incr(key);
883
+ if (current === 1) {
884
+ await this.redis.expire(key, 1); // 1 segundo ventana
885
+ }
886
+ return current <= 100; // 100 por segundo
887
+ }
426
888
 
427
- stelar.on('chat', (ctx) => {
428
- ctx.broadcast('chat', ctx.data);
429
- });
889
+ async reset(id) {
890
+ await this.redis.del(`ratelimit:${id}`);
891
+ }
430
892
 
431
- stelar.start();
432
- console.log('Chat at http://localhost:3000');
433
- ```
893
+ async cleanup() {
894
+ // Redis maneja la expiración automáticamente
895
+ }
434
896
 
435
- **cliente.html**
436
- ```html
437
- <script type="module">
438
- import { StelarClient } from 'stelar-time-real';
897
+ async size() {
898
+ return 0; // No aplicable con Redis
899
+ }
900
+ }
439
901
 
440
- const client = new StelarClient(3000);
902
+ const stelar = new StelarServer({
903
+ port: 3000,
904
+ customRateLimiter: new RedisRateLimiter(redisClient),
905
+ });
906
+ ```
441
907
 
442
- client.on('connect', () => console.log('Connected'));
443
- client.on('chat', (msg) => console.log('Chat:', msg));
444
- client.on('system', (msg) => console.log('System:', msg));
908
+ ### Custom IP Tracker
445
909
 
446
- client.connect();
910
+ Reemplaza el per-IP connection tracker con tu propia lógica. Útil para usar una base de datos de IPs bloqueadas o lógica de whitelist:
447
911
 
448
- // Send messages
449
- function send(message) {
450
- client.emit('chat', message);
912
+ ```javascript
913
+ class CustomIPTracker implements IIPTracker {
914
+ private blockedIPs = new Set(['1.2.3.4', '5.6.7.8']);
915
+ private vipIPs = new Set(['10.0.0.1']);
916
+ private counts = new Map<string, number>();
917
+
918
+ check(ip) {
919
+ if (this.blockedIPs.has(ip)) return false; // IP bloqueada
920
+ if (this.vipIPs.has(ip)) return true; // VIP sin límite
921
+ return (this.counts.get(ip) || 0) < 20; // 20 para normales
451
922
  }
452
- </script>
923
+
924
+ add(ip) { this.counts.set(ip, (this.counts.get(ip) || 0) + 1); }
925
+ remove(ip) { /* ... */ }
926
+ getCount(ip) { return this.counts.get(ip) || 0; }
927
+ cleanup() { /* limpiar entradas expiradas */ }
928
+ }
929
+
930
+ const stelar = new StelarServer({
931
+ port: 3000,
932
+ customIPTracker: new CustomIPTracker(),
933
+ });
453
934
  ```
454
935
 
455
- ### Room System
936
+ ### Custom Client ID Generator
937
+
938
+ Genera IDs de cliente con tu propio formato. Por defecto usa UUID v4:
456
939
 
457
940
  ```javascript
458
- // Server
459
- stelar.on('join-room', (ctx) => {
460
- const room = ctx.data.room;
461
- ctx.joinRoom(room);
462
- ctx.emit('welcome', `You joined ${room}`);
941
+ const stelar = new StelarServer({
942
+ port: 3000,
943
+ generateClientId: () => {
944
+ return `user_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
945
+ },
463
946
  });
947
+ ```
948
+
949
+ ### Event-Specific Rate Limits
464
950
 
465
- stelar.on('room-message', (ctx) => {
466
- ctx.to(ctx.data.room, 'room-message', ctx.data.message);
951
+ Cada evento puede tener su propio rate limit, independiente del global:
952
+
953
+ ```javascript
954
+ const stelar = new StelarServer({
955
+ port: 3000,
956
+ rateLimit: { maxPoints: 100, windowMs: 1000 }, // Global: 100 msg/sec
957
+ eventRateLimits: {
958
+ 'chat': { maxPoints: 5, windowMs: 1000 }, // Chat: 5 msg/sec
959
+ 'file-upload': { maxPoints: 2, windowMs: 10000 }, // Archivos: 2 cada 10s
960
+ 'typing': { maxPoints: 10, windowMs: 1000 }, // Typing: 10 msg/sec
961
+ 'location': { maxPoints: 1, windowMs: 5000 }, // Ubicación: 1 cada 5s
962
+ },
467
963
  });
468
964
 
469
- // Client
470
- client.on('join-room', (room) => client.joinRoom(room));
965
+ // También puedes agregar/remover en runtime:
966
+ stelar.setEventRateLimit('voice', { maxPoints: 50, windowMs: 1000 });
967
+ stelar.removeEventRateLimit('voice');
471
968
  ```
472
969
 
473
- ### With Auth Middleware
970
+ ### Per-Client Rate Limits
971
+
972
+ Dale a clientes específicos rate limits diferentes. Útil para usuarios premium vs gratuitos:
474
973
 
475
974
  ```javascript
476
- stelar.use((ctx, next) => {
477
- const token = ctx.req.headers['authorization'];
478
- if (token && token.startsWith('Bearer ')) {
479
- next(); // Allow connection
480
- } else {
481
- ctx.socket.close(); // Reject
975
+ stelar.onConnection((ctx) => {
976
+ const role = ctx.getMetadata('role');
977
+
978
+ // Usuario premium: 500 msg/sec
979
+ if (role === 'premium') {
980
+ stelar.setClientRateLimit(ctx.id, { maxPoints: 500, windowMs: 1000 });
981
+ }
982
+ // Usuario bot verificado: 1000 msg/sec
983
+ else if (role === 'bot') {
984
+ stelar.setClientRateLimit(ctx.id, { maxPoints: 1000, windowMs: 1000 });
482
985
  }
986
+ // Usuario normal: usa el rate limit global (100 msg/sec)
483
987
  });
988
+
989
+ // Remover override (vuelve al global):
990
+ stelar.removeClientRateLimit(clientId);
484
991
  ```
485
992
 
486
- ### With Auto Reconnection
993
+ La prioridad de rate limiting es: **per-client override > event-specific > global > custom rate limiter**.
487
994
 
488
- ```javascript
489
- import { StelarClient } from 'stelar-time-real';
995
+ ### Hook System (Servidor)
490
996
 
491
- const client = new StelarClient('localhost:3000', {
492
- reconnection: true,
493
- reconnectionAttempts: 5,
494
- reconnectionDelay: 1000
997
+ Hooks te permiten personalizar lo que pasa cuando el servidor detecta un evento. Cada hook puede retornar `false` para cancelar la acción por defecto:
998
+
999
+ ```javascript
1000
+ const stelar = new StelarServer({
1001
+ port: 3000,
1002
+ hooks: {
1003
+ // Cuando un cliente excede el rate limit
1004
+ // Return false para NO desconectar (ej: solo warn)
1005
+ onRateLimitExceeded: ({ clientId, event, protocol }) => {
1006
+ console.warn(`Rate limit: ${clientId} en evento ${event}`);
1007
+ // return false; // Descomenta para NO desconectar al cliente
1008
+ },
1009
+
1010
+ // Cuando se alcanza el máximo de conexiones
1011
+ onMaxConnectionsReached: ({ activeConnections, max, ip }) => {
1012
+ console.error(`Servidor lleno: ${activeConnections}/${max} desde ${ip}`);
1013
+ // Enviar alerta a Slack, etc.
1014
+ },
1015
+
1016
+ // Cuando un cliente intenta unirse a un room
1017
+ // Return false para RECHAZAR el join
1018
+ onClientJoinRoom: ({ clientId, room, metadata }) => {
1019
+ const role = metadata.get('role');
1020
+ if (room.startsWith('admin-') && role !== 'admin') {
1021
+ return false; // Rechazar: solo admins
1022
+ }
1023
+ },
1024
+
1025
+ // Cuando un cliente sale de un room
1026
+ // Return false para RECHAZAR el leave
1027
+ onClientLeaveRoom: ({ clientId, room }) => {
1028
+ // Lógica custom...
1029
+ },
1030
+
1031
+ // Cuando se alcanza el máximo de rooms global
1032
+ onMaxRoomsReached: ({ clientId, room, totalRooms, max }) => {
1033
+ console.warn(`Max rooms: ${totalRooms}/${max}`);
1034
+ },
1035
+
1036
+ // Cuando un cliente excede rooms por cliente
1037
+ onMaxRoomsPerClientReached: ({ clientId, room, currentRooms, max }) => {
1038
+ console.warn(`Cliente ${clientId}: ${currentRooms}/${max} rooms`);
1039
+ },
1040
+
1041
+ // Cuando un payload es demasiado grande
1042
+ onPayloadTooLarge: ({ clientId, event, size, max }) => {
1043
+ console.warn(`Payload grande: ${size} bytes de ${clientId}`);
1044
+ },
1045
+
1046
+ // Cuando se recibe un mensaje inválido
1047
+ onInvalidMessage: ({ clientId, reason, protocol }) => {
1048
+ console.warn(`Mensaje inválido de ${clientId}: ${reason}`);
1049
+ },
1050
+
1051
+ // Antes de un broadcast
1052
+ // Return false para CANCELAR el broadcast
1053
+ onBeforeBroadcast: ({ event, data, excludeId }) => {
1054
+ if (event === 'spam') return false; // Cancelar broadcast de spam
1055
+ },
1056
+
1057
+ // Cuando un cliente se conecta
1058
+ onClientConnect: ({ clientId, ip, protocol, metadata }) => {
1059
+ console.log(`Conectado: ${clientId} via ${protocol} desde ${ip}`);
1060
+ },
1061
+
1062
+ // Cuando un cliente se desconecta
1063
+ onClientDisconnect: ({ clientId, ip, protocol, rooms }) => {
1064
+ console.log(`Desconectado: ${clientId} estaba en ${rooms.size} rooms`);
1065
+ },
1066
+ },
495
1067
  });
1068
+ ```
496
1069
 
497
- client.on('connect', () => console.log('Connected!'));
498
- client.on('disconnect', () => console.log('Disconnected'));
499
- client.on('reconnecting', (attempt) => console.log(`Retrying ${attempt}/5`));
1070
+ ### Custom Health Check
500
1071
 
501
- client.connect();
1072
+ Reemplaza el health check built-in con tu propio handler. Útil para agregar checks de base de datos, disk space, etc:
1073
+
1074
+ ```javascript
1075
+ const stelar = new StelarServer({
1076
+ port: 3000,
1077
+ customHealthHandler: (req, res, stats) => {
1078
+ // stats contiene todas las estadísticas del servidor
1079
+
1080
+ const dbConnected = await checkDatabase();
1081
+ const diskSpace = checkDiskSpace();
1082
+
1083
+ res.writeHead(dbConnected && diskSpace > 100 ? 200 : 503, {
1084
+ 'Content-Type': 'application/json',
1085
+ });
1086
+ res.end(JSON.stringify({
1087
+ status: dbConnected && diskSpace > 100 ? 'healthy' : 'degraded',
1088
+ server: stats,
1089
+ database: dbConnected ? 'connected' : 'disconnected',
1090
+ diskSpaceMB: diskSpace,
1091
+ version: '3.2.0',
1092
+ }));
1093
+ },
1094
+ });
502
1095
  ```
503
1096
 
504
- ### Send Binary Files
1097
+ ### Runtime Configuration
1098
+
1099
+ Cambia la configuración del servidor sin reiniciar:
505
1100
 
506
1101
  ```javascript
507
- // Server - receive image
508
- stelar.on('image', (ctx) => {
509
- // ctx.buffer is a Uint8Array
510
- console.log('Received:', ctx.buffer.byteLength, 'bytes');
511
- // Save or process the image
512
- saveImage(ctx.buffer);
1102
+ const stelar = new StelarServer({ port: 3000, maxConnections: 100 });
1103
+ await stelar.start();
513
1104
 
514
- // Respond to client
515
- ctx.emit('imageSaved', { success: true });
1105
+ // Más tarde... necesitas más capacidad
1106
+ stelar.updateConfig({
1107
+ maxConnections: 500,
1108
+ maxRooms: 5000,
1109
+ rateLimit: { maxPoints: 200, windowMs: 1000 },
1110
+ allowedOrigins: ['https://app.com', 'https://admin.app.com'],
516
1111
  });
517
1112
 
518
- // Client - send image
519
- const input = document.querySelector('input[type="file"]');
520
- input.addEventListener('change', async (e) => {
521
- const file = e.target.files[0];
522
- const buffer = await file.arrayBuffer();
523
- client.emitBinary('image', buffer);
1113
+ // Cambiar hooks en runtime
1114
+ stelar.updateConfig({
1115
+ hooks: {
1116
+ onRateLimitExceeded: ({ clientId }) => {
1117
+ banUser(clientId); // Ban automático en vez de desconectar
1118
+ return false; // No desconectar, ya lo baneaste
1119
+ },
1120
+ },
524
1121
  });
525
1122
 
526
- // Client - receive image
527
- client.on('image', (buffer) => {
528
- const blob = new Blob([buffer], { type: 'image/png' });
529
- const url = URL.createObjectURL(blob);
530
- document.getElementById('img').src = url;
1123
+ // Ver configuración actual
1124
+ const config = stelar.getConfig();
1125
+ console.log(config);
1126
+ // {
1127
+ // maxConnections: 500,
1128
+ // maxRooms: 5000,
1129
+ // hasCustomRateLimiter: false,
1130
+ // eventRateLimits: [],
1131
+ // hooks: ['onRateLimitExceeded'],
1132
+ // ...
1133
+ // }
1134
+ ```
1135
+
1136
+ ### Client Hooks
1137
+
1138
+ Personaliza el comportamiento del cliente con hooks:
1139
+
1140
+ ```javascript
1141
+ const client = new StelarClient('localhost:3000', {
1142
+ hooks: {
1143
+ // Antes de enviar un mensaje — return false para cancelar
1144
+ onBeforeEmit: ({ event, data }) => {
1145
+ if (event === 'debug') return false; // No enviar debug en producción
1146
+ console.log(`Enviando: ${event}`);
1147
+ },
1148
+
1149
+ // Cuando se recibe cualquier mensaje
1150
+ onMessage: ({ event, data, isBinary }) => {
1151
+ metrics.increment('messages.received');
1152
+ if (isBinary) metrics.increment('binary.received');
1153
+ },
1154
+
1155
+ // Cuando cambia el estado de conexión
1156
+ onStateChange: ({ from, to }) => {
1157
+ console.log(`Estado: ${from} -> ${to}`);
1158
+ if (to === 'reconnecting') showReconnectingUI();
1159
+ if (to === 'connected') hideReconnectingUI();
1160
+ },
1161
+
1162
+ // Personalizar el delay de reconexión
1163
+ onReconnectDelay: ({ attempt, defaultDelay }) => {
1164
+ // Horario laboral: reconexión rápida
1165
+ const hour = new Date().getHours();
1166
+ if (hour >= 9 && hour <= 18) return 500;
1167
+ return defaultDelay; // Fuera de horario: delay normal
1168
+ },
1169
+
1170
+ // Cuando un mensaje se encola (desconectado)
1171
+ onMessageQueued: ({ event, queueSize }) => {
1172
+ console.log(`Mensaje encolado: ${event} (cola: ${queueSize})`);
1173
+ },
1174
+
1175
+ // Cuando se drena la cola después de reconectar
1176
+ onQueueDrained: ({ count }) => {
1177
+ console.log(`${count} mensajes enviados después de reconectar`);
1178
+ },
1179
+
1180
+ // Cuando ocurre un error
1181
+ onError: ({ error, context }) => {
1182
+ errorReporter.report(error, { context });
1183
+ },
1184
+ },
531
1185
  });
532
1186
  ```
533
1187
 
534
- ### Binary Broadcast
1188
+ ### Custom Reconnect Delay
1189
+
1190
+ Controla exactamente cuánto esperar antes de cada reintento de reconexión:
535
1191
 
536
1192
  ```javascript
537
- // Server - share file with everyone
538
- stelar.on('upload', (ctx) => {
539
- ctx.broadcastBinary('file', ctx.buffer);
1193
+ // Opción 1: Función custom
1194
+ const client = new StelarClient('localhost:3000', {
1195
+ customReconnectDelay: (attempt, baseDelay, maxDelay) => {
1196
+ // Retry rápido los primeros 3 intentos, luego lento
1197
+ if (attempt <= 3) return 200;
1198
+ if (attempt <= 10) return 2000;
1199
+ return 30000; // 30s para intentos posteriores
1200
+ },
540
1201
  });
541
1202
 
542
- // Client - send file
543
- const fileData = await file.arrayBuffer();
544
- client.emitBinary('upload', fileData);
1203
+ // Opción 2: Via hook (puedes cambiar en runtime)
1204
+ const client = new StelarClient('localhost:3000', {
1205
+ hooks: {
1206
+ onReconnectDelay: ({ attempt, defaultDelay }) => {
1207
+ return Math.min(100 * attempt, 10000); // Lineal en vez de exponencial
1208
+ },
1209
+ },
1210
+ });
545
1211
  ```
546
1212
 
547
- ---
1213
+ ### Client Runtime Configuration
1214
+
1215
+ Cambia la configuración del cliente sin reconectar:
548
1216
 
549
- ## Difference with Socket.io
1217
+ ```javascript
1218
+ const client = new StelarClient('localhost:3000');
1219
+ client.connect();
550
1220
 
551
- | Feature | stelar-time-real | Socket.io |
552
- |---------|------------------|-----------|
553
- | Heap size | ~13 MB | ~50-100 MB |
554
- | Dependencies | ws (1) | multiple |
555
- | Configuration | minimal | complex |
556
- | Flexibility | total | opinionated |
557
- | Ideal for | own projects | quick production |
1221
+ // Más tarde... necesitas ajustar timeouts
1222
+ client.updateOptions({
1223
+ heartbeatInterval: 15000,
1224
+ ackTimeout: 10000,
1225
+ maxPayloadSize: 50 * 1024 * 1024, // 50MB
1226
+ hooks: {
1227
+ onBeforeEmit: ({ event }) => {
1228
+ if (event === 'log') return false; // Ya no enviar logs
1229
+ },
1230
+ },
1231
+ });
558
1232
 
559
- ## License
1233
+ // Ver configuración actual
1234
+ const opts = client.getOptions();
1235
+ console.log(opts);
1236
+ ```
560
1237
 
561
- MIT - Stelar
1238
+ ---
562
1239
 
563
- ## Author
1240
+ ## License
564
1241
 
565
- Stelar
1242
+ MIT — Stelar