@onebun/core 0.1.1 → 0.1.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/README.md +233 -0
- package/package.json +1 -1
- package/src/application.test.ts +119 -0
- package/src/application.ts +112 -5
- package/src/docs-examples.test.ts +753 -0
- package/src/index.ts +96 -0
- package/src/module.ts +10 -4
- package/src/redis-client.ts +502 -0
- package/src/shared-redis.ts +231 -0
- package/src/types.ts +50 -0
- package/src/ws-base-gateway.test.ts +479 -0
- package/src/ws-base-gateway.ts +514 -0
- package/src/ws-client.test.ts +511 -0
- package/src/ws-client.ts +628 -0
- package/src/ws-client.types.ts +129 -0
- package/src/ws-decorators.test.ts +331 -0
- package/src/ws-decorators.ts +417 -0
- package/src/ws-guards.test.ts +334 -0
- package/src/ws-guards.ts +298 -0
- package/src/ws-handler.ts +658 -0
- package/src/ws-integration.test.ts +517 -0
- package/src/ws-pattern-matcher.test.ts +152 -0
- package/src/ws-pattern-matcher.ts +240 -0
- package/src/ws-service-definition.ts +223 -0
- package/src/ws-socketio-protocol.test.ts +344 -0
- package/src/ws-socketio-protocol.ts +567 -0
- package/src/ws-storage-memory.test.ts +246 -0
- package/src/ws-storage-memory.ts +222 -0
- package/src/ws-storage-redis.ts +302 -0
- package/src/ws-storage.ts +210 -0
- package/src/ws.types.ts +342 -0
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* - docs/api/decorators.md
|
|
9
9
|
* - docs/api/services.md
|
|
10
10
|
* - docs/api/validation.md
|
|
11
|
+
* - docs/api/websocket.md
|
|
11
12
|
* - docs/examples/basic-app.md
|
|
12
13
|
* - docs/examples/crud-api.md
|
|
14
|
+
* - docs/examples/websocket-chat.md
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import { type } from 'arktype';
|
|
@@ -19,6 +21,13 @@ import {
|
|
|
19
21
|
expect,
|
|
20
22
|
} from 'bun:test';
|
|
21
23
|
|
|
24
|
+
import type {
|
|
25
|
+
WsClientData,
|
|
26
|
+
WsExecutionContext,
|
|
27
|
+
WsServerType,
|
|
28
|
+
} from './';
|
|
29
|
+
import type { ServerWebSocket } from 'bun';
|
|
30
|
+
|
|
22
31
|
import {
|
|
23
32
|
Controller,
|
|
24
33
|
Get,
|
|
@@ -48,6 +57,29 @@ import {
|
|
|
48
57
|
OneBunApplication,
|
|
49
58
|
createServiceDefinition,
|
|
50
59
|
createServiceClient,
|
|
60
|
+
WebSocketGateway,
|
|
61
|
+
BaseWebSocketGateway,
|
|
62
|
+
OnConnect,
|
|
63
|
+
OnDisconnect,
|
|
64
|
+
OnJoinRoom,
|
|
65
|
+
OnLeaveRoom,
|
|
66
|
+
OnMessage,
|
|
67
|
+
Client,
|
|
68
|
+
Socket,
|
|
69
|
+
MessageData,
|
|
70
|
+
RoomName,
|
|
71
|
+
PatternParams,
|
|
72
|
+
WsServer,
|
|
73
|
+
UseWsGuards,
|
|
74
|
+
WsAuthGuard,
|
|
75
|
+
WsPermissionGuard,
|
|
76
|
+
WsAnyPermissionGuard,
|
|
77
|
+
createGuard,
|
|
78
|
+
createInMemoryWsStorage,
|
|
79
|
+
SharedRedisProvider,
|
|
80
|
+
createWsServiceDefinition,
|
|
81
|
+
createWsClient,
|
|
82
|
+
matchPattern,
|
|
51
83
|
} from './';
|
|
52
84
|
|
|
53
85
|
/**
|
|
@@ -2164,3 +2196,724 @@ describe('Getting Started Documentation (docs/getting-started.md)', () => {
|
|
|
2164
2196
|
});
|
|
2165
2197
|
});
|
|
2166
2198
|
});
|
|
2199
|
+
|
|
2200
|
+
// ============================================================================
|
|
2201
|
+
// WebSocket Gateway Documentation Tests
|
|
2202
|
+
// ============================================================================
|
|
2203
|
+
|
|
2204
|
+
describe('WebSocket Gateway API Documentation (docs/api/websocket.md)', () => {
|
|
2205
|
+
describe('@WebSocketGateway decorator', () => {
|
|
2206
|
+
/**
|
|
2207
|
+
* @source docs/api/websocket.md#websocketgateway-decorator
|
|
2208
|
+
*/
|
|
2209
|
+
it('should define gateway with path and namespace', () => {
|
|
2210
|
+
// From docs: WebSocketGateway Decorator example
|
|
2211
|
+
@WebSocketGateway({ path: '/ws', namespace: 'chat' })
|
|
2212
|
+
class ChatGateway extends BaseWebSocketGateway {
|
|
2213
|
+
// handlers...
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
expect(ChatGateway).toBeDefined();
|
|
2217
|
+
});
|
|
2218
|
+
});
|
|
2219
|
+
|
|
2220
|
+
describe('Event Decorators', () => {
|
|
2221
|
+
/**
|
|
2222
|
+
* @source docs/api/websocket.md#onconnect
|
|
2223
|
+
*/
|
|
2224
|
+
it('should handle @OnConnect decorator', () => {
|
|
2225
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2226
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2227
|
+
@OnConnect()
|
|
2228
|
+
handleConnect(@Client() client: WsClientData) {
|
|
2229
|
+
// eslint-disable-next-line no-console
|
|
2230
|
+
console.log(`Client ${client.id} connected`);
|
|
2231
|
+
|
|
2232
|
+
return { event: 'welcome', data: { message: 'Welcome!' } };
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
expect(TestGateway).toBeDefined();
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
/**
|
|
2240
|
+
* @source docs/api/websocket.md#ondisconnect
|
|
2241
|
+
*/
|
|
2242
|
+
it('should handle @OnDisconnect decorator', () => {
|
|
2243
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2244
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2245
|
+
@OnDisconnect()
|
|
2246
|
+
handleDisconnect(@Client() client: WsClientData) {
|
|
2247
|
+
// eslint-disable-next-line no-console
|
|
2248
|
+
console.log(`Client ${client.id} disconnected`);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
expect(TestGateway).toBeDefined();
|
|
2253
|
+
});
|
|
2254
|
+
|
|
2255
|
+
/**
|
|
2256
|
+
* @source docs/api/websocket.md#onjoinroom
|
|
2257
|
+
*/
|
|
2258
|
+
it('should handle @OnJoinRoom decorator with pattern', () => {
|
|
2259
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2260
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2261
|
+
@OnJoinRoom('room:{roomId}')
|
|
2262
|
+
handleJoinRoom(
|
|
2263
|
+
@Client() client: WsClientData,
|
|
2264
|
+
@RoomName() room: string,
|
|
2265
|
+
@PatternParams() params: { roomId: string },
|
|
2266
|
+
) {
|
|
2267
|
+
this.emitToRoom(room, 'user:joined', { userId: client.id });
|
|
2268
|
+
|
|
2269
|
+
return { event: 'joined', data: { roomId: params.roomId } };
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
expect(TestGateway).toBeDefined();
|
|
2274
|
+
});
|
|
2275
|
+
|
|
2276
|
+
/**
|
|
2277
|
+
* @source docs/api/websocket.md#onleaveroom
|
|
2278
|
+
*/
|
|
2279
|
+
it('should handle @OnLeaveRoom decorator with wildcard', () => {
|
|
2280
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2281
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2282
|
+
@OnLeaveRoom('room:*')
|
|
2283
|
+
handleLeaveRoom(@Client() client: WsClientData, @RoomName() room: string) {
|
|
2284
|
+
this.emitToRoom(room, 'user:left', { userId: client.id });
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
expect(TestGateway).toBeDefined();
|
|
2289
|
+
});
|
|
2290
|
+
|
|
2291
|
+
/**
|
|
2292
|
+
* @source docs/api/websocket.md#onmessage
|
|
2293
|
+
*/
|
|
2294
|
+
it('should handle @OnMessage decorator', () => {
|
|
2295
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2296
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2297
|
+
@OnMessage('chat:message')
|
|
2298
|
+
handleMessage(@Client() client: WsClientData, @MessageData() data: { text: string }) {
|
|
2299
|
+
this.broadcast('chat:message', { userId: client.id, text: data.text });
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
expect(TestGateway).toBeDefined();
|
|
2304
|
+
});
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
describe('Pattern Syntax', () => {
|
|
2308
|
+
/**
|
|
2309
|
+
* @source docs/api/websocket.md#pattern-syntax
|
|
2310
|
+
*/
|
|
2311
|
+
it('should match exact patterns', () => {
|
|
2312
|
+
const match = matchPattern('chat:message', 'chat:message');
|
|
2313
|
+
expect(match.matched).toBe(true);
|
|
2314
|
+
});
|
|
2315
|
+
|
|
2316
|
+
it('should match wildcard patterns', () => {
|
|
2317
|
+
const match = matchPattern('chat:*', 'chat:general');
|
|
2318
|
+
expect(match.matched).toBe(true);
|
|
2319
|
+
});
|
|
2320
|
+
|
|
2321
|
+
it('should match named parameter patterns', () => {
|
|
2322
|
+
const match = matchPattern('chat:{roomId}', 'chat:general');
|
|
2323
|
+
expect(match.matched).toBe(true);
|
|
2324
|
+
expect(match.params?.roomId).toBe('general');
|
|
2325
|
+
});
|
|
2326
|
+
|
|
2327
|
+
it('should match combined patterns', () => {
|
|
2328
|
+
const match = matchPattern('user:{id}:*', 'user:123:action');
|
|
2329
|
+
expect(match.matched).toBe(true);
|
|
2330
|
+
expect(match.params?.id).toBe('123');
|
|
2331
|
+
});
|
|
2332
|
+
});
|
|
2333
|
+
|
|
2334
|
+
describe('Parameter Decorators', () => {
|
|
2335
|
+
/**
|
|
2336
|
+
* @source docs/api/websocket.md#client
|
|
2337
|
+
*/
|
|
2338
|
+
it('should use @Client() decorator', () => {
|
|
2339
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2340
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2341
|
+
@OnMessage('ping')
|
|
2342
|
+
handlePing(@Client() client: WsClientData) {
|
|
2343
|
+
// eslint-disable-next-line no-console
|
|
2344
|
+
console.log(`Ping from ${client.id}`);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
expect(TestGateway).toBeDefined();
|
|
2349
|
+
});
|
|
2350
|
+
|
|
2351
|
+
/**
|
|
2352
|
+
* @source docs/api/websocket.md#socket
|
|
2353
|
+
*/
|
|
2354
|
+
it('should use @Socket() decorator', () => {
|
|
2355
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2356
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2357
|
+
@OnMessage('raw')
|
|
2358
|
+
handleRaw(@Socket() socket: ServerWebSocket<WsClientData>) {
|
|
2359
|
+
socket.send('raw message');
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
expect(TestGateway).toBeDefined();
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
/**
|
|
2367
|
+
* @source docs/api/websocket.md#messagedata
|
|
2368
|
+
*/
|
|
2369
|
+
it('should use @MessageData() decorator with property', () => {
|
|
2370
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2371
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2372
|
+
// Full data
|
|
2373
|
+
@OnMessage('chat:full')
|
|
2374
|
+
handleFull(@MessageData() data: { text: string }) {
|
|
2375
|
+
return data;
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
// Specific property
|
|
2379
|
+
@OnMessage('chat:text')
|
|
2380
|
+
handleText(@MessageData('text') text: string) {
|
|
2381
|
+
return text;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
expect(TestGateway).toBeDefined();
|
|
2386
|
+
});
|
|
2387
|
+
|
|
2388
|
+
/**
|
|
2389
|
+
* @source docs/api/websocket.md#roomname
|
|
2390
|
+
*/
|
|
2391
|
+
it('should use @RoomName() decorator', () => {
|
|
2392
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2393
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2394
|
+
@OnJoinRoom()
|
|
2395
|
+
handleJoin(@RoomName() room: string) {
|
|
2396
|
+
return { room };
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
expect(TestGateway).toBeDefined();
|
|
2401
|
+
});
|
|
2402
|
+
|
|
2403
|
+
/**
|
|
2404
|
+
* @source docs/api/websocket.md#patternparams
|
|
2405
|
+
*/
|
|
2406
|
+
it('should use @PatternParams() decorator', () => {
|
|
2407
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2408
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2409
|
+
@OnMessage('chat:{roomId}:message')
|
|
2410
|
+
handleMessage(@PatternParams() params: { roomId: string }) {
|
|
2411
|
+
return { roomId: params.roomId };
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
expect(TestGateway).toBeDefined();
|
|
2416
|
+
});
|
|
2417
|
+
|
|
2418
|
+
/**
|
|
2419
|
+
* @source docs/api/websocket.md#wsserver
|
|
2420
|
+
*/
|
|
2421
|
+
it('should use @WsServer() decorator', () => {
|
|
2422
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2423
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2424
|
+
@OnMessage('broadcast')
|
|
2425
|
+
handleBroadcast(@WsServer() server: WsServerType) {
|
|
2426
|
+
server.publish('all', 'Hello everyone!');
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
expect(TestGateway).toBeDefined();
|
|
2431
|
+
});
|
|
2432
|
+
});
|
|
2433
|
+
|
|
2434
|
+
describe('Guards', () => {
|
|
2435
|
+
/**
|
|
2436
|
+
* @source docs/api/websocket.md#built-in-guards
|
|
2437
|
+
*/
|
|
2438
|
+
it('should use WsAuthGuard', () => {
|
|
2439
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2440
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2441
|
+
@UseWsGuards(WsAuthGuard)
|
|
2442
|
+
@OnMessage('protected:*')
|
|
2443
|
+
handleProtected(@Client() client: WsClientData) {
|
|
2444
|
+
return { userId: client.auth?.userId };
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
expect(TestGateway).toBeDefined();
|
|
2449
|
+
});
|
|
2450
|
+
|
|
2451
|
+
/**
|
|
2452
|
+
* @source docs/api/websocket.md#built-in-guards
|
|
2453
|
+
*/
|
|
2454
|
+
it('should use WsPermissionGuard', () => {
|
|
2455
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2456
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2457
|
+
@UseWsGuards(new WsPermissionGuard('admin'))
|
|
2458
|
+
@OnMessage('admin:*')
|
|
2459
|
+
handleAdmin(@Client() client: WsClientData) {
|
|
2460
|
+
return { admin: true, userId: client.id };
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
expect(TestGateway).toBeDefined();
|
|
2465
|
+
});
|
|
2466
|
+
|
|
2467
|
+
/**
|
|
2468
|
+
* @source docs/api/websocket.md#built-in-guards
|
|
2469
|
+
*/
|
|
2470
|
+
it('should use WsAnyPermissionGuard', () => {
|
|
2471
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2472
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2473
|
+
@UseWsGuards(new WsAnyPermissionGuard(['admin', 'moderator']))
|
|
2474
|
+
@OnMessage('manage:*')
|
|
2475
|
+
handleManage(@Client() client: WsClientData) {
|
|
2476
|
+
return { clientId: client.id };
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
expect(TestGateway).toBeDefined();
|
|
2481
|
+
});
|
|
2482
|
+
|
|
2483
|
+
/**
|
|
2484
|
+
* @source docs/api/websocket.md#custom-guards
|
|
2485
|
+
*/
|
|
2486
|
+
it('should create custom guard', () => {
|
|
2487
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
2488
|
+
const CustomGuard = createGuard((ctx: WsExecutionContext) => {
|
|
2489
|
+
return ctx.getClient().metadata.customCheck === true;
|
|
2490
|
+
});
|
|
2491
|
+
|
|
2492
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2493
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
2494
|
+
@UseWsGuards(CustomGuard)
|
|
2495
|
+
@OnMessage('custom:*')
|
|
2496
|
+
handleCustom(@Client() client: WsClientData) {
|
|
2497
|
+
return { clientId: client.id };
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
expect(TestGateway).toBeDefined();
|
|
2502
|
+
expect(CustomGuard).toBeDefined();
|
|
2503
|
+
});
|
|
2504
|
+
});
|
|
2505
|
+
|
|
2506
|
+
describe('Storage Adapters', () => {
|
|
2507
|
+
/**
|
|
2508
|
+
* @source docs/api/websocket.md#in-memory-storage-default
|
|
2509
|
+
*/
|
|
2510
|
+
it('should create in-memory storage', () => {
|
|
2511
|
+
const storage = createInMemoryWsStorage();
|
|
2512
|
+
expect(storage).toBeDefined();
|
|
2513
|
+
expect(typeof storage.addClient).toBe('function');
|
|
2514
|
+
expect(typeof storage.removeClient).toBe('function');
|
|
2515
|
+
expect(typeof storage.getClient).toBe('function');
|
|
2516
|
+
});
|
|
2517
|
+
|
|
2518
|
+
/**
|
|
2519
|
+
* @source docs/api/websocket.md#redis-storage
|
|
2520
|
+
*/
|
|
2521
|
+
it('should configure SharedRedisProvider', () => {
|
|
2522
|
+
// From docs: Redis Storage example
|
|
2523
|
+
// Note: This just tests the API, not actual connection
|
|
2524
|
+
expect(typeof SharedRedisProvider.configure).toBe('function');
|
|
2525
|
+
expect(typeof SharedRedisProvider.getClient).toBe('function');
|
|
2526
|
+
});
|
|
2527
|
+
});
|
|
2528
|
+
|
|
2529
|
+
describe('WebSocket Client', () => {
|
|
2530
|
+
/**
|
|
2531
|
+
* @source docs/api/websocket.md#creating-a-client
|
|
2532
|
+
*/
|
|
2533
|
+
it('should create typed client from definition', () => {
|
|
2534
|
+
@WebSocketGateway({ path: '/chat' })
|
|
2535
|
+
class ChatGateway extends BaseWebSocketGateway {
|
|
2536
|
+
@OnMessage('chat:message')
|
|
2537
|
+
handleMessage(@Client() _client: WsClientData, @MessageData() data: { text: string }) {
|
|
2538
|
+
return { event: 'received', data };
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
@Module({ controllers: [ChatGateway] })
|
|
2543
|
+
class ChatModule {}
|
|
2544
|
+
|
|
2545
|
+
const definition = createWsServiceDefinition(ChatModule);
|
|
2546
|
+
expect(definition).toBeDefined();
|
|
2547
|
+
expect(definition._gateways).toBeDefined();
|
|
2548
|
+
|
|
2549
|
+
// Client creation (without actual connection)
|
|
2550
|
+
const client = createWsClient(definition, {
|
|
2551
|
+
url: 'ws://localhost:3000',
|
|
2552
|
+
auth: { token: 'xxx' },
|
|
2553
|
+
reconnect: true,
|
|
2554
|
+
reconnectInterval: 1000,
|
|
2555
|
+
maxReconnectAttempts: 10,
|
|
2556
|
+
});
|
|
2557
|
+
|
|
2558
|
+
expect(client).toBeDefined();
|
|
2559
|
+
expect(typeof client.connect).toBe('function');
|
|
2560
|
+
expect(typeof client.disconnect).toBe('function');
|
|
2561
|
+
expect(typeof client.on).toBe('function');
|
|
2562
|
+
});
|
|
2563
|
+
});
|
|
2564
|
+
|
|
2565
|
+
describe('Application Configuration', () => {
|
|
2566
|
+
/**
|
|
2567
|
+
* @source docs/api/websocket.md#application-options
|
|
2568
|
+
*/
|
|
2569
|
+
it('should accept WebSocket configuration', () => {
|
|
2570
|
+
@WebSocketGateway({ path: '/ws' })
|
|
2571
|
+
class TestGateway extends BaseWebSocketGateway {}
|
|
2572
|
+
|
|
2573
|
+
@Module({ controllers: [TestGateway] })
|
|
2574
|
+
class AppModule {}
|
|
2575
|
+
|
|
2576
|
+
// From docs: Application Options example
|
|
2577
|
+
const app = new OneBunApplication(AppModule, {
|
|
2578
|
+
port: 3000,
|
|
2579
|
+
websocket: {
|
|
2580
|
+
enabled: true,
|
|
2581
|
+
storage: {
|
|
2582
|
+
type: 'memory',
|
|
2583
|
+
},
|
|
2584
|
+
pingInterval: 25000,
|
|
2585
|
+
pingTimeout: 20000,
|
|
2586
|
+
maxPayload: 1048576,
|
|
2587
|
+
},
|
|
2588
|
+
});
|
|
2589
|
+
|
|
2590
|
+
expect(app).toBeDefined();
|
|
2591
|
+
});
|
|
2592
|
+
});
|
|
2593
|
+
});
|
|
2594
|
+
|
|
2595
|
+
describe('WebSocket Chat Example (docs/examples/websocket-chat.md)', () => {
|
|
2596
|
+
describe('Chat Gateway', () => {
|
|
2597
|
+
/**
|
|
2598
|
+
* @source docs/examples/websocket-chat.md#chat-gateway
|
|
2599
|
+
*/
|
|
2600
|
+
it('should define ChatGateway with all handlers', () => {
|
|
2601
|
+
interface ChatMessage {
|
|
2602
|
+
text: string;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
// Simplified ChatService for testing
|
|
2606
|
+
@Service()
|
|
2607
|
+
class ChatService extends BaseService {
|
|
2608
|
+
async getMessageHistory(_roomId: string): Promise<unknown[]> {
|
|
2609
|
+
return [];
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
async saveMessage(data: { roomId: string; userId: string; text: string; timestamp: number }) {
|
|
2613
|
+
return { id: 'msg_1', ...data };
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
// From docs: Chat Gateway example
|
|
2618
|
+
@WebSocketGateway({ path: '/chat' })
|
|
2619
|
+
class ChatGateway extends BaseWebSocketGateway {
|
|
2620
|
+
constructor(private chatService: ChatService) {
|
|
2621
|
+
super();
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
@OnConnect()
|
|
2625
|
+
async handleConnect(@Client() client: WsClientData) {
|
|
2626
|
+
// eslint-disable-next-line no-console
|
|
2627
|
+
console.log(`Client ${client.id} connected`);
|
|
2628
|
+
|
|
2629
|
+
return {
|
|
2630
|
+
event: 'welcome',
|
|
2631
|
+
data: {
|
|
2632
|
+
message: 'Welcome to the chat!',
|
|
2633
|
+
clientId: client.id,
|
|
2634
|
+
timestamp: Date.now(),
|
|
2635
|
+
},
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
@OnDisconnect()
|
|
2640
|
+
async handleDisconnect(@Client() client: WsClientData) {
|
|
2641
|
+
// eslint-disable-next-line no-console
|
|
2642
|
+
console.log(`Client ${client.id} disconnected`);
|
|
2643
|
+
|
|
2644
|
+
for (const room of client.rooms) {
|
|
2645
|
+
this.emitToRoom(room, 'user:left', {
|
|
2646
|
+
userId: client.id,
|
|
2647
|
+
room,
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
@OnJoinRoom('room:{roomId}')
|
|
2653
|
+
async handleJoinRoom(
|
|
2654
|
+
@Client() client: WsClientData,
|
|
2655
|
+
@RoomName() room: string,
|
|
2656
|
+
@PatternParams() params: { roomId: string },
|
|
2657
|
+
) {
|
|
2658
|
+
// eslint-disable-next-line no-console
|
|
2659
|
+
console.log(`Client ${client.id} joining room ${params.roomId}`);
|
|
2660
|
+
|
|
2661
|
+
await this.joinRoom(client.id, room);
|
|
2662
|
+
|
|
2663
|
+
this.emitToRoom(room, 'user:joined', {
|
|
2664
|
+
userId: client.id,
|
|
2665
|
+
room,
|
|
2666
|
+
}, [client.id]);
|
|
2667
|
+
|
|
2668
|
+
const history = await this.chatService.getMessageHistory(params.roomId);
|
|
2669
|
+
|
|
2670
|
+
return {
|
|
2671
|
+
event: 'room:joined',
|
|
2672
|
+
data: {
|
|
2673
|
+
room: params.roomId,
|
|
2674
|
+
history,
|
|
2675
|
+
},
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
@OnLeaveRoom('room:{roomId}')
|
|
2680
|
+
async handleLeaveRoom(
|
|
2681
|
+
@Client() client: WsClientData,
|
|
2682
|
+
@RoomName() room: string,
|
|
2683
|
+
) {
|
|
2684
|
+
await this.leaveRoom(client.id, room);
|
|
2685
|
+
|
|
2686
|
+
this.emitToRoom(room, 'user:left', {
|
|
2687
|
+
userId: client.id,
|
|
2688
|
+
room,
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
@OnMessage('chat:{roomId}:message')
|
|
2693
|
+
async handleMessage(
|
|
2694
|
+
@Client() client: WsClientData,
|
|
2695
|
+
@MessageData() data: ChatMessage,
|
|
2696
|
+
@PatternParams() params: { roomId: string },
|
|
2697
|
+
) {
|
|
2698
|
+
if (!client.rooms.includes(`room:${params.roomId}`)) {
|
|
2699
|
+
return {
|
|
2700
|
+
event: 'error',
|
|
2701
|
+
data: { message: 'Not in room' },
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
const message = await this.chatService.saveMessage({
|
|
2706
|
+
roomId: params.roomId,
|
|
2707
|
+
userId: client.id,
|
|
2708
|
+
text: data.text,
|
|
2709
|
+
timestamp: Date.now(),
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
this.emitToRoom(`room:${params.roomId}`, 'chat:message', message);
|
|
2713
|
+
|
|
2714
|
+
return {
|
|
2715
|
+
event: 'chat:message:ack',
|
|
2716
|
+
data: { messageId: message.id },
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
@OnMessage('typing:{roomId}')
|
|
2721
|
+
handleTyping(
|
|
2722
|
+
@Client() client: WsClientData,
|
|
2723
|
+
@PatternParams() params: { roomId: string },
|
|
2724
|
+
) {
|
|
2725
|
+
this.emitToRoom(
|
|
2726
|
+
`room:${params.roomId}`,
|
|
2727
|
+
'typing',
|
|
2728
|
+
{ userId: client.id },
|
|
2729
|
+
[client.id],
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
expect(ChatGateway).toBeDefined();
|
|
2735
|
+
expect(ChatService).toBeDefined();
|
|
2736
|
+
});
|
|
2737
|
+
});
|
|
2738
|
+
|
|
2739
|
+
describe('Chat Service', () => {
|
|
2740
|
+
/**
|
|
2741
|
+
* @source docs/examples/websocket-chat.md#chat-service
|
|
2742
|
+
*/
|
|
2743
|
+
it('should define ChatService', () => {
|
|
2744
|
+
interface Message {
|
|
2745
|
+
id: string;
|
|
2746
|
+
roomId: string;
|
|
2747
|
+
userId: string;
|
|
2748
|
+
text: string;
|
|
2749
|
+
timestamp: number;
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
// From docs: Chat Service example
|
|
2753
|
+
@Service()
|
|
2754
|
+
class ChatService extends BaseService {
|
|
2755
|
+
private messages: Map<string, Message[]> = new Map();
|
|
2756
|
+
private messageIdCounter = 0;
|
|
2757
|
+
|
|
2758
|
+
async saveMessage(data: Omit<Message, 'id'>): Promise<Message> {
|
|
2759
|
+
const message: Message = {
|
|
2760
|
+
id: `msg_${++this.messageIdCounter}`,
|
|
2761
|
+
...data,
|
|
2762
|
+
};
|
|
2763
|
+
|
|
2764
|
+
const roomMessages = this.messages.get(data.roomId) || [];
|
|
2765
|
+
roomMessages.push(message);
|
|
2766
|
+
this.messages.set(data.roomId, roomMessages);
|
|
2767
|
+
|
|
2768
|
+
return message;
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
async getMessageHistory(roomId: string, limit = 50): Promise<Message[]> {
|
|
2772
|
+
const roomMessages = this.messages.get(roomId) || [];
|
|
2773
|
+
|
|
2774
|
+
return roomMessages.slice(-limit);
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
async clearRoom(roomId: string): Promise<void> {
|
|
2778
|
+
this.messages.delete(roomId);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
expect(ChatService).toBeDefined();
|
|
2783
|
+
});
|
|
2784
|
+
});
|
|
2785
|
+
|
|
2786
|
+
describe('Auth Guard', () => {
|
|
2787
|
+
/**
|
|
2788
|
+
* @source docs/examples/websocket-chat.md#auth-guard
|
|
2789
|
+
*/
|
|
2790
|
+
it('should define custom ChatAuthGuard', () => {
|
|
2791
|
+
// From docs: Auth Guard example
|
|
2792
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
2793
|
+
const ChatAuthGuard = createGuard((context: WsExecutionContext) => {
|
|
2794
|
+
const client = context.getClient();
|
|
2795
|
+
|
|
2796
|
+
if (!client.auth?.authenticated) {
|
|
2797
|
+
return false;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
return true;
|
|
2801
|
+
});
|
|
2802
|
+
|
|
2803
|
+
expect(ChatAuthGuard).toBeDefined();
|
|
2804
|
+
// createGuard returns a class, not an instance
|
|
2805
|
+
const guardInstance = new ChatAuthGuard();
|
|
2806
|
+
expect(typeof guardInstance.canActivate).toBe('function');
|
|
2807
|
+
});
|
|
2808
|
+
});
|
|
2809
|
+
|
|
2810
|
+
describe('Module Setup', () => {
|
|
2811
|
+
/**
|
|
2812
|
+
* @source docs/examples/websocket-chat.md#module-setup
|
|
2813
|
+
*/
|
|
2814
|
+
it('should define ChatModule', () => {
|
|
2815
|
+
@Service()
|
|
2816
|
+
class ChatService extends BaseService {}
|
|
2817
|
+
|
|
2818
|
+
@WebSocketGateway({ path: '/chat' })
|
|
2819
|
+
class ChatGateway extends BaseWebSocketGateway {
|
|
2820
|
+
constructor(private chatService: ChatService) {
|
|
2821
|
+
super();
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
// From docs: Module Setup example - Gateways go in controllers
|
|
2826
|
+
@Module({
|
|
2827
|
+
controllers: [ChatGateway],
|
|
2828
|
+
providers: [ChatService],
|
|
2829
|
+
})
|
|
2830
|
+
class ChatModule {}
|
|
2831
|
+
|
|
2832
|
+
expect(ChatModule).toBeDefined();
|
|
2833
|
+
});
|
|
2834
|
+
});
|
|
2835
|
+
|
|
2836
|
+
describe('Application Entry', () => {
|
|
2837
|
+
/**
|
|
2838
|
+
* @source docs/examples/websocket-chat.md#application-entry
|
|
2839
|
+
*/
|
|
2840
|
+
it('should create chat application', () => {
|
|
2841
|
+
@Service()
|
|
2842
|
+
class ChatService extends BaseService {}
|
|
2843
|
+
|
|
2844
|
+
@WebSocketGateway({ path: '/chat' })
|
|
2845
|
+
class ChatGateway extends BaseWebSocketGateway {
|
|
2846
|
+
constructor(private chatService: ChatService) {
|
|
2847
|
+
super();
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
@Module({
|
|
2852
|
+
controllers: [ChatGateway],
|
|
2853
|
+
providers: [ChatService],
|
|
2854
|
+
})
|
|
2855
|
+
class ChatModule {}
|
|
2856
|
+
|
|
2857
|
+
// From docs: Application Entry example
|
|
2858
|
+
const app = new OneBunApplication(ChatModule, {
|
|
2859
|
+
port: 3000,
|
|
2860
|
+
websocket: {
|
|
2861
|
+
pingInterval: 25000,
|
|
2862
|
+
pingTimeout: 20000,
|
|
2863
|
+
},
|
|
2864
|
+
});
|
|
2865
|
+
|
|
2866
|
+
expect(app).toBeDefined();
|
|
2867
|
+
});
|
|
2868
|
+
});
|
|
2869
|
+
|
|
2870
|
+
describe('Client Implementation', () => {
|
|
2871
|
+
/**
|
|
2872
|
+
* @source docs/examples/websocket-chat.md#typed-client
|
|
2873
|
+
*/
|
|
2874
|
+
it('should create typed chat client', () => {
|
|
2875
|
+
@Service()
|
|
2876
|
+
class ChatService extends BaseService {}
|
|
2877
|
+
|
|
2878
|
+
@WebSocketGateway({ path: '/chat' })
|
|
2879
|
+
class ChatGateway extends BaseWebSocketGateway {
|
|
2880
|
+
constructor(private chatService: ChatService) {
|
|
2881
|
+
super();
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
@OnMessage('chat:message')
|
|
2885
|
+
handleMessage(@MessageData() data: { text: string }) {
|
|
2886
|
+
return { event: 'received', data };
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
@Module({
|
|
2891
|
+
controllers: [ChatGateway],
|
|
2892
|
+
providers: [ChatService],
|
|
2893
|
+
})
|
|
2894
|
+
class ChatModule {}
|
|
2895
|
+
|
|
2896
|
+
// From docs: Typed Client example
|
|
2897
|
+
const definition = createWsServiceDefinition(ChatModule);
|
|
2898
|
+
const client = createWsClient(definition, {
|
|
2899
|
+
url: 'ws://localhost:3000/chat',
|
|
2900
|
+
auth: {
|
|
2901
|
+
token: 'user-jwt-token',
|
|
2902
|
+
},
|
|
2903
|
+
reconnect: true,
|
|
2904
|
+
reconnectInterval: 2000,
|
|
2905
|
+
maxReconnectAttempts: 5,
|
|
2906
|
+
});
|
|
2907
|
+
|
|
2908
|
+
// Lifecycle events
|
|
2909
|
+
expect(typeof client.on).toBe('function');
|
|
2910
|
+
|
|
2911
|
+
// Connect/disconnect
|
|
2912
|
+
expect(typeof client.connect).toBe('function');
|
|
2913
|
+
expect(typeof client.disconnect).toBe('function');
|
|
2914
|
+
|
|
2915
|
+
// Gateway access
|
|
2916
|
+
expect(client.ChatGateway).toBeDefined();
|
|
2917
|
+
});
|
|
2918
|
+
});
|
|
2919
|
+
});
|