stelar-time-real 2.0.3 → 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 +1013 -336
- package/package.json +17 -13
- package/src/client.d.ts +115 -8
- package/src/client.d.ts.map +1 -1
- package/src/client.js +851 -102
- package/src/client.ts +915 -103
- package/src/index.d.ts +281 -15
- package/src/index.d.ts.map +1 -1
- package/src/index.js +1382 -142
- package/src/index.ts +1663 -201
- package/src/logger.d.ts +29 -0
- package/src/logger.d.ts.map +1 -0
- package/src/logger.js +98 -0
- package/src/logger.ts +115 -0
- package/src/protocol.d.ts +57 -0
- package/src/protocol.d.ts.map +1 -0
- package/src/protocol.js +193 -0
- package/src/protocol.ts +237 -0
- package/src/websocket.d.ts +67 -0
- package/src/websocket.d.ts.map +1 -0
- package/src/websocket.js +260 -0
- package/src/websocket.ts +316 -0
package/README.md
CHANGED
|
@@ -1,37 +1,106 @@
|
|
|
1
|
-
# stelar-time-real
|
|
1
|
+
# stelar-time-real v3
|
|
2
2
|
|
|
3
|
-
|
|
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
|

|
|
6
6
|

|
|
7
|
-

|
|
8
|
+

|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
---
|
|
10
11
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
---
|
|
29
92
|
|
|
30
|
-
|
|
31
|
-
|
|
93
|
+
## Instalación
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm install stelar-time-real
|
|
32
97
|
```
|
|
33
98
|
|
|
34
|
-
|
|
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('
|
|
47
|
-
client.emit('welcome', '
|
|
115
|
+
console.log('Conectado:', client.id);
|
|
116
|
+
client.emit('welcome', { message: 'Bienvenido al servidor!' });
|
|
48
117
|
});
|
|
49
118
|
|
|
50
|
-
stelar.on('
|
|
51
|
-
ctx.broadcast('
|
|
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
|
-
###
|
|
126
|
+
### Servidor con configuracion de produccion
|
|
58
127
|
|
|
59
128
|
```javascript
|
|
60
|
-
import
|
|
129
|
+
import express from 'express';
|
|
130
|
+
import { StelarServer } from 'stelar-time-real';
|
|
61
131
|
|
|
62
|
-
const
|
|
132
|
+
const app = express();
|
|
133
|
+
const server = app.listen(3000);
|
|
63
134
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
+
stelar.onDisconnect((client) => {
|
|
166
|
+
console.log('Cliente desconectado:', client.id);
|
|
167
|
+
});
|
|
76
168
|
|
|
77
|
-
|
|
169
|
+
stelar.on('chat', (ctx) => {
|
|
170
|
+
ctx.broadcast('chat', ctx.data, ctx.id);
|
|
171
|
+
});
|
|
78
172
|
|
|
79
|
-
|
|
173
|
+
stelar.onAck('getUser', (ctx) => {
|
|
174
|
+
return { id: ctx.data.id, name: 'Juan', role: ctx.getMetadata('role') };
|
|
175
|
+
});
|
|
80
176
|
|
|
81
|
-
|
|
82
|
-
|
|
177
|
+
await stelar.start();
|
|
178
|
+
console.log('Servidor listo en puerto', stelar.getPort());
|
|
83
179
|
```
|
|
84
180
|
|
|
85
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
108
|
-
Listen for client events.
|
|
217
|
+
### Cliente con modo TCP (Node.js unicamente — maxima eficiencia)
|
|
109
218
|
|
|
110
219
|
```javascript
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
127
|
-
Execute when a client connects.
|
|
248
|
+
### Servidor con TLS
|
|
128
249
|
|
|
129
250
|
```javascript
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
Send to all clients.
|
|
263
|
+
---
|
|
137
264
|
|
|
138
|
-
|
|
139
|
-
stelar.broadcast('chat', { message: 'Hello everyone' });
|
|
140
|
-
```
|
|
265
|
+
## Arquitectura
|
|
141
266
|
|
|
142
|
-
|
|
143
|
-
Send to a specific room.
|
|
267
|
+
### Protocolo Dual
|
|
144
268
|
|
|
145
|
-
```
|
|
146
|
-
stelar
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
Get list of clients.
|
|
292
|
+
### WebSocket Mode vs TCP Mode
|
|
158
293
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
165
|
-
Get the port where it's running.
|
|
303
|
+
### Formato del Protocolo Binario (TCP)
|
|
166
304
|
|
|
167
|
-
```
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
```
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## API Completa
|
|
178
344
|
|
|
179
|
-
|
|
180
|
-
Stop the server.
|
|
345
|
+
### StelarServer — Opciones
|
|
181
346
|
|
|
182
347
|
```javascript
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
ctx.
|
|
194
|
-
ctx.
|
|
195
|
-
ctx.
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
500
|
+
### StelarClient — Métodos
|
|
211
501
|
|
|
212
|
-
|
|
502
|
+
#### Eventos
|
|
213
503
|
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
218
|
-
const main = new StelarServer({ server, namespace: '/' });
|
|
512
|
+
#### Envío
|
|
219
513
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
####
|
|
536
|
+
#### Estado y métricas
|
|
234
537
|
|
|
235
|
-
|
|
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
|
-
|
|
552
|
+
### Eventos del Cliente
|
|
238
553
|
|
|
239
554
|
```javascript
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return { id: ctx.data.id, name: 'John' };
|
|
555
|
+
client.on('connect', () => {
|
|
556
|
+
// Conexión establecida
|
|
243
557
|
});
|
|
244
558
|
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
255
|
-
//
|
|
256
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
578
|
+
## Health Check
|
|
274
579
|
|
|
275
|
-
|
|
580
|
+
El endpoint de health check está diseñado para integrarse con orquestadores como Kubernetes, Docker Swarm, o cualquier load balancer.
|
|
276
581
|
|
|
277
|
-
```
|
|
278
|
-
|
|
582
|
+
```bash
|
|
583
|
+
curl http://localhost:3000/health
|
|
279
584
|
```
|
|
280
585
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
//
|
|
291
|
-
|
|
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
|
-
//
|
|
294
|
-
|
|
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
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
647
|
+
Múltiples middlewares se ejecutan en orden. Si un middleware no llama a `next()`, la conexión se rechaza.
|
|
305
648
|
|
|
306
|
-
|
|
307
|
-
Listen for server events.
|
|
649
|
+
---
|
|
308
650
|
|
|
309
|
-
|
|
310
|
-
client.on('welcome', (data) => {
|
|
311
|
-
console.log(data);
|
|
312
|
-
});
|
|
313
|
-
```
|
|
651
|
+
## Rooms
|
|
314
652
|
|
|
315
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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
|
-
|
|
337
|
-
client.
|
|
338
|
-
|
|
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
|
-
|
|
342
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
-
//
|
|
349
|
-
const
|
|
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
|
-
|
|
353
|
-
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
## Métricas del Servidor
|
|
354
734
|
|
|
355
735
|
```javascript
|
|
356
|
-
|
|
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
|
-
|
|
360
|
-
|
|
755
|
+
---
|
|
756
|
+
|
|
757
|
+
## Métricas del Cliente
|
|
361
758
|
|
|
362
759
|
```javascript
|
|
363
|
-
client.
|
|
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
|
-
|
|
367
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
376
|
-
Manually disconnect.
|
|
798
|
+
---
|
|
377
799
|
|
|
378
|
-
|
|
379
|
-
client.disconnect();
|
|
380
|
-
```
|
|
800
|
+
## Performance
|
|
381
801
|
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
392
|
-
Get connection URL.
|
|
834
|
+
## TypeScript
|
|
393
835
|
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
845
|
+
---
|
|
399
846
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
##
|
|
861
|
+
## Configuracion Extensible
|
|
410
862
|
|
|
411
|
-
|
|
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
|
|
416
|
-
import { StelarServer } from 'stelar-time-real';
|
|
870
|
+
import { StelarServer, IRateLimiter } from 'stelar-time-real';
|
|
417
871
|
|
|
418
|
-
|
|
419
|
-
|
|
872
|
+
// Tu propio rate limiter con Redis
|
|
873
|
+
class RedisRateLimiter implements IRateLimiter {
|
|
874
|
+
private redis; // tu conexión Redis
|
|
420
875
|
|
|
421
|
-
|
|
876
|
+
constructor(redisClient) {
|
|
877
|
+
this.redis = redisClient;
|
|
878
|
+
}
|
|
422
879
|
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
889
|
+
async reset(id) {
|
|
890
|
+
await this.redis.del(`ratelimit:${id}`);
|
|
891
|
+
}
|
|
430
892
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
893
|
+
async cleanup() {
|
|
894
|
+
// Redis maneja la expiración automáticamente
|
|
895
|
+
}
|
|
434
896
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
897
|
+
async size() {
|
|
898
|
+
return 0; // No aplicable con Redis
|
|
899
|
+
}
|
|
900
|
+
}
|
|
439
901
|
|
|
440
|
-
|
|
902
|
+
const stelar = new StelarServer({
|
|
903
|
+
port: 3000,
|
|
904
|
+
customRateLimiter: new RedisRateLimiter(redisClient),
|
|
905
|
+
});
|
|
906
|
+
```
|
|
441
907
|
|
|
442
|
-
|
|
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
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
//
|
|
470
|
-
|
|
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
|
-
###
|
|
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.
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
ctx.
|
|
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
|
-
|
|
993
|
+
La prioridad de rate limiting es: **per-client override > event-specific > global > custom rate limiter**.
|
|
487
994
|
|
|
488
|
-
|
|
489
|
-
import { StelarClient } from 'stelar-time-real';
|
|
995
|
+
### Hook System (Servidor)
|
|
490
996
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
498
|
-
client.on('disconnect', () => console.log('Disconnected'));
|
|
499
|
-
client.on('reconnecting', (attempt) => console.log(`Retrying ${attempt}/5`));
|
|
1070
|
+
### Custom Health Check
|
|
500
1071
|
|
|
501
|
-
|
|
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
|
-
###
|
|
1097
|
+
### Runtime Configuration
|
|
1098
|
+
|
|
1099
|
+
Cambia la configuración del servidor sin reiniciar:
|
|
505
1100
|
|
|
506
1101
|
```javascript
|
|
507
|
-
|
|
508
|
-
stelar.
|
|
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
|
-
|
|
515
|
-
|
|
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
|
-
//
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
###
|
|
1188
|
+
### Custom Reconnect Delay
|
|
1189
|
+
|
|
1190
|
+
Controla exactamente cuánto esperar antes de cada reintento de reconexión:
|
|
535
1191
|
|
|
536
1192
|
```javascript
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
//
|
|
543
|
-
const
|
|
544
|
-
|
|
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
|
-
|
|
1217
|
+
```javascript
|
|
1218
|
+
const client = new StelarClient('localhost:3000');
|
|
1219
|
+
client.connect();
|
|
550
1220
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
1233
|
+
// Ver configuración actual
|
|
1234
|
+
const opts = client.getOptions();
|
|
1235
|
+
console.log(opts);
|
|
1236
|
+
```
|
|
560
1237
|
|
|
561
|
-
|
|
1238
|
+
---
|
|
562
1239
|
|
|
563
|
-
##
|
|
1240
|
+
## License
|
|
564
1241
|
|
|
565
|
-
Stelar
|
|
1242
|
+
MIT — Stelar
|