stelar-time-real 1.0.2 → 1.1.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 +113 -7
- package/package.json +1 -1
- package/src/client.js +67 -2
- package/src/index.js +42 -0
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ Tu propio sistema de tiempo real personalizado. Una librería ligera y sin depen
|
|
|
13
13
|
- 🎯 **Personalizable** - Vos controlás todo, nada de código ajeno
|
|
14
14
|
- 🔌 **Compatible** - Funciona con Express, Fastify, HTTP nativo, etc.
|
|
15
15
|
- 💓 **Heartbeat incluido** - Detecta desconexiones automáticamente
|
|
16
|
+
- 🌐 **Namespaces** - Múltiples canales independientes (`/chat`, `/game`, etc.)
|
|
17
|
+
- ⚡ **ACK ultra rápido** - Request-response con Promesas, sin overhead
|
|
16
18
|
|
|
17
19
|
## Instalación
|
|
18
20
|
|
|
@@ -22,11 +24,17 @@ npm install stelar-time-real
|
|
|
22
24
|
|
|
23
25
|
## Inicio Rápido
|
|
24
26
|
|
|
27
|
+
### Un solo import para todo
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
import StelarServer, { StelarClient } from 'stelar-time-real';
|
|
31
|
+
```
|
|
32
|
+
|
|
25
33
|
### Servidor
|
|
26
34
|
|
|
27
35
|
```javascript
|
|
28
36
|
import express from 'express';
|
|
29
|
-
import StelarServer from 'stelar-time-real';
|
|
37
|
+
import { StelarServer } from 'stelar-time-real';
|
|
30
38
|
|
|
31
39
|
const app = express();
|
|
32
40
|
const server = app.listen(3000);
|
|
@@ -48,7 +56,7 @@ stelar.start();
|
|
|
48
56
|
### Cliente
|
|
49
57
|
|
|
50
58
|
```javascript
|
|
51
|
-
import StelarClient from 'stelar-time-real
|
|
59
|
+
import { StelarClient } from 'stelar-time-real';
|
|
52
60
|
|
|
53
61
|
const client = new StelarClient('localhost:3000');
|
|
54
62
|
|
|
@@ -184,15 +192,78 @@ stelar.on('mensaje', (ctx) => {
|
|
|
184
192
|
ctx.socket // WebSocket del cliente
|
|
185
193
|
ctx.req // Request HTTP original
|
|
186
194
|
ctx.data // Datos recibidos
|
|
187
|
-
|
|
195
|
+
|
|
188
196
|
// Métodos disponibles:
|
|
189
197
|
ctx.emit('evento', data) // Enviar solo a este cliente
|
|
198
|
+
ctx.send('respuesta', data) // Responder a un ACK
|
|
190
199
|
ctx.broadcast('evento', data) // Enviar a todos
|
|
191
200
|
ctx.to('sala', 'evento', data) // Enviar a una sala
|
|
192
201
|
ctx.toId('id', 'evento', data) // Enviar a un cliente específico
|
|
193
202
|
ctx.getClients('sala') // Ver clientes en sala
|
|
194
203
|
ctx.joinRoom('sala') // Unir a sala
|
|
195
204
|
ctx.leaveRoom() // Salir de sala
|
|
205
|
+
ctx.ack('miAck', data) // Responder a un ACK personalizado
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### Namespaces
|
|
210
|
+
|
|
211
|
+
Crear canales independientes:
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
import { StelarServer } from 'stelar-time-real';
|
|
215
|
+
|
|
216
|
+
// Namespace principal
|
|
217
|
+
const main = new StelarServer({ server, namespace: '/' });
|
|
218
|
+
|
|
219
|
+
// Namespace de chat
|
|
220
|
+
const chat = StelarServer.of('/chat', { server });
|
|
221
|
+
chat.on('message', (ctx) => {
|
|
222
|
+
ctx.broadcast('message', ctx.data);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Namespace de game
|
|
226
|
+
const game = StelarServer.of('/game', { server });
|
|
227
|
+
game.on('move', (ctx) => {
|
|
228
|
+
ctx.to(ctx.data.room, 'move', ctx.data);
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### ACK (Request-Response)
|
|
233
|
+
|
|
234
|
+
Sistema ultra eficiente con Promesas:
|
|
235
|
+
|
|
236
|
+
**Servidor:**
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
// Registrar un ACK handler
|
|
240
|
+
stelar.onAck('getUser', (ctx) => {
|
|
241
|
+
return { id: ctx.data.id, name: 'John' };
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// O con lógica más compleja
|
|
245
|
+
stelar.onAck('saveData', (ctx) => {
|
|
246
|
+
const result = saveToDatabase(ctx.data);
|
|
247
|
+
return { success: true, id: result.id };
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Cliente:**
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
// Usando request() - retorna Promise
|
|
255
|
+
const user = await client.request('getUser', { id: 1 }, 'userData');
|
|
256
|
+
console.log(user); // { id: 1, name: 'John' }
|
|
257
|
+
|
|
258
|
+
// O emitiendo con callback
|
|
259
|
+
client.emit('getUser', { id: 1 }, { ack: 'userData' });
|
|
260
|
+
client.on('userData', (data) => {
|
|
261
|
+
console.log(data);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ACK desde el servidor al cliente
|
|
265
|
+
client.onAck('serverPush', (data) => {
|
|
266
|
+
console.log('El servidor envió:', data);
|
|
196
267
|
});
|
|
197
268
|
```
|
|
198
269
|
|
|
@@ -249,11 +320,32 @@ client.onAll(({ event, data }) => {
|
|
|
249
320
|
});
|
|
250
321
|
```
|
|
251
322
|
|
|
252
|
-
**`.
|
|
253
|
-
|
|
323
|
+
**`.onAck(name, handler)`**
|
|
324
|
+
Escuchar respuestas de ACK del servidor.
|
|
325
|
+
|
|
326
|
+
```javascript
|
|
327
|
+
client.onAck('userData', (data) => {
|
|
328
|
+
console.log('Datos recibidos:', data);
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**`.emit(event, data, opts)`**
|
|
333
|
+
Enviar eventos al servidor. Soporta `opts.ack` para ACKs.
|
|
254
334
|
|
|
255
335
|
```javascript
|
|
256
336
|
client.emit('chat', { mensaje: 'Hola!' });
|
|
337
|
+
client.emit('getUser', { id: 1 }, { ack: 'userData' });
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**`.request(event, data, ackName)`**
|
|
341
|
+
Enviar y esperar respuesta como Promise.
|
|
342
|
+
|
|
343
|
+
```javascript
|
|
344
|
+
const result = await client.request('getUser', { id: 1 }, 'userData');
|
|
345
|
+
console.log(result); // { id: 1, name: 'John' }
|
|
346
|
+
|
|
347
|
+
// Con timeout opcional
|
|
348
|
+
const client = new StelarClient(3000, { ackTimeout: 10000 });
|
|
257
349
|
```
|
|
258
350
|
|
|
259
351
|
**`.joinRoom(room)`**
|
|
@@ -320,7 +412,7 @@ client.on('error', (err) => {}); // Cuando hay error
|
|
|
320
412
|
**server.js**
|
|
321
413
|
```javascript
|
|
322
414
|
import express from 'express';
|
|
323
|
-
import StelarServer from 'stelar-time-real';
|
|
415
|
+
import { StelarServer } from 'stelar-time-real';
|
|
324
416
|
|
|
325
417
|
const app = express();
|
|
326
418
|
const server = app.listen(3000);
|
|
@@ -342,7 +434,7 @@ console.log('Chat en http://localhost:3000');
|
|
|
342
434
|
**cliente.html**
|
|
343
435
|
```html
|
|
344
436
|
<script type="module">
|
|
345
|
-
import StelarClient from 'stelar-time-real
|
|
437
|
+
import { StelarClient } from 'stelar-time-real';
|
|
346
438
|
|
|
347
439
|
const client = new StelarClient(3000);
|
|
348
440
|
|
|
@@ -393,6 +485,8 @@ stelar.use((ctx, next) => {
|
|
|
393
485
|
### Con Reconexión Automática
|
|
394
486
|
|
|
395
487
|
```javascript
|
|
488
|
+
import { StelarClient } from 'stelar-time-real';
|
|
489
|
+
|
|
396
490
|
const client = new StelarClient('localhost:3000', {
|
|
397
491
|
reconnection: true,
|
|
398
492
|
reconnectionAttempts: 5,
|
|
@@ -420,6 +514,18 @@ client.connect();
|
|
|
420
514
|
|
|
421
515
|
## Changelog
|
|
422
516
|
|
|
517
|
+
### v1.1.0
|
|
518
|
+
- Namespaces: `StelarServer.of('/chat')` para múltiples canales
|
|
519
|
+
- ACK personalizado: `ctx.ack('nombre', data)` y `client.request(event, data, 'ackName')`
|
|
520
|
+
- Sistema de request-response con Promesas, ultra eficiente
|
|
521
|
+
|
|
522
|
+
### v1.0.3
|
|
523
|
+
- Exportación mejorada: un solo import para servidor y cliente
|
|
524
|
+
- `import { StelarServer, StelarClient } from 'stelar-time-real'`
|
|
525
|
+
|
|
526
|
+
### v1.0.2
|
|
527
|
+
- Exportación mejorada: un solo import para servidor y cliente
|
|
528
|
+
|
|
423
529
|
### v1.0.0
|
|
424
530
|
- Lanzamiento inicial
|
|
425
531
|
- WebSockets con ws
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -12,7 +12,8 @@ class StelarClient {
|
|
|
12
12
|
reconnection: options.reconnection !== false,
|
|
13
13
|
reconnectionAttempts: options.reconnectionAttempts || 5,
|
|
14
14
|
reconnectionDelay: options.reconnectionDelay || 1000,
|
|
15
|
-
heartbeatInterval: options.heartbeatInterval || 30000
|
|
15
|
+
heartbeatInterval: options.heartbeatInterval || 30000,
|
|
16
|
+
ackTimeout: options.ackTimeout || 5000
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
this.ws = null;
|
|
@@ -23,6 +24,65 @@ class StelarClient {
|
|
|
23
24
|
this._reconnectAttempts = 0;
|
|
24
25
|
this._hbTimer = null;
|
|
25
26
|
this._isManualClose = false;
|
|
27
|
+
this._acks = {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setUrl(url) {
|
|
31
|
+
this.url = url;
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
on(event, handler) {
|
|
36
|
+
this.events[event] = handler;
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onAll(handler) {
|
|
41
|
+
this._wildcardHandler = handler;
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onAck(name, handler) {
|
|
46
|
+
this._acks[name] = handler;
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
emit(event, data, opts = {}) {
|
|
51
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
52
|
+
const payload = { event, data };
|
|
53
|
+
if (opts.ack) {
|
|
54
|
+
payload._ackName = opts.ack;
|
|
55
|
+
}
|
|
56
|
+
this.ws.send(JSON.stringify(payload));
|
|
57
|
+
}
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
request(event, data, ackName) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const timeout = setTimeout(() => {
|
|
64
|
+
reject(new Error(`ACK '${ackName}' timeout`));
|
|
65
|
+
}, this.options.ackTimeout);
|
|
66
|
+
|
|
67
|
+
const handler = (responseData) => {
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
this.off(ackName, handler);
|
|
70
|
+
resolve(responseData);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
this.on(ackName, handler);
|
|
74
|
+
this.emit(event, data, { ack: ackName });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
joinRoom(room) {
|
|
79
|
+
this.emit('join-room', room);
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
leaveRoom() {
|
|
84
|
+
this.emit('leave-room', {});
|
|
85
|
+
return this;
|
|
26
86
|
}
|
|
27
87
|
|
|
28
88
|
setUrl(url) {
|
|
@@ -86,10 +146,15 @@ class StelarClient {
|
|
|
86
146
|
this.ws.onmessage = (e) => {
|
|
87
147
|
try {
|
|
88
148
|
const msg = JSON.parse(e.data);
|
|
89
|
-
const { event, data } = msg;
|
|
149
|
+
const { event, data, _isAck } = msg;
|
|
90
150
|
|
|
91
151
|
if (event === 'ping') return;
|
|
92
152
|
|
|
153
|
+
if (_isAck && this._acks[event]) {
|
|
154
|
+
this._acks[event](data);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
93
158
|
if (this.events[event]) this.events[event](data);
|
|
94
159
|
|
|
95
160
|
if (this._wildcardHandler) {
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ class StelarServer {
|
|
|
5
5
|
constructor(options = {}) {
|
|
6
6
|
this.port = options.port || 3000;
|
|
7
7
|
this.server = options.server || null;
|
|
8
|
+
this.namespace = options.namespace || '/';
|
|
8
9
|
this.wss = null;
|
|
9
10
|
this.clients = new Map();
|
|
10
11
|
this.events = {};
|
|
@@ -13,6 +14,15 @@ class StelarServer {
|
|
|
13
14
|
this._hbTimer = null;
|
|
14
15
|
this._wildcardHandler = null;
|
|
15
16
|
this._connectionHandler = null;
|
|
17
|
+
this._acks = {};
|
|
18
|
+
this._parent = null;
|
|
19
|
+
this._children = new Map();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static of(path, options = {}) {
|
|
23
|
+
const ns = new StelarServer({ ...options, namespace: path });
|
|
24
|
+
ns._parent = this;
|
|
25
|
+
return ns;
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
use(middleware) {
|
|
@@ -35,6 +45,11 @@ class StelarServer {
|
|
|
35
45
|
return this;
|
|
36
46
|
}
|
|
37
47
|
|
|
48
|
+
onAck(name, handler) {
|
|
49
|
+
this._acks[name] = handler;
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
broadcast(event, data) {
|
|
39
54
|
this.clients.forEach((info, client) => {
|
|
40
55
|
client.send(JSON.stringify({ event, data }));
|
|
@@ -98,6 +113,14 @@ class StelarServer {
|
|
|
98
113
|
}
|
|
99
114
|
|
|
100
115
|
_handleConnection(client, req) {
|
|
116
|
+
const urlPath = new URL(req.url, `http://localhost`).pathname;
|
|
117
|
+
const nsPath = this.namespace === '/' ? '/' : this.namespace;
|
|
118
|
+
|
|
119
|
+
if (nsPath !== '/' && urlPath !== nsPath) {
|
|
120
|
+
client.close();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
101
124
|
const clientId = Math.random().toString(36).substring(7);
|
|
102
125
|
const clientInfo = { id: clientId, room: null, lastPing: Date.now() };
|
|
103
126
|
this.clients.set(client, clientInfo);
|
|
@@ -107,6 +130,7 @@ class StelarServer {
|
|
|
107
130
|
socket: client,
|
|
108
131
|
req,
|
|
109
132
|
emit: (evt, d) => client.send(JSON.stringify({ event: evt, data: d })),
|
|
133
|
+
send: (respId, d) => client.send(JSON.stringify({ event: respId, data: d, _isAck: true })),
|
|
110
134
|
broadcast: (evt, d) => this.broadcast(evt, d),
|
|
111
135
|
to: (room, evt, d) => this.to(room, evt, d),
|
|
112
136
|
toId: (id, evt, d) => this.toId(id, evt, d),
|
|
@@ -117,6 +141,15 @@ class StelarServer {
|
|
|
117
141
|
},
|
|
118
142
|
leaveRoom: () => {
|
|
119
143
|
clientInfo.room = null;
|
|
144
|
+
},
|
|
145
|
+
ack: (ackName, data) => {
|
|
146
|
+
const ackHandler = this._acks[ackName];
|
|
147
|
+
if (ackHandler) {
|
|
148
|
+
const result = ackHandler({ ...ctx, data });
|
|
149
|
+
if (result !== undefined) {
|
|
150
|
+
client.send(JSON.stringify({ event: ackName, data: result, _isAck: true }));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
120
153
|
}
|
|
121
154
|
};
|
|
122
155
|
|
|
@@ -146,6 +179,15 @@ class StelarServer {
|
|
|
146
179
|
client.send(JSON.stringify({ event: 'left-room', data }));
|
|
147
180
|
}
|
|
148
181
|
|
|
182
|
+
if (msg._ackName && this._acks[msg._ackName]) {
|
|
183
|
+
const ackHandler = this._acks[msg._ackName];
|
|
184
|
+
const result = ackHandler({ ...ctx, data });
|
|
185
|
+
if (result !== undefined) {
|
|
186
|
+
client.send(JSON.stringify({ event: msg._ackName, data: result, _isAck: true }));
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
149
191
|
const eventCtx = { ...ctx, data };
|
|
150
192
|
|
|
151
193
|
if (this.events[event]) {
|