@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/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.