@moostjs/event-ws 0.6.6 → 0.6.8

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": "@moostjs/event-ws",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "@moostjs/event-ws",
5
5
  "keywords": [
6
6
  "composables",
@@ -22,13 +22,8 @@
22
22
  "url": "git+https://github.com/moostjs/moostjs.git",
23
23
  "directory": "packages/event-ws"
24
24
  },
25
- "bin": {
26
- "moostjs-event-ws-skill": "./scripts/setup-skills.js"
27
- },
28
25
  "files": [
29
- "dist",
30
- "skills",
31
- "scripts/setup-skills.js"
26
+ "dist"
32
27
  ],
33
28
  "type": "module",
34
29
  "sideEffects": false,
@@ -44,19 +39,18 @@
44
39
  }
45
40
  },
46
41
  "dependencies": {
47
- "@wooksjs/event-ws": "^0.7.8"
42
+ "@wooksjs/event-ws": "^0.7.10"
48
43
  },
49
44
  "devDependencies": {
50
45
  "vitest": "3.2.4"
51
46
  },
52
47
  "peerDependencies": {
53
48
  "@prostojs/infact": "^0.4.1",
54
- "@wooksjs/event-core": "^0.7.8",
55
- "moost": "^0.6.6"
49
+ "@wooksjs/event-core": "^0.7.10",
50
+ "moost": "^0.6.8"
56
51
  },
57
52
  "scripts": {
58
53
  "pub": "pnpm publish --access public",
59
- "test": "vitest",
60
- "setup-skills": "node ./scripts/setup-skills.js"
54
+ "test": "vitest"
61
55
  }
62
56
  }
@@ -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 = 'moostjs-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,42 +0,0 @@
1
- ---
2
- name: moostjs-event-ws
3
- description: Use this skill when working with @moostjs/event-ws — to build WebSocket servers with Moost using MoostWs adapter or WsApp quick factory, register message handlers with @Message(), handle connections with @Connect()/@Disconnect(), extract data with @MessageData()/@ConnectionId()/@RawMessage()/@MessageId()/@MessageType()/@MessagePath(), use composables like useWsConnection(), useWsMessage(), useWsRooms(), useWsServer(), manage rooms and broadcasting, integrate with @moostjs/event-http via @Upgrade() routes, throw WsError for error replies, or test handlers with prepareTestWsConnectionContext()/prepareTestWsMessageContext(). Covers the wire protocol (WsClientMessage, WsReplyMessage, WsPushMessage), standalone and HTTP-integrated modes, heartbeat, custom serializers, and multi-instance broadcasting with WsBroadcastTransport.
4
- ---
5
-
6
- # @moostjs/event-ws
7
-
8
- Moost WebSocket adapter — decorator-based routing, DI, interceptors, and pipes for WebSocket handlers, wrapping `@wooksjs/event-ws`.
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) | Starting a new project, choosing standalone vs HTTP-integrated mode, configuring MoostWs or WsApp |
17
- | Handlers | [handlers.md](handlers.md) | Defining @Message, @Connect, @Disconnect handlers, understanding handler lifecycle |
18
- | Routing | [routing.md](routing.md) | Event+path routing, controller prefixes, parametric routes, wildcards |
19
- | Request data | [request-data.md](request-data.md) | Extracting message data, connection info, route params with resolver decorators |
20
- | Rooms & broadcasting | [rooms.md](rooms.md) | Room management, broadcasting, direct sends, server-wide queries, multi-instance scaling |
21
- | Wire protocol | [protocol.md](protocol.md) | JSON message format, client/server message types, error codes, heartbeat, custom serialization |
22
- | Testing | [testing.md](testing.md) | Unit-testing handlers with prepareTestWsMessageContext/prepareTestWsConnectionContext |
23
-
24
- ## Quick reference
25
-
26
- ```ts
27
- import {
28
- // Adapter & factory
29
- MoostWs, WsApp, WooksWs,
30
- // Decorators
31
- Message, Connect, Disconnect,
32
- MessageData, RawMessage, MessageId, MessageType, MessagePath, ConnectionId,
33
- // Composables
34
- useWsConnection, useWsMessage, useWsRooms, useWsServer, currentConnection,
35
- // Errors
36
- WsError,
37
- // Testing
38
- prepareTestWsMessageContext, prepareTestWsConnectionContext,
39
- // Re-exports from moost
40
- Controller, Param, Intercept, Description,
41
- } from '@moostjs/event-ws'
42
- ```
@@ -1,157 +0,0 @@
1
- # Core concepts & setup — @moostjs/event-ws
2
-
3
- > Installation, mental model, standalone vs HTTP-integrated modes, and adapter configuration.
4
-
5
- ## Concepts
6
-
7
- `@moostjs/event-ws` is a Moost adapter for WebSocket events. It wraps `@wooksjs/event-ws` and adds decorator-based routing, dependency injection, interceptors, and pipes to WebSocket handlers.
8
-
9
- **Two modes:**
10
- - **Standalone** — dedicated WebSocket server, no HTTP. Use `WsApp` for quick setup or `MoostWs` with `listen()`.
11
- - **HTTP-integrated** (recommended for production) — shares the HTTP port, requires explicit `@Upgrade()` route from `@moostjs/event-http`.
12
-
13
- **Wire protocol:** JSON-over-WebSocket with `event` + `path` routing. Clients send `{ event, path, data?, id? }`. Server replies with `{ id, data?, error? }` or pushes `{ event, path, data? }`.
14
-
15
- ## Installation
16
-
17
- ```bash
18
- npm install @moostjs/event-ws
19
- ```
20
-
21
- For HTTP-integrated mode, also install:
22
- ```bash
23
- npm install @moostjs/event-ws @moostjs/event-http
24
- ```
25
-
26
- ## Standalone Mode — WsApp
27
-
28
- `WsApp` extends `Moost` and sets up a standalone `MoostWs` adapter automatically:
29
-
30
- ```ts
31
- import { WsApp, Message, MessageData, Connect, ConnectionId } from '@moostjs/event-ws'
32
- import { Controller } from 'moost'
33
-
34
- @Controller()
35
- class ChatController {
36
- @Connect()
37
- onConnect(@ConnectionId() id: string) {
38
- console.log(`Connected: ${id}`)
39
- }
40
-
41
- @Message('echo', '/echo')
42
- echo(@MessageData() data: unknown) {
43
- return data
44
- }
45
- }
46
-
47
- new WsApp()
48
- .controllers(ChatController)
49
- .start(3000)
50
- ```
51
-
52
- ### WsApp API
53
-
54
- ```ts
55
- class WsApp extends Moost {
56
- controllers(...controllers: (object | Function | [string, object | Function])[]): this
57
- useWsOptions(opts: { ws?: TWooksWsOptions }): this
58
- getWsAdapter(): MoostWs | undefined
59
- start(port: number, hostname?: string): Promise<void>
60
- }
61
- ```
62
-
63
- ## HTTP-Integrated Mode — MoostWs
64
-
65
- Pass the HTTP app to share the port. Requires an `@Upgrade()` route:
66
-
67
- ```ts
68
- import { MoostHttp } from '@moostjs/event-http'
69
- import { MoostWs } from '@moostjs/event-ws'
70
- import { Moost } from 'moost'
71
-
72
- const app = new Moost()
73
- const http = new MoostHttp()
74
- const ws = new MoostWs({ httpApp: http.getHttpApp() })
75
-
76
- app.adapter(http)
77
- app.adapter(ws)
78
- app.registerControllers(AppController, ChatController)
79
-
80
- await http.listen(3000)
81
- await app.init()
82
- ```
83
-
84
- The upgrade controller:
85
-
86
- ```ts
87
- import { Upgrade } from '@moostjs/event-http'
88
- import type { WooksWs } from '@moostjs/event-ws'
89
- import { Controller, Inject } from 'moost'
90
-
91
- @Controller()
92
- export class AppController {
93
- constructor(@Inject('WooksWs') private ws: WooksWs) {}
94
-
95
- @Upgrade('ws')
96
- upgrade() {
97
- return this.ws.upgrade()
98
- }
99
- }
100
- ```
101
-
102
- ### MoostWs API
103
-
104
- ```ts
105
- interface TMoostWsOpts {
106
- wooksWs?: WooksWs | TWooksWsOptions
107
- }
108
-
109
- class MoostWs {
110
- constructor(opts?: TMoostWsOpts & { httpApp?: { getHttpApp(): unknown } | object })
111
- getWsApp(): WooksWs
112
- listen(port: number, hostname?: string): Promise<void> // standalone only
113
- close(): void
114
- }
115
- ```
116
-
117
- ### TWooksWsOptions
118
-
119
- ```ts
120
- interface TWooksWsOptions {
121
- heartbeatInterval?: number // ping interval ms (default: 30000, 0 = disabled)
122
- heartbeatTimeout?: number // pong timeout ms (default: 5000)
123
- messageParser?: (raw: Buffer | string) => WsClientMessage
124
- messageSerializer?: (msg: WsReplyMessage | WsPushMessage) => string | Buffer
125
- logger?: TConsoleBase
126
- maxMessageSize?: number // bytes (default: 1MB)
127
- wsServerAdapter?: WsServerAdapter
128
- broadcastTransport?: WsBroadcastTransport
129
- }
130
- ```
131
-
132
- ## DI: Injecting Adapter Instances
133
-
134
- The adapter registers both class and string keys:
135
-
136
- | Key | Resolves To |
137
- |-----|-------------|
138
- | `MoostWs` / `'MoostWs'` | The `MoostWs` adapter instance |
139
- | `WooksWs` / `'WooksWs'` | The underlying `WooksWs` instance |
140
-
141
- Use string keys for reliability (avoids esbuild/tsx metadata issues):
142
-
143
- ```ts
144
- constructor(@Inject('WooksWs') private ws: WooksWs) {}
145
- ```
146
-
147
- ## Best Practices
148
-
149
- - Use HTTP-integrated mode for production — single port, explicit upgrade control, easier auth
150
- - Use `WsApp` for quick prototyping or standalone WebSocket services
151
- - Use `@Inject('WooksWs')` (string key) rather than class reference to avoid module init order issues
152
-
153
- ## Gotchas
154
-
155
- - `WsApp.start()` must be awaited — it calls `this.init()` and `listen()` internally
156
- - In HTTP-integrated mode, `ws.listen()` is NOT called — the HTTP server handles the port
157
- - The package is marked experimental — the API may change without semver until stable
@@ -1,162 +0,0 @@
1
- # Handlers — @moostjs/event-ws
2
-
3
- > Defining WebSocket event handlers with @Message, @Connect, and @Disconnect decorators.
4
-
5
- ## Concepts
6
-
7
- Moost WS provides three handler decorators:
8
- - `@Message(event, path?)` — handles routed WebSocket messages
9
- - `@Connect()` — runs when a new connection is established
10
- - `@Disconnect()` — runs when a connection closes
11
-
12
- All handlers participate in the full Moost event lifecycle (scope registration, interceptor init, argument resolution, handler execution, interceptor after/onError, scope cleanup).
13
-
14
- ## API Reference
15
-
16
- ### `@Message(event: string, path?: string)`
17
-
18
- Registers a handler for routed WebSocket messages. Matches on both the `event` field and `path` from the client message.
19
-
20
- ```ts
21
- import { Message, MessageData } from '@moostjs/event-ws'
22
- import { Controller } from 'moost'
23
-
24
- @Controller()
25
- export class EchoController {
26
- @Message('echo', '/echo')
27
- echo(@MessageData() data: unknown) {
28
- return data // sent back as reply if client included an id
29
- }
30
- }
31
- ```
32
-
33
- | Parameter | Type | Description |
34
- |-----------|------|-------------|
35
- | `event` | `string` | Message event type to match (e.g. `"message"`, `"join"`, `"rpc"`) |
36
- | `path` | `string` (optional) | Route path with optional params. When omitted, the method name is used. |
37
-
38
- **Return values:** The return value is sent as a reply only when the client included a correlation `id` (RPC). Fire-and-forget messages (no `id`) ignore the return value.
39
-
40
- ### `@Connect()`
41
-
42
- Runs when a new WebSocket connection is established. Executes inside the connection context.
43
-
44
- ```ts
45
- import { Connect, ConnectionId } from '@moostjs/event-ws'
46
- import { Controller } from 'moost'
47
-
48
- @Controller()
49
- export class LifecycleController {
50
- @Connect()
51
- onConnect(@ConnectionId() id: string) {
52
- console.log(`New connection: ${id}`)
53
- }
54
- }
55
- ```
56
-
57
- If the handler throws or returns a rejected promise, the connection is closed immediately.
58
-
59
- ### `@Disconnect()`
60
-
61
- Runs when a WebSocket connection closes. Use for cleanup.
62
-
63
- ```ts
64
- import { Disconnect, ConnectionId } from '@moostjs/event-ws'
65
- import { Controller } from 'moost'
66
-
67
- @Controller()
68
- export class LifecycleController {
69
- @Disconnect()
70
- onDisconnect(@ConnectionId() id: string) {
71
- console.log(`Connection ${id} closed`)
72
- }
73
- }
74
- ```
75
-
76
- Room membership is automatically cleaned up on disconnect — no need to manually leave rooms.
77
-
78
- ## Common Patterns
79
-
80
- ### Pattern: Multiple events on the same path
81
-
82
- ```ts
83
- @Controller('chat')
84
- export class ChatController {
85
- @Message('join', ':room')
86
- join(@Param('room') room: string) { /* ... */ }
87
-
88
- @Message('leave', ':room')
89
- leave(@Param('room') room: string) { /* ... */ }
90
-
91
- @Message('message', ':room')
92
- message(@Param('room') room: string) { /* ... */ }
93
- }
94
- ```
95
-
96
- ### Pattern: Mixed HTTP and WS handlers in one controller
97
-
98
- A single controller can contain both HTTP and WebSocket handlers when both adapters are registered:
99
-
100
- ```ts
101
- import { Get } from '@moostjs/event-http'
102
- import { Message, MessageData } from '@moostjs/event-ws'
103
- import { Controller } from 'moost'
104
-
105
- @Controller('api')
106
- export class ApiController {
107
- @Get('status')
108
- getStatus() { return { online: true } }
109
-
110
- @Message('query', '/status')
111
- wsStatus() { return { online: true } }
112
- }
113
- ```
114
-
115
- ### Pattern: Protected handlers with interceptors
116
-
117
- ```ts
118
- import { Message, MessageData } from '@moostjs/event-ws'
119
- import { Controller, Intercept, Validate } from 'moost'
120
- import { AuthGuard } from './auth.guard'
121
-
122
- @Controller('admin')
123
- @Intercept(AuthGuard)
124
- export class AdminController {
125
- @Message('broadcast', '/announce')
126
- announce(@MessageData() @Validate() data: AnnounceDto) {
127
- // protected by AuthGuard and validated
128
- }
129
- }
130
- ```
131
-
132
- ### Pattern: Full lifecycle controller
133
-
134
- ```ts
135
- @Controller('chat')
136
- export class ChatController {
137
- @Connect()
138
- onConnect(@ConnectionId() id: string) { /* ... */ }
139
-
140
- @Disconnect()
141
- onDisconnect(@ConnectionId() id: string) { /* ... */ }
142
-
143
- @Message('join', ':room')
144
- join(@Param('room') room: string) { /* ... */ }
145
-
146
- @Message('message', ':room')
147
- message(@Param('room') room: string) { /* ... */ }
148
- }
149
- ```
150
-
151
- ## Best Practices
152
-
153
- - Keep `@Connect` handlers lightweight — they block the connection establishment
154
- - Use `@Disconnect` for cleanup, but don't rely on it for room management (rooms auto-clean)
155
- - One controller can have at most one `@Connect` and one `@Disconnect` handler
156
- - Use interceptors (`@Intercept`) for cross-cutting concerns like auth and logging
157
-
158
- ## Gotchas
159
-
160
- - Throwing in `@Connect` closes the connection immediately — use `WsError` for meaningful error codes
161
- - Fire-and-forget messages (no `id`) silently discard handler return values
162
- - `@Message` path is relative to the controller prefix, not absolute
@@ -1,181 +0,0 @@
1
- # Wire protocol — @moostjs/event-ws
2
-
3
- > JSON message format, message types, error codes, heartbeat, and custom serialization.
4
-
5
- ## Concepts
6
-
7
- Moost WS uses a simple JSON-over-WebSocket protocol. Messages are plain JSON objects sent as text frames. There are three message types:
8
-
9
- 1. **Client → Server** (`WsClientMessage`) — routed by event + path
10
- 2. **Server → Client: Reply** (`WsReplyMessage`) — response to an RPC call
11
- 3. **Server → Client: Push** (`WsPushMessage`) — server-initiated message
12
-
13
- ## Message Types
14
-
15
- ### WsClientMessage (Client → Server)
16
-
17
- ```ts
18
- interface WsClientMessage {
19
- event: string // Router method (e.g. "message", "join", "query")
20
- path: string // Route path (e.g. "/chat/general")
21
- data?: unknown // Payload
22
- id?: string | number // Correlation ID — present for RPC, absent for fire-and-forget
23
- }
24
- ```
25
-
26
- **Fire-and-forget** (no reply expected):
27
- ```json
28
- { "event": "message", "path": "/chat/general", "data": { "text": "Hello!" } }
29
- ```
30
-
31
- **RPC** (reply expected):
32
- ```json
33
- { "event": "join", "path": "/chat/general", "data": { "name": "Alice" }, "id": 1 }
34
- ```
35
-
36
- ### WsReplyMessage (Server → Client)
37
-
38
- Sent in response to a client message that included an `id`. Exactly one reply per request.
39
-
40
- ```ts
41
- interface WsReplyMessage {
42
- id: string | number // Matches client's correlation ID
43
- data?: unknown // Handler return value
44
- error?: { code: number; message: string } // Error details (if handler threw)
45
- }
46
- ```
47
-
48
- **Success:**
49
- ```json
50
- { "id": 1, "data": { "joined": true, "room": "general" } }
51
- ```
52
-
53
- **Error:**
54
- ```json
55
- { "id": 1, "error": { "code": 400, "message": "Name is required" } }
56
- ```
57
-
58
- ### WsPushMessage (Server → Client)
59
-
60
- Server-initiated messages from broadcasts, subscriptions, or direct sends.
61
-
62
- ```ts
63
- interface WsPushMessage {
64
- event: string // Event type
65
- path: string // Concrete path
66
- params?: Record<string, string> // Route params extracted by router
67
- data?: unknown // Payload
68
- }
69
- ```
70
-
71
- ```json
72
- { "event": "message", "path": "/chat/general", "data": { "from": "Alice", "text": "Hello!" } }
73
- ```
74
-
75
- ## Error Codes
76
-
77
- | Code | Meaning |
78
- |------|---------|
79
- | 400 | Bad request / validation error |
80
- | 401 | Unauthorized |
81
- | 403 | Forbidden |
82
- | 404 | Not found (auto-sent for unmatched routes) |
83
- | 409 | Conflict |
84
- | 429 | Too many requests |
85
- | 500 | Internal server error (auto-sent for unhandled exceptions) |
86
- | 503 | Service unavailable |
87
-
88
- ### WsError
89
-
90
- Throw `WsError` for structured error responses:
91
-
92
- ```ts
93
- import { WsError } from '@moostjs/event-ws'
94
-
95
- throw new WsError(400, 'Name is required')
96
- throw new WsError(401, 'Unauthorized')
97
- ```
98
-
99
- ```ts
100
- class WsError extends Error {
101
- readonly code: number
102
- constructor(code: number, message?: string)
103
- }
104
- ```
105
-
106
- `WsError` works in:
107
- - `@Message` handlers — sends error reply to client (if RPC)
108
- - `@Upgrade` handlers — rejects the WebSocket connection
109
- - `@Connect` handlers — closes the connection
110
-
111
- Unhandled (non-`WsError`) exceptions send a generic `{ code: 500, message: "Internal Error" }` without leaking details.
112
-
113
- ## Heartbeat
114
-
115
- The server sends periodic WebSocket `ping` frames to detect stale connections. Configure via `TWooksWsOptions`:
116
-
117
- ```ts
118
- const ws = new MoostWs({
119
- wooksWs: {
120
- heartbeatInterval: 30000, // ms (default: 30000)
121
- heartbeatTimeout: 5000, // ms (default: 5000)
122
- },
123
- })
124
- ```
125
-
126
- Set `heartbeatInterval: 0` to disable.
127
-
128
- ## Custom Serialization
129
-
130
- Both server and client support pluggable serialization (e.g. MessagePack, CBOR):
131
-
132
- ```ts
133
- const ws = new MoostWs({
134
- wooksWs: {
135
- messageParser: (raw: string) => myCustomParse(raw),
136
- messageSerializer: (msg: unknown) => myCustomSerialize(msg),
137
- },
138
- })
139
- ```
140
-
141
- Both sides must use the same serialization format.
142
-
143
- ## Client Library
144
-
145
- Use `@wooksjs/ws-client` for a type-safe client:
146
-
147
- ```bash
148
- npm install @wooksjs/ws-client
149
- ```
150
-
151
- ```ts
152
- import { createWsClient } from '@wooksjs/ws-client'
153
-
154
- const client = createWsClient('ws://localhost:3000/ws', {
155
- reconnect: true,
156
- rpcTimeout: 5000,
157
- })
158
-
159
- // RPC
160
- const result = await client.call('join', '/chat/general', { name: 'Alice' })
161
-
162
- // Listen for pushes
163
- client.on('message', '/chat/general', ({ data }) => {
164
- console.log(`${data.from}: ${data.text}`)
165
- })
166
-
167
- // Fire-and-forget
168
- client.send('message', '/chat/general', { from: 'Alice', text: 'Hello!' })
169
- ```
170
-
171
- ## Best Practices
172
-
173
- - Use `id` (RPC) when the client needs a response, omit for fire-and-forget
174
- - Use HTTP-style numeric codes for errors (400, 401, 404, etc.)
175
- - Keep payloads small — default `maxMessageSize` is 1MB
176
-
177
- ## Gotchas
178
-
179
- - Fire-and-forget messages that hit unmatched routes are logged but no error is sent to the client
180
- - Oversized messages (exceeding `maxMessageSize`) are silently dropped
181
- - Reply is only sent when client message includes `id` — handler return values are discarded otherwise
@@ -1,185 +0,0 @@
1
- # Request data — @moostjs/event-ws
2
-
3
- > Resolver decorators for extracting message data, connection info, and route parameters from WebSocket events.
4
-
5
- ## Concepts
6
-
7
- Moost WS provides parameter decorators that resolve values from the WebSocket event context. These decorators are applied to handler method arguments and participate in the Moost pipes pipeline (resolve, transform, validate).
8
-
9
- There are two categories:
10
- - **Message decorators** — only available in `@Message` handlers
11
- - **Connection decorators** — available in all handler types (`@Message`, `@Connect`, `@Disconnect`)
12
-
13
- Additionally, Wooks composable functions (`useWsMessage()`, `useWsConnection()`, etc.) can be called directly inside handler bodies.
14
-
15
- ## API Reference
16
-
17
- ### `@MessageData()`
18
-
19
- Resolves the parsed message payload (the `data` field from the client message).
20
-
21
- ```ts
22
- @Message('message', '/chat/:room')
23
- onMessage(@MessageData() data: { from: string; text: string }) {
24
- console.log(`${data.from}: ${data.text}`)
25
- }
26
- ```
27
-
28
- **Returns:** `unknown` (typed by the parameter's type annotation)
29
- **Available in:** `@Message` only
30
-
31
- ### `@RawMessage()`
32
-
33
- Resolves the raw message before JSON parsing.
34
-
35
- ```ts
36
- @Message('debug', '/raw')
37
- onRaw(@RawMessage() raw: Buffer | string) {
38
- console.log('Raw message:', raw.toString())
39
- }
40
- ```
41
-
42
- **Returns:** `Buffer | string`
43
- **Available in:** `@Message` only
44
-
45
- ### `@MessageId()`
46
-
47
- Resolves the message correlation ID. `undefined` for fire-and-forget, `string | number` for RPC calls.
48
-
49
- ```ts
50
- @Message('query', '/info')
51
- info(@MessageId() messageId: string | number | undefined) {
52
- console.log('Correlation ID:', messageId)
53
- return { timestamp: Date.now() }
54
- }
55
- ```
56
-
57
- **Returns:** `string | number | undefined`
58
- **Available in:** `@Message` only
59
-
60
- ### `@MessageType()`
61
-
62
- Resolves the event type string from the message.
63
-
64
- ```ts
65
- @Message('*', '/log')
66
- onAny(@MessageType() event: string) {
67
- console.log('Event type:', event)
68
- }
69
- ```
70
-
71
- **Returns:** `string`
72
- **Available in:** `@Message` only
73
-
74
- ### `@MessagePath()`
75
-
76
- Resolves the concrete message path (after routing).
77
-
78
- ```ts
79
- @Message('action', '/game/:id')
80
- onAction(@MessagePath() path: string) {
81
- console.log('Message path:', path) // e.g. "/game/42"
82
- }
83
- ```
84
-
85
- **Returns:** `string`
86
- **Available in:** `@Message` only
87
-
88
- ### `@ConnectionId()`
89
-
90
- Resolves the unique connection identifier (UUID).
91
-
92
- ```ts
93
- @Connect()
94
- onConnect(@ConnectionId() id: string) {
95
- console.log(`Connected: ${id}`)
96
- }
97
-
98
- @Message('ping', '/ping')
99
- ping(@ConnectionId() id: string) {
100
- return { pong: true, connectionId: id }
101
- }
102
-
103
- @Disconnect()
104
- onDisconnect(@ConnectionId() id: string) {
105
- console.log(`Disconnected: ${id}`)
106
- }
107
- ```
108
-
109
- **Returns:** `string`
110
- **Available in:** All handlers (`@Message`, `@Connect`, `@Disconnect`)
111
-
112
- ### `@Param(name: string)`
113
-
114
- Resolves a named route parameter from the message path. Same decorator as HTTP routing (re-exported from `moost`).
115
-
116
- ```ts
117
- @Message('message', ':room')
118
- onMessage(@Param('room') room: string, @MessageData() data: { text: string }) {
119
- console.log(`[${room}] ${data.text}`)
120
- }
121
- ```
122
-
123
- **Returns:** `string`
124
- **Available in:** `@Message` only
125
-
126
- ### `@Params()`
127
-
128
- Resolves all route parameters as an object. Re-exported from `moost`.
129
-
130
- ```ts
131
- @Message('move', '/game/:gameId/player/:playerId')
132
- onMove(@Params() params: { gameId: string; playerId: string }) {
133
- console.log(params) // { gameId: '1', playerId: 'alice' }
134
- }
135
- ```
136
-
137
- **Returns:** `Record<string, string>`
138
- **Available in:** `@Message` only
139
-
140
- ## Using Composables Directly
141
-
142
- You can also call Wooks composables inside handler bodies instead of using decorators:
143
-
144
- ```ts
145
- import { useWsMessage, useWsConnection } from '@moostjs/event-ws'
146
-
147
- @Message('echo', '/echo')
148
- echo() {
149
- const { data, id, path, event } = useWsMessage()
150
- const { id: connId, send } = useWsConnection()
151
- return data
152
- }
153
- ```
154
-
155
- ## HTTP Context in WS Handlers
156
-
157
- In HTTP-integrated mode, HTTP composables from the upgrade request are available:
158
-
159
- ```ts
160
- import { Connect, ConnectionId } from '@moostjs/event-ws'
161
- import { useHeaders, useRequest } from '@wooksjs/event-http'
162
-
163
- @Connect()
164
- onConnect(@ConnectionId() id: string) {
165
- const { url } = useRequest()
166
- const headers = useHeaders()
167
- console.log('Upgrade URL:', url)
168
- console.log('User-Agent:', headers['user-agent'])
169
- }
170
- ```
171
-
172
- HTTP composables are read-only — response composables like `useResponse()` are not available in WS handlers.
173
-
174
- ## Summary
175
-
176
- | Decorator | Returns | Available In |
177
- |-----------|---------|-------------|
178
- | `@MessageData()` | Parsed message payload | `@Message` |
179
- | `@RawMessage()` | Raw `Buffer \| string` | `@Message` |
180
- | `@MessageId()` | Correlation ID `string \| number \| undefined` | `@Message` |
181
- | `@MessageType()` | Event type `string` | `@Message` |
182
- | `@MessagePath()` | Concrete message path `string` | `@Message` |
183
- | `@ConnectionId()` | Connection UUID `string` | All handlers |
184
- | `@Param(name)` | Named route parameter `string` | `@Message` |
185
- | `@Params()` | All route parameters `object` | `@Message` |
@@ -1,196 +0,0 @@
1
- # Rooms & broadcasting — @moostjs/event-ws
2
-
3
- > Room management, broadcasting, direct sends, server-wide queries, and multi-instance scaling.
4
-
5
- ## Concepts
6
-
7
- Rooms group WebSocket connections for targeted broadcasting. A connection can join multiple rooms. Room names are strings — by default, the current message path is used as the room name.
8
-
9
- Three composables handle communication:
10
- - `useWsRooms()` — room-scoped operations (join, leave, broadcast). Available in message handlers only.
11
- - `useWsConnection()` — direct send to the current connection. Available in all handlers.
12
- - `useWsServer()` — server-wide operations (broadcast to all, query connections). Available in any context.
13
-
14
- ## API Reference
15
-
16
- ### `useWsRooms()`
17
-
18
- Room management for the current connection. Only available in `@Message` handlers.
19
-
20
- ```ts
21
- const { join, leave, broadcast, rooms } = useWsRooms()
22
- ```
23
-
24
- | Method | Description |
25
- |--------|-------------|
26
- | `join(room?)` | Join a room (default: current message path) |
27
- | `leave(room?)` | Leave a room (default: current message path) |
28
- | `broadcast(event, data?, opts?)` | Broadcast to room members |
29
- | `rooms()` | List rooms this connection has joined (`string[]`) |
30
-
31
- **Broadcast options:**
32
- ```ts
33
- broadcast('message', data, {
34
- room: '/custom-room', // target a different room (default: current path)
35
- excludeSelf: false, // include the sender (default: true)
36
- })
37
- ```
38
-
39
- ### `useWsConnection()`
40
-
41
- Access the current WebSocket connection. Available in all handler types.
42
-
43
- ```ts
44
- const { id, send, close } = useWsConnection()
45
- ```
46
-
47
- | Property/Method | Description |
48
- |----------------|-------------|
49
- | `id` | Connection UUID (`string`) |
50
- | `send(event, path, data?, params?)` | Push a message to this client |
51
- | `close(code?, reason?)` | Close the connection |
52
- | `context` | The connection `EventContext` |
53
-
54
- ### `useWsServer()`
55
-
56
- Server-wide operations. Available in any context.
57
-
58
- ```ts
59
- const server = useWsServer()
60
- ```
61
-
62
- | Method | Description |
63
- |--------|-------------|
64
- | `broadcast(event, path, data?)` | Broadcast to ALL connected clients |
65
- | `connections()` | Get all connections (`Map<string, WsConnection>`) |
66
- | `roomConnections(room)` | Get connections in a room (`Set<WsConnection>`) |
67
- | `getConnection(id)` | Get connection by ID (`WsConnection \| undefined`) |
68
-
69
- ### `currentConnection()`
70
-
71
- Returns the connection `EventContext` regardless of context level:
72
- - In `@Connect`/`@Disconnect`: returns `current()` directly
73
- - In `@Message`: returns `current().parent` (the connection context)
74
-
75
- ## Common Patterns
76
-
77
- ### Pattern: Join a room and broadcast
78
-
79
- ```ts
80
- @Controller('chat')
81
- export class ChatController {
82
- @Message('join', ':room')
83
- join(
84
- @Param('room') room: string,
85
- @ConnectionId() id: string,
86
- @MessageData() data: { name: string },
87
- ) {
88
- const { join, broadcast, rooms } = useWsRooms()
89
- join() // joins room matching current path (e.g. "/chat/general")
90
- broadcast('system', { text: `${data.name} joined` })
91
- return { joined: true, room, rooms: rooms() }
92
- }
93
- }
94
- ```
95
-
96
- ### Pattern: Broadcast a message to a room
97
-
98
- ```ts
99
- @Message('message', ':room')
100
- onMessage(@MessageData() data: { from: string; text: string }) {
101
- const { broadcast } = useWsRooms()
102
- broadcast('message', { from: data.from, text: data.text })
103
- // all room members except sender receive the message
104
- }
105
- ```
106
-
107
- ### Pattern: Direct send to current connection
108
-
109
- ```ts
110
- @Message('notify', '/self')
111
- notify() {
112
- const { send } = useWsConnection()
113
- send('notification', '/alerts', { text: 'Just for you' })
114
- }
115
- ```
116
-
117
- ### Pattern: Server-wide broadcast
118
-
119
- ```ts
120
- @Message('admin', '/announce')
121
- announce(@MessageData() data: { text: string }) {
122
- const server = useWsServer()
123
- server.broadcast('announcement', '/announce', { text: data.text })
124
- return { announced: true }
125
- }
126
- ```
127
-
128
- ### Pattern: Send to a specific connection by ID
129
-
130
- ```ts
131
- @Message('dm', '/direct')
132
- directMessage(@MessageData() data: { targetId: string; text: string }) {
133
- const server = useWsServer()
134
- const target = server.getConnection(data.targetId)
135
- if (target) {
136
- target.send('dm', '/direct', { text: data.text })
137
- }
138
- }
139
- ```
140
-
141
- ## Multi-Instance Broadcasting
142
-
143
- For horizontal scaling, implement `WsBroadcastTransport` to relay room broadcasts across instances:
144
-
145
- ```ts
146
- import type { WsBroadcastTransport } from '@moostjs/event-ws'
147
-
148
- class RedisBroadcastTransport implements WsBroadcastTransport {
149
- publish(channel: string, payload: string) {
150
- redis.publish(channel, payload)
151
- }
152
- subscribe(channel: string, handler: (payload: string) => void) {
153
- redis.subscribe(channel, handler)
154
- }
155
- unsubscribe(channel: string) {
156
- redis.unsubscribe(channel)
157
- }
158
- }
159
- ```
160
-
161
- Pass it in adapter options:
162
-
163
- ```ts
164
- const ws = new MoostWs({
165
- httpApp: http.getHttpApp(),
166
- wooksWs: {
167
- broadcastTransport: new RedisBroadcastTransport(),
168
- },
169
- })
170
- ```
171
-
172
- Channels follow the pattern `ws:room:<room-path>`.
173
-
174
- ### WsBroadcastTransport Interface
175
-
176
- ```ts
177
- interface WsBroadcastTransport {
178
- publish(channel: string, payload: string): void | Promise<void>
179
- subscribe(channel: string, handler: (payload: string) => void): void | Promise<void>
180
- unsubscribe(channel: string): void | Promise<void>
181
- }
182
- ```
183
-
184
- ## Best Practices
185
-
186
- - Let rooms auto-clean on disconnect — don't manually leave in `@Disconnect` handlers
187
- - Use `excludeSelf: true` (default) to prevent echo in chat-like scenarios
188
- - Use `useWsServer()` sparingly — prefer room-scoped broadcasts over server-wide
189
- - For large-scale deployments, implement `WsBroadcastTransport` with Redis/NATS
190
-
191
- ## Gotchas
192
-
193
- - `useWsRooms()` throws if called outside a message context (e.g. inside `@Connect`)
194
- - `join()` without arguments uses the current message path as the room name, including the controller prefix
195
- - `useWsConnection().send()` silently drops messages if the socket is not in OPEN state
196
- - Broadcast `excludeId` only prevents echo on the originating server instance — the same user's other connections still receive it
@@ -1,115 +0,0 @@
1
- # Routing — @moostjs/event-ws
2
-
3
- > Event+path routing, controller prefixes, parametric routes, and wildcards.
4
-
5
- ## Concepts
6
-
7
- WebSocket message routing uses a two-dimensional scheme: messages are matched by both **event type** and **path**. This is powered by the same Wooks router used for HTTP routes.
8
-
9
- Every client message carries: `{ event: "message", path: "/chat/general", data: {...} }`
10
-
11
- The `@Message` decorator matches both dimensions. The `event` must match exactly. The `path` supports parametric patterns (`:param`) and wildcards (`*`).
12
-
13
- ## Routing Rules
14
-
15
- ### Event + Path
16
-
17
- ```ts
18
- @Message('message', '/chat/general')
19
- onMessage(@MessageData() data: { text: string }) {
20
- // matches event="message" at path="/chat/general"
21
- }
22
- ```
23
-
24
- ### Controller Prefixes
25
-
26
- The `@Controller` prefix is prepended to handler paths:
27
-
28
- ```ts
29
- @Controller('game')
30
- export class GameController {
31
- @Message('move', 'board/:id')
32
- // effective path: /game/board/:id
33
- onMove(@Param('id') id: string) { /* ... */ }
34
- }
35
- ```
36
-
37
- Nested controllers with `@ImportController` compose prefixes:
38
-
39
- ```ts
40
- @Controller('v2')
41
- export class V2Controller {
42
- @ImportController(() => GameController)
43
- game!: GameController
44
- // GameController routes become /v2/game/board/:id
45
- }
46
- ```
47
-
48
- ### Parametric Routes
49
-
50
- Use `:param` syntax for path parameters:
51
-
52
- ```ts
53
- @Controller('chat')
54
- export class ChatController {
55
- @Message('join', ':room')
56
- join(@Param('room') room: string) { /* ... */ }
57
-
58
- @Message('dm', ':sender/:receiver')
59
- dm(
60
- @Param('sender') sender: string,
61
- @Param('receiver') receiver: string,
62
- ) { /* ... */ }
63
- }
64
- ```
65
-
66
- Client message `{ event: "dm", path: "/chat/alice/bob" }` resolves `sender="alice"`, `receiver="bob"`.
67
-
68
- ### All Route Parameters
69
-
70
- Use `@Params()` to get all parameters as an object:
71
-
72
- ```ts
73
- import { Params } from 'moost'
74
-
75
- @Message('action', ':type/:id')
76
- handle(@Params() params: { type: string; id: string }) {
77
- console.log(params) // { type: 'move', id: '42' }
78
- }
79
- ```
80
-
81
- ### Wildcards
82
-
83
- ```ts
84
- @Message('log', '/events/*')
85
- handleAllEvents(@Param('*') subPath: string) {
86
- // matches /events/user/login, /events/system/error, etc.
87
- }
88
- ```
89
-
90
- ### Path Omission
91
-
92
- When `path` is omitted, the method name becomes the path:
93
-
94
- ```ts
95
- @Controller('api')
96
- export class ApiController {
97
- @Message('query')
98
- status() {
99
- // effective path: /api/status
100
- return { ok: true }
101
- }
102
- }
103
- ```
104
-
105
- ## Best Practices
106
-
107
- - Use controller prefixes to namespace related handlers
108
- - Prefer explicit `path` argument over relying on method name inference for clarity
109
- - Use parametric routes (`:room`) rather than separate handlers per room
110
-
111
- ## Gotchas
112
-
113
- - Event matching is exact — no wildcard support on the event field itself
114
- - Path parameters are always strings, even if they look like numbers
115
- - Leading slash in `@Message` path is optional — `':room'` and `'/:room'` behave the same when composed with a controller prefix
@@ -1,209 +0,0 @@
1
- # Testing — @moostjs/event-ws
2
-
3
- > Unit-testing WebSocket handlers with mock contexts using prepareTestWsMessageContext and prepareTestWsConnectionContext.
4
-
5
- ## Concepts
6
-
7
- `@moostjs/event-ws` re-exports test helpers from `@wooksjs/event-ws` that create mock event contexts for unit-testing handlers and composables without starting a real server.
8
-
9
- Two context factories match the two context layers:
10
- 1. **`prepareTestWsConnectionContext`** — for `@Connect`/`@Disconnect` handler logic
11
- 2. **`prepareTestWsMessageContext`** — for `@Message` handler logic (includes a parent connection context)
12
-
13
- Both return a **runner function** `<T>(cb: () => T) => T` that executes a callback inside a fully initialized event context.
14
-
15
- ## API Reference
16
-
17
- ### `prepareTestWsMessageContext(options)`
18
-
19
- Creates a message context with a parent connection context. Both contexts are fully seeded.
20
-
21
- ```ts
22
- interface TTestWsMessageContext {
23
- event: string // required — message event type
24
- path: string // required — message route path
25
- data?: unknown // parsed message payload
26
- messageId?: string | number // correlation ID
27
- rawMessage?: Buffer | string // raw message before parsing
28
- id?: string // connection ID (default: 'test-conn-id')
29
- params?: Record<string, string | string[]> // pre-set route parameters
30
- parentCtx?: EventContext // optional parent context (e.g. HTTP)
31
- }
32
- ```
33
-
34
- **Returns:** `<T>(cb: (...a: any[]) => T) => T`
35
-
36
- ```ts
37
- import { prepareTestWsMessageContext, useWsMessage, useWsConnection } from '@moostjs/event-ws'
38
-
39
- const runInCtx = prepareTestWsMessageContext({
40
- event: 'message',
41
- path: '/chat/general',
42
- data: { from: 'Alice', text: 'Hello!' },
43
- messageId: 1,
44
- })
45
-
46
- runInCtx(() => {
47
- const { data, id, path, event } = useWsMessage<{ from: string; text: string }>()
48
- expect(data.from).toBe('Alice')
49
- expect(id).toBe(1)
50
- })
51
- ```
52
-
53
- ### `prepareTestWsConnectionContext(options?)`
54
-
55
- Creates a connection context for testing connection lifecycle handlers.
56
-
57
- ```ts
58
- interface TTestWsConnectionContext {
59
- id?: string // connection ID (default: 'test-conn-id')
60
- params?: Record<string, string | string[]> // pre-set route parameters
61
- parentCtx?: EventContext // optional parent context
62
- }
63
- ```
64
-
65
- **Returns:** `<T>(cb: (...a: any[]) => T) => T`
66
-
67
- ```ts
68
- import { prepareTestWsConnectionContext, useWsConnection } from '@moostjs/event-ws'
69
-
70
- const runInCtx = prepareTestWsConnectionContext({ id: 'conn-456' })
71
-
72
- runInCtx(() => {
73
- const { id } = useWsConnection()
74
- expect(id).toBe('conn-456')
75
- })
76
- ```
77
-
78
- ## Common Patterns
79
-
80
- ### Pattern: Testing message data access
81
-
82
- ```ts
83
- import { describe, it, expect } from 'vitest'
84
- import { prepareTestWsMessageContext, useWsMessage } from '@moostjs/event-ws'
85
-
86
- describe('ChatController', () => {
87
- it('should access message data', () => {
88
- const runInCtx = prepareTestWsMessageContext({
89
- event: 'message',
90
- path: '/chat/general',
91
- data: { from: 'Alice', text: 'Hello!' },
92
- messageId: 1,
93
- })
94
-
95
- runInCtx(() => {
96
- const { data, id, path, event } = useWsMessage<{ from: string; text: string }>()
97
- expect(data.from).toBe('Alice')
98
- expect(data.text).toBe('Hello!')
99
- expect(id).toBe(1)
100
- expect(path).toBe('/chat/general')
101
- expect(event).toBe('message')
102
- })
103
- })
104
- })
105
- ```
106
-
107
- ### Pattern: Testing with route parameters
108
-
109
- ```ts
110
- import { prepareTestWsMessageContext } from '@moostjs/event-ws'
111
- import { useRouteParams } from '@wooksjs/event-core'
112
-
113
- it('should resolve route params', () => {
114
- const runInCtx = prepareTestWsMessageContext({
115
- event: 'join',
116
- path: '/chat/rooms/lobby',
117
- params: { room: 'lobby' },
118
- data: { name: 'Alice' },
119
- })
120
-
121
- runInCtx(() => {
122
- const { get } = useRouteParams<{ room: string }>()
123
- expect(get('room')).toBe('lobby')
124
- })
125
- })
126
- ```
127
-
128
- ### Pattern: Testing connection ID
129
-
130
- ```ts
131
- it('should access connection id in message context', () => {
132
- const runInCtx = prepareTestWsMessageContext({
133
- event: 'join',
134
- path: '/chat/general',
135
- data: { name: 'Alice' },
136
- id: 'conn-123',
137
- })
138
-
139
- runInCtx(() => {
140
- const { id } = useWsConnection()
141
- expect(id).toBe('conn-123')
142
- })
143
- })
144
- ```
145
-
146
- ### Pattern: Testing with HTTP parent context
147
-
148
- For handlers that access HTTP composables from the upgrade request:
149
-
150
- ```ts
151
- import { EventContext } from '@wooksjs/event-core'
152
- import { prepareTestWsMessageContext, currentConnection } from '@moostjs/event-ws'
153
-
154
- it('should have access to parent HTTP context', () => {
155
- const httpCtx = new EventContext({ logger: console as any })
156
-
157
- const runInCtx = prepareTestWsMessageContext({
158
- event: 'test',
159
- path: '/test',
160
- parentCtx: httpCtx,
161
- })
162
-
163
- runInCtx(() => {
164
- const connCtx = currentConnection()
165
- expect(connCtx.parent).toBe(httpCtx)
166
- })
167
- })
168
- ```
169
-
170
- ### Pattern: Testing handler functions directly
171
-
172
- Extract handler logic into testable functions:
173
-
174
- ```ts
175
- import { prepareTestWsMessageContext, useWsRooms } from '@moostjs/event-ws'
176
-
177
- function handleJoin(room: string, name: string) {
178
- const { join, broadcast, rooms } = useWsRooms()
179
- join()
180
- broadcast('system', { text: `${name} joined` })
181
- return { joined: true, room, rooms: rooms() }
182
- }
183
-
184
- it('should join a room and return room list', () => {
185
- const runInCtx = prepareTestWsMessageContext({
186
- event: 'join',
187
- path: '/chat/general',
188
- data: { name: 'Alice' },
189
- })
190
-
191
- const result = runInCtx(() => handleJoin('general', 'Alice'))
192
- expect(result.joined).toBe(true)
193
- expect(result.room).toBe('general')
194
- })
195
- ```
196
-
197
- ## Best Practices
198
-
199
- - Use test helpers rather than manually constructing `EventContext`
200
- - Keep handler logic testable by extracting business logic into composable-using functions
201
- - Test edge cases with different message data, missing fields, and error conditions
202
- - Use `parentCtx` to simulate HTTP-integrated mode
203
- - Default connection ID is `'test-conn-id'` — override with the `id` option when needed
204
-
205
- ## Gotchas
206
-
207
- - `useWsRooms()` and `useWsServer()` depend on adapter state — for full integration testing with rooms and broadcasting, you may need to set up `WsRoomManager` manually
208
- - The runner function is synchronous — wrap async handler logic in a returned promise if needed
209
- - Route parameters must be pre-set via the `params` option — the test context doesn't run the router