stelar-time-real 3.2.0 → 3.3.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 +429 -429
- package/package.json +1 -1
- package/src/client.d.ts +47 -59
- package/src/client.d.ts.map +1 -1
- package/src/client.js +406 -728
- package/src/client.ts +317 -908
- package/src/index.d.ts +84 -124
- package/src/index.d.ts.map +1 -1
- package/src/index.js +740 -1165
- package/src/index.ts +552 -1574
- package/src/logger.d.ts +12 -17
- package/src/logger.d.ts.map +1 -1
- package/src/logger.js +34 -90
- package/src/logger.ts +31 -98
- package/src/protocol.d.ts +16 -34
- package/src/protocol.d.ts.map +1 -1
- package/src/protocol.js +56 -148
- package/src/protocol.ts +66 -188
- package/src/websocket.d.ts +21 -43
- package/src/websocket.d.ts.map +1 -1
- package/src/websocket.js +106 -216
- package/src/websocket.ts +78 -279
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# stelar-time-real v3
|
|
2
2
|
|
|
3
|
-
**
|
|
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
|

|
|
6
6
|

|
|
@@ -9,88 +9,88 @@
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## What is stelar-time-real?
|
|
13
13
|
|
|
14
|
-
stelar-time-real
|
|
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
15
|
|
|
16
|
-
|
|
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
17
|
|
|
18
|
-
###
|
|
18
|
+
### What is it for?
|
|
19
19
|
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
- IoT
|
|
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
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
-
##
|
|
31
|
+
## Main Features
|
|
32
32
|
|
|
33
|
-
###
|
|
33
|
+
### Dual Protocol
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
The server supports **two protocols simultaneously** on different ports:
|
|
36
36
|
|
|
37
|
-
- **WebSocket** —
|
|
38
|
-
- **TCP
|
|
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
39
|
|
|
40
|
-
|
|
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
41
|
|
|
42
|
-
### Zero
|
|
42
|
+
### Zero Dependencies
|
|
43
43
|
|
|
44
44
|
```
|
|
45
45
|
dependencies: {}
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
No
|
|
48
|
+
No `ws`, no `engine.io`, nothing. Just pure Node.js. This means:
|
|
49
49
|
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
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
55
|
|
|
56
|
-
###
|
|
56
|
+
### Production-Ready
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
Every feature was designed with a real environment in mind — with users, attacks, and errors:
|
|
59
59
|
|
|
60
|
-
| Feature |
|
|
60
|
+
| Feature | Description |
|
|
61
61
|
|---------|-------------|
|
|
62
|
-
| **Rate Limiting** | Token bucket
|
|
63
|
-
| **Per-IP Throttling** |
|
|
64
|
-
| **Max Connections** |
|
|
65
|
-
| **Max Rooms** |
|
|
66
|
-
| **Graceful Shutdown** |
|
|
67
|
-
| **Health Check** |
|
|
68
|
-
| **Server Metrics** |
|
|
69
|
-
| **TLS/SSL** |
|
|
70
|
-
| **Origin Checking** | Whitelist
|
|
71
|
-
| **CORS** |
|
|
72
|
-
| **Input Validation** |
|
|
73
|
-
| **Backpressure Handling** |
|
|
74
|
-
| **Message Queue** |
|
|
75
|
-
| **Exponential Backoff** |
|
|
76
|
-
| **O(1) Client Lookup** |
|
|
77
|
-
| **No Signal Handler Leaks** |
|
|
78
|
-
| **Timer unref** |
|
|
79
|
-
| **Custom Rate Limiter** |
|
|
80
|
-
| **Custom IP Tracker** |
|
|
81
|
-
| **Custom Client ID** |
|
|
82
|
-
| **Event Rate Limits** | Rate limits
|
|
83
|
-
| **Per-Client Rate Limits** | Rate limits
|
|
84
|
-
| **Hook System** | Callbacks
|
|
85
|
-
| **Custom Health Handler** |
|
|
86
|
-
| **Runtime Config** |
|
|
87
|
-
| **Client Hooks** | Hooks
|
|
88
|
-
| **Custom Reconnect** |
|
|
89
|
-
| **Client Runtime Config** |
|
|
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
90
|
|
|
91
91
|
---
|
|
92
92
|
|
|
93
|
-
##
|
|
93
|
+
## Installation
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
96
|
npm install stelar-time-real
|
|
@@ -100,7 +100,7 @@ npm install stelar-time-real
|
|
|
100
100
|
|
|
101
101
|
## Quick Start
|
|
102
102
|
|
|
103
|
-
###
|
|
103
|
+
### Basic Server
|
|
104
104
|
|
|
105
105
|
```javascript
|
|
106
106
|
import express from 'express';
|
|
@@ -112,8 +112,8 @@ const server = app.listen(3000);
|
|
|
112
112
|
const stelar = new StelarServer({ server });
|
|
113
113
|
|
|
114
114
|
stelar.onConnection((client) => {
|
|
115
|
-
console.log('
|
|
116
|
-
client.emit('welcome', { message: '
|
|
115
|
+
console.log('Connected:', client.id);
|
|
116
|
+
client.emit('welcome', { message: 'Welcome to the server!' });
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
stelar.on('chat', (ctx) => {
|
|
@@ -123,7 +123,7 @@ stelar.on('chat', (ctx) => {
|
|
|
123
123
|
await stelar.start();
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
###
|
|
126
|
+
### Server with Production Configuration
|
|
127
127
|
|
|
128
128
|
```javascript
|
|
129
129
|
import express from 'express';
|
|
@@ -145,25 +145,25 @@ const stelar = new StelarServer({
|
|
|
145
145
|
heartbeatTimeout: 60000,
|
|
146
146
|
gracefulShutdown: true,
|
|
147
147
|
shutdownTimeout: 10000,
|
|
148
|
-
allowedOrigins: ['https://
|
|
148
|
+
allowedOrigins: ['https://mydomain.com'],
|
|
149
149
|
logger: 'info',
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
-
//
|
|
152
|
+
// Authentication middleware
|
|
153
153
|
stelar.use((ctx, next) => {
|
|
154
154
|
const token = ctx.req?.headers?.authorization;
|
|
155
|
-
if (!token) return ctx.ack('error', { message: 'Token
|
|
155
|
+
if (!token) return ctx.ack('error', { message: 'Token required' });
|
|
156
156
|
next();
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
stelar.onConnection((client) => {
|
|
160
|
-
console.log(`[${client.protocol}]
|
|
160
|
+
console.log(`[${client.protocol}] Client connected: ${client.id} from ${client.remoteAddress}`);
|
|
161
161
|
client.setMetadata('role', 'user');
|
|
162
162
|
client.emit('welcome', { id: client.id });
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
stelar.onDisconnect((client) => {
|
|
166
|
-
console.log('
|
|
166
|
+
console.log('Client disconnected:', client.id);
|
|
167
167
|
});
|
|
168
168
|
|
|
169
169
|
stelar.on('chat', (ctx) => {
|
|
@@ -171,14 +171,14 @@ stelar.on('chat', (ctx) => {
|
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
stelar.onAck('getUser', (ctx) => {
|
|
174
|
-
return { id: ctx.data.id, name: '
|
|
174
|
+
return { id: ctx.data.id, name: 'John', role: ctx.getMetadata('role') };
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
await stelar.start();
|
|
178
|
-
console.log('
|
|
178
|
+
console.log('Server ready on port', stelar.getPort());
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
-
###
|
|
181
|
+
### Client (Browser or Node.js)
|
|
182
182
|
|
|
183
183
|
```javascript
|
|
184
184
|
import { StelarClient } from 'stelar-time-real';
|
|
@@ -192,29 +192,29 @@ const client = new StelarClient('localhost:3000', {
|
|
|
192
192
|
messageQueueSize: 100,
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
-
client.on('connect', () => console.log('
|
|
196
|
-
client.on('disconnect', () => console.log('
|
|
197
|
-
client.on('welcome', (data) => console.log('
|
|
195
|
+
client.on('connect', () => console.log('Connected!'));
|
|
196
|
+
client.on('disconnect', () => console.log('Disconnected'));
|
|
197
|
+
client.on('welcome', (data) => console.log('Welcome:', data));
|
|
198
198
|
|
|
199
199
|
client.connect();
|
|
200
200
|
|
|
201
|
-
//
|
|
202
|
-
client.emit('chat', { message: '
|
|
201
|
+
// Send message
|
|
202
|
+
client.emit('chat', { message: 'Hello everyone!' });
|
|
203
203
|
|
|
204
|
-
// Request-response
|
|
204
|
+
// Request-response with Promise
|
|
205
205
|
const user = await client.request('getUser', { id: 1 }, 'getUser');
|
|
206
|
-
console.log(user); // { id: 1, name: '
|
|
206
|
+
console.log(user); // { id: 1, name: 'John', role: 'user' }
|
|
207
207
|
|
|
208
|
-
//
|
|
208
|
+
// Join rooms
|
|
209
209
|
client.joinRoom('general');
|
|
210
210
|
client.joinRoom('random');
|
|
211
211
|
|
|
212
|
-
//
|
|
213
|
-
const buffer = Buffer.from('
|
|
212
|
+
// Send binary
|
|
213
|
+
const buffer = Buffer.from('binary data');
|
|
214
214
|
client.emitBinary('file', buffer);
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
-
###
|
|
217
|
+
### Client with TCP Mode (Node.js only — maximum efficiency)
|
|
218
218
|
|
|
219
219
|
```javascript
|
|
220
220
|
const client = new StelarClient('localhost:3001', {
|
|
@@ -222,30 +222,30 @@ const client = new StelarClient('localhost:3001', {
|
|
|
222
222
|
reconnection: true,
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
-
client.on('connect', () => console.log('TCP
|
|
225
|
+
client.on('connect', () => console.log('TCP connected!'));
|
|
226
226
|
client.connect();
|
|
227
227
|
```
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
TCP mode uses the custom binary protocol instead of WebSocket. Less overhead, lower latency, ideal for server-to-server communication.
|
|
230
230
|
|
|
231
|
-
###
|
|
231
|
+
### Client with TLS/WSS (secure connections)
|
|
232
232
|
|
|
233
233
|
```javascript
|
|
234
|
-
// WSS — WebSocket
|
|
235
|
-
const client = new StelarClient('wss://secure.
|
|
234
|
+
// WSS — Secure WebSocket
|
|
235
|
+
const client = new StelarClient('wss://secure.mydomain.com', {
|
|
236
236
|
tls: true,
|
|
237
237
|
rejectUnauthorized: true,
|
|
238
238
|
});
|
|
239
239
|
|
|
240
240
|
// TCP + TLS
|
|
241
|
-
const client = new StelarClient('secure.
|
|
241
|
+
const client = new StelarClient('secure.mydomain.com:3001', {
|
|
242
242
|
mode: 'tcp',
|
|
243
243
|
tls: true,
|
|
244
244
|
rejectUnauthorized: true,
|
|
245
245
|
});
|
|
246
246
|
```
|
|
247
247
|
|
|
248
|
-
###
|
|
248
|
+
### Server with TLS
|
|
249
249
|
|
|
250
250
|
```javascript
|
|
251
251
|
import { readFileSync } from 'fs';
|
|
@@ -262,45 +262,45 @@ const stelar = new StelarServer({
|
|
|
262
262
|
|
|
263
263
|
---
|
|
264
264
|
|
|
265
|
-
##
|
|
265
|
+
## Architecture
|
|
266
266
|
|
|
267
|
-
###
|
|
267
|
+
### Dual Protocol
|
|
268
268
|
|
|
269
269
|
```
|
|
270
|
-
|
|
270
|
+
stelar-time-real Server
|
|
271
271
|
┌──────────────────────────┐
|
|
272
272
|
│ │
|
|
273
|
-
|
|
273
|
+
Browsers ──────► │ Port 3000 (WebSocket) │
|
|
274
274
|
(ws://) │ │ │
|
|
275
|
-
│
|
|
276
|
-
Node.js
|
|
277
|
-
(
|
|
275
|
+
│ Same logic │
|
|
276
|
+
Node.js ──────► │ Port 3001 (Custom TCP) │
|
|
277
|
+
(tcp mode) │ │ │
|
|
278
278
|
│ │
|
|
279
279
|
└──────────────────────────┘
|
|
280
280
|
```
|
|
281
281
|
|
|
282
|
-
|
|
283
|
-
-
|
|
284
|
-
-
|
|
285
|
-
-
|
|
286
|
-
-
|
|
287
|
-
-
|
|
288
|
-
-
|
|
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
|
|
289
289
|
|
|
290
|
-
|
|
290
|
+
A WebSocket client and a TCP client can be in the same room and communicate without issues.
|
|
291
291
|
|
|
292
292
|
### WebSocket Mode vs TCP Mode
|
|
293
293
|
|
|
294
|
-
|
|
|
294
|
+
| Aspect | WebSocket | Custom TCP |
|
|
295
295
|
|---------|-----------|------------|
|
|
296
|
-
|
|
|
297
|
-
| Node.js |
|
|
298
|
-
| Overhead
|
|
299
|
-
|
|
|
300
|
-
| TLS | wss:// | TLS
|
|
301
|
-
|
|
|
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 |
|
|
302
302
|
|
|
303
|
-
###
|
|
303
|
+
### Binary Protocol Format (TCP)
|
|
304
304
|
|
|
305
305
|
```
|
|
306
306
|
┌──────────────┬──────────┬───────────────┬──────────────┬──────────────┐
|
|
@@ -309,267 +309,267 @@ Un cliente WebSocket y un cliente TCP pueden estar en el mismo room y comunicars
|
|
|
309
309
|
└──────────────┴──────────┴───────────────┴──────────────┴──────────────┘
|
|
310
310
|
```
|
|
311
311
|
|
|
312
|
-
**11
|
|
312
|
+
**11 frame types:**
|
|
313
313
|
|
|
314
|
-
|
|
|
314
|
+
| Type | Code | Description |
|
|
315
315
|
|------|--------|-------------|
|
|
316
|
-
| JSON | 0 |
|
|
317
|
-
| Binary | 1 |
|
|
318
|
-
| Ping | 2 |
|
|
319
|
-
| Pong | 3 |
|
|
320
|
-
| ACK Request | 4 |
|
|
321
|
-
| ACK Response | 5 |
|
|
322
|
-
| Connect | 6 |
|
|
323
|
-
| Disconnect | 7 |
|
|
324
|
-
| Join Room | 8 |
|
|
325
|
-
| Leave Room | 9 |
|
|
326
|
-
| Error | 10 |
|
|
327
|
-
|
|
328
|
-
### WebSocket
|
|
329
|
-
|
|
330
|
-
stelar-time-real
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
- **Handshake** —
|
|
334
|
-
- **Framing** —
|
|
335
|
-
- **Masking** —
|
|
336
|
-
- **
|
|
337
|
-
- **Close codes** —
|
|
338
|
-
- **
|
|
339
|
-
- **PING/PONG** —
|
|
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
|
|
340
340
|
|
|
341
341
|
---
|
|
342
342
|
|
|
343
|
-
## API
|
|
343
|
+
## Complete API
|
|
344
344
|
|
|
345
|
-
### StelarServer —
|
|
345
|
+
### StelarServer — Options
|
|
346
346
|
|
|
347
347
|
```javascript
|
|
348
348
|
new StelarServer({
|
|
349
|
-
//
|
|
350
|
-
port: 3000, //
|
|
351
|
-
server: httpServer, //
|
|
352
|
-
namespace: '/', //
|
|
353
|
-
tcpPort: 3001, //
|
|
354
|
-
|
|
355
|
-
//
|
|
356
|
-
maxConnections: 10000, //
|
|
357
|
-
maxConnectionsPerIP: 50, //
|
|
358
|
-
maxRooms: 10000, //
|
|
359
|
-
maxRoomsPerClient: 50, //
|
|
360
|
-
maxPayloadSize: 10 * 1024 * 1024, //
|
|
361
|
-
maxFrameSize: 10 * 1024 * 1024, //
|
|
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
362
|
|
|
363
363
|
// Rate Limiting
|
|
364
364
|
rateLimit: {
|
|
365
|
-
maxPoints: 100, //
|
|
366
|
-
windowMs: 1000, //
|
|
365
|
+
maxPoints: 100, // Maximum points (messages) per window
|
|
366
|
+
windowMs: 1000, // Time window in milliseconds
|
|
367
367
|
},
|
|
368
368
|
|
|
369
369
|
// Timeouts
|
|
370
|
-
heartbeatInterval: 30000, //
|
|
371
|
-
heartbeatTimeout: 60000, // Timeout
|
|
372
|
-
connectTimeout: 10000, //
|
|
370
|
+
heartbeatInterval: 30000, // Ping interval (30s)
|
|
371
|
+
heartbeatTimeout: 60000, // Timeout before disconnecting (60s)
|
|
372
|
+
connectTimeout: 10000, // Initial connection timeout (10s)
|
|
373
373
|
|
|
374
|
-
//
|
|
375
|
-
healthEndpoint: '/health', //
|
|
376
|
-
gracefulShutdown: true, //
|
|
377
|
-
shutdownTimeout: 10000, //
|
|
378
|
-
allowedOrigins: ['https://
|
|
379
|
-
tls: { key, cert }, //
|
|
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
380
|
|
|
381
381
|
// Logging
|
|
382
|
-
logger: 'info', //
|
|
383
|
-
//
|
|
382
|
+
logger: 'info', // Level: 'debug'|'info'|'warn'|'error'|'silent'
|
|
383
|
+
// Also accepts Logger instance or false
|
|
384
384
|
});
|
|
385
385
|
```
|
|
386
386
|
|
|
387
|
-
### StelarServer —
|
|
387
|
+
### StelarServer — Methods
|
|
388
388
|
|
|
389
|
-
####
|
|
389
|
+
#### Events
|
|
390
390
|
|
|
391
|
-
|
|
|
391
|
+
| Method | Description |
|
|
392
392
|
|--------|-------------|
|
|
393
|
-
| `.on(event, handler)` |
|
|
394
|
-
| `.onAll(handler)` |
|
|
395
|
-
| `.onConnection(handler)` |
|
|
396
|
-
| `.onDisconnect(handler)` |
|
|
397
|
-
| `.onAck(name, handler)` |
|
|
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) |
|
|
398
398
|
|
|
399
|
-
####
|
|
399
|
+
#### Message Sending
|
|
400
400
|
|
|
401
|
-
|
|
|
401
|
+
| Method | Description |
|
|
402
402
|
|--------|-------------|
|
|
403
|
-
| `.broadcast(event, data, excludeId?)` |
|
|
404
|
-
| `.to(room, event, data, excludeId?)` |
|
|
405
|
-
| `.toId(id, event, data)` |
|
|
406
|
-
| `.broadcastBinary(event, buffer)` | Broadcast
|
|
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
407
|
|
|
408
|
-
####
|
|
408
|
+
#### Information
|
|
409
409
|
|
|
410
|
-
|
|
|
410
|
+
| Method | Description |
|
|
411
411
|
|--------|-------------|
|
|
412
|
-
| `.getClients(room?)` |
|
|
413
|
-
| `.getRoomMembers(room)` | IDs
|
|
414
|
-
| `.getRooms()` |
|
|
415
|
-
| `.getStats()` |
|
|
416
|
-
| `.getPort()` |
|
|
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
417
|
|
|
418
418
|
#### Lifecycle
|
|
419
419
|
|
|
420
|
-
|
|
|
420
|
+
| Method | Description |
|
|
421
421
|
|--------|-------------|
|
|
422
|
-
| `.use(middleware)` |
|
|
423
|
-
| `.start(callback?)` |
|
|
424
|
-
| `.stop()` |
|
|
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
425
|
|
|
426
|
-
### StelarContext (ctx) —
|
|
426
|
+
### StelarContext (ctx) — Inside handlers
|
|
427
427
|
|
|
428
|
-
|
|
428
|
+
Every event handler receives a context (`ctx`) with all available information and actions:
|
|
429
429
|
|
|
430
430
|
```javascript
|
|
431
431
|
stelar.on('message', (ctx) => {
|
|
432
|
-
//
|
|
433
|
-
ctx.id //
|
|
434
|
-
ctx.socket // net.Socket
|
|
435
|
-
ctx.req // HTTP request (null
|
|
436
|
-
ctx.data //
|
|
437
|
-
ctx.clientInfo //
|
|
438
|
-
ctx.clientInfo.rooms //
|
|
439
|
-
ctx.clientInfo.metadata //
|
|
440
|
-
ctx.clientInfo.remoteAddress //
|
|
441
|
-
ctx.clientInfo.protocol // 'ws'
|
|
442
|
-
|
|
443
|
-
//
|
|
444
|
-
ctx.emit('event', data) //
|
|
445
|
-
ctx.send('response', data) //
|
|
446
|
-
ctx.emitBinary('event', buffer) //
|
|
447
|
-
ctx.broadcast('event', data) //
|
|
448
|
-
ctx.broadcastBinary('event', buf) //
|
|
449
|
-
ctx.to('room', 'event', data) //
|
|
450
|
-
ctx.toId('id', 'event', data) //
|
|
451
|
-
|
|
452
|
-
//
|
|
453
|
-
ctx.joinRoom('room') //
|
|
454
|
-
ctx.leaveRoom('room') //
|
|
455
|
-
ctx.getClients('room') //
|
|
456
|
-
|
|
457
|
-
//
|
|
458
|
-
ctx.setMetadata('role', 'admin') //
|
|
459
|
-
ctx.getMetadata('role') //
|
|
460
|
-
|
|
461
|
-
//
|
|
462
|
-
ctx.ack('myAck', 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
463
|
});
|
|
464
464
|
```
|
|
465
465
|
|
|
466
|
-
### StelarClient —
|
|
466
|
+
### StelarClient — Options
|
|
467
467
|
|
|
468
468
|
```javascript
|
|
469
469
|
new StelarClient(urlOrPort, {
|
|
470
|
-
//
|
|
471
|
-
reconnection: true, // Auto
|
|
472
|
-
reconnectionAttempts: 10, //
|
|
473
|
-
reconnectionDelay: 1000, //
|
|
474
|
-
maxReconnectionDelay: 30000, //
|
|
475
|
-
heartbeatInterval: 30000, //
|
|
476
|
-
|
|
477
|
-
//
|
|
478
|
-
mode: 'ws', // 'ws'
|
|
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
479
|
maxPayloadSize: 10 * 1024 * 1024,
|
|
480
480
|
maxFrameSize: 10 * 1024 * 1024,
|
|
481
481
|
|
|
482
482
|
// ACK
|
|
483
|
-
ackTimeout: 5000, //
|
|
483
|
+
ackTimeout: 5000, // ACK timeout (ms)
|
|
484
484
|
|
|
485
|
-
//
|
|
486
|
-
messageQueueSize: 100, //
|
|
485
|
+
// Message queue
|
|
486
|
+
messageQueueSize: 100, // Queued messages when disconnected
|
|
487
487
|
|
|
488
|
-
//
|
|
489
|
-
tls: false, //
|
|
490
|
-
rejectUnauthorized: true, //
|
|
488
|
+
// Security
|
|
489
|
+
tls: false, // Enable TLS for wss:// or TCP TLS
|
|
490
|
+
rejectUnauthorized: true, // Validate TLS certificate
|
|
491
491
|
|
|
492
|
-
//
|
|
493
|
-
headers: {}, // Headers
|
|
492
|
+
// Custom headers
|
|
493
|
+
headers: {}, // Headers for WebSocket handshake
|
|
494
494
|
|
|
495
495
|
// Logging
|
|
496
|
-
logger: 'warn', //
|
|
496
|
+
logger: 'warn', // Log level
|
|
497
497
|
});
|
|
498
498
|
```
|
|
499
499
|
|
|
500
|
-
### StelarClient —
|
|
500
|
+
### StelarClient — Methods
|
|
501
501
|
|
|
502
|
-
####
|
|
502
|
+
#### Events
|
|
503
503
|
|
|
504
|
-
|
|
|
504
|
+
| Method | Description |
|
|
505
505
|
|--------|-------------|
|
|
506
|
-
| `.on(event, handler)` |
|
|
507
|
-
| `.off(event, handler)` |
|
|
508
|
-
| `.once(event, handler)` |
|
|
509
|
-
| `.onAll(handler)` |
|
|
510
|
-
| `.onAck(name, handler)` |
|
|
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 |
|
|
511
511
|
|
|
512
|
-
####
|
|
512
|
+
#### Sending
|
|
513
513
|
|
|
514
|
-
|
|
|
514
|
+
| Method | Description |
|
|
515
515
|
|--------|-------------|
|
|
516
|
-
| `.emit(event, data, opts?)` |
|
|
517
|
-
| `.emitBinary(event, data)` |
|
|
518
|
-
| `.sendFile(file)` |
|
|
519
|
-
| `.sendImage(blob)` |
|
|
520
|
-
| `.request(event, data, ackName)` | Request-response
|
|
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 |
|
|
521
521
|
|
|
522
522
|
#### Rooms
|
|
523
523
|
|
|
524
|
-
|
|
|
524
|
+
| Method | Description |
|
|
525
525
|
|--------|-------------|
|
|
526
|
-
| `.joinRoom(room)` |
|
|
527
|
-
| `.leaveRoom(room)` |
|
|
526
|
+
| `.joinRoom(room)` | Join a room |
|
|
527
|
+
| `.leaveRoom(room)` | Leave a room |
|
|
528
528
|
|
|
529
529
|
#### Lifecycle
|
|
530
530
|
|
|
531
|
-
|
|
|
531
|
+
| Method | Description |
|
|
532
532
|
|--------|-------------|
|
|
533
|
-
| `.connect(callback?)` |
|
|
534
|
-
| `.disconnect()` |
|
|
533
|
+
| `.connect(callback?)` | Connect to server |
|
|
534
|
+
| `.disconnect()` | Disconnect and clean up all resources |
|
|
535
535
|
|
|
536
|
-
####
|
|
536
|
+
#### State and Metrics
|
|
537
537
|
|
|
538
|
-
|
|
|
538
|
+
| Method | Description |
|
|
539
539
|
|--------|-------------|
|
|
540
|
-
| `.isConnected()` |
|
|
541
|
-
| `.getState()` |
|
|
542
|
-
| `.getId()` | ID
|
|
543
|
-
| `.getUrl()` | URL
|
|
544
|
-
| `.setUrl(url)` |
|
|
545
|
-
| `.getMessagesSent()` | Total
|
|
546
|
-
| `.getMessagesReceived()` | Total
|
|
547
|
-
| `.getLastError()` |
|
|
548
|
-
| `.getConnectTime()` | Timestamp
|
|
549
|
-
| `.getQueueSize()` |
|
|
550
|
-
| `.removeAllListeners(event?)` |
|
|
551
|
-
|
|
552
|
-
###
|
|
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
|
|
553
553
|
|
|
554
554
|
```javascript
|
|
555
555
|
client.on('connect', () => {
|
|
556
|
-
//
|
|
556
|
+
// Connection established
|
|
557
557
|
});
|
|
558
558
|
|
|
559
559
|
client.on('disconnect', (info) => {
|
|
560
|
-
// info = { code, reason }
|
|
560
|
+
// info = { code, reason } for WebSocket
|
|
561
561
|
});
|
|
562
562
|
|
|
563
563
|
client.on('reconnecting', (attempt) => {
|
|
564
|
-
//
|
|
564
|
+
// Reconnection attempt number `attempt`
|
|
565
565
|
});
|
|
566
566
|
|
|
567
567
|
client.on('reconnect_failed', () => {
|
|
568
|
-
//
|
|
568
|
+
// Reconnection attempts exhausted
|
|
569
569
|
});
|
|
570
570
|
|
|
571
571
|
client.on('error', (err) => {
|
|
572
|
-
//
|
|
572
|
+
// Connection or protocol error
|
|
573
573
|
});
|
|
574
574
|
```
|
|
575
575
|
|
|
@@ -577,13 +577,13 @@ client.on('error', (err) => {
|
|
|
577
577
|
|
|
578
578
|
## Health Check
|
|
579
579
|
|
|
580
|
-
|
|
580
|
+
The health check endpoint is designed to integrate with orchestrators like Kubernetes, Docker Swarm, or any load balancer.
|
|
581
581
|
|
|
582
582
|
```bash
|
|
583
583
|
curl http://localhost:3000/health
|
|
584
584
|
```
|
|
585
585
|
|
|
586
|
-
|
|
586
|
+
Response:
|
|
587
587
|
|
|
588
588
|
```json
|
|
589
589
|
{
|
|
@@ -608,27 +608,27 @@ Respuesta:
|
|
|
608
608
|
}
|
|
609
609
|
```
|
|
610
610
|
|
|
611
|
-
CORS
|
|
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
612
|
|
|
613
613
|
---
|
|
614
614
|
|
|
615
615
|
## Middleware
|
|
616
616
|
|
|
617
|
-
|
|
617
|
+
The middleware system allows validating connections before a client is accepted:
|
|
618
618
|
|
|
619
619
|
```javascript
|
|
620
|
-
//
|
|
620
|
+
// Token authentication
|
|
621
621
|
stelar.use((ctx, next) => {
|
|
622
622
|
const token = ctx.req?.headers?.authorization;
|
|
623
623
|
if (!token) {
|
|
624
|
-
return ctx.ack('error', { message: 'Token
|
|
624
|
+
return ctx.ack('error', { message: 'Token required' });
|
|
625
625
|
}
|
|
626
|
-
//
|
|
626
|
+
// Validate token...
|
|
627
627
|
ctx.setMetadata('userId', getUserIdFromToken(token));
|
|
628
628
|
next();
|
|
629
629
|
});
|
|
630
630
|
|
|
631
|
-
//
|
|
631
|
+
// Custom rate limiting
|
|
632
632
|
stelar.use((ctx, next) => {
|
|
633
633
|
const ip = ctx.req?.headers?.['x-forwarded-for'] || ctx.socket.remoteAddress;
|
|
634
634
|
if (isBlocked(ip)) {
|
|
@@ -639,21 +639,21 @@ stelar.use((ctx, next) => {
|
|
|
639
639
|
|
|
640
640
|
// Logging
|
|
641
641
|
stelar.use((ctx, next) => {
|
|
642
|
-
console.log(`
|
|
642
|
+
console.log(`New connection from ${ctx.clientInfo.remoteAddress}`);
|
|
643
643
|
next();
|
|
644
644
|
});
|
|
645
645
|
```
|
|
646
646
|
|
|
647
|
-
|
|
647
|
+
Multiple middlewares execute in order. If a middleware doesn't call `next()`, the connection is rejected.
|
|
648
648
|
|
|
649
649
|
---
|
|
650
650
|
|
|
651
651
|
## Rooms
|
|
652
652
|
|
|
653
|
-
|
|
653
|
+
Rooms are communication channels. A client can be in multiple rooms simultaneously:
|
|
654
654
|
|
|
655
655
|
```javascript
|
|
656
|
-
//
|
|
656
|
+
// Server
|
|
657
657
|
stelar.on('joinChannel', (ctx) => {
|
|
658
658
|
ctx.joinRoom(ctx.data.channel);
|
|
659
659
|
ctx.to(ctx.data.channel, 'userJoined', { userId: ctx.id });
|
|
@@ -666,71 +666,71 @@ stelar.on('channelMessage', (ctx) => {
|
|
|
666
666
|
}
|
|
667
667
|
});
|
|
668
668
|
|
|
669
|
-
//
|
|
669
|
+
// Client
|
|
670
670
|
client.joinRoom('general');
|
|
671
671
|
client.joinRoom('random');
|
|
672
672
|
client.joinRoom('project-alpha');
|
|
673
673
|
```
|
|
674
674
|
|
|
675
|
-
|
|
675
|
+
Rooms are automatically cleaned up when the last client leaves or disconnects. No manual resource release needed.
|
|
676
676
|
|
|
677
677
|
---
|
|
678
678
|
|
|
679
679
|
## ACK (Request-Response)
|
|
680
680
|
|
|
681
|
-
|
|
681
|
+
The ACK system enables reliable request-response communication over the real-time protocol:
|
|
682
682
|
|
|
683
683
|
```javascript
|
|
684
|
-
//
|
|
684
|
+
// Server — Register ACK handler
|
|
685
685
|
stelar.onAck('getUsers', (ctx) => {
|
|
686
|
-
return { users: ['
|
|
686
|
+
return { users: ['John', 'Mary', 'Peter'] };
|
|
687
687
|
});
|
|
688
688
|
|
|
689
689
|
stelar.onAck('validateToken', (ctx) => {
|
|
690
690
|
const valid = validateToken(ctx.data.token);
|
|
691
|
-
if (!valid) throw new Error('
|
|
691
|
+
if (!valid) throw new Error('Invalid token');
|
|
692
692
|
return { userId: 123 };
|
|
693
693
|
});
|
|
694
694
|
|
|
695
|
-
//
|
|
695
|
+
// Client — Send request and wait for response
|
|
696
696
|
const users = await client.request('getUsers', {}, 'getUsers');
|
|
697
|
-
console.log(users); // { users: ['
|
|
697
|
+
console.log(users); // { users: ['John', 'Mary', 'Peter'] }
|
|
698
698
|
|
|
699
699
|
try {
|
|
700
700
|
const result = await client.request('validateToken', { token: 'abc' }, 'validateToken');
|
|
701
701
|
} catch (err) {
|
|
702
|
-
console.log('
|
|
702
|
+
console.log('Invalid token');
|
|
703
703
|
}
|
|
704
704
|
```
|
|
705
705
|
|
|
706
|
-
|
|
706
|
+
ACK requests have configurable timeout (`ackTimeout`). If the server doesn't respond within that time, the Promise is rejected.
|
|
707
707
|
|
|
708
708
|
---
|
|
709
709
|
|
|
710
|
-
##
|
|
710
|
+
## Binary Data
|
|
711
711
|
|
|
712
|
-
|
|
712
|
+
Send files, images, audio, or any binary data without base64 overhead:
|
|
713
713
|
|
|
714
714
|
```javascript
|
|
715
|
-
//
|
|
715
|
+
// Server — Receive and forward binary
|
|
716
716
|
stelar.on('file', (ctx) => {
|
|
717
|
-
ctx.broadcastBinary('file', ctx.data); // ctx.data
|
|
717
|
+
ctx.broadcastBinary('file', ctx.data); // ctx.data is a Buffer
|
|
718
718
|
});
|
|
719
719
|
|
|
720
|
-
//
|
|
720
|
+
// Client — Send binary
|
|
721
721
|
const imageBuffer = await fs.readFile('photo.png');
|
|
722
722
|
client.emitBinary('file', imageBuffer);
|
|
723
723
|
|
|
724
|
-
//
|
|
724
|
+
// Client — Receive binary
|
|
725
725
|
client.on('file', (buffer) => {
|
|
726
|
-
console.log('
|
|
726
|
+
console.log('File received:', buffer.length, 'bytes');
|
|
727
727
|
fs.writeFile('received.png', buffer);
|
|
728
728
|
});
|
|
729
729
|
```
|
|
730
730
|
|
|
731
731
|
---
|
|
732
732
|
|
|
733
|
-
##
|
|
733
|
+
## Server Metrics
|
|
734
734
|
|
|
735
735
|
```javascript
|
|
736
736
|
const stats = stelar.getStats();
|
|
@@ -754,23 +754,23 @@ console.log(stats);
|
|
|
754
754
|
|
|
755
755
|
---
|
|
756
756
|
|
|
757
|
-
##
|
|
757
|
+
## Client Metrics
|
|
758
758
|
|
|
759
759
|
```javascript
|
|
760
|
-
console.log('
|
|
761
|
-
console.log('
|
|
762
|
-
console.log('
|
|
763
|
-
console.log('
|
|
764
|
-
console.log('
|
|
765
|
-
console.log('
|
|
766
|
-
console.log('
|
|
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());
|
|
767
767
|
```
|
|
768
768
|
|
|
769
769
|
---
|
|
770
770
|
|
|
771
|
-
##
|
|
771
|
+
## Horizontal Scalability
|
|
772
772
|
|
|
773
|
-
stelar-time-real
|
|
773
|
+
stelar-time-real runs on a single server per instance. To scale to multiple instances, use Redis Pub/Sub as a bridge:
|
|
774
774
|
|
|
775
775
|
```javascript
|
|
776
776
|
import { StelarServer } from 'stelar-time-real';
|
|
@@ -779,7 +779,7 @@ import Redis from 'redis';
|
|
|
779
779
|
const redis = Redis.createClient();
|
|
780
780
|
const stelar = new StelarServer({ port: 3000, tcpPort: 3001 });
|
|
781
781
|
|
|
782
|
-
//
|
|
782
|
+
// When a broadcast happens on this instance, publish to Redis
|
|
783
783
|
stelar.onAll((ctx) => {
|
|
784
784
|
redis.publish('stelar:events', JSON.stringify({
|
|
785
785
|
event: ctx.eventName,
|
|
@@ -788,7 +788,7 @@ stelar.onAll((ctx) => {
|
|
|
788
788
|
}));
|
|
789
789
|
});
|
|
790
790
|
|
|
791
|
-
//
|
|
791
|
+
// When another instance publishes, emit locally
|
|
792
792
|
redis.subscribe('stelar:events', (message) => {
|
|
793
793
|
const { event, data, excludeId } = JSON.parse(message);
|
|
794
794
|
stelar.broadcast(event, data, excludeId);
|
|
@@ -799,31 +799,31 @@ redis.subscribe('stelar:events', (message) => {
|
|
|
799
799
|
|
|
800
800
|
## Performance
|
|
801
801
|
|
|
802
|
-
|
|
802
|
+
Measurements with stress test (50 WebSocket + 20 TCP clients):
|
|
803
803
|
|
|
804
|
-
|
|
|
804
|
+
| Metric | Value |
|
|
805
805
|
|---------|-------|
|
|
806
|
-
|
|
|
807
|
-
| RAM
|
|
806
|
+
| Simultaneous connections | 70 |
|
|
807
|
+
| RAM per client | ~58 KB |
|
|
808
808
|
| Throughput | 3,425 msg/sec |
|
|
809
|
-
|
|
|
810
|
-
| Memory leaks |
|
|
809
|
+
| Stable heap | ~10 MB |
|
|
810
|
+
| Memory leaks | None detected |
|
|
811
811
|
| MaxListeners warnings | 0 |
|
|
812
812
|
|
|
813
|
-
|
|
813
|
+
The library uses ~58KB per connected client. A server with 1GB of RAM can handle approximately 17,000 simultaneous connections.
|
|
814
814
|
|
|
815
815
|
---
|
|
816
816
|
|
|
817
|
-
##
|
|
817
|
+
## Project Structure
|
|
818
818
|
|
|
819
819
|
```
|
|
820
820
|
stelar-time-real/
|
|
821
821
|
├── src/
|
|
822
|
-
│ ├── index.ts #
|
|
823
|
-
│ ├── client.ts #
|
|
824
|
-
│ ├── protocol.ts #
|
|
825
|
-
│ ├── websocket.ts # WebSocket
|
|
826
|
-
│ └── logger.ts # Logger
|
|
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
827
|
├── package.json
|
|
828
828
|
├── tsconfig.json
|
|
829
829
|
└── README.md
|
|
@@ -833,7 +833,7 @@ stelar-time-real/
|
|
|
833
833
|
|
|
834
834
|
## TypeScript
|
|
835
835
|
|
|
836
|
-
stelar-time-real
|
|
836
|
+
stelar-time-real is written in TypeScript and includes type definitions (.d.ts). You don't need to install separate @types:
|
|
837
837
|
|
|
838
838
|
```typescript
|
|
839
839
|
import { StelarServer, StelarClient, StelarStats } from 'stelar-time-real';
|
|
@@ -847,31 +847,31 @@ const stats: StelarStats = server.getStats();
|
|
|
847
847
|
## Tests
|
|
848
848
|
|
|
849
849
|
```bash
|
|
850
|
-
#
|
|
850
|
+
# Production tests (54 assertions, 16 suites)
|
|
851
851
|
node test-production.mjs
|
|
852
852
|
|
|
853
|
-
# Stress test (70
|
|
853
|
+
# Stress test (70 clients, throughput, memory)
|
|
854
854
|
node test-stress.mjs
|
|
855
855
|
```
|
|
856
856
|
|
|
857
|
-
|
|
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
858
|
|
|
859
859
|
---
|
|
860
860
|
|
|
861
|
-
##
|
|
861
|
+
## Extensible Configuration
|
|
862
862
|
|
|
863
|
-
stelar-time-real v3.2
|
|
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
864
|
|
|
865
865
|
### Custom Rate Limiter
|
|
866
866
|
|
|
867
|
-
|
|
867
|
+
Replace the built-in rate limiter (token bucket) with your own implementation. Ideal for using Redis, MongoDB, or any other store:
|
|
868
868
|
|
|
869
869
|
```javascript
|
|
870
870
|
import { StelarServer, IRateLimiter } from 'stelar-time-real';
|
|
871
871
|
|
|
872
|
-
//
|
|
872
|
+
// Your own rate limiter with Redis
|
|
873
873
|
class RedisRateLimiter implements IRateLimiter {
|
|
874
|
-
private redis; //
|
|
874
|
+
private redis; // your Redis connection
|
|
875
875
|
|
|
876
876
|
constructor(redisClient) {
|
|
877
877
|
this.redis = redisClient;
|
|
@@ -881,9 +881,9 @@ class RedisRateLimiter implements IRateLimiter {
|
|
|
881
881
|
const key = `ratelimit:${id}`;
|
|
882
882
|
const current = await this.redis.incr(key);
|
|
883
883
|
if (current === 1) {
|
|
884
|
-
await this.redis.expire(key, 1); // 1
|
|
884
|
+
await this.redis.expire(key, 1); // 1 second window
|
|
885
885
|
}
|
|
886
|
-
return current <= 100; // 100
|
|
886
|
+
return current <= 100; // 100 per second
|
|
887
887
|
}
|
|
888
888
|
|
|
889
889
|
async reset(id) {
|
|
@@ -891,11 +891,11 @@ class RedisRateLimiter implements IRateLimiter {
|
|
|
891
891
|
}
|
|
892
892
|
|
|
893
893
|
async cleanup() {
|
|
894
|
-
// Redis
|
|
894
|
+
// Redis handles expiration automatically
|
|
895
895
|
}
|
|
896
896
|
|
|
897
897
|
async size() {
|
|
898
|
-
return 0; //
|
|
898
|
+
return 0; // Not applicable with Redis
|
|
899
899
|
}
|
|
900
900
|
}
|
|
901
901
|
|
|
@@ -907,7 +907,7 @@ const stelar = new StelarServer({
|
|
|
907
907
|
|
|
908
908
|
### Custom IP Tracker
|
|
909
909
|
|
|
910
|
-
|
|
910
|
+
Replace the per-IP connection tracker with your own logic. Useful for using a database of blocked IPs or whitelist logic:
|
|
911
911
|
|
|
912
912
|
```javascript
|
|
913
913
|
class CustomIPTracker implements IIPTracker {
|
|
@@ -916,15 +916,15 @@ class CustomIPTracker implements IIPTracker {
|
|
|
916
916
|
private counts = new Map<string, number>();
|
|
917
917
|
|
|
918
918
|
check(ip) {
|
|
919
|
-
if (this.blockedIPs.has(ip)) return false; // IP
|
|
920
|
-
if (this.vipIPs.has(ip)) return true; // VIP
|
|
921
|
-
return (this.counts.get(ip) || 0) < 20; // 20
|
|
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
|
|
922
922
|
}
|
|
923
923
|
|
|
924
924
|
add(ip) { this.counts.set(ip, (this.counts.get(ip) || 0) + 1); }
|
|
925
925
|
remove(ip) { /* ... */ }
|
|
926
926
|
getCount(ip) { return this.counts.get(ip) || 0; }
|
|
927
|
-
cleanup() { /*
|
|
927
|
+
cleanup() { /* clean expired entries */ }
|
|
928
928
|
}
|
|
929
929
|
|
|
930
930
|
const stelar = new StelarServer({
|
|
@@ -935,7 +935,7 @@ const stelar = new StelarServer({
|
|
|
935
935
|
|
|
936
936
|
### Custom Client ID Generator
|
|
937
937
|
|
|
938
|
-
|
|
938
|
+
Generate client IDs with your own format. By default uses UUID v4:
|
|
939
939
|
|
|
940
940
|
```javascript
|
|
941
941
|
const stelar = new StelarServer({
|
|
@@ -948,7 +948,7 @@ const stelar = new StelarServer({
|
|
|
948
948
|
|
|
949
949
|
### Event-Specific Rate Limits
|
|
950
950
|
|
|
951
|
-
|
|
951
|
+
Each event can have its own rate limit, independent from the global one:
|
|
952
952
|
|
|
953
953
|
```javascript
|
|
954
954
|
const stelar = new StelarServer({
|
|
@@ -956,112 +956,112 @@ const stelar = new StelarServer({
|
|
|
956
956
|
rateLimit: { maxPoints: 100, windowMs: 1000 }, // Global: 100 msg/sec
|
|
957
957
|
eventRateLimits: {
|
|
958
958
|
'chat': { maxPoints: 5, windowMs: 1000 }, // Chat: 5 msg/sec
|
|
959
|
-
'file-upload': { maxPoints: 2, windowMs: 10000 }, //
|
|
959
|
+
'file-upload': { maxPoints: 2, windowMs: 10000 }, // Files: 2 every 10s
|
|
960
960
|
'typing': { maxPoints: 10, windowMs: 1000 }, // Typing: 10 msg/sec
|
|
961
|
-
'location': { maxPoints: 1, windowMs: 5000 }, //
|
|
961
|
+
'location': { maxPoints: 1, windowMs: 5000 }, // Location: 1 every 5s
|
|
962
962
|
},
|
|
963
963
|
});
|
|
964
964
|
|
|
965
|
-
//
|
|
965
|
+
// You can also add/remove at runtime:
|
|
966
966
|
stelar.setEventRateLimit('voice', { maxPoints: 50, windowMs: 1000 });
|
|
967
967
|
stelar.removeEventRateLimit('voice');
|
|
968
968
|
```
|
|
969
969
|
|
|
970
970
|
### Per-Client Rate Limits
|
|
971
971
|
|
|
972
|
-
|
|
972
|
+
Give specific clients different rate limits. Useful for premium vs free users:
|
|
973
973
|
|
|
974
974
|
```javascript
|
|
975
975
|
stelar.onConnection((ctx) => {
|
|
976
976
|
const role = ctx.getMetadata('role');
|
|
977
977
|
|
|
978
|
-
//
|
|
978
|
+
// Premium user: 500 msg/sec
|
|
979
979
|
if (role === 'premium') {
|
|
980
980
|
stelar.setClientRateLimit(ctx.id, { maxPoints: 500, windowMs: 1000 });
|
|
981
981
|
}
|
|
982
|
-
//
|
|
982
|
+
// Verified bot: 1000 msg/sec
|
|
983
983
|
else if (role === 'bot') {
|
|
984
984
|
stelar.setClientRateLimit(ctx.id, { maxPoints: 1000, windowMs: 1000 });
|
|
985
985
|
}
|
|
986
|
-
//
|
|
986
|
+
// Normal user: uses global rate limit (100 msg/sec)
|
|
987
987
|
});
|
|
988
988
|
|
|
989
|
-
//
|
|
989
|
+
// Remove override (reverts to global):
|
|
990
990
|
stelar.removeClientRateLimit(clientId);
|
|
991
991
|
```
|
|
992
992
|
|
|
993
|
-
|
|
993
|
+
Rate limiting priority is: **per-client override > event-specific > global > custom rate limiter**.
|
|
994
994
|
|
|
995
|
-
### Hook System (
|
|
995
|
+
### Hook System (Server)
|
|
996
996
|
|
|
997
|
-
Hooks
|
|
997
|
+
Hooks let you customize what happens when the server detects an event. Each hook can return `false` to cancel the default action:
|
|
998
998
|
|
|
999
999
|
```javascript
|
|
1000
1000
|
const stelar = new StelarServer({
|
|
1001
1001
|
port: 3000,
|
|
1002
1002
|
hooks: {
|
|
1003
|
-
//
|
|
1004
|
-
// Return false
|
|
1003
|
+
// When a client exceeds the rate limit
|
|
1004
|
+
// Return false to NOT disconnect (e.g.: just warn)
|
|
1005
1005
|
onRateLimitExceeded: ({ clientId, event, protocol }) => {
|
|
1006
|
-
console.warn(`Rate limit: ${clientId}
|
|
1007
|
-
// return false; //
|
|
1006
|
+
console.warn(`Rate limit: ${clientId} on event ${event}`);
|
|
1007
|
+
// return false; // Uncomment to NOT disconnect the client
|
|
1008
1008
|
},
|
|
1009
1009
|
|
|
1010
|
-
//
|
|
1010
|
+
// When maximum connections is reached
|
|
1011
1011
|
onMaxConnectionsReached: ({ activeConnections, max, ip }) => {
|
|
1012
|
-
console.error(`
|
|
1013
|
-
//
|
|
1012
|
+
console.error(`Server full: ${activeConnections}/${max} from ${ip}`);
|
|
1013
|
+
// Send alert to Slack, etc.
|
|
1014
1014
|
},
|
|
1015
1015
|
|
|
1016
|
-
//
|
|
1017
|
-
// Return false
|
|
1016
|
+
// When a client tries to join a room
|
|
1017
|
+
// Return false to REJECT the join
|
|
1018
1018
|
onClientJoinRoom: ({ clientId, room, metadata }) => {
|
|
1019
1019
|
const role = metadata.get('role');
|
|
1020
1020
|
if (room.startsWith('admin-') && role !== 'admin') {
|
|
1021
|
-
return false; //
|
|
1021
|
+
return false; // Reject: admins only
|
|
1022
1022
|
}
|
|
1023
1023
|
},
|
|
1024
1024
|
|
|
1025
|
-
//
|
|
1026
|
-
// Return false
|
|
1025
|
+
// When a client leaves a room
|
|
1026
|
+
// Return false to REJECT the leave
|
|
1027
1027
|
onClientLeaveRoom: ({ clientId, room }) => {
|
|
1028
|
-
//
|
|
1028
|
+
// Custom logic...
|
|
1029
1029
|
},
|
|
1030
1030
|
|
|
1031
|
-
//
|
|
1031
|
+
// When global maximum rooms is reached
|
|
1032
1032
|
onMaxRoomsReached: ({ clientId, room, totalRooms, max }) => {
|
|
1033
1033
|
console.warn(`Max rooms: ${totalRooms}/${max}`);
|
|
1034
1034
|
},
|
|
1035
1035
|
|
|
1036
|
-
//
|
|
1036
|
+
// When a client exceeds rooms per client
|
|
1037
1037
|
onMaxRoomsPerClientReached: ({ clientId, room, currentRooms, max }) => {
|
|
1038
|
-
console.warn(`
|
|
1038
|
+
console.warn(`Client ${clientId}: ${currentRooms}/${max} rooms`);
|
|
1039
1039
|
},
|
|
1040
1040
|
|
|
1041
|
-
//
|
|
1041
|
+
// When a payload is too large
|
|
1042
1042
|
onPayloadTooLarge: ({ clientId, event, size, max }) => {
|
|
1043
|
-
console.warn(`
|
|
1043
|
+
console.warn(`Large payload: ${size} bytes from ${clientId}`);
|
|
1044
1044
|
},
|
|
1045
1045
|
|
|
1046
|
-
//
|
|
1046
|
+
// When an invalid message is received
|
|
1047
1047
|
onInvalidMessage: ({ clientId, reason, protocol }) => {
|
|
1048
|
-
console.warn(`
|
|
1048
|
+
console.warn(`Invalid message from ${clientId}: ${reason}`);
|
|
1049
1049
|
},
|
|
1050
1050
|
|
|
1051
|
-
//
|
|
1052
|
-
// Return false
|
|
1051
|
+
// Before a broadcast
|
|
1052
|
+
// Return false to CANCEL the broadcast
|
|
1053
1053
|
onBeforeBroadcast: ({ event, data, excludeId }) => {
|
|
1054
|
-
if (event === 'spam') return false; //
|
|
1054
|
+
if (event === 'spam') return false; // Cancel spam broadcast
|
|
1055
1055
|
},
|
|
1056
1056
|
|
|
1057
|
-
//
|
|
1057
|
+
// When a client connects
|
|
1058
1058
|
onClientConnect: ({ clientId, ip, protocol, metadata }) => {
|
|
1059
|
-
console.log(`
|
|
1059
|
+
console.log(`Connected: ${clientId} via ${protocol} from ${ip}`);
|
|
1060
1060
|
},
|
|
1061
1061
|
|
|
1062
|
-
//
|
|
1062
|
+
// When a client disconnects
|
|
1063
1063
|
onClientDisconnect: ({ clientId, ip, protocol, rooms }) => {
|
|
1064
|
-
console.log(`
|
|
1064
|
+
console.log(`Disconnected: ${clientId} was in ${rooms.size} rooms`);
|
|
1065
1065
|
},
|
|
1066
1066
|
},
|
|
1067
1067
|
});
|
|
@@ -1069,13 +1069,13 @@ const stelar = new StelarServer({
|
|
|
1069
1069
|
|
|
1070
1070
|
### Custom Health Check
|
|
1071
1071
|
|
|
1072
|
-
|
|
1072
|
+
Replace the built-in health check with your own handler. Useful for adding database checks, disk space, etc:
|
|
1073
1073
|
|
|
1074
1074
|
```javascript
|
|
1075
1075
|
const stelar = new StelarServer({
|
|
1076
1076
|
port: 3000,
|
|
1077
1077
|
customHealthHandler: (req, res, stats) => {
|
|
1078
|
-
// stats
|
|
1078
|
+
// stats contains all server statistics
|
|
1079
1079
|
|
|
1080
1080
|
const dbConnected = await checkDatabase();
|
|
1081
1081
|
const diskSpace = checkDiskSpace();
|
|
@@ -1096,13 +1096,13 @@ const stelar = new StelarServer({
|
|
|
1096
1096
|
|
|
1097
1097
|
### Runtime Configuration
|
|
1098
1098
|
|
|
1099
|
-
|
|
1099
|
+
Change server configuration without restarting:
|
|
1100
1100
|
|
|
1101
1101
|
```javascript
|
|
1102
1102
|
const stelar = new StelarServer({ port: 3000, maxConnections: 100 });
|
|
1103
1103
|
await stelar.start();
|
|
1104
1104
|
|
|
1105
|
-
//
|
|
1105
|
+
// Later... you need more capacity
|
|
1106
1106
|
stelar.updateConfig({
|
|
1107
1107
|
maxConnections: 500,
|
|
1108
1108
|
maxRooms: 5000,
|
|
@@ -1110,17 +1110,17 @@ stelar.updateConfig({
|
|
|
1110
1110
|
allowedOrigins: ['https://app.com', 'https://admin.app.com'],
|
|
1111
1111
|
});
|
|
1112
1112
|
|
|
1113
|
-
//
|
|
1113
|
+
// Change hooks at runtime
|
|
1114
1114
|
stelar.updateConfig({
|
|
1115
1115
|
hooks: {
|
|
1116
1116
|
onRateLimitExceeded: ({ clientId }) => {
|
|
1117
|
-
banUser(clientId); //
|
|
1118
|
-
return false; //
|
|
1117
|
+
banUser(clientId); // Auto-ban instead of disconnecting
|
|
1118
|
+
return false; // Don't disconnect, you already banned them
|
|
1119
1119
|
},
|
|
1120
1120
|
},
|
|
1121
1121
|
});
|
|
1122
1122
|
|
|
1123
|
-
//
|
|
1123
|
+
// View current configuration
|
|
1124
1124
|
const config = stelar.getConfig();
|
|
1125
1125
|
console.log(config);
|
|
1126
1126
|
// {
|
|
@@ -1135,49 +1135,49 @@ console.log(config);
|
|
|
1135
1135
|
|
|
1136
1136
|
### Client Hooks
|
|
1137
1137
|
|
|
1138
|
-
|
|
1138
|
+
Customize client behavior with hooks:
|
|
1139
1139
|
|
|
1140
1140
|
```javascript
|
|
1141
1141
|
const client = new StelarClient('localhost:3000', {
|
|
1142
1142
|
hooks: {
|
|
1143
|
-
//
|
|
1143
|
+
// Before sending a message — return false to cancel
|
|
1144
1144
|
onBeforeEmit: ({ event, data }) => {
|
|
1145
|
-
if (event === 'debug') return false; //
|
|
1146
|
-
console.log(`
|
|
1145
|
+
if (event === 'debug') return false; // Don't send debug in production
|
|
1146
|
+
console.log(`Sending: ${event}`);
|
|
1147
1147
|
},
|
|
1148
1148
|
|
|
1149
|
-
//
|
|
1149
|
+
// When any message is received
|
|
1150
1150
|
onMessage: ({ event, data, isBinary }) => {
|
|
1151
1151
|
metrics.increment('messages.received');
|
|
1152
1152
|
if (isBinary) metrics.increment('binary.received');
|
|
1153
1153
|
},
|
|
1154
1154
|
|
|
1155
|
-
//
|
|
1155
|
+
// When connection state changes
|
|
1156
1156
|
onStateChange: ({ from, to }) => {
|
|
1157
|
-
console.log(`
|
|
1157
|
+
console.log(`State: ${from} -> ${to}`);
|
|
1158
1158
|
if (to === 'reconnecting') showReconnectingUI();
|
|
1159
1159
|
if (to === 'connected') hideReconnectingUI();
|
|
1160
1160
|
},
|
|
1161
1161
|
|
|
1162
|
-
//
|
|
1162
|
+
// Customize reconnection delay
|
|
1163
1163
|
onReconnectDelay: ({ attempt, defaultDelay }) => {
|
|
1164
|
-
//
|
|
1164
|
+
// Business hours: fast reconnection
|
|
1165
1165
|
const hour = new Date().getHours();
|
|
1166
1166
|
if (hour >= 9 && hour <= 18) return 500;
|
|
1167
|
-
return defaultDelay; //
|
|
1167
|
+
return defaultDelay; // Off-hours: normal delay
|
|
1168
1168
|
},
|
|
1169
1169
|
|
|
1170
|
-
//
|
|
1170
|
+
// When a message is queued (disconnected)
|
|
1171
1171
|
onMessageQueued: ({ event, queueSize }) => {
|
|
1172
|
-
console.log(`
|
|
1172
|
+
console.log(`Message queued: ${event} (queue: ${queueSize})`);
|
|
1173
1173
|
},
|
|
1174
1174
|
|
|
1175
|
-
//
|
|
1175
|
+
// When queue is drained after reconnecting
|
|
1176
1176
|
onQueueDrained: ({ count }) => {
|
|
1177
|
-
console.log(`${count}
|
|
1177
|
+
console.log(`${count} messages sent after reconnecting`);
|
|
1178
1178
|
},
|
|
1179
1179
|
|
|
1180
|
-
//
|
|
1180
|
+
// When an error occurs
|
|
1181
1181
|
onError: ({ error, context }) => {
|
|
1182
1182
|
errorReporter.report(error, { context });
|
|
1183
1183
|
},
|
|
@@ -1187,24 +1187,24 @@ const client = new StelarClient('localhost:3000', {
|
|
|
1187
1187
|
|
|
1188
1188
|
### Custom Reconnect Delay
|
|
1189
1189
|
|
|
1190
|
-
|
|
1190
|
+
Control exactly how long to wait before each reconnection attempt:
|
|
1191
1191
|
|
|
1192
1192
|
```javascript
|
|
1193
|
-
//
|
|
1193
|
+
// Option 1: Custom function
|
|
1194
1194
|
const client = new StelarClient('localhost:3000', {
|
|
1195
1195
|
customReconnectDelay: (attempt, baseDelay, maxDelay) => {
|
|
1196
|
-
//
|
|
1196
|
+
// Fast retry for first 3 attempts, then slow
|
|
1197
1197
|
if (attempt <= 3) return 200;
|
|
1198
1198
|
if (attempt <= 10) return 2000;
|
|
1199
|
-
return 30000; // 30s
|
|
1199
|
+
return 30000; // 30s for later attempts
|
|
1200
1200
|
},
|
|
1201
1201
|
});
|
|
1202
1202
|
|
|
1203
|
-
//
|
|
1203
|
+
// Option 2: Via hook (can change at runtime)
|
|
1204
1204
|
const client = new StelarClient('localhost:3000', {
|
|
1205
1205
|
hooks: {
|
|
1206
1206
|
onReconnectDelay: ({ attempt, defaultDelay }) => {
|
|
1207
|
-
return Math.min(100 * attempt, 10000); //
|
|
1207
|
+
return Math.min(100 * attempt, 10000); // Linear instead of exponential
|
|
1208
1208
|
},
|
|
1209
1209
|
},
|
|
1210
1210
|
});
|
|
@@ -1212,25 +1212,25 @@ const client = new StelarClient('localhost:3000', {
|
|
|
1212
1212
|
|
|
1213
1213
|
### Client Runtime Configuration
|
|
1214
1214
|
|
|
1215
|
-
|
|
1215
|
+
Change client configuration without reconnecting:
|
|
1216
1216
|
|
|
1217
1217
|
```javascript
|
|
1218
1218
|
const client = new StelarClient('localhost:3000');
|
|
1219
1219
|
client.connect();
|
|
1220
1220
|
|
|
1221
|
-
//
|
|
1221
|
+
// Later... adjust timeouts
|
|
1222
1222
|
client.updateOptions({
|
|
1223
1223
|
heartbeatInterval: 15000,
|
|
1224
1224
|
ackTimeout: 10000,
|
|
1225
1225
|
maxPayloadSize: 50 * 1024 * 1024, // 50MB
|
|
1226
1226
|
hooks: {
|
|
1227
1227
|
onBeforeEmit: ({ event }) => {
|
|
1228
|
-
if (event === 'log') return false; //
|
|
1228
|
+
if (event === 'log') return false; // No longer send logs
|
|
1229
1229
|
},
|
|
1230
1230
|
},
|
|
1231
1231
|
});
|
|
1232
1232
|
|
|
1233
|
-
//
|
|
1233
|
+
// View current configuration
|
|
1234
1234
|
const opts = client.getOptions();
|
|
1235
1235
|
console.log(opts);
|
|
1236
1236
|
```
|