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