@wooksjs/event-ws 0.7.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.
@@ -0,0 +1,139 @@
1
+ # Rooms & broadcasting — @wooksjs/event-ws
2
+
3
+ > Room-based connection grouping, local and cross-instance broadcasting, and the broadcast transport interface.
4
+
5
+ ## Concepts
6
+
7
+ Rooms are named groups of connections. A connection can join multiple rooms. Broadcasting sends a push message to all connections in a room, optionally excluding the sender.
8
+
9
+ The room system has two layers:
10
+
11
+ 1. **Local** — `WsRoomManager` tracks `Map<string, Set<WsConnection>>` in memory
12
+ 2. **Distributed** — optional `WsBroadcastTransport` for cross-instance pub/sub (e.g., Redis)
13
+
14
+ When a message is broadcast:
15
+
16
+ 1. Local connections in the room receive it directly
17
+ 2. If a transport is configured, the message is published to `ws:room:{roomName}` for other instances
18
+ 3. Other instances receive it via the transport subscription and forward to their local connections
19
+
20
+ ## API Reference
21
+
22
+ ### `WsRoomManager`
23
+
24
+ Internal class that manages room → connections mapping. Not typically used directly — use `useWsRooms()` composable instead.
25
+
26
+ ```ts
27
+ class WsRoomManager {
28
+ join(connection: WsConnection, room: string): void
29
+ leave(connection: WsConnection, room: string): void
30
+ leaveAll(connection: WsConnection): void // called automatically on disconnect
31
+ connections(room: string): Set<WsConnection>
32
+ broadcast(room, event, path, data?, params?, exclude?): void
33
+ }
34
+ ```
35
+
36
+ ### `WsBroadcastTransport` interface
37
+
38
+ Pluggable transport for cross-instance broadcasting:
39
+
40
+ ```ts
41
+ interface WsBroadcastTransport {
42
+ publish(channel: string, payload: string): void | Promise<void>
43
+ subscribe(channel: string, handler: (payload: string) => void): void | Promise<void>
44
+ unsubscribe(channel: string): void | Promise<void>
45
+ }
46
+ ```
47
+
48
+ Channels follow the pattern `ws:room:{roomName}`. The payload is a JSON-stringified object containing `{ event, path, data, params, excludeId }`.
49
+
50
+ ### `useWsRooms()` composable
51
+
52
+ See [composables.md](composables.md) for full API. The composable provides `join()`, `leave()`, `broadcast()`, and `rooms()` scoped to the current connection and message path.
53
+
54
+ ## Common Patterns
55
+
56
+ ### Pattern: Chat rooms
57
+
58
+ ```ts
59
+ ws.onMessage('subscribe', '/chat/rooms/:roomId', () => {
60
+ const { join } = useWsRooms()
61
+ join() // joins room = "/chat/rooms/:roomId" (resolved path)
62
+ return { subscribed: true }
63
+ })
64
+
65
+ ws.onMessage('unsubscribe', '/chat/rooms/:roomId', () => {
66
+ const { leave } = useWsRooms()
67
+ leave()
68
+ return { unsubscribed: true }
69
+ })
70
+
71
+ ws.onMessage('message', '/chat/rooms/:roomId', () => {
72
+ const { data } = useWsMessage<{ text: string }>()
73
+ const { broadcast } = useWsRooms()
74
+ broadcast('message', data) // excludes sender by default
75
+ return { sent: true }
76
+ })
77
+ ```
78
+
79
+ ### Pattern: Redis broadcast transport
80
+
81
+ ```ts
82
+ import Redis from 'ioredis'
83
+
84
+ const pub = new Redis()
85
+ const sub = new Redis()
86
+ const handlers = new Map<string, (payload: string) => void>()
87
+
88
+ const redisTransport: WsBroadcastTransport = {
89
+ publish(channel, payload) {
90
+ pub.publish(channel, payload)
91
+ },
92
+ subscribe(channel, handler) {
93
+ handlers.set(channel, handler)
94
+ sub.subscribe(channel)
95
+ },
96
+ unsubscribe(channel) {
97
+ handlers.delete(channel)
98
+ sub.unsubscribe(channel)
99
+ },
100
+ }
101
+
102
+ sub.on('message', (channel, payload) => {
103
+ handlers.get(channel)?.(payload)
104
+ })
105
+
106
+ const ws = createWsApp({ broadcastTransport: redisTransport })
107
+ ```
108
+
109
+ ### Pattern: Server-wide broadcast (no rooms)
110
+
111
+ ```ts
112
+ const { broadcast } = useWsServer()
113
+ broadcast('announcement', '/system/alert', { message: 'Server restarting' })
114
+ ```
115
+
116
+ ### Pattern: Broadcast to a specific room from outside message context
117
+
118
+ ```ts
119
+ // From a different handler (e.g., HTTP endpoint)
120
+ const { roomConnections } = useWsServer()
121
+ const connections = roomConnections('/chat/rooms/lobby')
122
+ for (const conn of connections) {
123
+ conn.send('notification', '/chat/rooms/lobby', { text: 'New event!' })
124
+ }
125
+ ```
126
+
127
+ ## Best Practices
128
+
129
+ - Room names default to the message path — this is the most natural mapping (subscribe to `/chat/rooms/lobby` → join room `/chat/rooms/lobby`)
130
+ - Use `excludeSelf: true` (default) for chat-style broadcasts where the sender already has their own confirmation
131
+ - Implement `WsBroadcastTransport` for horizontal scaling — without it, broadcasts only reach connections on the same instance
132
+ - Connections are automatically removed from all rooms on disconnect (`leaveAll` is called internally)
133
+
134
+ ## Gotchas
135
+
136
+ - The transport channel format is `ws:room:{roomName}` — make sure your Redis/NATS key patterns don't conflict
137
+ - Transport messages include `excludeId` to prevent echo on the originating instance, but the exclude only works by connection ID — if the same user has multiple connections, other connections will still receive the broadcast
138
+ - Empty rooms are automatically cleaned up (removed from the internal Map)
139
+ - `useWsRooms()` is only available in message context — you cannot join/leave rooms from `onConnect`/`onDisconnect`
@@ -0,0 +1,150 @@
1
+ # Testing — @wooksjs/event-ws
2
+
3
+ > Test helpers for creating mock WS contexts and running handler code outside a real server.
4
+
5
+ ## Concepts
6
+
7
+ `@wooksjs/event-ws` provides test utilities that create fully initialized `EventContext` instances with mock WebSocket sockets. These let you unit-test composables and handler logic without starting a server or establishing real connections.
8
+
9
+ Two context types match the two context layers:
10
+
11
+ 1. **Connection context** — for testing `onConnect`/`onDisconnect` handlers
12
+ 2. **Message context** — for testing `onMessage` handlers (includes a parent connection context)
13
+
14
+ Each helper returns a **runner function** `<T>(cb: (...a: any[]) => T) => T` that executes a callback inside the scoped context.
15
+
16
+ ## API Reference
17
+
18
+ ### `prepareTestWsConnectionContext(options?)`
19
+
20
+ Creates a connection context with a mock `WsSocket`.
21
+
22
+ Options:
23
+
24
+ ```ts
25
+ interface TTestWsConnectionContext {
26
+ id?: string // default: 'test-conn-id'
27
+ params?: Record<string, string | string[]> // pre-set route params
28
+ parentCtx?: EventContext // optional parent (e.g., HTTP context)
29
+ }
30
+ ```
31
+
32
+ Returns: `<T>(cb: (...a: any[]) => T) => T`
33
+
34
+ ```ts
35
+ import { prepareTestWsConnectionContext } from '@wooksjs/event-ws'
36
+
37
+ const runInCtx = prepareTestWsConnectionContext({ id: 'conn-1' })
38
+
39
+ runInCtx(() => {
40
+ const { id } = useWsConnection()
41
+ expect(id).toBe('conn-1')
42
+ })
43
+ ```
44
+
45
+ ### `prepareTestWsMessageContext(options)`
46
+
47
+ Creates a message context with a parent connection context. Both contexts are fully seeded.
48
+
49
+ Options:
50
+
51
+ ```ts
52
+ interface TTestWsMessageContext extends TTestWsConnectionContext {
53
+ event: string // required
54
+ path: string // required
55
+ data?: unknown
56
+ messageId?: string | number
57
+ rawMessage?: Buffer | string // default: JSON.stringify of the message
58
+ }
59
+ ```
60
+
61
+ Returns: `<T>(cb: (...a: any[]) => T) => T`
62
+
63
+ ```ts
64
+ import { prepareTestWsMessageContext } from '@wooksjs/event-ws'
65
+
66
+ const runInCtx = prepareTestWsMessageContext({
67
+ event: 'message',
68
+ path: '/chat/lobby',
69
+ data: { text: 'hello' },
70
+ messageId: 42,
71
+ })
72
+
73
+ runInCtx(() => {
74
+ const { data, id, path } = useWsMessage<{ text: string }>()
75
+ expect(data.text).toBe('hello')
76
+ expect(id).toBe(42)
77
+ expect(path).toBe('/chat/lobby')
78
+ })
79
+ ```
80
+
81
+ ## Common Patterns
82
+
83
+ ### Pattern: Testing composables with adapter state
84
+
85
+ When testing composables that access adapter state (`useWsConnection`, `useWsRooms`), you must set up the adapter state first:
86
+
87
+ ```ts
88
+ import { setAdapterState } from '@wooksjs/event-ws/composables/state' // internal
89
+ import { WsRoomManager, WsConnection } from '@wooksjs/event-ws'
90
+
91
+ const connections = new Map<string, WsConnection>()
92
+ const roomManager = new WsRoomManager()
93
+ setAdapterState({
94
+ connections,
95
+ roomManager,
96
+ serializer: JSON.stringify,
97
+ wooks: {} as any,
98
+ })
99
+ ```
100
+
101
+ Note: `setAdapterState` is an internal function — import it from the source in tests.
102
+
103
+ ### Pattern: Testing with HTTP parent context
104
+
105
+ ```ts
106
+ import { EventContext } from '@wooksjs/event-core'
107
+
108
+ const httpCtx = new EventContext({ logger: console as any })
109
+ // Seed httpCtx with HTTP-specific data if needed
110
+
111
+ const runInCtx = prepareTestWsMessageContext({
112
+ event: 'test',
113
+ path: '/test',
114
+ parentCtx: httpCtx,
115
+ })
116
+
117
+ runInCtx(() => {
118
+ const connCtx = currentConnection()
119
+ expect(connCtx.parent).toBe(httpCtx)
120
+ })
121
+ ```
122
+
123
+ ### Pattern: Testing route params
124
+
125
+ ```ts
126
+ const runInCtx = prepareTestWsMessageContext({
127
+ event: 'message',
128
+ path: '/chat/rooms/lobby',
129
+ params: { roomId: 'lobby' },
130
+ data: { text: 'hi' },
131
+ })
132
+
133
+ runInCtx(() => {
134
+ const { params } = useRouteParams()
135
+ expect(params.roomId).toBe('lobby')
136
+ })
137
+ ```
138
+
139
+ ## Best Practices
140
+
141
+ - Always use the test helpers rather than manually constructing `EventContext` — they ensure proper kind seeding
142
+ - Set up adapter state before testing composables that depend on it (`useWsConnection`, `useWsRooms`, `useWsServer`)
143
+ - Use `parentCtx` to simulate HTTP-integrated mode when testing composables that traverse the parent chain
144
+ - The mock `WsSocket` has no-op methods — if you need to assert on sent messages, create a custom mock and use `WsConnection` directly
145
+
146
+ ## Gotchas
147
+
148
+ - The mock socket's `readyState` is set to `1` (OPEN) by default — `send()` calls will pass the guard check
149
+ - `prepareTestWsMessageContext` requires `event` and `path` — they are not optional
150
+ - Test contexts use `console` as the logger — override in `TTestWsConnectionContext` is not directly supported; wrap with a custom `EventContext` if needed