@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.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/index.cjs +725 -0
- package/dist/index.d.ts +335 -0
- package/dist/index.mjs +677 -0
- package/package.json +64 -0
- package/scripts/setup-skills.js +78 -0
- package/skills/wooksjs-event-ws/SKILL.md +47 -0
- package/skills/wooksjs-event-ws/composables.md +157 -0
- package/skills/wooksjs-event-ws/core.md +229 -0
- package/skills/wooksjs-event-ws/rooms.md +139 -0
- package/skills/wooksjs-event-ws/testing.md +150 -0
|
@@ -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
|