@wooksjs/event-ws 0.7.7 → 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 +7 -13
- package/scripts/setup-skills.js +0 -78
- package/skills/wooksjs-event-ws/SKILL.md +0 -50
- package/skills/wooksjs-event-ws/composables.md +0 -157
- package/skills/wooksjs-event-ws/core.md +0 -229
- package/skills/wooksjs-event-ws/rooms.md +0 -139
- package/skills/wooksjs-event-ws/testing.md +0 -150
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooksjs/event-ws",
|
|
3
|
-
"version": "0.7.
|
|
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.
|
|
51
|
-
"wooks": "^0.7.
|
|
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.
|
|
58
|
-
"wooks": "^0.7.
|
|
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
|
}
|
package/scripts/setup-skills.js
DELETED
|
@@ -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
|