stelar-time-real 1.0.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,9 @@ 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
18
+ - 📦 **Binarios** - Envía imágenes, archivos, audio, video sin overhead de base64
16
19
 
17
20
  ## Instalación
18
21
 
@@ -190,15 +193,78 @@ stelar.on('mensaje', (ctx) => {
190
193
  ctx.socket // WebSocket del cliente
191
194
  ctx.req // Request HTTP original
192
195
  ctx.data // Datos recibidos
193
-
196
+
194
197
  // Métodos disponibles:
195
198
  ctx.emit('evento', data) // Enviar solo a este cliente
199
+ ctx.send('respuesta', data) // Responder a un ACK
196
200
  ctx.broadcast('evento', data) // Enviar a todos
197
201
  ctx.to('sala', 'evento', data) // Enviar a una sala
198
202
  ctx.toId('id', 'evento', data) // Enviar a un cliente específico
199
203
  ctx.getClients('sala') // Ver clientes en sala
200
204
  ctx.joinRoom('sala') // Unir a sala
201
205
  ctx.leaveRoom() // Salir de sala
206
+ ctx.ack('miAck', data) // Responder a un ACK personalizado
207
+ });
208
+ ```
209
+
210
+ #### Namespaces
211
+
212
+ Crear canales independientes:
213
+
214
+ ```javascript
215
+ import { StelarServer } from 'stelar-time-real';
216
+
217
+ // Namespace principal
218
+ const main = new StelarServer({ server, namespace: '/' });
219
+
220
+ // Namespace de chat
221
+ const chat = StelarServer.of('/chat', { server });
222
+ chat.on('message', (ctx) => {
223
+ ctx.broadcast('message', ctx.data);
224
+ });
225
+
226
+ // Namespace de game
227
+ const game = StelarServer.of('/game', { server });
228
+ game.on('move', (ctx) => {
229
+ ctx.to(ctx.data.room, 'move', ctx.data);
230
+ });
231
+ ```
232
+
233
+ #### ACK (Request-Response)
234
+
235
+ Sistema ultra eficiente con Promesas:
236
+
237
+ **Servidor:**
238
+
239
+ ```javascript
240
+ // Registrar un ACK handler
241
+ stelar.onAck('getUser', (ctx) => {
242
+ return { id: ctx.data.id, name: 'John' };
243
+ });
244
+
245
+ // O con lógica más compleja
246
+ stelar.onAck('saveData', (ctx) => {
247
+ const result = saveToDatabase(ctx.data);
248
+ return { success: true, id: result.id };
249
+ });
250
+ ```
251
+
252
+ **Cliente:**
253
+
254
+ ```javascript
255
+ // Usando request() - retorna Promise
256
+ const user = await client.request('getUser', { id: 1 }, 'userData');
257
+ console.log(user); // { id: 1, name: 'John' }
258
+
259
+ // O emitiendo con callback
260
+ client.emit('getUser', { id: 1 }, { ack: 'userData' });
261
+ client.on('userData', (data) => {
262
+ console.log(data);
263
+ });
264
+
265
+ // ACK desde el servidor al cliente
266
+ client.onAck('serverPush', (data) => {
267
+ console.log('El servidor envió:', data);
202
268
  });
203
269
  ```
204
270
 
@@ -255,11 +321,32 @@ client.onAll(({ event, data }) => {
255
321
  });
256
322
  ```
257
323
 
258
- **`.emit(event, data)`**
259
- Enviar eventos al servidor.
324
+ **`.onAck(name, handler)`**
325
+ Escuchar respuestas de ACK del servidor.
326
+
327
+ ```javascript
328
+ client.onAck('userData', (data) => {
329
+ console.log('Datos recibidos:', data);
330
+ });
331
+ ```
332
+
333
+ **`.emit(event, data, opts)`**
334
+ Enviar eventos al servidor. Soporta `opts.ack` para ACKs.
260
335
 
261
336
  ```javascript
262
337
  client.emit('chat', { mensaje: 'Hola!' });
338
+ client.emit('getUser', { id: 1 }, { ack: 'userData' });
339
+ ```
340
+
341
+ **`.request(event, data, ackName)`**
342
+ Enviar y esperar respuesta como Promise.
343
+
344
+ ```javascript
345
+ const result = await client.request('getUser', { id: 1 }, 'userData');
346
+ console.log(result); // { id: 1, name: 'John' }
347
+
348
+ // Con timeout opcional
349
+ const client = new StelarClient(3000, { ackTimeout: 10000 });
263
350
  ```
264
351
 
265
352
  **`.joinRoom(room)`**
@@ -414,6 +501,49 @@ client.on('reconnecting', (attempt) => console.log(`Reintentando ${attempt}/5`))
414
501
  client.connect();
415
502
  ```
416
503
 
504
+ ### Enviar Archivos Binarios
505
+
506
+ ```javascript
507
+ // Servidor - recibir imagen
508
+ stelar.on('image', (ctx) => {
509
+ // ctx.buffer es un Uint8Array
510
+ console.log('Recibido:', ctx.buffer.byteLength, 'bytes');
511
+ // Guardar o procesar la imagen
512
+ saveImage(ctx.buffer);
513
+
514
+ // Responder al cliente
515
+ ctx.emit('imageSaved', { success: true });
516
+ });
517
+
518
+ // Cliente - enviar imagen
519
+ const input = document.querySelector('input[type="file"]');
520
+ input.addEventListener('change', async (e) => {
521
+ const file = e.target.files[0];
522
+ const buffer = await file.arrayBuffer();
523
+ client.emitBinary('image', buffer);
524
+ });
525
+
526
+ // Cliente - recibir imagen
527
+ client.on('image', (buffer) => {
528
+ const blob = new Blob([buffer], { type: 'image/png' });
529
+ const url = URL.createObjectURL(blob);
530
+ document.getElementById('img').src = url;
531
+ });
532
+ ```
533
+
534
+ ### Broadcast de Binarios
535
+
536
+ ```javascript
537
+ // Servidor - compartir archivo con todos
538
+ stelar.on('upload', (ctx) => {
539
+ ctx.broadcastBinary('file', ctx.buffer);
540
+ });
541
+
542
+ // Cliente - enviar archivo
543
+ const fileData = await file.arrayBuffer();
544
+ client.emitBinary('upload', fileData);
545
+ ```
546
+
417
547
  ---
418
548
 
419
549
  ## Diferencia con Socket.io
@@ -428,6 +558,17 @@ client.connect();
428
558
 
429
559
  ## Changelog
430
560
 
561
+ ### v1.2.0
562
+ - Soporte binarios: envía imágenes, archivos, audio, video sin base64
563
+ - `ctx.emitBinary('event', buffer)` y `client.emitBinary('event', buffer)`
564
+ - `client.sendFile(file)` y `client.sendImage(blob)`
565
+ - Eficiente: usa WebSocket binary diretamente, sin overhead
566
+
567
+ ### v1.1.0
568
+ - Namespaces: `StelarServer.of('/chat')` para múltiples canales
569
+ - ACK personalizado: `ctx.ack('nombre', data)` y `client.request(event, data, 'ackName')`
570
+ - Sistema de request-response con Promesas, ultra eficiente
571
+
431
572
  ### v1.0.3
432
573
  - Exportación mejorada: un solo import para servidor y cliente
433
574
  - `import { StelarServer, StelarClient } from 'stelar-time-real'`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stelar-time-real",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
4
4
  "description": "Tu propio sistema de tiempo real personalizado - WebSocket ligero sin dependencias",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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,86 @@ 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
+ emitBinary(event, data) {
62
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
63
+ const header = JSON.stringify({ event });
64
+ const headerBytes = new TextEncoder().encode(header);
65
+ const combined = new Uint8Array(headerBytes.length + 1 + data.byteLength);
66
+ combined.set(headerBytes, 0);
67
+ combined[headerBytes.length] = 0;
68
+ combined.set(new Uint8Array(data), headerBytes.length + 1);
69
+ this.ws.send(combined);
70
+ }
71
+ return this;
72
+ }
73
+
74
+ sendFile(file) {
75
+ return this.emitBinary('file', file);
76
+ }
77
+
78
+ sendImage(blob) {
79
+ return this.emitBinary('image', blob);
80
+ }
81
+
82
+ request(event, data, ackName) {
83
+ return new Promise((resolve, reject) => {
84
+ const timeout = setTimeout(() => {
85
+ reject(new Error(`ACK '${ackName}' timeout`));
86
+ }, this.options.ackTimeout);
87
+
88
+ const handler = (responseData) => {
89
+ clearTimeout(timeout);
90
+ this.off(ackName, handler);
91
+ resolve(responseData);
92
+ };
93
+
94
+ this.on(ackName, handler);
95
+ this.emit(event, data, { ack: ackName });
96
+ });
97
+ }
98
+
99
+ joinRoom(room) {
100
+ this.emit('join-room', room);
101
+ return this;
102
+ }
103
+
104
+ leaveRoom() {
105
+ this.emit('leave-room', {});
106
+ return this;
26
107
  }
27
108
 
28
109
  setUrl(url) {
@@ -83,13 +164,44 @@ class StelarClient {
83
164
  this._startHeartbeat();
84
165
  };
85
166
 
167
+ this.ws.binaryType = 'arraybuffer';
168
+
86
169
  this.ws.onmessage = (e) => {
87
170
  try {
171
+ if (e.data instanceof ArrayBuffer) {
172
+ const view = new Uint8Array(e.data);
173
+ let headerEnd = -1;
174
+ for (let i = 0; i < view.length; i++) {
175
+ if (view[i] === 0) {
176
+ headerEnd = i;
177
+ break;
178
+ }
179
+ }
180
+ if (headerEnd === -1) return;
181
+
182
+ const headerStr = new TextDecoder().decode(view.slice(0, headerEnd));
183
+ const header = JSON.parse(headerStr);
184
+ const buffer = view.slice(headerEnd + 1);
185
+
186
+ if (this.events[header.event]) {
187
+ this.events[header.event](buffer);
188
+ }
189
+ if (this._wildcardHandler) {
190
+ this._wildcardHandler({ event: header.event, data: buffer, buffer, isBinary: true });
191
+ }
192
+ return;
193
+ }
194
+
88
195
  const msg = JSON.parse(e.data);
89
- const { event, data } = msg;
196
+ const { event, data, _isAck } = msg;
90
197
 
91
198
  if (event === 'ping') return;
92
199
 
200
+ if (_isAck && this._acks[event]) {
201
+ this._acks[event](data);
202
+ return;
203
+ }
204
+
93
205
  if (this.events[event]) this.events[event](data);
94
206
 
95
207
  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 }));
@@ -42,6 +57,20 @@ class StelarServer {
42
57
  return this;
43
58
  }
44
59
 
60
+ broadcastBinary(event, buffer) {
61
+ const header = JSON.stringify({ event, _binary: true });
62
+ const headerBytes = new TextEncoder().encode(header);
63
+ const combined = new Uint8Array(headerBytes.length + 1 + buffer.byteLength);
64
+ combined.set(headerBytes, 0);
65
+ combined[headerBytes.length] = 0;
66
+ combined.set(new Uint8Array(buffer), headerBytes.length + 1);
67
+
68
+ this.clients.forEach((info, client) => {
69
+ client.send(combined);
70
+ });
71
+ return this;
72
+ }
73
+
45
74
  to(room, event, data) {
46
75
  this.clients.forEach((info, client) => {
47
76
  if (info.room === room) {
@@ -98,6 +127,14 @@ class StelarServer {
98
127
  }
99
128
 
100
129
  _handleConnection(client, req) {
130
+ const urlPath = new URL(req.url, `http://localhost`).pathname;
131
+ const nsPath = this.namespace === '/' ? '/' : this.namespace;
132
+
133
+ if (nsPath !== '/' && urlPath !== nsPath) {
134
+ client.close();
135
+ return;
136
+ }
137
+
101
138
  const clientId = Math.random().toString(36).substring(7);
102
139
  const clientInfo = { id: clientId, room: null, lastPing: Date.now() };
103
140
  this.clients.set(client, clientInfo);
@@ -107,7 +144,10 @@ class StelarServer {
107
144
  socket: client,
108
145
  req,
109
146
  emit: (evt, d) => client.send(JSON.stringify({ event: evt, data: d })),
147
+ send: (respId, d) => client.send(JSON.stringify({ event: respId, data: d, _isAck: true })),
148
+ emitBinary: (evt, buffer) => client.send(buffer),
110
149
  broadcast: (evt, d) => this.broadcast(evt, d),
150
+ broadcastBinary: (evt, buffer) => this.broadcastBinary(evt, buffer),
111
151
  to: (room, evt, d) => this.to(room, evt, d),
112
152
  toId: (id, evt, d) => this.toId(id, evt, d),
113
153
  getClients: (room) => this.getClients(room),
@@ -117,6 +157,15 @@ class StelarServer {
117
157
  },
118
158
  leaveRoom: () => {
119
159
  clientInfo.room = null;
160
+ },
161
+ ack: (ackName, data) => {
162
+ const ackHandler = this._acks[ackName];
163
+ if (ackHandler) {
164
+ const result = ackHandler({ ...ctx, data });
165
+ if (result !== undefined) {
166
+ client.send(JSON.stringify({ event: ackName, data: result, _isAck: true }));
167
+ }
168
+ }
120
169
  }
121
170
  };
122
171
 
@@ -126,7 +175,35 @@ class StelarServer {
126
175
  }
127
176
  });
128
177
 
129
- client.on('message', (raw) => {
178
+ client.on('message', (raw, isBinary) => {
179
+ if (isBinary) {
180
+ try {
181
+ const view = new Uint8Array(raw);
182
+ let headerEnd = -1;
183
+ for (let i = 0; i < view.length; i++) {
184
+ if (view[i] === 0) {
185
+ headerEnd = i;
186
+ break;
187
+ }
188
+ }
189
+ if (headerEnd === -1) return;
190
+
191
+ const headerStr = new TextDecoder().decode(view.slice(0, headerEnd));
192
+ const header = JSON.parse(headerStr);
193
+ const data = view.slice(headerEnd + 1);
194
+
195
+ const eventCtx = { ...ctx, data, buffer: data, isBinary: true };
196
+
197
+ if (this.events[header.event]) {
198
+ this.events[header.event](eventCtx);
199
+ }
200
+ if (this._wildcardHandler) {
201
+ this._wildcardHandler({ event: header.event, data: eventCtx });
202
+ }
203
+ } catch (e) {}
204
+ return;
205
+ }
206
+
130
207
  try {
131
208
  const msg = JSON.parse(raw);
132
209
  const { event, data } = msg;
@@ -146,6 +223,15 @@ class StelarServer {
146
223
  client.send(JSON.stringify({ event: 'left-room', data }));
147
224
  }
148
225
 
226
+ if (msg._ackName && this._acks[msg._ackName]) {
227
+ const ackHandler = this._acks[msg._ackName];
228
+ const result = ackHandler({ ...ctx, data });
229
+ if (result !== undefined) {
230
+ client.send(JSON.stringify({ event: msg._ackName, data: result, _isAck: true }));
231
+ }
232
+ return;
233
+ }
234
+
149
235
  const eventCtx = { ...ctx, data };
150
236
 
151
237
  if (this.events[event]) {