@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 +6 -12
- package/scripts/setup-skills.js +0 -78
- package/skills/moostjs-event-ws/SKILL.md +0 -42
- package/skills/moostjs-event-ws/core.md +0 -157
- package/skills/moostjs-event-ws/handlers.md +0 -162
- package/skills/moostjs-event-ws/protocol.md +0 -181
- package/skills/moostjs-event-ws/request-data.md +0 -185
- package/skills/moostjs-event-ws/rooms.md +0 -196
- package/skills/moostjs-event-ws/routing.md +0 -115
- package/skills/moostjs-event-ws/testing.md +0 -209
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moostjs/event-ws",
|
|
3
|
-
"version": "0.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.
|
|
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.
|
|
55
|
-
"moost": "^0.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
|
}
|
package/scripts/setup-skills.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/* prettier-ignore */
|
|
3
|
-
import fs from 'fs'
|
|
4
|
-
import path from 'path'
|
|
5
|
-
import os from 'os'
|
|
6
|
-
import { fileURLToPath } from 'url'
|
|
7
|
-
|
|
8
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
-
|
|
10
|
-
const SKILL_NAME = '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
|