@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.
@@ -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
+ });