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 +1012 -335
- package/package.json +14 -10
- package/src/client.d.ts +115 -8
- package/src/client.d.ts.map +1 -1
- package/src/client.js +851 -102
- package/src/client.ts +915 -103
- package/src/index.d.ts +281 -15
- package/src/index.d.ts.map +1 -1
- package/src/index.js +1382 -142
- package/src/index.ts +1663 -201
- package/src/logger.d.ts +29 -0
- package/src/logger.d.ts.map +1 -0
- package/src/logger.js +98 -0
- package/src/logger.ts +115 -0
- package/src/protocol.d.ts +57 -0
- package/src/protocol.d.ts.map +1 -0
- package/src/protocol.js +193 -0
- package/src/protocol.ts +237 -0
- package/src/websocket.d.ts +67 -0
- package/src/websocket.d.ts.map +1 -0
- package/src/websocket.js +260 -0
- package/src/websocket.ts +316 -0
package/README.md
CHANGED
|
@@ -1,21 +1,94 @@
|
|
|
1
|
-
# stelar-time-real
|
|
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
|

|
|
7
|
-

|
|
8
|
+

|
|
8
9
|
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
### One import for everything
|
|
99
|
+
---
|
|
29
100
|
|
|
30
|
-
|
|
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('
|
|
47
|
-
client.emit('welcome', '
|
|
115
|
+
console.log('Connected:', client.id);
|
|
116
|
+
client.emit('welcome', { message: 'Welcome to the server!' });
|
|
48
117
|
});
|
|
49
118
|
|
|
50
|
-
stelar.on('
|
|
51
|
-
ctx.broadcast('
|
|
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
|
-
###
|
|
126
|
+
### Server with Production Configuration
|
|
58
127
|
|
|
59
128
|
```javascript
|
|
60
|
-
import
|
|
129
|
+
import express from 'express';
|
|
130
|
+
import { StelarServer } from 'stelar-time-real';
|
|
61
131
|
|
|
62
|
-
const
|
|
132
|
+
const app = express();
|
|
133
|
+
const server = app.listen(3000);
|
|
63
134
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
+
stelar.onDisconnect((client) => {
|
|
166
|
+
console.log('Client disconnected:', client.id);
|
|
167
|
+
});
|
|
76
168
|
|
|
77
|
-
|
|
169
|
+
stelar.on('chat', (ctx) => {
|
|
170
|
+
ctx.broadcast('chat', ctx.data, ctx.id);
|
|
171
|
+
});
|
|
78
172
|
|
|
79
|
-
|
|
173
|
+
stelar.onAck('getUser', (ctx) => {
|
|
174
|
+
return { id: ctx.data.id, name: 'John', role: ctx.getMetadata('role') };
|
|
175
|
+
});
|
|
80
176
|
|
|
81
|
-
|
|
82
|
-
|
|
177
|
+
await stelar.start();
|
|
178
|
+
console.log('Server ready on port', stelar.getPort());
|
|
83
179
|
```
|
|
84
180
|
|
|
85
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
108
|
-
Listen for client events.
|
|
217
|
+
### Client with TCP Mode (Node.js only — maximum efficiency)
|
|
109
218
|
|
|
110
219
|
```javascript
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
127
|
-
Execute when a client connects.
|
|
248
|
+
### Server with TLS
|
|
128
249
|
|
|
129
250
|
```javascript
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
Send to all clients.
|
|
263
|
+
---
|
|
137
264
|
|
|
138
|
-
|
|
139
|
-
stelar.broadcast('chat', { message: 'Hello everyone' });
|
|
140
|
-
```
|
|
265
|
+
## Architecture
|
|
141
266
|
|
|
142
|
-
|
|
143
|
-
Send to a specific room.
|
|
267
|
+
### Dual Protocol
|
|
144
268
|
|
|
145
|
-
```
|
|
146
|
-
stelar
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
Get list of clients.
|
|
292
|
+
### WebSocket Mode vs TCP Mode
|
|
158
293
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
165
|
-
Get the port where it's running.
|
|
303
|
+
### Binary Protocol Format (TCP)
|
|
166
304
|
|
|
167
|
-
```
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
await stelar.start();
|
|
176
|
-
console.log('Started!');
|
|
177
|
-
```
|
|
341
|
+
---
|
|
178
342
|
|
|
179
|
-
|
|
180
|
-
|
|
343
|
+
## Complete API
|
|
344
|
+
|
|
345
|
+
### StelarServer — Options
|
|
181
346
|
|
|
182
347
|
```javascript
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
ctx.
|
|
194
|
-
ctx.
|
|
195
|
-
ctx.
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
500
|
+
### StelarClient — Methods
|
|
211
501
|
|
|
212
|
-
|
|
502
|
+
#### Events
|
|
213
503
|
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
218
|
-
const main = new StelarServer({ server, namespace: '/' });
|
|
512
|
+
#### Sending
|
|
219
513
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
522
|
+
#### Rooms
|
|
523
|
+
|
|
524
|
+
| Method | Description |
|
|
525
|
+
|--------|-------------|
|
|
526
|
+
| `.joinRoom(room)` | Join a room |
|
|
527
|
+
| `.leaveRoom(room)` | Leave a room |
|
|
232
528
|
|
|
233
|
-
####
|
|
529
|
+
#### Lifecycle
|
|
234
530
|
|
|
235
|
-
|
|
531
|
+
| Method | Description |
|
|
532
|
+
|--------|-------------|
|
|
533
|
+
| `.connect(callback?)` | Connect to server |
|
|
534
|
+
| `.disconnect()` | Disconnect and clean up all resources |
|
|
236
535
|
|
|
237
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
return { id: ctx.data.id, name: 'John' };
|
|
555
|
+
client.on('connect', () => {
|
|
556
|
+
// Connection established
|
|
243
557
|
});
|
|
244
558
|
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
console.log(user); // { id: 1, name: 'John' }
|
|
563
|
+
client.on('reconnecting', (attempt) => {
|
|
564
|
+
// Reconnection attempt number `attempt`
|
|
565
|
+
});
|
|
258
566
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
client.on('userData', (data) => {
|
|
262
|
-
console.log(data);
|
|
567
|
+
client.on('reconnect_failed', () => {
|
|
568
|
+
// Reconnection attempts exhausted
|
|
263
569
|
});
|
|
264
570
|
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
578
|
+
## Health Check
|
|
274
579
|
|
|
275
|
-
|
|
580
|
+
The health check endpoint is designed to integrate with orchestrators like Kubernetes, Docker Swarm, or any load balancer.
|
|
276
581
|
|
|
277
|
-
```
|
|
278
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
//
|
|
291
|
-
|
|
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
|
-
//
|
|
294
|
-
|
|
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
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
647
|
+
Multiple middlewares execute in order. If a middleware doesn't call `next()`, the connection is rejected.
|
|
305
648
|
|
|
306
|
-
|
|
307
|
-
Listen for server events.
|
|
649
|
+
---
|
|
308
650
|
|
|
309
|
-
|
|
310
|
-
client.on('welcome', (data) => {
|
|
311
|
-
console.log(data);
|
|
312
|
-
});
|
|
313
|
-
```
|
|
651
|
+
## Rooms
|
|
314
652
|
|
|
315
|
-
|
|
316
|
-
Listen for all events.
|
|
653
|
+
Rooms are communication channels. A client can be in multiple rooms simultaneously:
|
|
317
654
|
|
|
318
655
|
```javascript
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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
|
-
|
|
329
|
-
|
|
684
|
+
// Server — Register ACK handler
|
|
685
|
+
stelar.onAck('getUsers', (ctx) => {
|
|
686
|
+
return { users: ['John', 'Mary', 'Peter'] };
|
|
330
687
|
});
|
|
331
|
-
```
|
|
332
688
|
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
337
|
-
client.
|
|
338
|
-
|
|
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
|
-
|
|
342
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
-
//
|
|
349
|
-
|
|
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
|
-
|
|
353
|
-
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
## Server Metrics
|
|
354
734
|
|
|
355
735
|
```javascript
|
|
356
|
-
|
|
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
|
-
|
|
360
|
-
|
|
755
|
+
---
|
|
756
|
+
|
|
757
|
+
## Client Metrics
|
|
361
758
|
|
|
362
759
|
```javascript
|
|
363
|
-
client.
|
|
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
|
-
|
|
367
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
376
|
-
Manually disconnect.
|
|
798
|
+
---
|
|
377
799
|
|
|
378
|
-
|
|
379
|
-
client.disconnect();
|
|
380
|
-
```
|
|
800
|
+
## Performance
|
|
381
801
|
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
```
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
392
|
-
Get connection URL.
|
|
832
|
+
---
|
|
393
833
|
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
845
|
+
---
|
|
399
846
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
416
|
-
import { StelarServer } from 'stelar-time-real';
|
|
870
|
+
import { StelarServer, IRateLimiter } from 'stelar-time-real';
|
|
417
871
|
|
|
418
|
-
|
|
419
|
-
|
|
872
|
+
// Your own rate limiter with Redis
|
|
873
|
+
class RedisRateLimiter implements IRateLimiter {
|
|
874
|
+
private redis; // your Redis connection
|
|
420
875
|
|
|
421
|
-
|
|
876
|
+
constructor(redisClient) {
|
|
877
|
+
this.redis = redisClient;
|
|
878
|
+
}
|
|
422
879
|
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
889
|
+
async reset(id) {
|
|
890
|
+
await this.redis.del(`ratelimit:${id}`);
|
|
891
|
+
}
|
|
430
892
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
893
|
+
async cleanup() {
|
|
894
|
+
// Redis handles expiration automatically
|
|
895
|
+
}
|
|
434
896
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
897
|
+
async size() {
|
|
898
|
+
return 0; // Not applicable with Redis
|
|
899
|
+
}
|
|
900
|
+
}
|
|
439
901
|
|
|
440
|
-
|
|
902
|
+
const stelar = new StelarServer({
|
|
903
|
+
port: 3000,
|
|
904
|
+
customRateLimiter: new RedisRateLimiter(redisClient),
|
|
905
|
+
});
|
|
906
|
+
```
|
|
441
907
|
|
|
442
|
-
|
|
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
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
//
|
|
470
|
-
|
|
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
|
-
###
|
|
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.
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
ctx.
|
|
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
|
-
|
|
993
|
+
Rate limiting priority is: **per-client override > event-specific > global > custom rate limiter**.
|
|
487
994
|
|
|
488
|
-
|
|
489
|
-
import { StelarClient } from 'stelar-time-real';
|
|
995
|
+
### Hook System (Server)
|
|
490
996
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
498
|
-
client.on('disconnect', () => console.log('Disconnected'));
|
|
499
|
-
client.on('reconnecting', (attempt) => console.log(`Retrying ${attempt}/5`));
|
|
1070
|
+
### Custom Health Check
|
|
500
1071
|
|
|
501
|
-
|
|
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
|
-
###
|
|
1097
|
+
### Runtime Configuration
|
|
1098
|
+
|
|
1099
|
+
Change server configuration without restarting:
|
|
505
1100
|
|
|
506
1101
|
```javascript
|
|
507
|
-
|
|
508
|
-
stelar.
|
|
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
|
-
|
|
515
|
-
|
|
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
|
-
//
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
###
|
|
1188
|
+
### Custom Reconnect Delay
|
|
1189
|
+
|
|
1190
|
+
Control exactly how long to wait before each reconnection attempt:
|
|
535
1191
|
|
|
536
1192
|
```javascript
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
//
|
|
543
|
-
const
|
|
544
|
-
|
|
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
|
-
|
|
1217
|
+
```javascript
|
|
1218
|
+
const client = new StelarClient('localhost:3000');
|
|
1219
|
+
client.connect();
|
|
550
1220
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
1233
|
+
// View current configuration
|
|
1234
|
+
const opts = client.getOptions();
|
|
1235
|
+
console.log(opts);
|
|
1236
|
+
```
|
|
560
1237
|
|
|
561
|
-
|
|
1238
|
+
---
|
|
562
1239
|
|
|
563
|
-
##
|
|
1240
|
+
## License
|
|
564
1241
|
|
|
565
|
-
Stelar
|
|
1242
|
+
MIT — Stelar
|