@wooksjs/event-ws 0.7.8 → 0.7.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooksjs/event-ws",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
4
4
  "description": "WebSocket adapter for Wooks with path-based message routing, rooms, and broadcasting",
5
5
  "keywords": [
6
6
  "api",
@@ -24,13 +24,8 @@
24
24
  "url": "git+https://github.com/wooksjs/wooksjs.git",
25
25
  "directory": "packages/event-ws"
26
26
  },
27
- "bin": {
28
- "wooksjs-event-ws-skill": "./scripts/setup-skills.js"
29
- },
30
27
  "files": [
31
- "dist",
32
- "skills",
33
- "scripts/setup-skills.js"
28
+ "dist"
34
29
  ],
35
30
  "main": "dist/index.cjs",
36
31
  "module": "dist/index.mjs",
@@ -47,18 +42,17 @@
47
42
  "@types/ws": "^8.18.1",
48
43
  "typescript": "^5.9.3",
49
44
  "vitest": "^3.2.4",
50
- "@wooksjs/event-core": "^0.7.8",
51
- "wooks": "^0.7.8"
45
+ "@wooksjs/event-core": "^0.7.9",
46
+ "wooks": "^0.7.9"
52
47
  },
53
48
  "peerDependencies": {
54
49
  "@prostojs/logger": "^0.4.3",
55
50
  "@prostojs/router": "^0.3.3",
56
51
  "ws": "^8.0.0",
57
- "@wooksjs/event-core": "^0.7.8",
58
- "wooks": "^0.7.8"
52
+ "@wooksjs/event-core": "^0.7.9",
53
+ "wooks": "^0.7.9"
59
54
  },
60
55
  "scripts": {
61
- "build": "rolldown -c ../../rolldown.config.mjs",
62
- "setup-skills": "node ./scripts/setup-skills.js"
56
+ "build": "rolldown -c ../../rolldown.config.mjs"
63
57
  }
64
58
  }
@@ -1,78 +0,0 @@
1
- #!/usr/bin/env node
2
- /* prettier-ignore */
3
- import fs from 'fs'
4
- import path from 'path'
5
- import os from 'os'
6
- import { fileURLToPath } from 'url'
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
-
10
- const SKILL_NAME = 'wooksjs-event-ws'
11
- const SKILL_SRC = path.join(__dirname, '..', 'skills', SKILL_NAME)
12
-
13
- if (!fs.existsSync(SKILL_SRC)) {
14
- console.error(`No skills found at ${SKILL_SRC}`)
15
- console.error('Add your SKILL.md files to the skills/' + SKILL_NAME + '/ directory first.')
16
- process.exit(1)
17
- }
18
-
19
- const AGENTS = {
20
- 'Claude Code': { dir: '.claude/skills', global: path.join(os.homedir(), '.claude', 'skills') },
21
- 'Cursor': { dir: '.cursor/skills', global: path.join(os.homedir(), '.cursor', 'skills') },
22
- 'Windsurf': { dir: '.windsurf/skills', global: path.join(os.homedir(), '.windsurf', 'skills') },
23
- 'Codex': { dir: '.codex/skills', global: path.join(os.homedir(), '.codex', 'skills') },
24
- 'OpenCode': { dir: '.opencode/skills', global: path.join(os.homedir(), '.opencode', 'skills') },
25
- }
26
-
27
- const args = process.argv.slice(2)
28
- const isGlobal = args.includes('--global') || args.includes('-g')
29
- const isPostinstall = args.includes('--postinstall')
30
- let installed = 0, skipped = 0
31
- const installedDirs = []
32
-
33
- for (const [agentName, cfg] of Object.entries(AGENTS)) {
34
- const targetBase = isGlobal ? cfg.global : path.join(process.cwd(), cfg.dir)
35
- const agentRootDir = path.dirname(cfg.global) // Check if the agent has ever been installed globally
36
-
37
- // In postinstall mode: silently skip agents that aren't set up globally
38
- if (isPostinstall || isGlobal) {
39
- if (!fs.existsSync(agentRootDir)) { skipped++; continue }
40
- }
41
-
42
- const dest = path.join(targetBase, SKILL_NAME)
43
- try {
44
- fs.mkdirSync(dest, { recursive: true })
45
- fs.cpSync(SKILL_SRC, dest, { recursive: true })
46
- console.log(`✅ ${agentName}: installed to ${dest}`)
47
- installed++
48
- if (!isGlobal) installedDirs.push(cfg.dir + '/' + SKILL_NAME)
49
- } catch (err) {
50
- console.warn(`⚠️ ${agentName}: failed — ${err.message}`)
51
- }
52
- }
53
-
54
- // Add locally-installed skill dirs to .gitignore
55
- if (!isGlobal && installedDirs.length > 0) {
56
- const gitignorePath = path.join(process.cwd(), '.gitignore')
57
- let gitignoreContent = ''
58
- try { gitignoreContent = fs.readFileSync(gitignorePath, 'utf8') } catch {}
59
- const linesToAdd = installedDirs.filter(d => !gitignoreContent.includes(d))
60
- if (linesToAdd.length > 0) {
61
- const hasHeader = gitignoreContent.includes('# AI agent skills')
62
- const block = (gitignoreContent && !gitignoreContent.endsWith('\n') ? '\n' : '')
63
- + (hasHeader ? '' : '\n# AI agent skills (auto-generated by setup-skills)\n')
64
- + linesToAdd.join('\n') + '\n'
65
- fs.appendFileSync(gitignorePath, block)
66
- console.log(`📝 Added ${linesToAdd.length} entries to .gitignore`)
67
- }
68
- }
69
-
70
- if (installed === 0 && isPostinstall) {
71
- // Silence is fine — no agents present, nothing to do
72
- } else if (installed === 0 && skipped === Object.keys(AGENTS).length) {
73
- console.log('No agent directories detected. Try --global or run without it for project-local install.')
74
- } else if (installed === 0) {
75
- console.log('Nothing installed. Run without --global to install project-locally.')
76
- } else {
77
- console.log(`\n✨ Done! Restart your AI agent to pick up the "${SKILL_NAME}" skill.`)
78
- }
@@ -1,50 +0,0 @@
1
- ---
2
- name: wooksjs-event-ws
3
- description: Use this skill when working with @wooksjs/event-ws — to create a WebSocket server with createWsApp() or WooksWs, register message handlers with onMessage(), handle connections with onConnect()/onDisconnect(), use composables like useWsConnection(), useWsMessage(), useWsRooms(), useWsServer(), manage rooms and broadcasting with WsRoomManager and WsBroadcastTransport, integrate with event-http via upgrade(), throw WsError for error replies, or test handlers with prepareTestWsConnectionContext()/prepareTestWsMessageContext(). Covers the wire protocol (WsClientMessage, WsReplyMessage, WsPushMessage), standalone and HTTP-integrated modes, heartbeat, and custom serializers.
4
- ---
5
-
6
- # @wooksjs/event-ws
7
-
8
- WebSocket adapter for Wooks with path-based message routing, rooms, broadcasting, and composable context — runs standalone or integrated with `@wooksjs/event-http`.
9
-
10
- ## How to use this skill
11
-
12
- Read the domain file that matches the task. Do not load all files — only what you need.
13
-
14
- | Domain | File | Load when... |
15
- | --------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------- |
16
- | Core concepts & setup | [core.md](core.md) | Creating a WS server, understanding the wire protocol, configuring options, standalone vs HTTP-integrated mode |
17
- | Composables | [composables.md](composables.md) | Using `useWsConnection`, `useWsMessage`, `useWsRooms`, `useWsServer`, or `currentConnection` inside handlers |
18
- | Rooms & broadcasting | [rooms.md](rooms.md) | Managing rooms, broadcasting to groups, cross-instance pub/sub with `WsBroadcastTransport` |
19
- | Testing | [testing.md](testing.md) | Unit-testing WS handlers with `prepareTestWsConnectionContext` and `prepareTestWsMessageContext` |
20
-
21
- ## Quick reference
22
-
23
- ```ts
24
- import {
25
- // factory
26
- createWsApp,
27
- WooksWs,
28
- // composables
29
- useWsConnection,
30
- useWsMessage,
31
- useWsRooms,
32
- useWsServer,
33
- currentConnection,
34
- // context factories
35
- createWsConnectionContext,
36
- createWsMessageContext,
37
- // kinds & types
38
- wsConnectionKind,
39
- wsMessageKind,
40
- WsError,
41
- WsConnection,
42
- WsRoomManager,
43
- // testing
44
- prepareTestWsConnectionContext,
45
- prepareTestWsMessageContext,
46
- // re-exports from event-core
47
- useRouteParams,
48
- useLogger,
49
- } from '@wooksjs/event-ws'
50
- ```
@@ -1,157 +0,0 @@
1
- # Composables — @wooksjs/event-ws
2
-
3
- > Composable functions for accessing WebSocket connection, message, room, and server state inside handlers.
4
-
5
- ## Concepts
6
-
7
- All composables follow the Wooks `defineWook` pattern — they read from the current `EventContext` via `AsyncLocalStorage`. They are only valid inside handler functions registered with `onMessage`, `onConnect`, or `onDisconnect`.
8
-
9
- The WS adapter creates two context levels:
10
-
11
- - **Connection context** — available in `onConnect`, `onDisconnect`, and `onMessage` (via parent chain)
12
- - **Message context** — only available in `onMessage` handlers
13
-
14
- ## API Reference
15
-
16
- ### `useWsConnection(ctx?)`
17
-
18
- Access the current WebSocket connection. Works in both connection and message contexts.
19
-
20
- Returns:
21
-
22
- ```ts
23
- {
24
- id: string // unique connection ID (UUID)
25
- send(event, path, data?, params?): void // push a message to this client
26
- close(code?, reason?): void // close the connection
27
- context: EventContext // the connection EventContext
28
- }
29
- ```
30
-
31
- ```ts
32
- ws.onMessage('message', '/chat/:room', () => {
33
- const { id, send } = useWsConnection()
34
- send('ack', '/chat/lobby', { received: true })
35
- })
36
- ```
37
-
38
- ### `useWsMessage<T>(ctx?)`
39
-
40
- Access the current WebSocket message data. **Only available in message context** (inside `onMessage` handlers).
41
-
42
- Returns:
43
-
44
- ```ts
45
- {
46
- data: T // parsed message data (generic typed)
47
- raw: Buffer | string // raw message before parsing
48
- id: string | number | undefined // correlation ID
49
- path: string // message path
50
- event: string // message event type
51
- }
52
- ```
53
-
54
- ```ts
55
- ws.onMessage('message', '/chat/:room', () => {
56
- const { data, id, path } = useWsMessage<{ text: string }>()
57
- console.log(data.text) // typed as string
58
- })
59
- ```
60
-
61
- ### `useWsRooms(ctx?)`
62
-
63
- Room management for the current connection. **Only available in message context.** Defaults to the current message path as the room name.
64
-
65
- Returns:
66
-
67
- ```ts
68
- {
69
- join(room?): void // join a room (default: current message path)
70
- leave(room?): void // leave a room (default: current message path)
71
- broadcast(event, data?, options?): void // broadcast to a room
72
- rooms(): string[] // list rooms this connection has joined
73
- }
74
- ```
75
-
76
- `WsBroadcastOptions`:
77
-
78
- ```ts
79
- {
80
- room?: string // target room (default: current message path)
81
- excludeSelf?: boolean // exclude sender (default: true)
82
- }
83
- ```
84
-
85
- ```ts
86
- ws.onMessage('subscribe', '/chat/rooms/:roomId', () => {
87
- const { join } = useWsRooms()
88
- join() // joins the room matching the current path
89
- })
90
-
91
- ws.onMessage('message', '/chat/rooms/:roomId', () => {
92
- const { data } = useWsMessage<{ text: string }>()
93
- const { broadcast } = useWsRooms()
94
- broadcast('message', data) // sends to all in room except sender
95
- })
96
- ```
97
-
98
- ### `useWsServer()`
99
-
100
- Server-wide operations. Available in any context (not scoped to a specific connection). Reads directly from adapter state, not from `EventContext`.
101
-
102
- Returns:
103
-
104
- ```ts
105
- {
106
- connections(): Map<string, WsConnection> // all active connections
107
- broadcast(event, path, data?, params?): void // broadcast to ALL connections
108
- getConnection(id: string): WsConnection | undefined
109
- roomConnections(room: string): Set<WsConnection> // connections in a room
110
- }
111
- ```
112
-
113
- ```ts
114
- ws.onMessage('rpc', '/admin/broadcast', () => {
115
- const { data } = useWsMessage<{ text: string }>()
116
- const { broadcast } = useWsServer()
117
- broadcast('announcement', '/system', data)
118
- })
119
- ```
120
-
121
- ### `currentConnection(ctx?)`
122
-
123
- Returns the connection `EventContext` regardless of whether you're in a connection or message handler.
124
-
125
- - In `onConnect`/`onDisconnect`: returns `current()` directly
126
- - In `onMessage`: returns `current().parent` (the connection context)
127
-
128
- ```ts
129
- const connCtx = currentConnection()
130
- ```
131
-
132
- ### Re-exports from `@wooksjs/event-core`
133
-
134
- These are re-exported for convenience:
135
-
136
- - `useRouteParams(ctx?)` — access path params from the Wooks router
137
- - `useLogger(ctx?)` — access the scoped logger
138
-
139
- ```ts
140
- ws.onMessage('rpc', '/users/:id', () => {
141
- const { params } = useRouteParams()
142
- return { userId: params.id }
143
- })
144
- ```
145
-
146
- ## Best Practices
147
-
148
- - Always type `useWsMessage<T>()` with your expected payload type for type safety
149
- - Use `useWsRooms()` without arguments to default to the current message path as the room — this is the most common pattern
150
- - Prefer `useWsServer().broadcast()` for server-wide announcements, `useWsRooms().broadcast()` for room-scoped messages
151
- - The `context` property from `useWsConnection()` gives access to the raw `EventContext` for advanced use (e.g., reading custom context keys set during `onConnect`)
152
-
153
- ## Gotchas
154
-
155
- - `useWsMessage()` and `useWsRooms()` throw if called outside a message context (e.g., inside `onConnect`)
156
- - `useWsServer()` is not a `defineWook` — it reads from module-level adapter state, so it works anywhere but requires the adapter to be initialized
157
- - `useWsConnection().send()` silently drops messages if the socket is not in OPEN state (readyState !== 1)
@@ -1,229 +0,0 @@
1
- # Core concepts & setup — @wooksjs/event-ws
2
-
3
- > WebSocket adapter for Wooks: creates a WS server, routes messages by event+path, manages connections, and supports standalone or HTTP-integrated modes.
4
-
5
- ## Concepts
6
-
7
- `@wooksjs/event-ws` follows the Wooks adapter pattern. It creates two nested context layers via dedicated context factories:
8
-
9
- 1. **Connection context** (`ws:connection` kind) — long-lived, one per connected client. Created via `createWsConnectionContext(options, seeds, fn)`, seeded with `id` and `ws` socket.
10
- 2. **Message context** (`ws:message` kind) — short-lived, one per incoming message. Created via `createWsMessageContext(options, seeds, fn)` with `parent` set to the connection context.
11
-
12
- Messages follow a JSON wire protocol with `event` + `path` for routing and an optional `id` for request-response correlation.
13
-
14
- ### Wire protocol
15
-
16
- **Client → Server (`WsClientMessage`):**
17
-
18
- ```ts
19
- interface WsClientMessage {
20
- event: string // router method (e.g. "message", "rpc", "subscribe")
21
- path: string // route path (e.g. "/chat/rooms/lobby")
22
- data?: unknown // payload
23
- id?: string | number // correlation ID — triggers a reply
24
- }
25
- ```
26
-
27
- **Server → Client reply (`WsReplyMessage`):**
28
-
29
- ```ts
30
- interface WsReplyMessage {
31
- id: string | number // matches the client's id
32
- data?: unknown // handler return value
33
- error?: { code: number; message: string }
34
- }
35
- ```
36
-
37
- **Server → Client push (`WsPushMessage`):**
38
-
39
- ```ts
40
- interface WsPushMessage {
41
- event: string
42
- path: string
43
- params?: Record<string, string>
44
- data?: unknown
45
- }
46
- ```
47
-
48
- ## Installation / Setup
49
-
50
- ```bash
51
- pnpm add @wooksjs/event-ws wooks @wooksjs/event-core ws
52
- ```
53
-
54
- `ws` is a peer dependency (the default WebSocket server implementation). You can substitute it with a custom `WsServerAdapter`.
55
-
56
- ## API Reference
57
-
58
- ### `createWsApp(wooksOrOpts?, opts?)`
59
-
60
- Factory that creates a `WooksWs` instance.
61
-
62
- - `wooksOrOpts` — a `Wooks` or `WooksAdapterBase` instance (HTTP integration), or `TWooksWsOptions` (standalone)
63
- - `opts` — `TWooksWsOptions` when the first arg is a Wooks instance
64
-
65
- Returns: `WooksWs`
66
-
67
- ### `TWooksWsOptions`
68
-
69
- ```ts
70
- interface TWooksWsOptions {
71
- heartbeatInterval?: number // ping interval in ms (default: 30000, 0 = disabled)
72
- heartbeatTimeout?: number // pong timeout in ms (default: 5000)
73
- messageParser?: (raw: Buffer | string) => WsClientMessage
74
- messageSerializer?: (msg: WsReplyMessage | WsPushMessage) => string | Buffer
75
- logger?: TConsoleBase
76
- maxMessageSize?: number // bytes (default: 1MB), oversized messages are dropped
77
- wsServerAdapter?: WsServerAdapter // custom WS engine (default: wraps `ws`)
78
- broadcastTransport?: WsBroadcastTransport // for multi-instance
79
- }
80
- ```
81
-
82
- ### `WooksWs` class
83
-
84
- Extends `WooksAdapterBase`, implements `WooksUpgradeHandler`.
85
-
86
- #### `ws.onMessage(event, path, handler)`
87
-
88
- Register a routed message handler. Uses the Wooks router — supports path params.
89
-
90
- ```ts
91
- ws.onMessage('message', '/chat/rooms/:roomId', () => {
92
- const { data } = useWsMessage<{ text: string }>()
93
- const { params } = useRouteParams()
94
- // ...
95
- })
96
- ```
97
-
98
- #### `ws.onConnect(handler)`
99
-
100
- Register a handler that runs when a new WebSocket connection is established. Runs inside the connection context. Throwing or rejecting closes the connection.
101
-
102
- ```ts
103
- ws.onConnect(() => {
104
- const { id } = useWsConnection()
105
- console.log('Connected:', id)
106
- })
107
- ```
108
-
109
- #### `ws.onDisconnect(handler)`
110
-
111
- Register a handler that runs when a connection closes.
112
-
113
- #### `ws.upgrade()`
114
-
115
- Complete the WebSocket handshake from inside an HTTP UPGRADE route handler. Reads `req`/`socket`/`head` from the current HTTP context. The HTTP context becomes the parent of the WS connection context.
116
-
117
- #### `ws.handleUpgrade(req, socket, head)`
118
-
119
- Fallback for when no UPGRADE route matches (called by the HTTP adapter automatically).
120
-
121
- #### `ws.listen(port, hostname?)`
122
-
123
- Start a standalone server (without `event-http`). Returns a `Promise<void>`.
124
-
125
- #### `ws.close()`
126
-
127
- Stop the server, close all connections, clean up heartbeat.
128
-
129
- #### `ws.getServer()`
130
-
131
- Returns the underlying `http.Server` (standalone mode only).
132
-
133
- ### `WsError`
134
-
135
- Error class with a numeric `code` following HTTP conventions. Throwing `WsError` in a handler sends an error reply to the client.
136
-
137
- ```ts
138
- throw new WsError(403, 'Forbidden')
139
- ```
140
-
141
- ### `WsSocket` interface
142
-
143
- Minimal WebSocket instance interface — compatible with `ws`, uWebSockets.js, and Bun.
144
-
145
- ### `WsServerAdapter` interface
146
-
147
- Factory for creating a custom WebSocket server:
148
-
149
- ```ts
150
- interface WsServerAdapter {
151
- create(): WsServerInstance
152
- }
153
- ```
154
-
155
- ## Common Patterns
156
-
157
- ### Pattern: HTTP-integrated mode (recommended)
158
-
159
- ```ts
160
- import { createHttpApp } from '@wooksjs/event-http'
161
- import { createWsApp } from '@wooksjs/event-ws'
162
-
163
- const http = createHttpApp()
164
- const ws = createWsApp(http) // auto-registers upgrade contract
165
-
166
- http.upgrade('/ws', () => ws.upgrade())
167
-
168
- ws.onMessage('message', '/chat/rooms/:roomId', () => {
169
- const { data } = useWsMessage<{ text: string }>()
170
- return { ok: true }
171
- })
172
-
173
- http.listen(3000)
174
- ```
175
-
176
- ### Pattern: Standalone mode
177
-
178
- ```ts
179
- import { createWsApp } from '@wooksjs/event-ws'
180
-
181
- const ws = createWsApp({ heartbeatInterval: 30_000 })
182
-
183
- ws.onMessage('rpc', '/users/:id', () => {
184
- const { data } = useWsMessage()
185
- const { params } = useRouteParams()
186
- return { userId: params.id }
187
- })
188
-
189
- ws.listen(3000)
190
- ```
191
-
192
- ### Pattern: Connection authentication
193
-
194
- ```ts
195
- ws.onConnect(() => {
196
- // Access HTTP context if using HTTP-integrated mode
197
- // Throw to reject the connection
198
- const token = getTokenFromSomewhere()
199
- if (!isValid(token)) {
200
- throw new WsError(401, 'Unauthorized')
201
- }
202
- })
203
- ```
204
-
205
- ### Pattern: Custom serializer (e.g. MessagePack)
206
-
207
- ```ts
208
- import { encode, decode } from '@msgpack/msgpack'
209
-
210
- const ws = createWsApp({
211
- messageParser: (raw) => decode(raw as Buffer) as WsClientMessage,
212
- messageSerializer: (msg) => Buffer.from(encode(msg)),
213
- })
214
- ```
215
-
216
- ## Best Practices
217
-
218
- - Use HTTP-integrated mode for production — it shares the HTTP server and handles UPGRADE routing cleanly
219
- - Set `maxMessageSize` appropriate for your use case to prevent memory abuse
220
- - Use `WsError` with HTTP-style codes for structured error replies
221
- - Keep `onConnect` handlers fast — they block connection acceptance
222
- - Heartbeat is enabled by default (30s); set to 0 only for short-lived connections
223
-
224
- ## Gotchas
225
-
226
- - Handler return values are only sent as replies when the client message included an `id` field. Fire-and-forget messages (no `id`) get no reply even if the handler returns a value.
227
- - The `ws` package is a peer dependency — you must install it explicitly.
228
- - `useWsRooms()` and `useWsMessage()` are only available in message context (inside `onMessage` handlers), not in `onConnect`/`onDisconnect`.
229
- - When a connection context has an HTTP parent (integrated mode), composables from `@wooksjs/event-http` can read HTTP headers/cookies via the parent chain.
@@ -1,139 +0,0 @@
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`
@@ -1,150 +0,0 @@
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