@tamng.npm/websocket 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 maxsida <maxsida.dev@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,647 @@
1
+ # @vutotoite/websocket
2
+
3
+ Thư viện WebSocket viết bằng TypeScript, hỗ trợ Server và Client (Node.js + trình duyệt), **scale theo chiều ngang qua NATS**.
4
+
5
+ ## 📦 Cài đặt
6
+
7
+ ```bash
8
+ npm install @vutotoite/websocket
9
+
10
+ # Tùy chọn: chỉ cần khi muốn scale ngang qua NATS
11
+ npm install @nats-io/transport-node
12
+ ```
13
+
14
+ > `nats` là **optional peer dependency**. Nếu chỉ chạy 1 instance (MemoryAdapter), bạn không cần cài.
15
+
16
+ ## ✨ Tính năng
17
+
18
+ ### Server
19
+ - ✅ Singleton pattern, tích hợp HTTP server
20
+ - ✅ Xác thực (Authentication) tùy chỉnh
21
+ - ✅ Quản lý phòng (Room) và client
22
+ - ✅ Middleware chain với throw error (dừng ngay khi lỗi)
23
+ - ✅ Ping/Pong tự động
24
+ - ✅ **Adapter pattern**: `MemoryAdapter` (1 node) hoặc `NatsAdapter` (nhiều node)
25
+ - ✅ **Scale ngang qua NATS** — broadcast/room message hoạt động xuyên node
26
+ - ✅ Codec tùy chỉnh (mặc định JSON)
27
+
28
+ ### Client
29
+ - ✅ Hỗ trợ Node.js và Browser, dùng chung logic qua `BaseClient`
30
+ - ✅ API `on`/`emit` đơn giản
31
+ - ✅ Tự động kết nối lại, queue tin nhắn khi offline
32
+ - ✅ Heartbeat tự động (Node)
33
+
34
+ ## 🏗️ Kiến trúc & Scale ngang
35
+
36
+ ```
37
+ ┌─────────── NATS ───────────┐
38
+ │ (message bus chung) │
39
+ └──────┬───────────────┬───────┘
40
+ │ │
41
+ ┌────────▼──────┐ ┌──────▼────────┐
42
+ │ Node A │ │ Node B │
43
+ │ WsServer │ │ WsServer │
44
+ │ +NatsAdapter │ │ +NatsAdapter │
45
+ └────┬─────┬────┘ └────┬─────┬────┘
46
+ client client client client
47
+ ```
48
+
49
+ Mỗi node giữ kết nối WebSocket của riêng nó. Khi `toRooms`/`toAll`/`toClients` phát message, `NatsAdapter` publish lên subject NATS phù hợp; node nào quan tâm sẽ nhận và deliver tới client local của mình. Nhờ đó broadcast hoạt động xuyên nhiều instance.
50
+
51
+ - **`MemoryAdapter`** (mặc định): state trong RAM, dùng cho 1 instance.
52
+ - **`NatsAdapter`**: nhận `NatsConnection` **truyền từ ngoài vào** (dependency injection), định tuyến qua core NATS pub/sub.
53
+
54
+ ### Định tuyến theo subject
55
+
56
+ `NatsAdapter` định tuyến theo loại target để giảm traffic xuyên node (subject mặc định prefix `ws`):
57
+
58
+ | Target | Subject publish | Subscription của node |
59
+ |--------|----------------|----------------------|
60
+ | `toAll()` | `ws.broadcast` | luôn subscribe `ws.broadcast` |
61
+ | `toClients(id)` | `ws.client.<id>` | wildcard `ws.client.*`, lọc client local |
62
+ | `toRooms(id)` | `ws.room.<id>` | **chỉ subscribe room khi có client local**; tự unsubscribe khi client cuối rời |
63
+
64
+ Điểm quan trọng: node **không** có client trong một room sẽ **không** nhận traffic của room đó. Subscription room được tạo/hủy tự động theo `join`/`leave`.
65
+
66
+ > **Về `toClients` xuyên node:** không có presence registry (`clientId → nodeId`), nên message client được publish và mọi node nhận qua wildcard rồi lọc local. Đây là tradeoff của thiết kế core NATS thuần. Nếu cần định tuyến client chính xác (chỉ node chứa client nhận), có thể bổ sung presence registry sau.
67
+
68
+ ## 🚀 Sử dụng
69
+
70
+ ### 1. Server cơ bản (1 instance — MemoryAdapter)
71
+
72
+ ```typescript
73
+ import { Server } from '@vutotoite/websocket';
74
+ import http from 'http';
75
+ import express from 'express';
76
+
77
+ const app = express();
78
+ const httpServer = http.createServer(app);
79
+
80
+ // init() là singleton: lần gọi đầu tạo instance, các lần sau trả lại đúng instance đó.
81
+ // noServer: true để tự kiểm soát quá trình HTTP upgrade qua attachServer().
82
+ const wsServer = Server.WebsocketServer.init({
83
+ ws: { noServer: true }
84
+ // adapter mặc định = MemoryAdapter (state trong RAM, 1 instance)
85
+ });
86
+
87
+ // Gắn vào HTTP server: lib lắng nghe sự kiện 'upgrade' để bắt tay WebSocket.
88
+ wsServer.attachServer(httpServer);
89
+
90
+ wsServer.connected({
91
+ // connectionHandler chạy MỖI khi có client mới kết nối thành công.
92
+ connectionHandler: (ws, wss) => {
93
+ console.log('Client đã kết nối');
94
+
95
+ // onS đăng ký listener cho event 'message' gửi từ client ({ event, data }).
96
+ ws.onS('message', (data) => {
97
+ console.log('Nhận:', data);
98
+ // emitS gửi event về CHÍNH client này.
99
+ ws.emitS('response', { status: 'ok' });
100
+ });
101
+ },
102
+ // errorHandler nhận mọi lỗi: parse message hỏng, hoặc throw trong middleware chain.
103
+ errorHandler: (error, ws) => {
104
+ ws.emitS('error', { message: error.message });
105
+ },
106
+ // closeHandler chạy khi client ngắt kết nối (đã được dọn khỏi room/adapter).
107
+ closeHandler: (ws) => {
108
+ console.log('Client ngắt kết nối');
109
+ }
110
+ });
111
+
112
+ // Lưu ý: vòng đời http.Server do BẠN quản lý — lib không tự listen/close nó.
113
+ httpServer.listen(8080, () => console.log('Server chạy trên cổng 8080'));
114
+ ```
115
+
116
+ ### 2. Scale ngang với NATS
117
+
118
+ Connection NATS được **tạo bên ngoài và truyền vào** `NatsAdapter`:
119
+
120
+ ```typescript
121
+ import { Server } from '@vutotoite/websocket';
122
+ import { connect } from '@nats-io/transport-node'; // hoặc 'nats'
123
+ import http from 'http';
124
+
125
+ // Bạn TỰ tạo kết nối NATS và quản lý vòng đời của nó (lib không tự connect).
126
+ const nats = await connect({ servers: 'nats://localhost:4222' });
127
+
128
+ const wsServer = Server.WebsocketServer.init({
129
+ ws: { noServer: true },
130
+ // Inject connection vào NatsAdapter -> mọi broadcast định tuyến qua NATS.
131
+ adapter: new Server.NatsAdapter(nats)
132
+ });
133
+
134
+ wsServer.attachServer(http.createServer().listen(8080));
135
+
136
+ wsServer.connected({
137
+ connectionHandler: (ws, wss) => {
138
+ ws.onS('send_message', (data: { room_id: string; message: string }) => {
139
+ // wss.toRooms(...).emitS(...): NatsAdapter publish lên subject ws.room.<id>.
140
+ // Mọi node CÓ client trong room đó sẽ nhận và deliver tới client local.
141
+ // -> client ở MỌI node (kể cả node khác) đều nhận được new_message.
142
+ wss.toRooms(data.room_id).emitS('new_message', {
143
+ from: ws.getId(),
144
+ message: data.message
145
+ });
146
+ });
147
+ },
148
+ errorHandler: (error, ws) => ws.emitS('error', { message: error.message })
149
+ });
150
+ ```
151
+
152
+ Chạy nhiều instance (mỗi instance 1 cổng / 1 container) cùng trỏ về NATS → tự động đồng bộ.
153
+ > **Lưu ý vận hành:** dùng load balancer với **sticky session** (hoặc bất kỳ LB nào, vì state được đồng bộ qua NATS) để client giữ kết nối ổn định. `NatsAdapter` hiện dùng core pub/sub (không lưu trữ message), phù hợp real-time.
154
+
155
+ #### Graceful shutdown
156
+
157
+ `close()` đóng có trật tự: ngừng nhận kết nối mới, đóng client local với close code `1001` ("going away"), dọn subscription NATS của node (không ảnh hưởng node khác), rồi chờ ws server đóng hẳn. **Không** đóng `http.Server` của bạn — vòng đời đó do bạn quản lý.
158
+
159
+ ```typescript
160
+ // Tự gọi close() khi nhận SIGTERM/SIGINT (K8s, docker stop)
161
+ wsServer.enableGracefulShutdown(); // mặc định ['SIGTERM', 'SIGINT']
162
+
163
+ // Hoặc tự quản lý
164
+ process.on('SIGTERM', async () => {
165
+ await wsServer.close();
166
+ httpServer.close(); // đóng http server của bạn nếu cần
167
+ process.exit(0);
168
+ });
169
+ ```
170
+
171
+
172
+ ### 3. Tổ chức trong NestJS
173
+
174
+ Mẫu khuyến nghị: bọc thư viện trong một **module** + **service**. Connection NATS được inject qua provider; server WebSocket được khởi tạo trong `onModuleInit` và gắn vào HTTP server của Nest; handler kết nối đăng ký trong một gateway service.
175
+
176
+ ```typescript
177
+ // nats.provider.ts
178
+ // Provider tạo & quản lý vòng đời kết nối NATS (lib KHÔNG tự connect).
179
+ import { connect, NatsConnection } from 'nats';
180
+ import { Provider } from '@nestjs/common';
181
+
182
+ export const NATS_CONNECTION = 'NATS_CONNECTION';
183
+
184
+ export const NatsProvider: Provider = {
185
+ provide: NATS_CONNECTION,
186
+ // useFactory chạy 1 lần khi module khởi tạo -> trả về connection dùng chung.
187
+ useFactory: async (): Promise<NatsConnection> =>
188
+ connect({ servers: process.env.NATS_URL ?? 'nats://localhost:4222' }),
189
+ };
190
+ ```
191
+
192
+ ```typescript
193
+ // websocket.service.ts
194
+ // Service bọc WebsocketServer: khởi tạo singleton, cấu hình auth + handler.
195
+ import { Inject, Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
196
+ import { HttpAdapterHost } from '@nestjs/core';
197
+ import { NatsConnection } from 'nats';
198
+ import { Server } from '@vutotoite/websocket';
199
+ import { NATS_CONNECTION } from './nats.provider';
200
+
201
+ @Injectable()
202
+ export class WebsocketService implements OnModuleInit, OnModuleDestroy {
203
+ private wsServer!: Server.WebsocketServer;
204
+
205
+ constructor(
206
+ @Inject(NATS_CONNECTION) private readonly nats: NatsConnection,
207
+ // HttpAdapterHost cho phép lấy http.Server thật mà Nest đang chạy.
208
+ private readonly httpAdapterHost: HttpAdapterHost,
209
+ ) {}
210
+
211
+ onModuleInit() {
212
+ // init() là singleton: chỉ tạo instance ở lần gọi đầu tiên.
213
+ this.wsServer = Server.WebsocketServer.init({
214
+ ws: { noServer: true }, // tự kiểm soát HTTP upgrade
215
+ adapter: new Server.NatsAdapter(this.nats), // inject connection -> scale ngang
216
+ });
217
+
218
+ // Lấy http.Server thật của Nest và gắn vào -> lib bắt sự kiện 'upgrade'.
219
+ const httpServer = this.httpAdapterHost.httpAdapter.getHttpServer();
220
+ this.wsServer.attachServer(httpServer);
221
+
222
+ // Xác thực: throw -> server trả 401 và từ chối kết nối.
223
+ this.wsServer.setAuth(async (req) => {
224
+ const token = req.headers.authorization?.split(' ')[1];
225
+ if (!token) throw new Error('Unauthorized');
226
+ return await this.verifyToken(token); // -> ws.getAuthData()
227
+ });
228
+
229
+ // Tự gọi close() khi nhận SIGTERM/SIGINT (graceful shutdown trong K8s).
230
+ this.wsServer.enableGracefulShutdown();
231
+ }
232
+
233
+ // Dọn dẹp khi module bị huỷ (vd hot-reload): đóng có trật tự + drain NATS.
234
+ async onModuleDestroy() {
235
+ await this.wsServer.close();
236
+ await this.nats.drain();
237
+ }
238
+
239
+ // Cho phép service khác lấy server để emit (vd từ REST controller).
240
+ getServer(): Server.WebsocketServer {
241
+ return this.wsServer;
242
+ }
243
+
244
+ private async verifyToken(token: string): Promise<{ userId: string }> {
245
+ // ... xác thực token, trả về payload
246
+ return { userId: 'demo' };
247
+ }
248
+ }
249
+ ```
250
+
251
+ ```typescript
252
+ // chat.gateway.ts
253
+ // Đăng ký handler kết nối/sự kiện. Tách khỏi service hạ tầng cho gọn.
254
+ import { Injectable, OnModuleInit } from '@nestjs/common';
255
+ import { WebsocketService } from './websocket.service';
256
+
257
+ @Injectable()
258
+ export class ChatGateway implements OnModuleInit {
259
+ constructor(private readonly ws: WebsocketService) {}
260
+
261
+ onModuleInit() {
262
+ this.ws.getServer().connected({
263
+ connectionHandler: (socket, server) => {
264
+ // authData lấy từ setAuth ở trên; gán id + cho vào room riêng của user.
265
+ const auth = socket.getAuthData<{ userId: string }>();
266
+ socket.setId(auth.userId);
267
+ socket.join(auth.userId);
268
+
269
+ // Lắng nghe client gửi 'send_message' -> phát tới room (xuyên node qua NATS).
270
+ socket.onS('send_message', (data: { room_id: string; message: string }) => {
271
+ server.toRooms(data.room_id).emitS('new_message', {
272
+ from: socket.getId(),
273
+ message: data.message,
274
+ });
275
+ });
276
+ },
277
+ errorHandler: (error, socket) => socket.emitS('error', { message: error.message }),
278
+ });
279
+ }
280
+ }
281
+ ```
282
+
283
+ ```typescript
284
+ // websocket.module.ts
285
+ import { Module } from '@nestjs/common';
286
+ import { NatsProvider } from './nats.provider';
287
+ import { WebsocketService } from './websocket.service';
288
+ import { ChatGateway } from './chat.gateway';
289
+
290
+ @Module({
291
+ providers: [NatsProvider, WebsocketService, ChatGateway],
292
+ exports: [WebsocketService], // export để controller/service khác emit được
293
+ })
294
+ export class WebsocketModule {}
295
+ ```
296
+
297
+ ```typescript
298
+ // notification.controller.ts
299
+ // Emit từ REST API: tái dùng WebsocketService -> đẩy event tới client cụ thể.
300
+ import { Body, Controller, Post } from '@nestjs/common';
301
+ import { WebsocketService } from './websocket.service';
302
+
303
+ @Controller('notify')
304
+ export class NotificationController {
305
+ constructor(private readonly ws: WebsocketService) {}
306
+
307
+ @Post()
308
+ notify(@Body() body: { userId: string; message: string }) {
309
+ // toClients(userId): tới client ở bất kỳ node nào (định tuyến qua NATS).
310
+ this.ws.getServer().toClients(body.userId).emitS('notification', {
311
+ message: body.message,
312
+ });
313
+ return { success: true };
314
+ }
315
+ }
316
+ ```
317
+
318
+ > Thứ tự lifecycle: `NatsProvider.useFactory` (tạo connection) → `WebsocketService.onModuleInit` (init + attach + auth) → `ChatGateway.onModuleInit` (đăng ký handler). Vì `init()` là singleton, mọi service đều thao tác trên cùng một server instance.
319
+
320
+ ### 4. Xác thực
321
+
322
+ ```typescript
323
+ wsServer.setAuth(async (req, query) => {
324
+ const token = req.headers.authorization?.split(' ')[1];
325
+ if (!token) throw new Error('Unauthorized');
326
+
327
+ const user = await verifyToken(token);
328
+ return user; // được truyền vào ws.getAuthData()
329
+ });
330
+ ```
331
+
332
+ Throw error trong `setAuth` → server trả `401` và từ chối kết nối.
333
+
334
+ ### 5. onInstanceInit (NestJS / module isolation)
335
+
336
+ ```typescript
337
+ // websocket-config.ts
338
+ import { Server } from '@vutotoite/websocket';
339
+
340
+ Server.WebsocketServer.onInstanceInit((wsServer) => {
341
+ wsServer.setAuth(async (req, query) => {
342
+ const token = req.headers.authorization?.split(' ')[1];
343
+ if (!token) throw new Error('Unauthorized');
344
+ return await verifyToken(token);
345
+ });
346
+
347
+ wsServer.connected({
348
+ connectionHandler: (ws) => {
349
+ const auth = ws.getAuthData();
350
+ ws.setId(auth.userId);
351
+ ws.join(auth.userId);
352
+ },
353
+ errorHandler: (error, ws) => ws.emitS('error', { message: error.message })
354
+ });
355
+ });
356
+
357
+ // server.ts
358
+ import './websocket-config';
359
+ import { Server } from '@vutotoite/websocket';
360
+
361
+ const wsServer = Server.WebsocketServer.init({ ws: { noServer: true } });
362
+ wsServer.attachServer(httpServer); // callback đã đăng ký chạy ngay khi init
363
+ ```
364
+
365
+ - Server đã init → callback chạy **ngay lập tức**.
366
+ - Chưa init → callback chạy **sau khi** `init()` được gọi.
367
+ - Callback chỉ chạy **một lần**.
368
+
369
+ ### 6. Query parameters
370
+
371
+ ```typescript
372
+ wsServer.connected({
373
+ connectionHandler: (ws) => {
374
+ // ws://localhost:8080?user_id=123&room=general
375
+ const query = ws.getQuery<{ user_id: string; room: string }>();
376
+ ws.setId(query.user_id);
377
+ ws.join(query.room);
378
+ },
379
+ errorHandler: (error, ws) => ws.emitS('error', { message: error.message })
380
+ });
381
+ ```
382
+
383
+ ### 7. Middleware chain với throw error
384
+
385
+ Middleware chạy **tuần tự trái → phải**. Khi một middleware throw, chain **dừng ngay** và `errorHandler` được gọi.
386
+
387
+ ```typescript
388
+ class WsError extends Error {
389
+ constructor(message: string, public code = 400) {
390
+ super(message);
391
+ this.name = 'WsError';
392
+ }
393
+ }
394
+
395
+ const validateRoom = (data: { room_id?: string }) => {
396
+ if (!data?.room_id) throw new WsError('Room ID bắt buộc', 400);
397
+ if (!wsServer.isExistRoom(data.room_id)) throw new WsError('Phòng không tồn tại', 404);
398
+ };
399
+
400
+ const validateMessage = (data: { message?: string }) => {
401
+ if (!data?.message?.trim()) throw new WsError('Tin nhắn rỗng', 400);
402
+ if (data.message.length > 1000) throw new WsError('Tin nhắn quá dài', 400);
403
+ };
404
+
405
+ wsServer.connected({
406
+ connectionHandler: (ws, wss) => {
407
+ ws.onS(
408
+ 'send_message',
409
+ validateRoom, // dừng chain nếu throw
410
+ validateMessage,
411
+ (data: { room_id: string; message: string }) => {
412
+ wss.toRooms(data.room_id).emitS('new_message', {
413
+ message: data.message,
414
+ from: ws.getId()
415
+ });
416
+ }
417
+ );
418
+ },
419
+ errorHandler: (error, ws) => {
420
+ if (error instanceof WsError) {
421
+ ws.emitS('validation_error', { message: error.message, code: error.code });
422
+ } else {
423
+ console.error('Unexpected:', error);
424
+ ws.emitS('server_error', { message: 'Lỗi không mong muốn' });
425
+ }
426
+ }
427
+ });
428
+ ```
429
+
430
+ > Mỗi middleware nhận `(data, prevResult)` — `prevResult` là giá trị trả về của middleware trước, cho phép truyền dữ liệu qua chain.
431
+
432
+ ### 8. Quản lý phòng
433
+
434
+ ```typescript
435
+ wsServer.connected({
436
+ connectionHandler: (ws, wss) => {
437
+ ws.onS('join_room', (data: { room_id: string }) => {
438
+ ws.join(data.room_id);
439
+ ws.emitS('joined', { room: data.room_id });
440
+ });
441
+
442
+ ws.onS('leave_room', (data: { room_id: string }) => {
443
+ ws.leave(data.room_id);
444
+ ws.emitS('left', { room: data.room_id });
445
+ });
446
+ },
447
+ errorHandler: (error, ws) => ws.emitS('error', { message: error.message })
448
+ });
449
+ ```
450
+
451
+ ### 9. Gửi message tới target
452
+
453
+ ```typescript
454
+ wsServer.toAll().emitS('broadcast', { message: 'Hello everyone!' });
455
+ wsServer.toClients('user_123', 'user_456').emitS('private', { message: 'Hi' });
456
+ wsServer.toRooms('room_1', 'room_2').emitS('announcement', { message: '...' });
457
+
458
+ // Lọc client LOCAL trên node hiện tại (không xuyên node)
459
+ wsServer.filter((c) => c.getVariable('premium') === true)
460
+ .emitS('premium_offer', { discount: 50 });
461
+ ```
462
+
463
+ > `filter()` chỉ duyệt client kết nối tại node hiện tại. Trong môi trường nhiều node, dùng room hoặc client id để định tuyến chính xác xuyên node.
464
+
465
+ ### 10. Biến tùy chỉnh
466
+
467
+ ```typescript
468
+ ws.setVariable('username', 'John');
469
+ ws.setVariable('premium', true);
470
+
471
+ const username = ws.getVariable<string>('username');
472
+ ```
473
+
474
+ ### 11. Tích hợp Express API
475
+
476
+ ```typescript
477
+ app.post('/api/notify', (req, res) => {
478
+ const { userId, message } = req.body;
479
+ wsServer.toClients(userId).emitS('notification', { message });
480
+ res.json({ success: true });
481
+ });
482
+ ```
483
+
484
+ ## 💻 WebSocket Client
485
+
486
+ ### Browser
487
+
488
+ ```typescript
489
+ import { Client } from '@vutotoite/websocket';
490
+
491
+ const ws = Client.WebsocketBrowser.getInstance();
492
+
493
+ ws.connect('ws://localhost:8080?user_id=123', 3000);
494
+
495
+ ws.on('__connection_open', () => console.log('Đã kết nối'));
496
+ ws.on('__connection_close', (d) => console.log('Mất kết nối', d));
497
+ ws.on('__connection_error', (e) => console.error('Lỗi', e));
498
+
499
+ ws.on('new_message', (data) => console.log('Tin nhắn:', data));
500
+
501
+ ws.emit('send_message', { room_id: 'general', message: 'Hello!' });
502
+
503
+ ws.disconnect();
504
+ ```
505
+
506
+ ### Node.js
507
+
508
+ ```typescript
509
+ import { Client } from '@vutotoite/websocket';
510
+
511
+ const ws = Client.WebsocketNode.getInstance();
512
+
513
+ ws.connect('ws://localhost:8080?user_id=456', 3000, {
514
+ headers: { Authorization: 'Bearer token' }
515
+ });
516
+
517
+ ws.on('message', (data) => console.log(data));
518
+ ws.emit('chat', { room: 'tech', message: 'Hi from Node' });
519
+
520
+ ws.setupHeartbeat(30000); // ping mỗi 30s
521
+ ws.disconnect(false, 1000, 'Client closing');
522
+ ```
523
+
524
+ ### Event handlers nâng cao
525
+
526
+ ```typescript
527
+ ws.once('welcome', (data) => console.log(data));
528
+
529
+ const handler = (d) => console.log(d);
530
+ ws.on('message', handler);
531
+ ws.off('message', handler); // hủy 1 handler
532
+ ws.off('message'); // hủy tất cả handler của event
533
+ ws.clearAllHandlers();
534
+ ws.getRegisteredEvents();
535
+ ```
536
+
537
+ ### Message queue
538
+
539
+ ```typescript
540
+ ws.setMaxQueueSize(200);
541
+ ws.getQueueSize();
542
+ ws.isConnected();
543
+ ws.getReadyState(); // 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED
544
+ ```
545
+
546
+ ## 🔧 Codec tùy chỉnh
547
+
548
+ Mặc định dùng JSON `{ event, data }`. Có thể thay bằng codec riêng:
549
+
550
+ ```typescript
551
+ import { Server } from '@vutotoite/websocket';
552
+
553
+ const myCodec: Server.Codec = {
554
+ encode: (msg) => JSON.stringify([msg.event, msg.data]),
555
+ decode: (raw) => {
556
+ const [event, data] = JSON.parse(raw);
557
+ return { event, data };
558
+ }
559
+ };
560
+
561
+ Server.WebsocketServer.init({ ws: { noServer: true }, codec: myCodec });
562
+ ```
563
+
564
+ ## 📖 API Reference
565
+
566
+ ### `WebsocketServer`
567
+
568
+ | Method | Tham số | Mô tả |
569
+ |--------|---------|-------|
570
+ | `init()` | `opts: ServerInitOptions` | Khởi tạo (singleton) |
571
+ | `getInstance()` | - | Lấy instance |
572
+ | `onInstanceInit()` | `cb: (server) => void` | Callback chạy sau init |
573
+ | `attachServer()` | `httpServer: http.Server` | Gắn vào HTTP server |
574
+ | `setAuth()` | `auth: (req, query) => any` | Xác thực; throw để từ chối |
575
+ | `connected()` | `options: ConnectedOptions` | Đăng ký handler kết nối |
576
+ | `toClients()` | `...ids: string[]` | Target theo client id (xuyên node) |
577
+ | `toRooms()` | `...ids: string[]` | Target theo room (xuyên node) |
578
+ | `toAll()` | - | Target tất cả (xuyên node) |
579
+ | `filter()` | `cb: (conn) => boolean` | Lọc client **local** |
580
+ | `isExistRoom()` | `roomId: string` | Kiểm tra room (local) |
581
+ | `close()` | - | Đóng có trật tự: client 1001, dọn adapter, chờ ws đóng |
582
+ | `enableGracefulShutdown()` | `signals?: NodeJS.Signals[]` | Hook SIGTERM/SIGINT → tự `close()`; trả hàm hủy |
583
+
584
+ `ServerInitOptions`: `{ ws: ws.ServerOptions; adapter?: Adapter; codec?: Codec }`
585
+
586
+ ### `SocketConnection`
587
+
588
+ | Method | Mô tả |
589
+ |--------|-------|
590
+ | `setId(id)` / `getId()` | Đặt / lấy id client |
591
+ | `onS(event, ...callbacks)` | Lắng nghe với middleware chain |
592
+ | `emitS(event, data?)` | Gửi tới chính client này |
593
+ | `join(room)` / `leave(room)` | Tham gia / rời phòng |
594
+ | `getRooms()` | Danh sách phòng |
595
+ | `setVariable(k, v)` / `getVariable(k)` | Lưu / lấy biến |
596
+ | `getQuery()` / `getAuthData()` | Lấy query / dữ liệu auth |
597
+ | `ping()` / `getAlive()` | Ping / trạng thái sống |
598
+ | `close(code?, reason?)` | Đóng kết nối |
599
+
600
+ ### `SocketLink`
601
+
602
+ | Method | Mô tả |
603
+ |--------|-------|
604
+ | `emitS(event, data?)` | Gửi tới tập target đã chọn |
605
+
606
+ ### `Adapter`
607
+
608
+ Implement interface này để tạo backend định tuyến tùy chỉnh. Có sẵn `MemoryAdapter` và `NatsAdapter`.
609
+
610
+ `new NatsAdapter(natsConnection, prefix?)` — `natsConnection` là connection NATS đã thiết lập, `prefix` mặc định `"ws"`.
611
+
612
+ ### Client (`WebsocketBrowser` / `WebsocketNode`)
613
+
614
+ | Method | Mô tả |
615
+ |--------|-------|
616
+ | `getInstance()` | Lấy instance (singleton) |
617
+ | `connect(url, reconnectInterval?, options?)` | Kết nối (`options` chỉ Node) |
618
+ | `disconnect(shouldReconnect?, code?, reason?)` | Ngắt kết nối |
619
+ | `emit(event, data?)` | Gửi event |
620
+ | `on/once/off(event, handler?)` | Quản lý handler |
621
+ | `isConnected()` / `getReadyState()` | Trạng thái |
622
+ | `setMaxQueueSize(n)` / `getQueueSize()` | Queue |
623
+ | `clearAllHandlers()` / `getRegisteredEvents()` | Handlers |
624
+
625
+ **Chỉ `WebsocketNode`:** `ping(data?)`, `setupHeartbeat(interval?)`, `getWebSocketInstance()`.
626
+
627
+ ## ⚠️ Breaking changes (v1 → v2)
628
+
629
+ - `init()` đổi signature: `init({ ws: ServerOptions, adapter?, codec? })` thay vì `init(ServerOptions, callback?)`.
630
+ - `WebsocketServer` không còn kế thừa `ws.WebSocketServer` (chuyển sang composition).
631
+ - `SocketLink` chỉ còn `emitS` (bỏ `join`/`leave`/`close` hàng loạt — không an toàn khi scale ngang).
632
+ - Client export đổi tên: `WebsocketBrowser` / `WebsocketNode` (thay cho `WsBrowserClient` / `WsNodeClient`).
633
+ - Middleware chain trong `onS` nay **dừng khi gặp lỗi** (trước đây vẫn chạy tiếp).
634
+
635
+ ## 🧪 Test
636
+
637
+ ```bash
638
+ npm test
639
+ ```
640
+
641
+ ## 📝 License
642
+
643
+ MIT
644
+
645
+ ## 👨‍💻 Tác giả
646
+
647
+ maxsida <maxsida.dev@gmail.com>
@@ -0,0 +1,12 @@
1
+ import { BaseClient } from "../core/base-client.js";
2
+ /** WebSocket client cho trình duyệt. */
3
+ export declare class WebsocketBrowser extends BaseClient {
4
+ private static instance;
5
+ private socket;
6
+ static getInstance(): WebsocketBrowser;
7
+ protected openSocket(): void;
8
+ protected sendRaw(raw: string): boolean;
9
+ protected closeSocket(): void;
10
+ isConnected(): boolean;
11
+ getReadyState(): number;
12
+ }