stelar-time-real 1.0.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 +438 -0
- package/package.json +33 -0
- package/src/client.js +150 -0
- package/src/index.js +219 -0
package/README.md
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# stelar-time-real
|
|
2
|
+
|
|
3
|
+
Tu propio sistema de tiempo real personalizado. Una librería ligera y sin dependencias para comunicación en tiempo real via WebSockets.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## ¿Por qué stelar-time-real?
|
|
10
|
+
|
|
11
|
+
- ⚡ **Ultra ligera** - Solo ~13MB de heap
|
|
12
|
+
- 🚀 **Sin dependencias** - Usa solo `ws` (WebSocket nativo)
|
|
13
|
+
- 🎯 **Personalizable** - Vos controlás todo, nada de código ajeno
|
|
14
|
+
- 🔌 **Compatible** - Funciona con Express, Fastify, HTTP nativo, etc.
|
|
15
|
+
- 💓 **Heartbeat incluido** - Detecta desconexiones automáticamente
|
|
16
|
+
|
|
17
|
+
## Instalación
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install stelar-time-real
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Inicio Rápido
|
|
24
|
+
|
|
25
|
+
### Servidor
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
import express from 'express';
|
|
29
|
+
import StelarServer from 'stelar-time-real';
|
|
30
|
+
|
|
31
|
+
const app = express();
|
|
32
|
+
const server = app.listen(3000);
|
|
33
|
+
|
|
34
|
+
const stelar = new StelarServer({ server });
|
|
35
|
+
|
|
36
|
+
stelar.onConnection((client) => {
|
|
37
|
+
console.log('Nuevo cliente:', client.id);
|
|
38
|
+
client.emit('bienvenida', '¡Hola! Bienvenido a stelar-time-real');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
stelar.on('mensaje', (ctx) => {
|
|
42
|
+
ctx.broadcast('mensaje', ctx.data);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
stelar.start();
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Cliente
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
import StelarClient from 'stelar-time-real/client';
|
|
52
|
+
|
|
53
|
+
const client = new StelarClient('localhost:3000');
|
|
54
|
+
|
|
55
|
+
client.on('connect', () => {
|
|
56
|
+
console.log('Conectado!');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
client.on('bienvenida', (msg) => {
|
|
60
|
+
console.log(msg);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
client.connect();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## API Completa
|
|
67
|
+
|
|
68
|
+
### StelarServer (Lado del Servidor)
|
|
69
|
+
|
|
70
|
+
#### Constructor
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
new StelarServer({ server, port, heartbeatInterval })
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Opción | Tipo | Default | Descripción |
|
|
77
|
+
|--------|------|---------|-------------|
|
|
78
|
+
| server | http.Server | null | Tu server HTTP existente |
|
|
79
|
+
| port | number | 3000 | Puerto si no pasás server |
|
|
80
|
+
| heartbeatInterval | number | 30000 | Intervalo de ping en ms |
|
|
81
|
+
|
|
82
|
+
#### Métodos
|
|
83
|
+
|
|
84
|
+
**`.use(middleware)`**
|
|
85
|
+
Agregar middleware para validar conexiones.
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
stelar.use((ctx, next) => {
|
|
89
|
+
const token = ctx.req.headers['x-token'];
|
|
90
|
+
if (token === 'secreto') {
|
|
91
|
+
next();
|
|
92
|
+
} else {
|
|
93
|
+
ctx.socket.close();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**`.on(event, handler)`**
|
|
99
|
+
Escuchar eventos del cliente.
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
stelar.on('chat', (ctx) => {
|
|
103
|
+
console.log('Mensaje:', ctx.data);
|
|
104
|
+
ctx.broadcast('chat', ctx.data);
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**`.onAll(handler)`**
|
|
109
|
+
Escuchar todos los eventos (útil para debug).
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
stelar.onAll(({ event, data }) => {
|
|
113
|
+
console.log(`Evento: ${event}`, data);
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**`.onConnection(handler)`**
|
|
118
|
+
Ejecutar cuando un cliente se conecta.
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
stelar.onConnection((client) => {
|
|
122
|
+
client.emit('bienvenida', 'Hola!');
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**`.broadcast(event, data)`**
|
|
127
|
+
Enviar a todos los clientes.
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
stelar.broadcast('chat', { mensaje: 'Hola a todos' });
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**`.to(room, event, data)`**
|
|
134
|
+
Enviar a una sala específica.
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
stelar.to('sala-1', 'chat', { mensaje: 'Hola sala 1' });
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**`.toId(id, event, data)`**
|
|
141
|
+
Enviar a un cliente específico por ID.
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
stelar.toId('abc123', 'privado', 'Solo para ti');
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**`.getClients(room)`**
|
|
148
|
+
Obtener lista de clientes.
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
const todos = stelar.getClients();
|
|
152
|
+
const sala = stelar.getClients('mi-sala');
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**`.getPort()`**
|
|
156
|
+
Obtener el puerto donde está corriendo.
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
console.log('Puerto:', stelar.getPort());
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**`.start(callback)`**
|
|
163
|
+
Iniciar el servidor WebSocket.
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
await stelar.start();
|
|
167
|
+
console.log('Iniciado!');
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**`.stop()`**
|
|
171
|
+
Detener el servidor.
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
stelar.stop();
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Contexto (ctx) en handlers
|
|
178
|
+
|
|
179
|
+
Cuando escuchás un evento, recibís un `ctx` con:
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
stelar.on('mensaje', (ctx) => {
|
|
183
|
+
ctx.id // ID único del cliente
|
|
184
|
+
ctx.socket // WebSocket del cliente
|
|
185
|
+
ctx.req // Request HTTP original
|
|
186
|
+
ctx.data // Datos recibidos
|
|
187
|
+
|
|
188
|
+
// Métodos disponibles:
|
|
189
|
+
ctx.emit('evento', data) // Enviar solo a este cliente
|
|
190
|
+
ctx.broadcast('evento', data) // Enviar a todos
|
|
191
|
+
ctx.to('sala', 'evento', data) // Enviar a una sala
|
|
192
|
+
ctx.toId('id', 'evento', data) // Enviar a un cliente específico
|
|
193
|
+
ctx.getClients('sala') // Ver clientes en sala
|
|
194
|
+
ctx.joinRoom('sala') // Unir a sala
|
|
195
|
+
ctx.leaveRoom() // Salir de sala
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### StelarClient (Lado del Cliente)
|
|
202
|
+
|
|
203
|
+
#### Constructor
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
new StelarClient(urlOrPort, options)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
| Param | Tipo | Default | Descripción |
|
|
210
|
+
|-------|------|---------|-------------|
|
|
211
|
+
| urlOrPort | string/number | localhost:3000 | URL o puerto del servidor |
|
|
212
|
+
| options.reconnection | boolean | true | Reconectar automáticamente |
|
|
213
|
+
| options.reconnectionAttempts | number | 5 | Intentos de reconexión |
|
|
214
|
+
| options.reconnectionDelay | number | 1000 | Delay entre intentos (ms) |
|
|
215
|
+
| options.heartbeatInterval | number | 30000 | Intervalo de ping |
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
// Solo puerto
|
|
219
|
+
const client = new StelarClient(3000);
|
|
220
|
+
|
|
221
|
+
// URL completa
|
|
222
|
+
const client = new StelarClient('ws://midominio.com/ws');
|
|
223
|
+
|
|
224
|
+
// Con opciones
|
|
225
|
+
const client = new StelarClient(3000, {
|
|
226
|
+
reconnection: true,
|
|
227
|
+
reconnectionAttempts: 10,
|
|
228
|
+
reconnectionDelay: 2000
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### Métodos
|
|
233
|
+
|
|
234
|
+
**`.on(event, handler)`**
|
|
235
|
+
Escuchar eventos del servidor.
|
|
236
|
+
|
|
237
|
+
```javascript
|
|
238
|
+
client.on('bienvenida', (data) => {
|
|
239
|
+
console.log(data);
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**`.onAll(handler)`**
|
|
244
|
+
Escuchar todos los eventos.
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
client.onAll(({ event, data }) => {
|
|
248
|
+
console.log(`${event}:`, data);
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**`.emit(event, data)`**
|
|
253
|
+
Enviar eventos al servidor.
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
client.emit('chat', { mensaje: 'Hola!' });
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**`.joinRoom(room)`**
|
|
260
|
+
Unirse a una sala.
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
client.joinRoom('sala-1');
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**`.leaveRoom()`**
|
|
267
|
+
Salir de la sala actual.
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
client.leaveRoom();
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**`.connect(callback)`**
|
|
274
|
+
Conectar al servidor.
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
client.connect(() => {
|
|
278
|
+
console.log('Conectado!');
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**`.disconnect()`**
|
|
283
|
+
Desconectar manualmente.
|
|
284
|
+
|
|
285
|
+
```javascript
|
|
286
|
+
client.disconnect();
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**`.isConnected()`**
|
|
290
|
+
Verificar estado de conexión.
|
|
291
|
+
|
|
292
|
+
```javascript
|
|
293
|
+
if (client.isConnected()) {
|
|
294
|
+
console.log('Conectado');
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**`.getUrl()`**
|
|
299
|
+
Obtener la URL de conexión.
|
|
300
|
+
|
|
301
|
+
```javascript
|
|
302
|
+
console.log(client.getUrl());
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
#### Eventos del Cliente
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
client.on('connect', () => {}); // Cuando se conecta
|
|
309
|
+
client.on('disconnect', () => {}); // Cuando se desconecta
|
|
310
|
+
client.on('reconnecting', (attempt) => {}); // Cuando intenta reconectar
|
|
311
|
+
client.on('error', (err) => {}); // Cuando hay error
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Ejemplos
|
|
317
|
+
|
|
318
|
+
### Chat Básico
|
|
319
|
+
|
|
320
|
+
**server.js**
|
|
321
|
+
```javascript
|
|
322
|
+
import express from 'express';
|
|
323
|
+
import StelarServer from 'stelar-time-real';
|
|
324
|
+
|
|
325
|
+
const app = express();
|
|
326
|
+
const server = app.listen(3000);
|
|
327
|
+
|
|
328
|
+
const stelar = new StelarServer({ server });
|
|
329
|
+
|
|
330
|
+
stelar.onConnection((client) => {
|
|
331
|
+
client.broadcast('system', 'Un usuario se unió');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
stelar.on('chat', (ctx) => {
|
|
335
|
+
ctx.broadcast('chat', ctx.data);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
stelar.start();
|
|
339
|
+
console.log('Chat en http://localhost:3000');
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**cliente.html**
|
|
343
|
+
```html
|
|
344
|
+
<script type="module">
|
|
345
|
+
import StelarClient from 'stelar-time-real/client.js';
|
|
346
|
+
|
|
347
|
+
const client = new StelarClient(3000);
|
|
348
|
+
|
|
349
|
+
client.on('connect', () => console.log('Conectado'));
|
|
350
|
+
client.on('chat', (msg) => console.log('Chat:', msg));
|
|
351
|
+
client.on('system', (msg) => console.log('Sistema:', msg));
|
|
352
|
+
|
|
353
|
+
client.connect();
|
|
354
|
+
|
|
355
|
+
// Enviar mensajes
|
|
356
|
+
function enviar(mensaje) {
|
|
357
|
+
client.emit('chat', mensaje);
|
|
358
|
+
}
|
|
359
|
+
</script>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Sistema de Rooms
|
|
363
|
+
|
|
364
|
+
```javascript
|
|
365
|
+
// Servidor
|
|
366
|
+
stelar.on('unirse-sala', (ctx) => {
|
|
367
|
+
const sala = ctx.data.sala;
|
|
368
|
+
ctx.joinRoom(sala);
|
|
369
|
+
ctx.emit('bienvenida', `Te uniste a ${sala}`);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
stelar.on('mensaje-sala', (ctx) => {
|
|
373
|
+
ctx.to(ctx.data.sala, 'mensaje-sala', ctx.data.mensaje);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Cliente
|
|
377
|
+
client.on('unirse-sala', (sala) => client.joinRoom(sala));
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Con Middleware de Auth
|
|
381
|
+
|
|
382
|
+
```javascript
|
|
383
|
+
stelar.use((ctx, next) => {
|
|
384
|
+
const token = ctx.req.headers['authorization'];
|
|
385
|
+
if (token && token.startsWith('Bearer ')) {
|
|
386
|
+
next(); // Permitir conexión
|
|
387
|
+
} else {
|
|
388
|
+
ctx.socket.close(); // Rechazar
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Con Reconexión Automática
|
|
394
|
+
|
|
395
|
+
```javascript
|
|
396
|
+
const client = new StelarClient('localhost:3000', {
|
|
397
|
+
reconnection: true,
|
|
398
|
+
reconnectionAttempts: 5,
|
|
399
|
+
reconnectionDelay: 1000
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
client.on('connect', () => console.log('Conectado!'));
|
|
403
|
+
client.on('disconnect', () => console.log('Desconectado'));
|
|
404
|
+
client.on('reconnecting', (attempt) => console.log(`Reintentando ${attempt}/5`));
|
|
405
|
+
|
|
406
|
+
client.connect();
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Diferencia con Socket.io
|
|
412
|
+
|
|
413
|
+
| Característica | stelar-time-real | Socket.io |
|
|
414
|
+
|----------------|------------------|-----------|
|
|
415
|
+
| Tamaño heap | ~13 MB | ~50-100 MB |
|
|
416
|
+
| Dependencias | ws (1) | múltiples |
|
|
417
|
+
| Configuración | mínima | compleja |
|
|
418
|
+
| Flexibilidad | total | opinionada |
|
|
419
|
+
| Ideal para | proyectos propios | producción rápida |
|
|
420
|
+
|
|
421
|
+
## Changelog
|
|
422
|
+
|
|
423
|
+
### v1.0.0
|
|
424
|
+
- Lanzamiento inicial
|
|
425
|
+
- WebSockets con ws
|
|
426
|
+
- Heartbeat automático
|
|
427
|
+
- Reconexión cliente
|
|
428
|
+
- Middlewares
|
|
429
|
+
- Rooms y broadcast
|
|
430
|
+
- Soporte para servidor externo
|
|
431
|
+
|
|
432
|
+
## Licencia
|
|
433
|
+
|
|
434
|
+
MIT - Stelar
|
|
435
|
+
|
|
436
|
+
## Autor
|
|
437
|
+
|
|
438
|
+
Stelar
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stelar-time-real",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Tu propio sistema de tiempo real personalizado - WebSocket ligero sin dependencias",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"websocket",
|
|
9
|
+
"real-time",
|
|
10
|
+
"socket",
|
|
11
|
+
" realtime",
|
|
12
|
+
"stelar",
|
|
13
|
+
"chat",
|
|
14
|
+
"webserver"
|
|
15
|
+
],
|
|
16
|
+
"author": "Stelar",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/stelar-time-real/stelar-time-real"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/stelar-time-real/stelar-time-real/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/stelar-time-real/stelar-time-real#readme",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./src/index.js",
|
|
28
|
+
"./client": "./src/client.js"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"ws": "^8.14.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
class StelarClient {
|
|
2
|
+
constructor(urlOrPort = 'localhost:3000', options = {}) {
|
|
3
|
+
if (typeof urlOrPort === 'number') {
|
|
4
|
+
this.url = `ws://localhost:${urlOrPort}`;
|
|
5
|
+
} else if (urlOrPort.includes('://')) {
|
|
6
|
+
this.url = urlOrPort.startsWith('http') ? 'ws' + urlOrPort.slice(4) : urlOrPort;
|
|
7
|
+
} else {
|
|
8
|
+
this.url = `ws://${urlOrPort}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
this.options = {
|
|
12
|
+
reconnection: options.reconnection !== false,
|
|
13
|
+
reconnectionAttempts: options.reconnectionAttempts || 5,
|
|
14
|
+
reconnectionDelay: options.reconnectionDelay || 1000,
|
|
15
|
+
heartbeatInterval: options.heartbeatInterval || 30000
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
this.ws = null;
|
|
19
|
+
this.events = {};
|
|
20
|
+
this._wildcardHandler = null;
|
|
21
|
+
this.connected = false;
|
|
22
|
+
this.id = null;
|
|
23
|
+
this._reconnectAttempts = 0;
|
|
24
|
+
this._hbTimer = null;
|
|
25
|
+
this._isManualClose = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setUrl(url) {
|
|
29
|
+
this.url = url;
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
on(event, handler) {
|
|
34
|
+
this.events[event] = handler;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onAll(handler) {
|
|
39
|
+
this._wildcardHandler = handler;
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
emit(event, data) {
|
|
44
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
45
|
+
this.ws.send(JSON.stringify({ event, data }));
|
|
46
|
+
}
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
joinRoom(room) {
|
|
51
|
+
this.emit('join-room', room);
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
leaveRoom() {
|
|
56
|
+
this.emit('leave-room', {});
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_startHeartbeat() {
|
|
61
|
+
this._hbTimer = setInterval(() => {
|
|
62
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
63
|
+
this.emit('pong', Date.now());
|
|
64
|
+
}
|
|
65
|
+
}, this.options.heartbeatInterval);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_stopHeartbeat() {
|
|
69
|
+
if (this._hbTimer) {
|
|
70
|
+
clearInterval(this._hbTimer);
|
|
71
|
+
this._hbTimer = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_connect() {
|
|
76
|
+
this._isManualClose = false;
|
|
77
|
+
this.ws = new WebSocket(this.url);
|
|
78
|
+
|
|
79
|
+
this.ws.onopen = () => {
|
|
80
|
+
this.connected = true;
|
|
81
|
+
this._reconnectAttempts = 0;
|
|
82
|
+
if (this.events['connect']) this.events['connect']();
|
|
83
|
+
this._startHeartbeat();
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
this.ws.onmessage = (e) => {
|
|
87
|
+
try {
|
|
88
|
+
const msg = JSON.parse(e.data);
|
|
89
|
+
const { event, data } = msg;
|
|
90
|
+
|
|
91
|
+
if (event === 'ping') return;
|
|
92
|
+
|
|
93
|
+
if (this.events[event]) this.events[event](data);
|
|
94
|
+
|
|
95
|
+
if (this._wildcardHandler) {
|
|
96
|
+
this._wildcardHandler({ event, data });
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
this.ws.onclose = () => {
|
|
102
|
+
this.connected = false;
|
|
103
|
+
this._stopHeartbeat();
|
|
104
|
+
if (this.events['disconnect']) this.events['disconnect']();
|
|
105
|
+
|
|
106
|
+
if (!this._isManualClose && this.options.reconnection && this._reconnectAttempts < this.options.reconnectionAttempts) {
|
|
107
|
+
this._reconnectAttempts++;
|
|
108
|
+
if (this.events['reconnecting']) this.events['reconnecting'](this._reconnectAttempts);
|
|
109
|
+
setTimeout(() => this._connect(), this.options.reconnectionDelay * this._reconnectAttempts);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this.ws.onerror = (err) => {
|
|
114
|
+
if (this.events['error']) this.events['error'](err);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
connect(callback) {
|
|
121
|
+
this._connect();
|
|
122
|
+
if (callback) {
|
|
123
|
+
const checkConnection = setInterval(() => {
|
|
124
|
+
if (this.connected) {
|
|
125
|
+
clearInterval(checkConnection);
|
|
126
|
+
callback();
|
|
127
|
+
}
|
|
128
|
+
}, 100);
|
|
129
|
+
}
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
disconnect() {
|
|
134
|
+
this._isManualClose = true;
|
|
135
|
+
this._stopHeartbeat();
|
|
136
|
+
if (this.ws) this.ws.close();
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
isConnected() {
|
|
141
|
+
return this.connected;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getUrl() {
|
|
145
|
+
return this.url;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof window !== 'undefined') window.StelarClient = StelarClient;
|
|
150
|
+
export default StelarClient;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { WebSocketServer } from 'ws';
|
|
3
|
+
|
|
4
|
+
class StelarServer {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.port = options.port || 3000;
|
|
7
|
+
this.server = options.server || null;
|
|
8
|
+
this.wss = null;
|
|
9
|
+
this.clients = new Map();
|
|
10
|
+
this.events = {};
|
|
11
|
+
this.middlewares = [];
|
|
12
|
+
this.heartbeatInterval = options.heartbeatInterval || 30000;
|
|
13
|
+
this._hbTimer = null;
|
|
14
|
+
this._wildcardHandler = null;
|
|
15
|
+
this._connectionHandler = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
use(middleware) {
|
|
19
|
+
this.middlewares.push(middleware);
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
on(event, handler) {
|
|
24
|
+
this.events[event] = handler;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
onAll(handler) {
|
|
29
|
+
this._wildcardHandler = handler;
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
onConnection(handler) {
|
|
34
|
+
this._connectionHandler = handler;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
broadcast(event, data) {
|
|
39
|
+
this.clients.forEach((info, client) => {
|
|
40
|
+
client.send(JSON.stringify({ event, data }));
|
|
41
|
+
});
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
to(room, event, data) {
|
|
46
|
+
this.clients.forEach((info, client) => {
|
|
47
|
+
if (info.room === room) {
|
|
48
|
+
client.send(JSON.stringify({ event, data }));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
toId(id, event, data) {
|
|
55
|
+
this.clients.forEach((info, client) => {
|
|
56
|
+
if (info.id === id) {
|
|
57
|
+
client.send(JSON.stringify({ event, data }));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getClients(room) {
|
|
64
|
+
const list = [];
|
|
65
|
+
this.clients.forEach((info, client) => {
|
|
66
|
+
if (!room || info.room === room) list.push({ id: info.id, room: info.room });
|
|
67
|
+
});
|
|
68
|
+
return list;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getPort() {
|
|
72
|
+
const address = this.server?.address();
|
|
73
|
+
if (address && typeof address === 'object') {
|
|
74
|
+
return address.port;
|
|
75
|
+
}
|
|
76
|
+
return this.port;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_runMiddlewares(ctx, next) {
|
|
80
|
+
const run = (i) => {
|
|
81
|
+
if (i >= this.middlewares.length) return next();
|
|
82
|
+
this.middlewares[i](ctx, () => run(i + 1));
|
|
83
|
+
};
|
|
84
|
+
run(0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_startHeartbeat() {
|
|
88
|
+
this._hbTimer = setInterval(() => {
|
|
89
|
+
this.clients.forEach((info, client) => {
|
|
90
|
+
if (info.lastPing && Date.now() - info.lastPing > this.heartbeatInterval * 2) {
|
|
91
|
+
client.close();
|
|
92
|
+
this.clients.delete(client);
|
|
93
|
+
} else {
|
|
94
|
+
client.send(JSON.stringify({ event: 'ping', data: Date.now() }));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}, this.heartbeatInterval);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_handleConnection(client, req) {
|
|
101
|
+
const clientId = Math.random().toString(36).substring(7);
|
|
102
|
+
const clientInfo = { id: clientId, room: null, lastPing: Date.now() };
|
|
103
|
+
this.clients.set(client, clientInfo);
|
|
104
|
+
|
|
105
|
+
const ctx = {
|
|
106
|
+
id: clientId,
|
|
107
|
+
socket: client,
|
|
108
|
+
req,
|
|
109
|
+
emit: (evt, d) => client.send(JSON.stringify({ event: evt, data: d })),
|
|
110
|
+
broadcast: (evt, d) => this.broadcast(evt, d),
|
|
111
|
+
to: (room, evt, d) => this.to(room, evt, d),
|
|
112
|
+
toId: (id, evt, d) => this.toId(id, evt, d),
|
|
113
|
+
getClients: (room) => this.getClients(room),
|
|
114
|
+
joinRoom: (room) => {
|
|
115
|
+
clientInfo.room = room;
|
|
116
|
+
client.send(JSON.stringify({ event: 'joined-room', data: room }));
|
|
117
|
+
},
|
|
118
|
+
leaveRoom: () => {
|
|
119
|
+
clientInfo.room = null;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
this._runMiddlewares(ctx, () => {
|
|
124
|
+
if (this._connectionHandler) {
|
|
125
|
+
this._connectionHandler(ctx);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
client.on('message', (raw) => {
|
|
130
|
+
try {
|
|
131
|
+
const msg = JSON.parse(raw);
|
|
132
|
+
const { event, data } = msg;
|
|
133
|
+
|
|
134
|
+
if (event === 'pong') {
|
|
135
|
+
clientInfo.lastPing = Date.now();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (event === 'join-room') {
|
|
140
|
+
clientInfo.room = data;
|
|
141
|
+
client.send(JSON.stringify({ event: 'joined-room', data }));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (event === 'leave-room') {
|
|
145
|
+
clientInfo.room = null;
|
|
146
|
+
client.send(JSON.stringify({ event: 'left-room', data }));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const eventCtx = { ...ctx, data };
|
|
150
|
+
|
|
151
|
+
if (this.events[event]) {
|
|
152
|
+
this.events[event](eventCtx);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (this._wildcardHandler) {
|
|
156
|
+
this._wildcardHandler({ event, data: eventCtx });
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
client.on('close', () => {
|
|
162
|
+
this.clients.delete(client);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
client.on('error', (err) => {
|
|
166
|
+
if (this.events['error']) {
|
|
167
|
+
this.events['error']({ id: clientId, error: err });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
start(callback) {
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
const startServer = (httpServer) => {
|
|
175
|
+
this.server = httpServer;
|
|
176
|
+
this.wss = new WebSocketServer({ server: httpServer });
|
|
177
|
+
this.wss.on('connection', (client, req) => this._handleConnection(client, req));
|
|
178
|
+
this._startHeartbeat();
|
|
179
|
+
|
|
180
|
+
const finalPort = this.getPort();
|
|
181
|
+
if (callback) callback(finalPort);
|
|
182
|
+
resolve(finalPort);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (this.server) {
|
|
186
|
+
startServer(this.server);
|
|
187
|
+
} else {
|
|
188
|
+
const tryListen = (port) => {
|
|
189
|
+
this.server = createServer((req, res) => {
|
|
190
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
191
|
+
res.end('Stelar Time Real Server');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
this.server.on('error', (err) => {
|
|
195
|
+
if (err.code === 'EADDRINUSE' && port < 65535) {
|
|
196
|
+
tryListen(port + 1);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
this.server.listen(port, () => {
|
|
201
|
+
this.port = port;
|
|
202
|
+
startServer(this.server);
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
tryListen(this.port);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
stop() {
|
|
211
|
+
if (this._hbTimer) clearInterval(this._hbTimer);
|
|
212
|
+
if (this.wss) this.wss.close();
|
|
213
|
+
if (this.server && !this._externalServer) this.server.close();
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export default StelarServer;
|
|
219
|
+
export { StelarServer };
|