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 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
+ ![npm](https://img.shields.io/npm/v/stelar-time-real)
6
+ ![license](https://img.shields.io/npm/l/stelar-time-real)
7
+ ![size](https://img.shields.io/bundlephobia/min/stelar-time-real)
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 };