stelar-time-real 1.2.2 → 2.0.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 +10 -0
- package/package.json +12 -3
- package/src/client.d.ts +52 -0
- package/src/client.d.ts.map +1 -0
- package/src/client.js +208 -230
- package/src/client.ts +257 -0
- package/src/index.d.ts +85 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +269 -299
- package/src/index.ts +356 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
|
|
2
|
+
import { WebSocketServer, WebSocket, RawData } from 'ws';
|
|
3
|
+
|
|
4
|
+
export interface StelarOptions {
|
|
5
|
+
port?: number;
|
|
6
|
+
server?: Server;
|
|
7
|
+
namespace?: string;
|
|
8
|
+
heartbeatInterval?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface StelarClientInfo {
|
|
12
|
+
id: string;
|
|
13
|
+
room: string | null;
|
|
14
|
+
lastPing: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface StelarContext {
|
|
18
|
+
id: string;
|
|
19
|
+
socket: WebSocket;
|
|
20
|
+
req: IncomingMessage;
|
|
21
|
+
data?: unknown;
|
|
22
|
+
buffer?: Uint8Array;
|
|
23
|
+
isBinary?: boolean;
|
|
24
|
+
event?: string;
|
|
25
|
+
error?: Error;
|
|
26
|
+
emit: (event: string, data: unknown) => void;
|
|
27
|
+
send: (respId: string, data: unknown) => void;
|
|
28
|
+
emitBinary: (event: string, buffer: ArrayBuffer) => void;
|
|
29
|
+
broadcast: (event: string, data: unknown) => void;
|
|
30
|
+
broadcastBinary: (event: string, buffer: ArrayBuffer) => void;
|
|
31
|
+
to: (room: string, event: string, data: unknown) => void;
|
|
32
|
+
toId: (id: string, event: string, data: unknown) => void;
|
|
33
|
+
getClients: (room?: string) => { id: string; room: string | null }[];
|
|
34
|
+
joinRoom: (room: string) => void;
|
|
35
|
+
leaveRoom: () => void;
|
|
36
|
+
ack: (ackName: string, data: unknown) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface StelarMiddleware {
|
|
40
|
+
(ctx: StelarContext, next: () => void): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type StelarEventHandler = (ctx: StelarContext) => void;
|
|
44
|
+
|
|
45
|
+
export type StelarWildcardHandler = (data: { event: string; data: StelarContext }) => void;
|
|
46
|
+
|
|
47
|
+
class StelarServer {
|
|
48
|
+
private port: number;
|
|
49
|
+
private server: Server | null = null;
|
|
50
|
+
private namespace: string;
|
|
51
|
+
private wss: WebSocketServer | null = null;
|
|
52
|
+
private clients = new Map<WebSocket, StelarClientInfo>();
|
|
53
|
+
private events: Map<string, StelarEventHandler> = new Map();
|
|
54
|
+
private middlewares: StelarMiddleware[] = [];
|
|
55
|
+
private heartbeatInterval: number;
|
|
56
|
+
private _hbTimer: ReturnType<typeof setInterval> | null = null;
|
|
57
|
+
private _wildcardHandler: StelarWildcardHandler | null = null;
|
|
58
|
+
private _connectionHandler: StelarEventHandler | null = null;
|
|
59
|
+
private _acks: Map<string, StelarEventHandler> = new Map();
|
|
60
|
+
private _externalServers = new WeakSet<Server>();
|
|
61
|
+
|
|
62
|
+
constructor(options: StelarOptions = {}) {
|
|
63
|
+
this.port = options.port || 3000;
|
|
64
|
+
this.server = options.server || null;
|
|
65
|
+
this.namespace = options.namespace || '/';
|
|
66
|
+
this.heartbeatInterval = options.heartbeatInterval || 30000;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static of(path: string, options: StelarOptions = {}): StelarServer {
|
|
70
|
+
return new StelarServer({ ...options, namespace: path });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
use(middleware: StelarMiddleware): this {
|
|
74
|
+
this.middlewares.push(middleware);
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
on(event: string, handler: StelarEventHandler): this {
|
|
79
|
+
this.events.set(event, handler);
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
onAll(handler: StelarWildcardHandler): this {
|
|
84
|
+
this._wildcardHandler = handler;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onConnection(handler: StelarEventHandler): this {
|
|
89
|
+
this._connectionHandler = handler;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onAck(name: string, handler: StelarEventHandler): this {
|
|
94
|
+
this._acks.set(name, handler);
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
broadcast(event: string, data: unknown): this {
|
|
99
|
+
this.clients.forEach((_, client) => {
|
|
100
|
+
client.send(JSON.stringify({ event, data }));
|
|
101
|
+
});
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
broadcastBinary(event: string, buffer: ArrayBuffer): void {
|
|
106
|
+
const header = JSON.stringify({ event, _binary: true });
|
|
107
|
+
const headerBytes = new TextEncoder().encode(header);
|
|
108
|
+
const combined = new Uint8Array(headerBytes.length + 1 + buffer.byteLength);
|
|
109
|
+
combined.set(headerBytes, 0);
|
|
110
|
+
combined[headerBytes.length] = 0;
|
|
111
|
+
combined.set(new Uint8Array(buffer), headerBytes.length + 1);
|
|
112
|
+
|
|
113
|
+
this.clients.forEach((_, client) => {
|
|
114
|
+
client.send(combined);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
to(room: string, event: string, data: unknown): this {
|
|
119
|
+
this.clients.forEach((info, client) => {
|
|
120
|
+
if (info.room === room) {
|
|
121
|
+
client.send(JSON.stringify({ event, data }));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
toId(id: string, event: string, data: unknown): this {
|
|
128
|
+
this.clients.forEach((info, client) => {
|
|
129
|
+
if (info.id === id) {
|
|
130
|
+
client.send(JSON.stringify({ event, data }));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getClients(room?: string): { id: string; room: string | null }[] {
|
|
137
|
+
const list: { id: string; room: string | null }[] = [];
|
|
138
|
+
this.clients.forEach((info) => {
|
|
139
|
+
if (!room || info.room === room) list.push({ id: info.id, room: info.room });
|
|
140
|
+
});
|
|
141
|
+
return list;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getPort(): number {
|
|
145
|
+
const address = this.server?.address();
|
|
146
|
+
if (address && typeof address === 'object') {
|
|
147
|
+
return address.port;
|
|
148
|
+
}
|
|
149
|
+
return this.port;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private runMiddlewares(ctx: StelarContext, next: () => void): void {
|
|
153
|
+
const run = (i: number): void => {
|
|
154
|
+
if (i >= this.middlewares.length) return next();
|
|
155
|
+
this.middlewares[i](ctx, () => run(i + 1));
|
|
156
|
+
};
|
|
157
|
+
run(0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private startHeartbeat(): void {
|
|
161
|
+
this._hbTimer = setInterval(() => {
|
|
162
|
+
this.clients.forEach((info, client) => {
|
|
163
|
+
if (info.lastPing && Date.now() - info.lastPing > this.heartbeatInterval * 2) {
|
|
164
|
+
client.close();
|
|
165
|
+
this.clients.delete(client);
|
|
166
|
+
} else {
|
|
167
|
+
client.send(JSON.stringify({ event: 'ping', data: Date.now() }));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}, this.heartbeatInterval);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private handleConnection(client: WebSocket, req: IncomingMessage): void {
|
|
174
|
+
const urlPath = new URL(req.url || '/', 'http://localhost').pathname;
|
|
175
|
+
const nsPath = this.namespace === '/' ? '/' : this.namespace;
|
|
176
|
+
|
|
177
|
+
if (nsPath !== '/' && urlPath !== nsPath) {
|
|
178
|
+
client.close();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const clientId = Math.random().toString(36).substring(7);
|
|
183
|
+
const clientInfo: StelarClientInfo = { id: clientId, room: null, lastPing: Date.now() };
|
|
184
|
+
this.clients.set(client, clientInfo);
|
|
185
|
+
|
|
186
|
+
const ctx: StelarContext = {
|
|
187
|
+
id: clientId,
|
|
188
|
+
socket: client,
|
|
189
|
+
req,
|
|
190
|
+
emit: (evt, d) => client.send(JSON.stringify({ event: evt, data: d })),
|
|
191
|
+
send: (respId, d) => client.send(JSON.stringify({ event: respId, data: d, _isAck: true })),
|
|
192
|
+
emitBinary: (evt, buffer) => client.send(buffer),
|
|
193
|
+
broadcast: (evt, d) => this.broadcast(evt, d),
|
|
194
|
+
broadcastBinary: (evt, buffer) => this.broadcastBinary(evt, buffer),
|
|
195
|
+
to: (room, evt, d) => this.to(room, evt, d),
|
|
196
|
+
toId: (id, evt, d) => this.toId(id, evt, d),
|
|
197
|
+
getClients: (room) => this.getClients(room),
|
|
198
|
+
joinRoom: (room) => {
|
|
199
|
+
clientInfo.room = room;
|
|
200
|
+
client.send(JSON.stringify({ event: 'joined-room', data: room }));
|
|
201
|
+
},
|
|
202
|
+
leaveRoom: () => {
|
|
203
|
+
clientInfo.room = null;
|
|
204
|
+
},
|
|
205
|
+
ack: (ackName, data) => {
|
|
206
|
+
const ackHandler = this._acks.get(ackName);
|
|
207
|
+
if (ackHandler) {
|
|
208
|
+
const result = ackHandler({ ...ctx, data });
|
|
209
|
+
if (result !== undefined) {
|
|
210
|
+
client.send(JSON.stringify({ event: ackName, data: result, _isAck: true }));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
this.runMiddlewares(ctx, () => {
|
|
217
|
+
if (this._connectionHandler) {
|
|
218
|
+
this._connectionHandler(ctx);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
client.on('message', (raw: RawData, isBinary: boolean) => {
|
|
223
|
+
if (isBinary) {
|
|
224
|
+
try {
|
|
225
|
+
const view = raw instanceof Uint8Array ? raw : new Uint8Array(raw as ArrayBuffer);
|
|
226
|
+
let headerEnd = -1;
|
|
227
|
+
for (let i = 0; i < view.length; i++) {
|
|
228
|
+
if (view[i] === 0) {
|
|
229
|
+
headerEnd = i;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (headerEnd === -1) return;
|
|
234
|
+
|
|
235
|
+
const headerStr = new TextDecoder().decode(view.slice(0, headerEnd));
|
|
236
|
+
const header = JSON.parse(headerStr);
|
|
237
|
+
const data = view.slice(headerEnd + 1);
|
|
238
|
+
|
|
239
|
+
const eventCtx: StelarContext = { ...ctx, data, buffer: data, isBinary: true };
|
|
240
|
+
|
|
241
|
+
const handler = this.events.get(header.event);
|
|
242
|
+
if (handler) {
|
|
243
|
+
handler(eventCtx);
|
|
244
|
+
} else if (this._wildcardHandler) {
|
|
245
|
+
this._wildcardHandler({ event: header.event, data: eventCtx });
|
|
246
|
+
}
|
|
247
|
+
} catch {}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const msg = JSON.parse(raw.toString());
|
|
253
|
+
const { event, data } = msg;
|
|
254
|
+
|
|
255
|
+
if (event === 'pong') {
|
|
256
|
+
clientInfo.lastPing = Date.now();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (event === 'join-room') {
|
|
261
|
+
clientInfo.room = data as string;
|
|
262
|
+
client.send(JSON.stringify({ event: 'joined-room', data }));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (event === 'leave-room') {
|
|
266
|
+
clientInfo.room = null;
|
|
267
|
+
client.send(JSON.stringify({ event: 'left-room', data }));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (msg._ackName && this._acks.has(msg._ackName)) {
|
|
271
|
+
const ackHandler = this._acks.get(msg._ackName)!;
|
|
272
|
+
const result = ackHandler({ ...ctx, data });
|
|
273
|
+
if (result !== undefined) {
|
|
274
|
+
client.send(JSON.stringify({ event: msg._ackName, data: result, _isAck: true }));
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const eventCtx: StelarContext = { ...ctx, data };
|
|
280
|
+
|
|
281
|
+
const handler = this.events.get(event);
|
|
282
|
+
if (handler) {
|
|
283
|
+
handler(eventCtx);
|
|
284
|
+
} else if (this._wildcardHandler) {
|
|
285
|
+
this._wildcardHandler({ event, data: eventCtx });
|
|
286
|
+
}
|
|
287
|
+
} catch {}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
client.on('close', () => {
|
|
291
|
+
const info = this.clients.get(client);
|
|
292
|
+
if (this.events.has('disconnect') && info) {
|
|
293
|
+
const handler = this.events.get('disconnect')!;
|
|
294
|
+
handler({ id: info.id, socket: client, req: req, emit: () => {}, send: () => {}, emitBinary: () => {}, broadcast: () => {}, broadcastBinary: () => {}, to: () => {}, toId: () => {}, getClients: () => [], joinRoom: () => {}, leaveRoom: () => {}, ack: () => {} });
|
|
295
|
+
}
|
|
296
|
+
this.clients.delete(client);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
client.on('error', (err) => {
|
|
300
|
+
if (this.events.has('error')) {
|
|
301
|
+
const handler = this.events.get('error')!;
|
|
302
|
+
handler({ id: clientId, socket: client, req: req, emit: () => {}, send: () => {}, emitBinary: () => {}, broadcast: () => {}, broadcastBinary: () => {}, to: () => {}, toId: () => {}, getClients: () => [], joinRoom: () => {}, leaveRoom: () => {}, ack: () => {}, error: err });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
start(callback?: (port: number) => void): Promise<number> {
|
|
308
|
+
return new Promise((resolve) => {
|
|
309
|
+
const startServer = (httpServer: Server): void => {
|
|
310
|
+
this.server = httpServer;
|
|
311
|
+
this.wss = new WebSocketServer({ server: httpServer });
|
|
312
|
+
this.wss.on('connection', (client, req) => this.handleConnection(client, req));
|
|
313
|
+
this.startHeartbeat();
|
|
314
|
+
|
|
315
|
+
const finalPort = this.getPort();
|
|
316
|
+
if (callback) callback(finalPort);
|
|
317
|
+
resolve(finalPort);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
if (this.server) {
|
|
321
|
+
this._externalServers.add(this.server);
|
|
322
|
+
startServer(this.server);
|
|
323
|
+
} else {
|
|
324
|
+
const tryListen = (port: number): void => {
|
|
325
|
+
this.server = createServer((_, res) => {
|
|
326
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
327
|
+
res.end('Stelar Time Real Server');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
this.server.on('error', (err: NodeJS.ErrnoException) => {
|
|
331
|
+
if (err.code === 'EADDRINUSE' && port < 65535) {
|
|
332
|
+
tryListen(port + 1);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
this.server.listen(port, () => {
|
|
337
|
+
this.port = port;
|
|
338
|
+
startServer(this.server!);
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
tryListen(this.port);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
stop(): this {
|
|
347
|
+
if (this._hbTimer) clearInterval(this._hbTimer);
|
|
348
|
+
if (this.wss) this.wss.close();
|
|
349
|
+
if (this.server && !this._externalServers.has(this.server)) this.server.close();
|
|
350
|
+
return this;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export default StelarServer;
|
|
355
|
+
export { StelarServer };
|
|
356
|
+
export { default as StelarClient } from './client.js';
|