stelar-time-real 2.0.4 → 3.2.1

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
@@ -1,21 +1,94 @@
1
- # stelar-time-real
1
+ # stelar-time-real v3
2
2
 
3
- Your own custom real-time system. A lightweight, dependency-free library for real-time communication via WebSockets.
3
+ **Real-time library for production.** Zero dependencies. Custom binary TCP protocol + Manual WebSocket (RFC 6455 implemented from scratch). No external packages.
4
4
 
5
5
  ![npm](https://img.shields.io/npm/v/stelar-time-real)
6
6
  ![license](https://img.shields.io/npm/l/stelar-time-real)
7
- ![size](https://img.shields.io/bundlephobia/min/stelar-time-real)
7
+ ![zero-deps](https://img.shields.io/badge/dependencies-0-green)
8
+ ![production](https://img.shields.io/badge/status-production_ready-blue)
8
9
 
9
- ## Why stelar-time-real?
10
+ ---
11
+
12
+ ## What is stelar-time-real?
13
+
14
+ stelar-time-real is a real-time communication library designed from scratch for production. It does not wrap nor depend on any external library — it implements its own binary TCP protocol and its own WebSocket (RFC 6455) using exclusively Node.js built-in modules (`http`, `net`, `crypto`, `tls`).
15
+
16
+ This means total control: no dependencies that break, no third-party vulnerabilities, no bloat, and no surprises. Every byte that travels across the network is controlled by the library.
17
+
18
+ ### What is it for?
19
+
20
+ - Real-time chat (messaging, notifications, typing indicators)
21
+ - Collaborative applications (editors, whiteboards, shared documents)
22
+ - Binary data streaming (images, files, audio, video)
23
+ - Internal microservices with ultra-fast communication via TCP
24
+ - Live dashboards (metrics, monitoring, trading)
25
+ - Real-time multiplayer games
26
+ - Social networks, Discord-like platforms
27
+ - IoT and connected devices
28
+
29
+ ---
30
+
31
+ ## Main Features
32
+
33
+ ### Dual Protocol
34
+
35
+ The server supports **two protocols simultaneously** on different ports:
36
+
37
+ - **WebSocket** — For browsers and web clients. Complete manual implementation of RFC 6455: handshake, framing, masking, close codes, RSV bits validation, max frame size enforcement.
38
+ - **Custom TCP** — For server-to-server and Node.js microservices communication. Custom binary protocol with minimal overhead (7-byte header). Ultra-low latency.
39
+
40
+ Both protocols share the same server API. A WebSocket client and a TCP client can be in the same room, receive the same broadcasts, and interact as if they were the same type of connection.
41
+
42
+ ### Zero Dependencies
43
+
44
+ ```
45
+ dependencies: {}
46
+ ```
10
47
 
11
- - **Ultra lightweight** - Only ~13MB of heap
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
48
+ No `ws`, no `engine.io`, nothing. Just pure Node.js. This means:
49
+
50
+ - No vulnerabilities in third-party dependencies
51
+ - No breaking changes from external updates
52
+ - No supply chain attacks
53
+ - Minimal size in node_modules
54
+ - Instant installation
55
+
56
+ ### Production-Ready
57
+
58
+ Every feature was designed with a real environment in mind — with users, attacks, and errors:
59
+
60
+ | Feature | Description |
61
+ |---------|-------------|
62
+ | **Rate Limiting** | Token bucket per client. Limits how many messages each client can send per time window. Prevents spam and abuse. |
63
+ | **Per-IP Throttling** | Limits simultaneous connections from the same IP address. Prevents brute force attacks and bots. |
64
+ | **Max Connections** | Global limit of concurrent connections. The server rejects new connections when the limit is reached. |
65
+ | **Max Rooms** | Global limit of rooms and per-client limit. Prevents a single client from creating thousands of rooms and consuming memory. |
66
+ | **Graceful Shutdown** | Captures SIGINT/SIGTERM, stops accepting new connections, waits for existing ones to close (with configurable timeout), and cleans up all resources. |
67
+ | **Health Check** | HTTP `/health` endpoint with live server statistics. Compatible with Kubernetes, Docker, and load balancers. |
68
+ | **Server Metrics** | `getStats()` method with: active connections, messages sent/received, rooms, uptime, memory usage, rate limiter entries. |
69
+ | **TLS/SSL** | Native support for `wss://` and TCP over TLS. Simple configuration with key and cert. |
70
+ | **Origin Checking** | Whitelist of allowed origins for WebSocket connections. Prevents CSRF and cross-origin abuse. |
71
+ | **CORS** | Automatic CORS headers on the health endpoint with support for OPTIONS preflight. |
72
+ | **Input Validation** | Validation of event names (non-empty strings), max payload size, max frame size. |
73
+ | **Backpressure Handling** | Handles the socket's `drain` event. No data is lost when the network buffer is full. |
74
+ | **Message Queue** | On the client: message queue when disconnected. Sent automatically upon reconnection. Configurable size, discards oldest if full. |
75
+ | **Exponential Backoff** | Smart reconnection with exponential backoff and jitter. Prevents thundering herd when the server restarts. |
76
+ | **O(1) Client Lookup** | Client lookup by ID in constant time using an indexed Map. Scalable to tens of thousands of clients. |
77
+ | **No Signal Handler Leaks** | SIGINT/SIGTERM handlers are properly cleaned up when calling `stop()`. Multiple instances don't cause `MaxListenersExceeded`. |
78
+ | **Timer unref** | All internal timers use `.unref()`. They don't prevent the Node.js process from terminating naturally. |
79
+ | **Custom Rate Limiter** | `IRateLimiter` interface to replace the built-in rate limiter with your own implementation (Redis, MongoDB, etc). |
80
+ | **Custom IP Tracker** | `IIPTracker` interface to replace the built-in IP tracker with your own logic. |
81
+ | **Custom Client ID** | `generateClientId` function to generate IDs with your own format. |
82
+ | **Event Rate Limits** | Rate limits per individual event. Each event can have its own message limit. |
83
+ | **Per-Client Rate Limits** | Rate limits per individual client with `setClientRateLimit()`. Override the global limit for specific clients. |
84
+ | **Hook System** | Callbacks for every server event: rate limit exceeded, max connections, payload too large, join/leave room, etc. |
85
+ | **Custom Health Handler** | `customHealthHandler` function to replace the built-in health check with your own logic. |
86
+ | **Runtime Config** | `updateConfig()` method to change server configuration on the fly, without restarting. |
87
+ | **Client Hooks** | Hooks on the client: `onBeforeEmit`, `onMessage`, `onStateChange`, `onReconnectDelay`, `onMessageQueued`, `onQueueDrained`, `onError`. |
88
+ | **Custom Reconnect** | `customReconnectDelay` function or `onReconnectDelay` hook to control client reconnection logic. |
89
+ | **Client Runtime Config** | `updateOptions()` method to change client configuration on the fly. |
90
+
91
+ ---
19
92
 
20
93
  ## Installation
21
94
 
@@ -23,15 +96,11 @@ Your own custom real-time system. A lightweight, dependency-free library for rea
23
96
  npm install stelar-time-real
24
97
  ```
25
98
 
26
- ## Quick Start
27
-
28
- ### One import for everything
99
+ ---
29
100
 
30
- ```javascript
31
- import StelarServer, { StelarClient } from 'stelar-time-real';
32
- ```
101
+ ## Quick Start
33
102
 
34
- ### Server
103
+ ### Basic Server
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('New client:', client.id);
47
- client.emit('welcome', 'Hello! Welcome to stelar-time-real');
115
+ console.log('Connected:', client.id);
116
+ client.emit('welcome', { message: 'Welcome to the server!' });
48
117
  });
49
118
 
50
- stelar.on('message', (ctx) => {
51
- ctx.broadcast('message', ctx.data);
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
- ### Client
126
+ ### Server with Production Configuration
58
127
 
59
128
  ```javascript
60
- import { StelarClient } from 'stelar-time-real';
129
+ import express from 'express';
130
+ import { StelarServer } from 'stelar-time-real';
61
131
 
62
- const client = new StelarClient('localhost:3000');
132
+ const app = express();
133
+ const server = app.listen(3000);
63
134
 
64
- client.on('connect', () => {
65
- console.log('Connected!');
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://mydomain.com'],
149
+ logger: 'info',
66
150
  });
67
151
 
68
- client.on('welcome', (msg) => {
69
- console.log(msg);
152
+ // Authentication middleware
153
+ stelar.use((ctx, next) => {
154
+ const token = ctx.req?.headers?.authorization;
155
+ if (!token) return ctx.ack('error', { message: 'Token required' });
156
+ next();
70
157
  });
71
158
 
72
- client.connect();
73
- ```
159
+ stelar.onConnection((client) => {
160
+ console.log(`[${client.protocol}] Client connected: ${client.id} from ${client.remoteAddress}`);
161
+ client.setMetadata('role', 'user');
162
+ client.emit('welcome', { id: client.id });
163
+ });
74
164
 
75
- ## Full API
165
+ stelar.onDisconnect((client) => {
166
+ console.log('Client disconnected:', client.id);
167
+ });
76
168
 
77
- ### StelarServer (Server Side)
169
+ stelar.on('chat', (ctx) => {
170
+ ctx.broadcast('chat', ctx.data, ctx.id);
171
+ });
78
172
 
79
- #### Constructor
173
+ stelar.onAck('getUser', (ctx) => {
174
+ return { id: ctx.data.id, name: 'John', role: ctx.getMetadata('role') };
175
+ });
80
176
 
81
- ```javascript
82
- new StelarServer({ server, port, heartbeatInterval })
177
+ await stelar.start();
178
+ console.log('Server ready on port', stelar.getPort());
83
179
  ```
84
180
 
85
- | Option | Type | Default | Description |
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
+ ### Client (Browser or Node.js)
95
182
 
96
183
  ```javascript
97
- stelar.use((ctx, next) => {
98
- const token = ctx.req.headers['x-token'];
99
- if (token === 'secret') {
100
- next();
101
- } else {
102
- ctx.socket.close();
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('Connected!'));
196
+ client.on('disconnect', () => console.log('Disconnected'));
197
+ client.on('welcome', (data) => console.log('Welcome:', data));
198
+
199
+ client.connect();
200
+
201
+ // Send message
202
+ client.emit('chat', { message: 'Hello everyone!' });
203
+
204
+ // Request-response with Promise
205
+ const user = await client.request('getUser', { id: 1 }, 'getUser');
206
+ console.log(user); // { id: 1, name: 'John', role: 'user' }
207
+
208
+ // Join rooms
209
+ client.joinRoom('general');
210
+ client.joinRoom('random');
211
+
212
+ // Send binary
213
+ const buffer = Buffer.from('binary data');
214
+ client.emitBinary('file', buffer);
105
215
  ```
106
216
 
107
- **`.on(event, handler)`**
108
- Listen for client events.
217
+ ### Client with TCP Mode (Node.js only — maximum efficiency)
109
218
 
110
219
  ```javascript
111
- stelar.on('chat', (ctx) => {
112
- console.log('Message:', ctx.data);
113
- ctx.broadcast('chat', ctx.data);
220
+ const client = new StelarClient('localhost:3001', {
221
+ mode: 'tcp',
222
+ reconnection: true,
114
223
  });
224
+
225
+ client.on('connect', () => console.log('TCP connected!'));
226
+ client.connect();
115
227
  ```
116
228
 
117
- **`.onAll(handler)`**
118
- Listen for all events (useful for debug).
229
+ TCP mode uses the custom binary protocol instead of WebSocket. Less overhead, lower latency, ideal for server-to-server communication.
230
+
231
+ ### Client with TLS/WSS (secure connections)
119
232
 
120
233
  ```javascript
121
- stelar.onAll(({ event, data }) => {
122
- console.log(`Event: ${event}`, data);
234
+ // WSS Secure WebSocket
235
+ const client = new StelarClient('wss://secure.mydomain.com', {
236
+ tls: true,
237
+ rejectUnauthorized: true,
238
+ });
239
+
240
+ // TCP + TLS
241
+ const client = new StelarClient('secure.mydomain.com:3001', {
242
+ mode: 'tcp',
243
+ tls: true,
244
+ rejectUnauthorized: true,
123
245
  });
124
246
  ```
125
247
 
126
- **`.onConnection(handler)`**
127
- Execute when a client connects.
248
+ ### Server with TLS
128
249
 
129
250
  ```javascript
130
- stelar.onConnection((client) => {
131
- client.emit('welcome', 'Hello!');
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
- **`.broadcast(event, data)`**
136
- Send to all clients.
263
+ ---
137
264
 
138
- ```javascript
139
- stelar.broadcast('chat', { message: 'Hello everyone' });
140
- ```
265
+ ## Architecture
141
266
 
142
- **`.to(room, event, data)`**
143
- Send to a specific room.
267
+ ### Dual Protocol
144
268
 
145
- ```javascript
146
- stelar.to('room-1', 'chat', { message: 'Hello room 1' });
269
+ ```
270
+ stelar-time-real Server
271
+ ┌──────────────────────────┐
272
+ │ │
273
+ Browsers ──────► │ Port 3000 (WebSocket) │
274
+ (ws://) │ │ │
275
+ │ Same logic │
276
+ Node.js ──────► │ Port 3001 (Custom TCP) │
277
+ (tcp mode) │ │ │
278
+ │ │
279
+ └──────────────────────────┘
147
280
  ```
148
281
 
149
- **`.toId(id, event, data)`**
150
- Send to a specific client by ID.
282
+ Both protocols share:
283
+ - Same event handlers
284
+ - Same rooms
285
+ - Same broadcast system
286
+ - Same ACK system
287
+ - Same middleware
288
+ - Same metrics
151
289
 
152
- ```javascript
153
- stelar.toId('abc123', 'private', 'Just for you');
154
- ```
290
+ A WebSocket client and a TCP client can be in the same room and communicate without issues.
155
291
 
156
- **`.getClients(room)`**
157
- Get list of clients.
292
+ ### WebSocket Mode vs TCP Mode
158
293
 
159
- ```javascript
160
- const all = stelar.getClients();
161
- const room = stelar.getClients('my-room');
162
- ```
294
+ | Aspect | WebSocket | Custom TCP |
295
+ |---------|-----------|------------|
296
+ | Browser | Yes | No |
297
+ | Node.js | Yes | Yes |
298
+ | Overhead per frame | 2-14 bytes (RFC 6455) | 7 bytes (custom header) |
299
+ | Latency | Low | Ultra low |
300
+ | TLS | wss:// | Native TLS |
301
+ | Use case | Frontend, web apps | Microservices, backend |
163
302
 
164
- **`.getPort()`**
165
- Get the port where it's running.
303
+ ### Binary Protocol Format (TCP)
166
304
 
167
- ```javascript
168
- console.log('Port:', stelar.getPort());
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
- **`.start(callback)`**
172
- Start the WebSocket server.
312
+ **11 frame types:**
313
+
314
+ | Type | Code | Description |
315
+ |------|--------|-------------|
316
+ | JSON | 0 | Event with JSON payload |
317
+ | Binary | 1 | Pure binary data |
318
+ | Ping | 2 | Client heartbeat |
319
+ | Pong | 3 | Server response |
320
+ | ACK Request | 4 | Request expecting response |
321
+ | ACK Response | 5 | Response to an ACK request |
322
+ | Connect | 6 | Initial connection frame |
323
+ | Disconnect | 7 | Disconnection frame |
324
+ | Join Room | 8 | Join a room |
325
+ | Leave Room | 9 | Leave a room |
326
+ | Error | 10 | Error frame |
327
+
328
+ ### Manual WebSocket (RFC 6455)
329
+
330
+ stelar-time-real implements WebSocket from scratch using only Node.js `http` and `crypto`. It doesn't use the `ws` library or any other.
331
+
332
+ The implementation includes:
333
+ - **Handshake** — Calculates Sec-WebSocket-Accept with SHA-1 per RFC 6455
334
+ - **Framing** — Frame parsing and creation (text, binary, ping, pong, close)
335
+ - **Masking** — Applies/removes XOR mask (required client→server)
336
+ - **Fragmentation** — Fragmented frame handling
337
+ - **Close codes** — All close codes supported
338
+ - **Validation** — RSV bits, opcode validation, max frame size
339
+ - **PING/PONG** — Server responds PONG to PING correctly
173
340
 
174
- ```javascript
175
- await stelar.start();
176
- console.log('Started!');
177
- ```
341
+ ---
178
342
 
179
- **`.stop()`**
180
- Stop the server.
343
+ ## Complete API
344
+
345
+ ### StelarServer — Options
181
346
 
182
347
  ```javascript
183
- stelar.stop();
348
+ new StelarServer({
349
+ // Connection
350
+ port: 3000, // HTTP/WebSocket port
351
+ server: httpServer, // Existing HTTP server (alternative to port)
352
+ namespace: '/', // Namespace path
353
+ tcpPort: 3001, // TCP port (false = disabled)
354
+
355
+ // Limits
356
+ maxConnections: 10000, // Maximum concurrent connections
357
+ maxConnectionsPerIP: 50, // Maximum connections per IP address
358
+ maxRooms: 10000, // Maximum global rooms
359
+ maxRoomsPerClient: 50, // Maximum rooms per client
360
+ maxPayloadSize: 10 * 1024 * 1024, // Maximum payload size (10MB)
361
+ maxFrameSize: 10 * 1024 * 1024, // Maximum WebSocket frame size (10MB)
362
+
363
+ // Rate Limiting
364
+ rateLimit: {
365
+ maxPoints: 100, // Maximum points (messages) per window
366
+ windowMs: 1000, // Time window in milliseconds
367
+ },
368
+
369
+ // Timeouts
370
+ heartbeatInterval: 30000, // Ping interval (30s)
371
+ heartbeatTimeout: 60000, // Timeout before disconnecting (60s)
372
+ connectTimeout: 10000, // Initial connection timeout (10s)
373
+
374
+ // Production
375
+ healthEndpoint: '/health', // Health check URL (false = disabled)
376
+ gracefulShutdown: true, // Capture SIGINT/SIGTERM
377
+ shutdownTimeout: 10000, // Maximum wait time when closing (10s)
378
+ allowedOrigins: ['https://mydomain.com'], // Allowed origins (null = all)
379
+ tls: { key, cert }, // TLS options for wss:// and TCP TLS
380
+
381
+ // Logging
382
+ logger: 'info', // Level: 'debug'|'info'|'warn'|'error'|'silent'
383
+ // Also accepts Logger instance or false
384
+ });
184
385
  ```
185
386
 
186
- #### Context (ctx) in handlers
387
+ ### StelarServer Methods
388
+
389
+ #### Events
390
+
391
+ | Method | Description |
392
+ |--------|-------------|
393
+ | `.on(event, handler)` | Listen to client events |
394
+ | `.onAll(handler)` | Listen to all events |
395
+ | `.onConnection(handler)` | Client connected |
396
+ | `.onDisconnect(handler)` | Client disconnected |
397
+ | `.onAck(name, handler)` | Register ACK handler (returns value to client) |
187
398
 
188
- When you listen to an event, you receive a `ctx` with:
399
+ #### Message Sending
400
+
401
+ | Method | Description |
402
+ |--------|-------------|
403
+ | `.broadcast(event, data, excludeId?)` | Send to all clients (optionally exclude one) |
404
+ | `.to(room, event, data, excludeId?)` | Send to a room (optionally exclude) |
405
+ | `.toId(id, event, data)` | Send to a specific client — O(1) lookup |
406
+ | `.broadcastBinary(event, buffer)` | Broadcast binary data |
407
+
408
+ #### Information
409
+
410
+ | Method | Description |
411
+ |--------|-------------|
412
+ | `.getClients(room?)` | Client list with their rooms |
413
+ | `.getRoomMembers(room)` | Client IDs in a room |
414
+ | `.getRooms()` | List of active rooms |
415
+ | `.getStats()` | Server statistics |
416
+ | `.getPort()` | Port the server is running on |
417
+
418
+ #### Lifecycle
419
+
420
+ | Method | Description |
421
+ |--------|-------------|
422
+ | `.use(middleware)` | Add connection middleware |
423
+ | `.start(callback?)` | Start server, returns `Promise<number>` with the port |
424
+ | `.stop()` | Stop server, close connections, clean up handlers |
425
+
426
+ ### StelarContext (ctx) — Inside handlers
427
+
428
+ Every event handler receives a context (`ctx`) with all available information and actions:
189
429
 
190
430
  ```javascript
191
431
  stelar.on('message', (ctx) => {
192
- ctx.id // Unique client ID
193
- ctx.socket // Client's WebSocket
194
- ctx.req // Original HTTP request
195
- ctx.data // Received data
432
+ // Client information
433
+ ctx.id // Unique client ID
434
+ ctx.socket // Raw net.Socket
435
+ ctx.req // HTTP request (null for TCP)
436
+ ctx.data // Received data
437
+ ctx.clientInfo // Client info
438
+ ctx.clientInfo.rooms // Client's room Set
439
+ ctx.clientInfo.metadata // Custom metadata Map
440
+ ctx.clientInfo.remoteAddress // Client's IP address
441
+ ctx.clientInfo.protocol // 'ws' or 'tcp'
442
+
443
+ // Actions — Send messages
444
+ ctx.emit('event', data) // Send to this client
445
+ ctx.send('response', data) // Respond to ACK
446
+ ctx.emitBinary('event', buffer) // Send binary
447
+ ctx.broadcast('event', data) // Send to all (excluding self)
448
+ ctx.broadcastBinary('event', buf) // Binary broadcast
449
+ ctx.to('room', 'event', data) // Send to a room
450
+ ctx.toId('id', 'event', data) // Send to specific client (O(1))
451
+
452
+ // Actions — Rooms
453
+ ctx.joinRoom('room') // Join a room
454
+ ctx.leaveRoom('room') // Leave a room
455
+ ctx.getClients('room') // List room clients
456
+
457
+ // Actions — Metadata
458
+ ctx.setMetadata('role', 'admin') // Store custom data
459
+ ctx.getMetadata('role') // Read custom data
460
+
461
+ // Actions — ACK
462
+ ctx.ack('myAck', data) // Respond to an ACK request
463
+ });
464
+ ```
465
+
466
+ ### StelarClient — Options
196
467
 
197
- // Available methods:
198
- ctx.emit('event', data) // Send to this client only
199
- ctx.send('response', data) // Reply to an ACK
200
- ctx.broadcast('event', data) // Send to everyone
201
- ctx.to('room', 'event', data) // Send to a room
202
- ctx.toId('id', 'event', data) // Send to specific client
203
- ctx.getClients('room') // See clients in room
204
- ctx.joinRoom('room') // Join room
205
- ctx.leaveRoom() // Leave room
206
- ctx.ack('myAck', data) // Reply to a custom ACK
468
+ ```javascript
469
+ new StelarClient(urlOrPort, {
470
+ // Connection
471
+ reconnection: true, // Auto reconnect
472
+ reconnectionAttempts: 10, // Maximum attempts
473
+ reconnectionDelay: 1000, // Base delay (ms)
474
+ maxReconnectionDelay: 30000, // Maximum delay (ms)
475
+ heartbeatInterval: 30000, // Heartbeat interval
476
+
477
+ // Protocol
478
+ mode: 'ws', // 'ws' or 'tcp'
479
+ maxPayloadSize: 10 * 1024 * 1024,
480
+ maxFrameSize: 10 * 1024 * 1024,
481
+
482
+ // ACK
483
+ ackTimeout: 5000, // ACK timeout (ms)
484
+
485
+ // Message queue
486
+ messageQueueSize: 100, // Queued messages when disconnected
487
+
488
+ // Security
489
+ tls: false, // Enable TLS for wss:// or TCP TLS
490
+ rejectUnauthorized: true, // Validate TLS certificate
491
+
492
+ // Custom headers
493
+ headers: {}, // Headers for WebSocket handshake
494
+
495
+ // Logging
496
+ logger: 'warn', // Log level
207
497
  });
208
498
  ```
209
499
 
210
- #### Namespaces
500
+ ### StelarClient — Methods
211
501
 
212
- Create independent channels:
502
+ #### Events
213
503
 
214
- ```javascript
215
- import { StelarServer } from 'stelar-time-real';
504
+ | Method | Description |
505
+ |--------|-------------|
506
+ | `.on(event, handler)` | Listen to event |
507
+ | `.off(event, handler)` | Remove listener |
508
+ | `.once(event, handler)` | Listen once |
509
+ | `.onAll(handler)` | Listen to all events |
510
+ | `.onAck(name, handler)` | Listen to ACK responses |
216
511
 
217
- // Main namespace
218
- const main = new StelarServer({ server, namespace: '/' });
512
+ #### Sending
219
513
 
220
- // Chat namespace
221
- const chat = StelarServer.of('/chat', { server });
222
- chat.on('message', (ctx) => {
223
- ctx.broadcast('message', ctx.data);
224
- });
514
+ | Method | Description |
515
+ |--------|-------------|
516
+ | `.emit(event, data, opts?)` | Send event (`opts.ack` for ACK) |
517
+ | `.emitBinary(event, data)` | Send binary data |
518
+ | `.sendFile(file)` | Send file |
519
+ | `.sendImage(blob)` | Send image |
520
+ | `.request(event, data, ackName)` | Request-response with Promise |
225
521
 
226
- // Game namespace
227
- const game = StelarServer.of('/game', { server });
228
- game.on('move', (ctx) => {
229
- ctx.to(ctx.data.room, 'move', ctx.data);
230
- });
231
- ```
522
+ #### Rooms
523
+
524
+ | Method | Description |
525
+ |--------|-------------|
526
+ | `.joinRoom(room)` | Join a room |
527
+ | `.leaveRoom(room)` | Leave a room |
232
528
 
233
- #### ACK (Request-Response)
529
+ #### Lifecycle
234
530
 
235
- Ultra efficient system with Promises:
531
+ | Method | Description |
532
+ |--------|-------------|
533
+ | `.connect(callback?)` | Connect to server |
534
+ | `.disconnect()` | Disconnect and clean up all resources |
236
535
 
237
- **Server:**
536
+ #### State and Metrics
537
+
538
+ | Method | Description |
539
+ |--------|-------------|
540
+ | `.isConnected()` | Is connected? |
541
+ | `.getState()` | State: `'disconnected'` \| `'connecting'` \| `'connected'` \| `'reconnecting'` |
542
+ | `.getId()` | ID assigned by the server |
543
+ | `.getUrl()` | Server URL |
544
+ | `.setUrl(url)` | Change URL before connecting |
545
+ | `.getMessagesSent()` | Total messages sent |
546
+ | `.getMessagesReceived()` | Total messages received |
547
+ | `.getLastError()` | Last error |
548
+ | `.getConnectTime()` | Timestamp of last successful connection |
549
+ | `.getQueueSize()` | Pending messages in queue |
550
+ | `.removeAllListeners(event?)` | Clear listeners |
551
+
552
+ ### Client Events
238
553
 
239
554
  ```javascript
240
- // Register an ACK handler
241
- stelar.onAck('getUser', (ctx) => {
242
- return { id: ctx.data.id, name: 'John' };
555
+ client.on('connect', () => {
556
+ // Connection established
243
557
  });
244
558
 
245
- // Or with more complex logic
246
- stelar.onAck('saveData', (ctx) => {
247
- const result = saveToDatabase(ctx.data);
248
- return { success: true, id: result.id };
559
+ client.on('disconnect', (info) => {
560
+ // info = { code, reason } for WebSocket
249
561
  });
250
- ```
251
-
252
- **Client:**
253
562
 
254
- ```javascript
255
- // Using request() - returns Promise
256
- const user = await client.request('getUser', { id: 1 }, 'userData');
257
- console.log(user); // { id: 1, name: 'John' }
563
+ client.on('reconnecting', (attempt) => {
564
+ // Reconnection attempt number `attempt`
565
+ });
258
566
 
259
- // Or emit with callback
260
- client.emit('getUser', { id: 1 }, { ack: 'userData' });
261
- client.on('userData', (data) => {
262
- console.log(data);
567
+ client.on('reconnect_failed', () => {
568
+ // Reconnection attempts exhausted
263
569
  });
264
570
 
265
- // ACK from server to client
266
- client.onAck('serverPush', (data) => {
267
- console.log('Server sent:', data);
571
+ client.on('error', (err) => {
572
+ // Connection or protocol error
268
573
  });
269
574
  ```
270
575
 
271
576
  ---
272
577
 
273
- ### StelarClient (Client Side)
578
+ ## Health Check
274
579
 
275
- #### Constructor
580
+ The health check endpoint is designed to integrate with orchestrators like Kubernetes, Docker Swarm, or any load balancer.
276
581
 
277
- ```javascript
278
- new StelarClient(urlOrPort, options)
582
+ ```bash
583
+ curl http://localhost:3000/health
584
+ ```
585
+
586
+ Response:
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
+ }
279
609
  ```
280
610
 
281
- | Param | Type | Default | Description |
282
- |-------|------|---------|-------------|
283
- | urlOrPort | string/number | localhost:3000 | Server URL or port |
284
- | options.reconnection | boolean | true | Auto reconnect |
285
- | options.reconnectionAttempts | number | 5 | Reconnection attempts |
286
- | options.reconnectionDelay | number | 1000 | Delay between attempts (ms) |
287
- | options.heartbeatInterval | number | 30000 | Ping interval |
611
+ CORS is automatic on the health endpoint. If `allowedOrigins` is configured, the `Access-Control-Allow-Origin` header is added for matching origins. OPTIONS preflight requests return 204.
612
+
613
+ ---
614
+
615
+ ## Middleware
616
+
617
+ The middleware system allows validating connections before a client is accepted:
288
618
 
289
619
  ```javascript
290
- // Just port
291
- const client = new StelarClient(3000);
620
+ // Token authentication
621
+ stelar.use((ctx, next) => {
622
+ const token = ctx.req?.headers?.authorization;
623
+ if (!token) {
624
+ return ctx.ack('error', { message: 'Token required' });
625
+ }
626
+ // Validate token...
627
+ ctx.setMetadata('userId', getUserIdFromToken(token));
628
+ next();
629
+ });
292
630
 
293
- // Full URL
294
- const client = new StelarClient('ws://mydomain.com/ws');
631
+ // Custom rate limiting
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
- // With options
297
- const client = new StelarClient(3000, {
298
- reconnection: true,
299
- reconnectionAttempts: 10,
300
- reconnectionDelay: 2000
640
+ // Logging
641
+ stelar.use((ctx, next) => {
642
+ console.log(`New connection from ${ctx.clientInfo.remoteAddress}`);
643
+ next();
301
644
  });
302
645
  ```
303
646
 
304
- #### Methods
647
+ Multiple middlewares execute in order. If a middleware doesn't call `next()`, the connection is rejected.
305
648
 
306
- **`.on(event, handler)`**
307
- Listen for server events.
649
+ ---
308
650
 
309
- ```javascript
310
- client.on('welcome', (data) => {
311
- console.log(data);
312
- });
313
- ```
651
+ ## Rooms
314
652
 
315
- **`.onAll(handler)`**
316
- Listen for all events.
653
+ Rooms are communication channels. A client can be in multiple rooms simultaneously:
317
654
 
318
655
  ```javascript
319
- client.onAll(({ event, data }) => {
320
- console.log(`${event}:`, data);
656
+ // Server
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
+ // Client
670
+ client.joinRoom('general');
671
+ client.joinRoom('random');
672
+ client.joinRoom('project-alpha');
322
673
  ```
323
674
 
324
- **`.onAck(name, handler)`**
325
- Listen for ACK responses from the server.
675
+ Rooms are automatically cleaned up when the last client leaves or disconnects. No manual resource release needed.
676
+
677
+ ---
678
+
679
+ ## ACK (Request-Response)
680
+
681
+ The ACK system enables reliable request-response communication over the real-time protocol:
326
682
 
327
683
  ```javascript
328
- client.onAck('userData', (data) => {
329
- console.log('Data received:', data);
684
+ // Server Register ACK handler
685
+ stelar.onAck('getUsers', (ctx) => {
686
+ return { users: ['John', 'Mary', 'Peter'] };
330
687
  });
331
- ```
332
688
 
333
- **`.emit(event, data, opts)`**
334
- Send events to the server. Supports `opts.ack` for ACKs.
689
+ stelar.onAck('validateToken', (ctx) => {
690
+ const valid = validateToken(ctx.data.token);
691
+ if (!valid) throw new Error('Invalid token');
692
+ return { userId: 123 };
693
+ });
335
694
 
336
- ```javascript
337
- client.emit('chat', { message: 'Hello!' });
338
- client.emit('getUser', { id: 1 }, { ack: 'userData' });
695
+ // Client — Send request and wait for response
696
+ const users = await client.request('getUsers', {}, 'getUsers');
697
+ console.log(users); // { users: ['John', 'Mary', 'Peter'] }
698
+
699
+ try {
700
+ const result = await client.request('validateToken', { token: 'abc' }, 'validateToken');
701
+ } catch (err) {
702
+ console.log('Invalid token');
703
+ }
339
704
  ```
340
705
 
341
- **`.request(event, data, ackName)`**
342
- Send and wait for response as Promise.
706
+ ACK requests have configurable timeout (`ackTimeout`). If the server doesn't respond within that time, the Promise is rejected.
707
+
708
+ ---
709
+
710
+ ## Binary Data
711
+
712
+ Send files, images, audio, or any binary data without base64 overhead:
343
713
 
344
714
  ```javascript
345
- const result = await client.request('getUser', { id: 1 }, 'userData');
346
- console.log(result); // { id: 1, name: 'John' }
715
+ // Server Receive and forward binary
716
+ stelar.on('file', (ctx) => {
717
+ ctx.broadcastBinary('file', ctx.data); // ctx.data is a Buffer
718
+ });
719
+
720
+ // Client — Send binary
721
+ const imageBuffer = await fs.readFile('photo.png');
722
+ client.emitBinary('file', imageBuffer);
347
723
 
348
- // With optional timeout
349
- const client = new StelarClient(3000, { ackTimeout: 10000 });
724
+ // Client Receive binary
725
+ client.on('file', (buffer) => {
726
+ console.log('File received:', buffer.length, 'bytes');
727
+ fs.writeFile('received.png', buffer);
728
+ });
350
729
  ```
351
730
 
352
- **`.joinRoom(room)`**
353
- Join a room.
731
+ ---
732
+
733
+ ## Server Metrics
354
734
 
355
735
  ```javascript
356
- client.joinRoom('room-1');
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
- **`.leaveRoom()`**
360
- Leave current room.
755
+ ---
756
+
757
+ ## Client Metrics
361
758
 
362
759
  ```javascript
363
- client.leaveRoom();
760
+ console.log('Messages sent:', client.getMessagesSent());
761
+ console.log('Messages received:', client.getMessagesReceived());
762
+ console.log('Connection time:', client.getConnectTime());
763
+ console.log('Last error:', client.getLastError());
764
+ console.log('Messages in queue:', client.getQueueSize());
765
+ console.log('State:', client.getState());
766
+ console.log('Connected?', client.isConnected());
364
767
  ```
365
768
 
366
- **`.connect(callback)`**
367
- Connect to the server.
769
+ ---
770
+
771
+ ## Horizontal Scalability
772
+
773
+ stelar-time-real runs on a single server per instance. To scale to multiple instances, use Redis Pub/Sub as a bridge:
368
774
 
369
775
  ```javascript
370
- client.connect(() => {
371
- console.log('Connected!');
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
+ // When a broadcast happens on this instance, publish to 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
+ // When another instance publishes, emit locally
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
- **`.disconnect()`**
376
- Manually disconnect.
798
+ ---
377
799
 
378
- ```javascript
379
- client.disconnect();
380
- ```
800
+ ## Performance
381
801
 
382
- **`.isConnected()`**
383
- Check connection status.
802
+ Measurements with stress test (50 WebSocket + 20 TCP clients):
803
+
804
+ | Metric | Value |
805
+ |---------|-------|
806
+ | Simultaneous connections | 70 |
807
+ | RAM per client | ~58 KB |
808
+ | Throughput | 3,425 msg/sec |
809
+ | Stable heap | ~10 MB |
810
+ | Memory leaks | None detected |
811
+ | MaxListeners warnings | 0 |
812
+
813
+ The library uses ~58KB per connected client. A server with 1GB of RAM can handle approximately 17,000 simultaneous connections.
814
+
815
+ ---
816
+
817
+ ## Project Structure
384
818
 
385
- ```javascript
386
- if (client.isConnected()) {
387
- console.log('Connected');
388
- }
819
+ ```
820
+ stelar-time-real/
821
+ ├── src/
822
+ │ ├── index.ts # Server (StelarServer, RateLimiter, IPConnectionTracker)
823
+ │ ├── client.ts # Client (StelarClient, MessageQueue)
824
+ │ ├── protocol.ts # Binary TCP protocol (encode/decode, FrameParser)
825
+ │ ├── websocket.ts # Manual WebSocket RFC 6455 (WSFrameParser, framing)
826
+ │ └── logger.ts # Logger with levels
827
+ ├── package.json
828
+ ├── tsconfig.json
829
+ └── README.md
389
830
  ```
390
831
 
391
- **`.getUrl()`**
392
- Get connection URL.
832
+ ---
393
833
 
394
- ```javascript
395
- console.log(client.getUrl());
834
+ ## TypeScript
835
+
836
+ stelar-time-real is written in TypeScript and includes type definitions (.d.ts). You don't need to install separate @types:
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
- #### Client Events
845
+ ---
399
846
 
400
- ```javascript
401
- client.on('connect', () => {}); // When connected
402
- client.on('disconnect', () => {}); // When disconnected
403
- client.on('reconnecting', (attempt) => {}); // When trying to reconnect
404
- client.on('error', (err) => {}); // When there's an error
847
+ ## Tests
848
+
849
+ ```bash
850
+ # Production tests (54 assertions, 16 suites)
851
+ node test-production.mjs
852
+
853
+ # Stress test (70 clients, throughput, memory)
854
+ node test-stress.mjs
405
855
  ```
406
856
 
857
+ Coverage: 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
- ## Examples
861
+ ## Extensible Configuration
862
+
863
+ stelar-time-real v3.2 gives you total control over every aspect of the server and client. You can replace entire components, add hooks to customize behavior, and change configuration at runtime.
864
+
865
+ ### Custom Rate Limiter
410
866
 
411
- ### Basic Chat
867
+ Replace the built-in rate limiter (token bucket) with your own implementation. Ideal for using Redis, MongoDB, or any other store:
412
868
 
413
- **server.js**
414
869
  ```javascript
415
- import express from 'express';
416
- import { StelarServer } from 'stelar-time-real';
870
+ import { StelarServer, IRateLimiter } from 'stelar-time-real';
417
871
 
418
- const app = express();
419
- const server = app.listen(3000);
872
+ // Your own rate limiter with Redis
873
+ class RedisRateLimiter implements IRateLimiter {
874
+ private redis; // your Redis connection
420
875
 
421
- const stelar = new StelarServer({ server });
876
+ constructor(redisClient) {
877
+ this.redis = redisClient;
878
+ }
422
879
 
423
- stelar.onConnection((client) => {
424
- client.broadcast('system', 'A user joined');
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 second window
885
+ }
886
+ return current <= 100; // 100 per second
887
+ }
426
888
 
427
- stelar.on('chat', (ctx) => {
428
- ctx.broadcast('chat', ctx.data);
429
- });
889
+ async reset(id) {
890
+ await this.redis.del(`ratelimit:${id}`);
891
+ }
430
892
 
431
- stelar.start();
432
- console.log('Chat at http://localhost:3000');
433
- ```
893
+ async cleanup() {
894
+ // Redis handles expiration automatically
895
+ }
434
896
 
435
- **cliente.html**
436
- ```html
437
- <script type="module">
438
- import { StelarClient } from 'stelar-time-real';
897
+ async size() {
898
+ return 0; // Not applicable with Redis
899
+ }
900
+ }
439
901
 
440
- const client = new StelarClient(3000);
902
+ const stelar = new StelarServer({
903
+ port: 3000,
904
+ customRateLimiter: new RedisRateLimiter(redisClient),
905
+ });
906
+ ```
441
907
 
442
- client.on('connect', () => console.log('Connected'));
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
- client.connect();
910
+ Replace the per-IP connection tracker with your own logic. Useful for using a database of blocked IPs or whitelist logic:
447
911
 
448
- // Send messages
449
- function send(message) {
450
- client.emit('chat', message);
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; // Blocked IP
920
+ if (this.vipIPs.has(ip)) return true; // VIP no limit
921
+ return (this.counts.get(ip) || 0) < 20; // 20 for normal
451
922
  }
452
- </script>
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() { /* clean expired entries */ }
928
+ }
929
+
930
+ const stelar = new StelarServer({
931
+ port: 3000,
932
+ customIPTracker: new CustomIPTracker(),
933
+ });
453
934
  ```
454
935
 
455
- ### Room System
936
+ ### Custom Client ID Generator
937
+
938
+ Generate client IDs with your own format. By default uses UUID v4:
456
939
 
457
940
  ```javascript
458
- // Server
459
- stelar.on('join-room', (ctx) => {
460
- const room = ctx.data.room;
461
- ctx.joinRoom(room);
462
- ctx.emit('welcome', `You joined ${room}`);
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
- stelar.on('room-message', (ctx) => {
466
- ctx.to(ctx.data.room, 'room-message', ctx.data.message);
951
+ Each event can have its own rate limit, independent from the global one:
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 }, // Files: 2 every 10s
960
+ 'typing': { maxPoints: 10, windowMs: 1000 }, // Typing: 10 msg/sec
961
+ 'location': { maxPoints: 1, windowMs: 5000 }, // Location: 1 every 5s
962
+ },
467
963
  });
468
964
 
469
- // Client
470
- client.on('join-room', (room) => client.joinRoom(room));
965
+ // You can also add/remove at runtime:
966
+ stelar.setEventRateLimit('voice', { maxPoints: 50, windowMs: 1000 });
967
+ stelar.removeEventRateLimit('voice');
471
968
  ```
472
969
 
473
- ### With Auth Middleware
970
+ ### Per-Client Rate Limits
971
+
972
+ Give specific clients different rate limits. Useful for premium vs free users:
474
973
 
475
974
  ```javascript
476
- stelar.use((ctx, next) => {
477
- const token = ctx.req.headers['authorization'];
478
- if (token && token.startsWith('Bearer ')) {
479
- next(); // Allow connection
480
- } else {
481
- ctx.socket.close(); // Reject
975
+ stelar.onConnection((ctx) => {
976
+ const role = ctx.getMetadata('role');
977
+
978
+ // Premium user: 500 msg/sec
979
+ if (role === 'premium') {
980
+ stelar.setClientRateLimit(ctx.id, { maxPoints: 500, windowMs: 1000 });
482
981
  }
982
+ // Verified bot: 1000 msg/sec
983
+ else if (role === 'bot') {
984
+ stelar.setClientRateLimit(ctx.id, { maxPoints: 1000, windowMs: 1000 });
985
+ }
986
+ // Normal user: uses global rate limit (100 msg/sec)
483
987
  });
988
+
989
+ // Remove override (reverts to global):
990
+ stelar.removeClientRateLimit(clientId);
484
991
  ```
485
992
 
486
- ### With Auto Reconnection
993
+ Rate limiting priority is: **per-client override > event-specific > global > custom rate limiter**.
487
994
 
488
- ```javascript
489
- import { StelarClient } from 'stelar-time-real';
995
+ ### Hook System (Server)
490
996
 
491
- const client = new StelarClient('localhost:3000', {
492
- reconnection: true,
493
- reconnectionAttempts: 5,
494
- reconnectionDelay: 1000
997
+ Hooks let you customize what happens when the server detects an event. Each hook can return `false` to cancel the default action:
998
+
999
+ ```javascript
1000
+ const stelar = new StelarServer({
1001
+ port: 3000,
1002
+ hooks: {
1003
+ // When a client exceeds the rate limit
1004
+ // Return false to NOT disconnect (e.g.: just warn)
1005
+ onRateLimitExceeded: ({ clientId, event, protocol }) => {
1006
+ console.warn(`Rate limit: ${clientId} on event ${event}`);
1007
+ // return false; // Uncomment to NOT disconnect the client
1008
+ },
1009
+
1010
+ // When maximum connections is reached
1011
+ onMaxConnectionsReached: ({ activeConnections, max, ip }) => {
1012
+ console.error(`Server full: ${activeConnections}/${max} from ${ip}`);
1013
+ // Send alert to Slack, etc.
1014
+ },
1015
+
1016
+ // When a client tries to join a room
1017
+ // Return false to REJECT the join
1018
+ onClientJoinRoom: ({ clientId, room, metadata }) => {
1019
+ const role = metadata.get('role');
1020
+ if (room.startsWith('admin-') && role !== 'admin') {
1021
+ return false; // Reject: admins only
1022
+ }
1023
+ },
1024
+
1025
+ // When a client leaves a room
1026
+ // Return false to REJECT the leave
1027
+ onClientLeaveRoom: ({ clientId, room }) => {
1028
+ // Custom logic...
1029
+ },
1030
+
1031
+ // When global maximum rooms is reached
1032
+ onMaxRoomsReached: ({ clientId, room, totalRooms, max }) => {
1033
+ console.warn(`Max rooms: ${totalRooms}/${max}`);
1034
+ },
1035
+
1036
+ // When a client exceeds rooms per client
1037
+ onMaxRoomsPerClientReached: ({ clientId, room, currentRooms, max }) => {
1038
+ console.warn(`Client ${clientId}: ${currentRooms}/${max} rooms`);
1039
+ },
1040
+
1041
+ // When a payload is too large
1042
+ onPayloadTooLarge: ({ clientId, event, size, max }) => {
1043
+ console.warn(`Large payload: ${size} bytes from ${clientId}`);
1044
+ },
1045
+
1046
+ // When an invalid message is received
1047
+ onInvalidMessage: ({ clientId, reason, protocol }) => {
1048
+ console.warn(`Invalid message from ${clientId}: ${reason}`);
1049
+ },
1050
+
1051
+ // Before a broadcast
1052
+ // Return false to CANCEL the broadcast
1053
+ onBeforeBroadcast: ({ event, data, excludeId }) => {
1054
+ if (event === 'spam') return false; // Cancel spam broadcast
1055
+ },
1056
+
1057
+ // When a client connects
1058
+ onClientConnect: ({ clientId, ip, protocol, metadata }) => {
1059
+ console.log(`Connected: ${clientId} via ${protocol} from ${ip}`);
1060
+ },
1061
+
1062
+ // When a client disconnects
1063
+ onClientDisconnect: ({ clientId, ip, protocol, rooms }) => {
1064
+ console.log(`Disconnected: ${clientId} was in ${rooms.size} rooms`);
1065
+ },
1066
+ },
495
1067
  });
1068
+ ```
496
1069
 
497
- client.on('connect', () => console.log('Connected!'));
498
- client.on('disconnect', () => console.log('Disconnected'));
499
- client.on('reconnecting', (attempt) => console.log(`Retrying ${attempt}/5`));
1070
+ ### Custom Health Check
500
1071
 
501
- client.connect();
1072
+ Replace the built-in health check with your own handler. Useful for adding database checks, disk space, etc:
1073
+
1074
+ ```javascript
1075
+ const stelar = new StelarServer({
1076
+ port: 3000,
1077
+ customHealthHandler: (req, res, stats) => {
1078
+ // stats contains all server statistics
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
- ### Send Binary Files
1097
+ ### Runtime Configuration
1098
+
1099
+ Change server configuration without restarting:
505
1100
 
506
1101
  ```javascript
507
- // Server - receive image
508
- stelar.on('image', (ctx) => {
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
- // Respond to client
515
- ctx.emit('imageSaved', { success: true });
1105
+ // Later... you need more capacity
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
- // Client - send image
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);
1113
+ // Change hooks at runtime
1114
+ stelar.updateConfig({
1115
+ hooks: {
1116
+ onRateLimitExceeded: ({ clientId }) => {
1117
+ banUser(clientId); // Auto-ban instead of disconnecting
1118
+ return false; // Don't disconnect, you already banned them
1119
+ },
1120
+ },
524
1121
  });
525
1122
 
526
- // Client - receive image
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;
1123
+ // View current configuration
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
+ Customize client behavior with hooks:
1139
+
1140
+ ```javascript
1141
+ const client = new StelarClient('localhost:3000', {
1142
+ hooks: {
1143
+ // Before sending a message — return false to cancel
1144
+ onBeforeEmit: ({ event, data }) => {
1145
+ if (event === 'debug') return false; // Don't send debug in production
1146
+ console.log(`Sending: ${event}`);
1147
+ },
1148
+
1149
+ // When any message is received
1150
+ onMessage: ({ event, data, isBinary }) => {
1151
+ metrics.increment('messages.received');
1152
+ if (isBinary) metrics.increment('binary.received');
1153
+ },
1154
+
1155
+ // When connection state changes
1156
+ onStateChange: ({ from, to }) => {
1157
+ console.log(`State: ${from} -> ${to}`);
1158
+ if (to === 'reconnecting') showReconnectingUI();
1159
+ if (to === 'connected') hideReconnectingUI();
1160
+ },
1161
+
1162
+ // Customize reconnection delay
1163
+ onReconnectDelay: ({ attempt, defaultDelay }) => {
1164
+ // Business hours: fast reconnection
1165
+ const hour = new Date().getHours();
1166
+ if (hour >= 9 && hour <= 18) return 500;
1167
+ return defaultDelay; // Off-hours: normal delay
1168
+ },
1169
+
1170
+ // When a message is queued (disconnected)
1171
+ onMessageQueued: ({ event, queueSize }) => {
1172
+ console.log(`Message queued: ${event} (queue: ${queueSize})`);
1173
+ },
1174
+
1175
+ // When queue is drained after reconnecting
1176
+ onQueueDrained: ({ count }) => {
1177
+ console.log(`${count} messages sent after reconnecting`);
1178
+ },
1179
+
1180
+ // When an error occurs
1181
+ onError: ({ error, context }) => {
1182
+ errorReporter.report(error, { context });
1183
+ },
1184
+ },
531
1185
  });
532
1186
  ```
533
1187
 
534
- ### Binary Broadcast
1188
+ ### Custom Reconnect Delay
1189
+
1190
+ Control exactly how long to wait before each reconnection attempt:
535
1191
 
536
1192
  ```javascript
537
- // Server - share file with everyone
538
- stelar.on('upload', (ctx) => {
539
- ctx.broadcastBinary('file', ctx.buffer);
1193
+ // Option 1: Custom function
1194
+ const client = new StelarClient('localhost:3000', {
1195
+ customReconnectDelay: (attempt, baseDelay, maxDelay) => {
1196
+ // Fast retry for first 3 attempts, then slow
1197
+ if (attempt <= 3) return 200;
1198
+ if (attempt <= 10) return 2000;
1199
+ return 30000; // 30s for later attempts
1200
+ },
540
1201
  });
541
1202
 
542
- // Client - send file
543
- const fileData = await file.arrayBuffer();
544
- client.emitBinary('upload', fileData);
1203
+ // Option 2: Via hook (can change at runtime)
1204
+ const client = new StelarClient('localhost:3000', {
1205
+ hooks: {
1206
+ onReconnectDelay: ({ attempt, defaultDelay }) => {
1207
+ return Math.min(100 * attempt, 10000); // Linear instead of exponential
1208
+ },
1209
+ },
1210
+ });
545
1211
  ```
546
1212
 
547
- ---
1213
+ ### Client Runtime Configuration
1214
+
1215
+ Change client configuration without reconnecting:
548
1216
 
549
- ## Difference with Socket.io
1217
+ ```javascript
1218
+ const client = new StelarClient('localhost:3000');
1219
+ client.connect();
550
1220
 
551
- | Feature | stelar-time-real | Socket.io |
552
- |---------|------------------|-----------|
553
- | Heap size | ~13 MB | ~50-100 MB |
554
- | Dependencies | ws (1) | multiple |
555
- | Configuration | minimal | complex |
556
- | Flexibility | total | opinionated |
557
- | Ideal for | own projects | quick production |
1221
+ // Later... adjust 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; // No longer send logs
1229
+ },
1230
+ },
1231
+ });
558
1232
 
559
- ## License
1233
+ // View current configuration
1234
+ const opts = client.getOptions();
1235
+ console.log(opts);
1236
+ ```
560
1237
 
561
- MIT - Stelar
1238
+ ---
562
1239
 
563
- ## Author
1240
+ ## License
564
1241
 
565
- Stelar
1242
+ MIT — Stelar