@moostjs/event-ws 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/index.cjs +445 -0
- package/dist/index.d.ts +225 -0
- package/dist/index.mjs +298 -0
- package/package.json +62 -0
- package/scripts/setup-skills.js +78 -0
- package/skills/moostjs-event-ws/SKILL.md +42 -0
- package/skills/moostjs-event-ws/core.md +157 -0
- package/skills/moostjs-event-ws/handlers.md +162 -0
- package/skills/moostjs-event-ws/protocol.md +181 -0
- package/skills/moostjs-event-ws/request-data.md +185 -0
- package/skills/moostjs-event-ws/rooms.md +196 -0
- package/skills/moostjs-event-ws/routing.md +115 -0
- package/skills/moostjs-event-ws/testing.md +209 -0
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
```
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|