tina4-nodejs 3.0.0-rc.2
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/BENCHMARK_REPORT.md +96 -0
- package/CARBONAH.md +140 -0
- package/CLAUDE.md +599 -0
- package/COMPARISON.md +194 -0
- package/README.md +595 -0
- package/package.json +59 -0
- package/packages/cli/src/bin.ts +110 -0
- package/packages/cli/src/commands/init.ts +194 -0
- package/packages/cli/src/commands/migrate.ts +96 -0
- package/packages/cli/src/commands/migrateCreate.ts +59 -0
- package/packages/cli/src/commands/routes.ts +61 -0
- package/packages/cli/src/commands/serve.ts +58 -0
- package/packages/cli/src/commands/test.ts +83 -0
- package/packages/core/gallery/auth/meta.json +1 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +22 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +16 -0
- package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +97 -0
- package/packages/core/gallery/database/meta.json +1 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +13 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +17 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +23 -0
- package/packages/core/gallery/error-overlay/meta.json +1 -0
- package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +17 -0
- package/packages/core/gallery/orm/meta.json +1 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +12 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +7 -0
- package/packages/core/gallery/queue/meta.json +1 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +16 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +10 -0
- package/packages/core/gallery/rest-api/meta.json +1 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +6 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +7 -0
- package/packages/core/gallery/templates/meta.json +1 -0
- package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +15 -0
- package/packages/core/gallery/templates/src/templates/gallery_page.twig +257 -0
- package/packages/core/public/css/tina4.css +2463 -0
- package/packages/core/public/css/tina4.min.css +1 -0
- package/packages/core/public/favicon.ico +0 -0
- package/packages/core/public/images/logo.svg +5 -0
- package/packages/core/public/images/tina4-logo-icon.webp +0 -0
- package/packages/core/public/js/frond.min.js +420 -0
- package/packages/core/public/js/tina4-dev-admin.min.js +327 -0
- package/packages/core/public/js/tina4.min.js +93 -0
- package/packages/core/public/swagger/index.html +90 -0
- package/packages/core/public/swagger/oauth2-redirect.html +63 -0
- package/packages/core/src/ai.ts +359 -0
- package/packages/core/src/api.ts +248 -0
- package/packages/core/src/auth.ts +287 -0
- package/packages/core/src/cache.ts +121 -0
- package/packages/core/src/constants.ts +48 -0
- package/packages/core/src/container.ts +90 -0
- package/packages/core/src/devAdmin.ts +2024 -0
- package/packages/core/src/devMailbox.ts +316 -0
- package/packages/core/src/dotenv.ts +172 -0
- package/packages/core/src/errorOverlay.test.ts +122 -0
- package/packages/core/src/errorOverlay.ts +278 -0
- package/packages/core/src/events.ts +112 -0
- package/packages/core/src/fakeData.ts +309 -0
- package/packages/core/src/graphql.ts +812 -0
- package/packages/core/src/health.ts +31 -0
- package/packages/core/src/htmlElement.ts +172 -0
- package/packages/core/src/i18n.ts +136 -0
- package/packages/core/src/index.ts +88 -0
- package/packages/core/src/logger.ts +226 -0
- package/packages/core/src/messenger.ts +822 -0
- package/packages/core/src/middleware.ts +138 -0
- package/packages/core/src/queue.ts +481 -0
- package/packages/core/src/queueBackends/kafkaBackend.ts +348 -0
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +479 -0
- package/packages/core/src/rateLimiter.ts +107 -0
- package/packages/core/src/request.ts +189 -0
- package/packages/core/src/response.ts +146 -0
- package/packages/core/src/routeDiscovery.ts +87 -0
- package/packages/core/src/router.ts +398 -0
- package/packages/core/src/scss.ts +366 -0
- package/packages/core/src/server.ts +610 -0
- package/packages/core/src/service.ts +380 -0
- package/packages/core/src/session.ts +480 -0
- package/packages/core/src/sessionHandlers/mongoHandler.ts +286 -0
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +184 -0
- package/packages/core/src/static.ts +58 -0
- package/packages/core/src/testing.ts +233 -0
- package/packages/core/src/types.ts +98 -0
- package/packages/core/src/watcher.ts +37 -0
- package/packages/core/src/websocket.ts +408 -0
- package/packages/core/src/wsdl.ts +546 -0
- package/packages/core/templates/errors/302.twig +14 -0
- package/packages/core/templates/errors/401.twig +9 -0
- package/packages/core/templates/errors/403.twig +29 -0
- package/packages/core/templates/errors/404.twig +29 -0
- package/packages/core/templates/errors/500.twig +38 -0
- package/packages/core/templates/errors/502.twig +9 -0
- package/packages/core/templates/errors/503.twig +12 -0
- package/packages/core/templates/errors/base.twig +37 -0
- package/packages/frond/src/engine.ts +1475 -0
- package/packages/frond/src/index.ts +2 -0
- package/packages/orm/src/adapters/firebird.ts +455 -0
- package/packages/orm/src/adapters/mssql.ts +440 -0
- package/packages/orm/src/adapters/mysql.ts +355 -0
- package/packages/orm/src/adapters/postgres.ts +362 -0
- package/packages/orm/src/adapters/sqlite.ts +270 -0
- package/packages/orm/src/autoCrud.ts +231 -0
- package/packages/orm/src/baseModel.ts +536 -0
- package/packages/orm/src/database.ts +321 -0
- package/packages/orm/src/fakeData.ts +118 -0
- package/packages/orm/src/index.ts +49 -0
- package/packages/orm/src/migration.ts +392 -0
- package/packages/orm/src/model.ts +56 -0
- package/packages/orm/src/query.ts +113 -0
- package/packages/orm/src/seeder.ts +120 -0
- package/packages/orm/src/sqlTranslation.ts +272 -0
- package/packages/orm/src/types.ts +110 -0
- package/packages/orm/src/validation.ts +93 -0
- package/packages/swagger/src/generator.ts +189 -0
- package/packages/swagger/src/index.ts +2 -0
- package/packages/swagger/src/ui.ts +48 -0
- package/skills/tina4-developer.skill +0 -0
- package/skills/tina4-js.skill +0 -0
- package/skills/tina4-maintainer.skill +0 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tina4 WebSocket — Zero-dependency RFC 6455 implementation.
|
|
3
|
+
*
|
|
4
|
+
* Native WebSocket server using Node.js built-in `http` module.
|
|
5
|
+
*
|
|
6
|
+
* import { WebSocketServer } from "@tina4/core";
|
|
7
|
+
*
|
|
8
|
+
* const wss = new WebSocketServer({ port: 8080 });
|
|
9
|
+
* wss.on("connection", (client) => {
|
|
10
|
+
* console.log("Connected:", client.id);
|
|
11
|
+
* });
|
|
12
|
+
* wss.on("message", (client, message) => {
|
|
13
|
+
* wss.broadcast(message);
|
|
14
|
+
* });
|
|
15
|
+
* await wss.start();
|
|
16
|
+
*
|
|
17
|
+
* Supported:
|
|
18
|
+
* - HTTP Upgrade handshake (RFC 6455 Sec-WebSocket-Accept)
|
|
19
|
+
* - Frame protocol: text, binary, close, ping, pong
|
|
20
|
+
* - Masking / unmasking (client->server)
|
|
21
|
+
* - Extended payload lengths (7-bit, 16-bit, 64-bit)
|
|
22
|
+
* - Connection manager with broadcast
|
|
23
|
+
*/
|
|
24
|
+
import { createServer, IncomingMessage, ServerResponse } from "node:http";
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
|
+
import { randomUUID } from "node:crypto";
|
|
27
|
+
import type { Socket } from "node:net";
|
|
28
|
+
import type { Server } from "node:http";
|
|
29
|
+
|
|
30
|
+
// ── Constants ────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const MAGIC_STRING = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
33
|
+
|
|
34
|
+
// Opcodes
|
|
35
|
+
export const OP_CONTINUATION = 0x0;
|
|
36
|
+
export const OP_TEXT = 0x1;
|
|
37
|
+
export const OP_BINARY = 0x2;
|
|
38
|
+
export const OP_CLOSE = 0x8;
|
|
39
|
+
export const OP_PING = 0x9;
|
|
40
|
+
export const OP_PONG = 0xa;
|
|
41
|
+
|
|
42
|
+
// Close codes
|
|
43
|
+
export const CLOSE_NORMAL = 1000;
|
|
44
|
+
export const CLOSE_GOING_AWAY = 1001;
|
|
45
|
+
export const CLOSE_PROTOCOL_ERROR = 1002;
|
|
46
|
+
|
|
47
|
+
// ── Types ────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export interface WebSocketClient {
|
|
50
|
+
id: string;
|
|
51
|
+
socket: Socket;
|
|
52
|
+
ip: string;
|
|
53
|
+
connectedAt: number;
|
|
54
|
+
closed: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type EventHandler = (...args: unknown[]) => void;
|
|
58
|
+
|
|
59
|
+
// ── Frame Utilities (exported for testing) ───────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Compute Sec-WebSocket-Accept from Sec-WebSocket-Key per RFC 6455.
|
|
63
|
+
*/
|
|
64
|
+
export function computeAcceptKey(key: string): string {
|
|
65
|
+
return createHash("sha1")
|
|
66
|
+
.update(key + MAGIC_STRING)
|
|
67
|
+
.digest("base64");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse HTTP headers from raw upgrade request data.
|
|
72
|
+
*/
|
|
73
|
+
export function parseUpgradeHeaders(raw: string): Record<string, string> {
|
|
74
|
+
const headers: Record<string, string> = {};
|
|
75
|
+
const lines = raw.split("\r\n");
|
|
76
|
+
const requestLine = lines[0] ?? "";
|
|
77
|
+
const parts = requestLine.split(" ");
|
|
78
|
+
if (parts.length >= 2) {
|
|
79
|
+
headers["_method"] = parts[0];
|
|
80
|
+
headers["_path"] = parts[1];
|
|
81
|
+
}
|
|
82
|
+
for (let i = 1; i < lines.length; i++) {
|
|
83
|
+
const line = lines[i];
|
|
84
|
+
const colonIdx = line.indexOf(":");
|
|
85
|
+
if (colonIdx > 0) {
|
|
86
|
+
const key = line.slice(0, colonIdx).trim().toLowerCase();
|
|
87
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
88
|
+
headers[key] = value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return headers;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build a WebSocket frame (server->client, never masked).
|
|
96
|
+
*/
|
|
97
|
+
export function buildFrame(opcode: number, payload: Buffer, fin: boolean = true): Buffer {
|
|
98
|
+
const frame: number[] = [];
|
|
99
|
+
const firstByte = (fin ? 0x80 : 0x00) | opcode;
|
|
100
|
+
frame.push(firstByte);
|
|
101
|
+
|
|
102
|
+
const length = payload.length;
|
|
103
|
+
if (length < 126) {
|
|
104
|
+
frame.push(length);
|
|
105
|
+
} else if (length < 65536) {
|
|
106
|
+
frame.push(126);
|
|
107
|
+
frame.push((length >> 8) & 0xff);
|
|
108
|
+
frame.push(length & 0xff);
|
|
109
|
+
} else {
|
|
110
|
+
frame.push(127);
|
|
111
|
+
// 8 bytes for 64-bit length
|
|
112
|
+
const buf = Buffer.alloc(8);
|
|
113
|
+
buf.writeBigUInt64BE(BigInt(length));
|
|
114
|
+
for (let i = 0; i < 8; i++) {
|
|
115
|
+
frame.push(buf[i]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const header = Buffer.from(frame);
|
|
120
|
+
return Buffer.concat([header, payload]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parse a WebSocket frame from a buffer.
|
|
125
|
+
* Returns { fin, opcode, payload, bytesConsumed } or null if not enough data.
|
|
126
|
+
*/
|
|
127
|
+
export function parseFrame(
|
|
128
|
+
data: Buffer,
|
|
129
|
+
): { fin: boolean; opcode: number; payload: Buffer; bytesConsumed: number } | null {
|
|
130
|
+
if (data.length < 2) return null;
|
|
131
|
+
|
|
132
|
+
const fin = (data[0] >> 7) & 1;
|
|
133
|
+
const opcode = data[0] & 0x0f;
|
|
134
|
+
const masked = (data[1] >> 7) & 1;
|
|
135
|
+
let payloadLen = data[1] & 0x7f;
|
|
136
|
+
let offset = 2;
|
|
137
|
+
|
|
138
|
+
if (payloadLen === 126) {
|
|
139
|
+
if (data.length < 4) return null;
|
|
140
|
+
payloadLen = data.readUInt16BE(2);
|
|
141
|
+
offset = 4;
|
|
142
|
+
} else if (payloadLen === 127) {
|
|
143
|
+
if (data.length < 10) return null;
|
|
144
|
+
payloadLen = Number(data.readBigUInt64BE(2));
|
|
145
|
+
offset = 10;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (masked) {
|
|
149
|
+
if (data.length < offset + 4 + payloadLen) return null;
|
|
150
|
+
const maskKey = data.subarray(offset, offset + 4);
|
|
151
|
+
offset += 4;
|
|
152
|
+
const payload = Buffer.alloc(payloadLen);
|
|
153
|
+
for (let i = 0; i < payloadLen; i++) {
|
|
154
|
+
payload[i] = data[offset + i] ^ maskKey[i % 4];
|
|
155
|
+
}
|
|
156
|
+
return { fin: !!fin, opcode, payload, bytesConsumed: offset + payloadLen };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (data.length < offset + payloadLen) return null;
|
|
160
|
+
const payload = data.subarray(offset, offset + payloadLen);
|
|
161
|
+
return { fin: !!fin, opcode, payload: Buffer.from(payload), bytesConsumed: offset + payloadLen };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── WebSocket Server ─────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
export class WebSocketServer {
|
|
167
|
+
private port: number;
|
|
168
|
+
private server: Server | null = null;
|
|
169
|
+
private clients: Map<string, WebSocketClient> = new Map();
|
|
170
|
+
private handlers: Map<string, EventHandler[]> = new Map();
|
|
171
|
+
|
|
172
|
+
constructor(options?: { port?: number }) {
|
|
173
|
+
this.port = options?.port ?? parseInt(process.env.TINA4_WS_PORT ?? "8080", 10);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Register an event handler.
|
|
178
|
+
*/
|
|
179
|
+
on(event: string, handler: Function): WebSocketServer {
|
|
180
|
+
const list = this.handlers.get(event) ?? [];
|
|
181
|
+
list.push(handler as EventHandler);
|
|
182
|
+
this.handlers.set(event, list);
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Broadcast a message to all connected clients.
|
|
188
|
+
*/
|
|
189
|
+
broadcast(message: string, excludeIds?: string[]): void {
|
|
190
|
+
const frame = buildFrame(OP_TEXT, Buffer.from(message, "utf-8"));
|
|
191
|
+
const exclude = new Set(excludeIds ?? []);
|
|
192
|
+
|
|
193
|
+
for (const [id, client] of this.clients) {
|
|
194
|
+
if (exclude.has(id)) continue;
|
|
195
|
+
if (client.closed) continue;
|
|
196
|
+
try {
|
|
197
|
+
client.socket.write(frame);
|
|
198
|
+
} catch {
|
|
199
|
+
// client disconnected
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Send a message to a specific client.
|
|
206
|
+
*/
|
|
207
|
+
send(clientId: string, message: string): void {
|
|
208
|
+
const client = this.clients.get(clientId);
|
|
209
|
+
if (!client || client.closed) return;
|
|
210
|
+
|
|
211
|
+
const frame = buildFrame(OP_TEXT, Buffer.from(message, "utf-8"));
|
|
212
|
+
try {
|
|
213
|
+
client.socket.write(frame);
|
|
214
|
+
} catch {
|
|
215
|
+
// client disconnected
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Start the WebSocket server.
|
|
221
|
+
*/
|
|
222
|
+
async start(): Promise<void> {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
this.server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
225
|
+
res.writeHead(426, { "Content-Type": "text/plain" });
|
|
226
|
+
res.end("Upgrade Required");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
this.server.on("upgrade", (req: IncomingMessage, socket: Socket, head: Buffer) => {
|
|
230
|
+
this.handleUpgrade(req, socket, head);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
this.server.listen(this.port, () => {
|
|
234
|
+
resolve();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
this.server.on("error", (err) => {
|
|
238
|
+
this.emit("error", err);
|
|
239
|
+
reject(err);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Stop the server and disconnect all clients.
|
|
246
|
+
*/
|
|
247
|
+
stop(): void {
|
|
248
|
+
// Close all client connections
|
|
249
|
+
for (const [id, client] of this.clients) {
|
|
250
|
+
if (!client.closed) {
|
|
251
|
+
try {
|
|
252
|
+
const closeFrame = buildFrame(OP_CLOSE, Buffer.from([0x03, 0xe8])); // 1000
|
|
253
|
+
client.socket.write(closeFrame);
|
|
254
|
+
client.socket.end();
|
|
255
|
+
} catch {
|
|
256
|
+
// already closed
|
|
257
|
+
}
|
|
258
|
+
client.closed = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
this.clients.clear();
|
|
262
|
+
|
|
263
|
+
if (this.server) {
|
|
264
|
+
this.server.close();
|
|
265
|
+
this.server = null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get all connected clients.
|
|
271
|
+
*/
|
|
272
|
+
getClients(): Map<string, WebSocketClient> {
|
|
273
|
+
return this.clients;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── Private ────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
private emit(event: string, ...args: unknown[]): void {
|
|
279
|
+
const handlers = this.handlers.get(event) ?? [];
|
|
280
|
+
for (const handler of handlers) {
|
|
281
|
+
handler(...args);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private handleUpgrade(req: IncomingMessage, socket: Socket, head: Buffer): void {
|
|
286
|
+
const wsKey = req.headers["sec-websocket-key"];
|
|
287
|
+
if (!wsKey) {
|
|
288
|
+
socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
289
|
+
socket.destroy();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const wsVersion = req.headers["sec-websocket-version"];
|
|
294
|
+
if (wsVersion && wsVersion !== "13") {
|
|
295
|
+
socket.write("HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13\r\n\r\n");
|
|
296
|
+
socket.destroy();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Compute accept key and send upgrade response
|
|
301
|
+
const acceptKey = computeAcceptKey(wsKey);
|
|
302
|
+
const response = [
|
|
303
|
+
"HTTP/1.1 101 Switching Protocols",
|
|
304
|
+
"Upgrade: websocket",
|
|
305
|
+
"Connection: Upgrade",
|
|
306
|
+
`Sec-WebSocket-Accept: ${acceptKey}`,
|
|
307
|
+
"",
|
|
308
|
+
"",
|
|
309
|
+
].join("\r\n");
|
|
310
|
+
|
|
311
|
+
socket.write(response);
|
|
312
|
+
|
|
313
|
+
// Create client
|
|
314
|
+
const clientId = randomUUID().slice(0, 8);
|
|
315
|
+
const client: WebSocketClient = {
|
|
316
|
+
id: clientId,
|
|
317
|
+
socket,
|
|
318
|
+
ip: (socket.remoteAddress ?? "unknown"),
|
|
319
|
+
connectedAt: Date.now(),
|
|
320
|
+
closed: false,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
this.clients.set(clientId, client);
|
|
324
|
+
this.emit("connection", client);
|
|
325
|
+
|
|
326
|
+
// Handle incoming data
|
|
327
|
+
let buffer = Buffer.alloc(0);
|
|
328
|
+
if (head.length > 0) {
|
|
329
|
+
buffer = Buffer.concat([buffer, head]);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
socket.on("data", (chunk: Buffer) => {
|
|
333
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
334
|
+
this.processBuffer(client, buffer, (remaining) => {
|
|
335
|
+
buffer = remaining;
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
socket.on("close", () => {
|
|
340
|
+
client.closed = true;
|
|
341
|
+
this.clients.delete(clientId);
|
|
342
|
+
this.emit("close", client);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
socket.on("error", (err) => {
|
|
346
|
+
client.closed = true;
|
|
347
|
+
this.clients.delete(clientId);
|
|
348
|
+
this.emit("error", err, client);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private processBuffer(
|
|
353
|
+
client: WebSocketClient,
|
|
354
|
+
buffer: Buffer,
|
|
355
|
+
setBuffer: (remaining: Buffer) => void,
|
|
356
|
+
): void {
|
|
357
|
+
let remaining = buffer;
|
|
358
|
+
|
|
359
|
+
while (remaining.length > 0) {
|
|
360
|
+
const frame = parseFrame(remaining);
|
|
361
|
+
if (!frame) break;
|
|
362
|
+
|
|
363
|
+
remaining = remaining.subarray(frame.bytesConsumed);
|
|
364
|
+
|
|
365
|
+
switch (frame.opcode) {
|
|
366
|
+
case OP_TEXT:
|
|
367
|
+
this.emit("message", client, frame.payload.toString("utf-8"));
|
|
368
|
+
break;
|
|
369
|
+
|
|
370
|
+
case OP_BINARY:
|
|
371
|
+
this.emit("message", client, frame.payload);
|
|
372
|
+
break;
|
|
373
|
+
|
|
374
|
+
case OP_PING: {
|
|
375
|
+
const pongFrame = buildFrame(OP_PONG, frame.payload);
|
|
376
|
+
try {
|
|
377
|
+
client.socket.write(pongFrame);
|
|
378
|
+
} catch {
|
|
379
|
+
// client disconnected
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
case OP_PONG:
|
|
385
|
+
// ignore
|
|
386
|
+
break;
|
|
387
|
+
|
|
388
|
+
case OP_CLOSE: {
|
|
389
|
+
if (!client.closed) {
|
|
390
|
+
client.closed = true;
|
|
391
|
+
const closeFrame = buildFrame(OP_CLOSE, Buffer.from([0x03, 0xe8]));
|
|
392
|
+
try {
|
|
393
|
+
client.socket.write(closeFrame);
|
|
394
|
+
client.socket.end();
|
|
395
|
+
} catch {
|
|
396
|
+
// already closed
|
|
397
|
+
}
|
|
398
|
+
this.clients.delete(client.id);
|
|
399
|
+
this.emit("close", client);
|
|
400
|
+
}
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
setBuffer(remaining);
|
|
407
|
+
}
|
|
408
|
+
}
|