@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
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wooksjs/event-ws",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "WebSocket adapter for Wooks with path-based message routing, rooms, and broadcasting",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"api",
|
|
7
|
+
"composables",
|
|
8
|
+
"framework",
|
|
9
|
+
"prostojs",
|
|
10
|
+
"realtime",
|
|
11
|
+
"rooms",
|
|
12
|
+
"websocket",
|
|
13
|
+
"wooks",
|
|
14
|
+
"ws"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/wooksjs/wooksjs/tree/main/packages/event-ws#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/wooksjs/wooksjs/issues"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Artem Maltsev",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/wooksjs/wooksjs.git",
|
|
25
|
+
"directory": "packages/event-ws"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"wooksjs-event-ws-skill": "./scripts/setup-skills.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"skills",
|
|
33
|
+
"scripts/setup-skills.js"
|
|
34
|
+
],
|
|
35
|
+
"main": "dist/index.cjs",
|
|
36
|
+
"module": "dist/index.mjs",
|
|
37
|
+
"types": "dist/index.d.ts",
|
|
38
|
+
"exports": {
|
|
39
|
+
"./package.json": "./package.json",
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"require": "./dist/index.cjs",
|
|
43
|
+
"import": "./dist/index.mjs"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/ws": "^8.18.1",
|
|
48
|
+
"typescript": "^5.9.3",
|
|
49
|
+
"vitest": "^3.2.4",
|
|
50
|
+
"@wooksjs/event-core": "^0.7.0",
|
|
51
|
+
"wooks": "^0.7.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@prostojs/logger": "^0.4.3",
|
|
55
|
+
"@prostojs/router": "^0.3.2",
|
|
56
|
+
"ws": "^8.0.0",
|
|
57
|
+
"@wooksjs/event-core": "^0.7.0",
|
|
58
|
+
"wooks": "^0.7.0"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "rolldown -c ../../rolldown.config.mjs",
|
|
62
|
+
"setup-skills": "node ./scripts/setup-skills.js"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
// kinds & types
|
|
35
|
+
wsConnectionKind,
|
|
36
|
+
wsMessageKind,
|
|
37
|
+
WsError,
|
|
38
|
+
WsConnection,
|
|
39
|
+
WsRoomManager,
|
|
40
|
+
// testing
|
|
41
|
+
prepareTestWsConnectionContext,
|
|
42
|
+
prepareTestWsMessageContext,
|
|
43
|
+
// re-exports from event-core
|
|
44
|
+
useRouteParams,
|
|
45
|
+
useLogger,
|
|
46
|
+
} from '@wooksjs/event-ws'
|
|
47
|
+
```
|
|
@@ -0,0 +1,157 @@
|
|
|
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)
|
|
@@ -0,0 +1,229 @@
|
|
|
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:
|
|
8
|
+
|
|
9
|
+
1. **Connection context** (`ws:connection` kind) — long-lived, one per connected client. Seeded with `id` and `ws` socket.
|
|
10
|
+
2. **Message context** (`ws:message` kind) — short-lived, one per incoming message. Its `parent` is 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.
|