@znight/websocket 0.0.0 → 0.6.1

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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/dist/bun.d.mts +4 -0
  4. package/dist/bun.mjs +112 -0
  5. package/dist/bun.mjs.map +1 -0
  6. package/dist/compiler.d.mts +90 -0
  7. package/dist/compiler.mjs +864 -0
  8. package/dist/compiler.mjs.map +1 -0
  9. package/dist/index.d.mts +501 -0
  10. package/dist/index.mjs +2088 -0
  11. package/dist/index.mjs.map +1 -0
  12. package/dist/server-CI_Bh9o1.d.mts +331 -0
  13. package/dist/server-D40_bC88.mjs +72 -0
  14. package/dist/server-D40_bC88.mjs.map +1 -0
  15. package/dist/testing.d.mts +60 -0
  16. package/dist/testing.mjs +189 -0
  17. package/dist/testing.mjs.map +1 -0
  18. package/dist/transport-xnJqvB93.d.mts +63 -0
  19. package/dist/ws.d.mts +9 -0
  20. package/dist/ws.mjs +143 -0
  21. package/dist/ws.mjs.map +1 -0
  22. package/package.json +102 -1
  23. package/src/client.ts +246 -0
  24. package/src/compiler/analyze.ts +850 -0
  25. package/src/compiler/constants.ts +102 -0
  26. package/src/compiler/data.ts +78 -0
  27. package/src/compiler/generate.ts +262 -0
  28. package/src/compiler/index.ts +53 -0
  29. package/src/compiler/validate.ts +81 -0
  30. package/src/connection.ts +455 -0
  31. package/src/context.ts +43 -0
  32. package/src/decorators.ts +83 -0
  33. package/src/dispatcher.ts +343 -0
  34. package/src/envelope.ts +136 -0
  35. package/src/exceptions.ts +91 -0
  36. package/src/index.ts +89 -0
  37. package/src/metadata.ts +74 -0
  38. package/src/options.ts +343 -0
  39. package/src/plugin.ts +820 -0
  40. package/src/rate-limit.ts +27 -0
  41. package/src/rooms.ts +185 -0
  42. package/src/server.ts +125 -0
  43. package/src/testing/index.ts +279 -0
  44. package/src/tokens.ts +23 -0
  45. package/src/transport.ts +72 -0
  46. package/src/transports/bun.ts +163 -0
  47. package/src/transports/memory.ts +174 -0
  48. package/src/transports/ws.ts +237 -0
  49. package/src/validate.ts +31 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ugho STEPHAN
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,227 @@
1
+ # @znight/websocket
2
+
3
+ WebSocket **gateways** for Znight, served by the `HttpPlugin` server on the same port. A gateway is
4
+ a provider class whose methods handle the connection lifecycle (`@OnConnect`, `@OnDisconnect`) and
5
+ inbound events (`@OnMessage`), with Standard Schema validation, replies, rooms, broadcasts,
6
+ backpressure, heartbeats and a graceful shutdown. Like every Znight feature, gateways are resolved
7
+ **at build time**: the compiler extension reads the decorators, reports invalid gateways
8
+ (ZNIGHT050...) and generates one `invoke` function per handler.
9
+
10
+ - The handshake **is an HTTP request**: request scope, `http:request` hook, `HttpPlugin` global
11
+ guards, `@UseHandshakeGuard` guards, exception mappers, `trustProxy`. The request scope lasts as
12
+ long as the connection: request-scoped providers filled by a guard are there for every message.
13
+ - One server, every adapter: `node`, `fastify`, `express` and `hono` on Node.js and Bun. The `ws`
14
+ transport serves `node:http` servers (Bun ships its own `ws`); Bun's native WebSocket serves
15
+ `honoAdapter()` on `Bun.serve()`.
16
+ - A tiny wire protocol for `events` gateways (JSON `{ event, data?, id? }`), and `raw` gateways for
17
+ anything else.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ bun add @znight/core @znight/http @znight/websocket zod
23
+ npm i ws # Node.js only: Bun provides it
24
+ bun add -d @znight/compiler vite
25
+ ```
26
+
27
+ ```ts
28
+ import { Application } from '@znight/core'
29
+ import { HttpPlugin } from '@znight/http'
30
+ import { WebSocketPlugin } from '@znight/websocket'
31
+ import { __ZNIGHT_METADATA__ } from './.znight/container.generated.js'
32
+
33
+ const app = await Application.create({
34
+ metadata: __ZNIGHT_METADATA__,
35
+ plugins: [HttpPlugin.configure({ port: 3000 }), WebSocketPlugin.configure({ maxPayload: '64kb' })],
36
+ })
37
+ await app.start()
38
+ ```
39
+
40
+ ## Gateways
41
+
42
+ ```ts
43
+ import { z } from 'zod'
44
+ import {
45
+ Conn,
46
+ Gateway,
47
+ OnConnect,
48
+ OnDisconnect,
49
+ OnMessage,
50
+ Payload,
51
+ WsException,
52
+ WsServer,
53
+ type WsConnection,
54
+ } from '@znight/websocket'
55
+
56
+ export const JoinRoom = z.object({ room: z.string().regex(/^[a-z0-9-]{1,32}$/) })
57
+ export type JoinRoom = z.output<typeof JoinRoom>
58
+ export const SendMessage = z.object({ room: z.string(), text: z.string().min(1).max(2000) })
59
+ export type SendMessage = z.output<typeof SendMessage>
60
+
61
+ interface ChatEvents {
62
+ 'message.new': { readonly room: string; readonly text: string }
63
+ }
64
+
65
+ @Gateway({ path: '/chat', protocols: ['chat.v1'], rateLimit: { messages: 30, intervalMs: 1_000 } })
66
+ export class ChatGateway {
67
+ constructor(private readonly ws: WsServer) {}
68
+
69
+ @OnConnect()
70
+ connected(@Conn() conn: WsConnection): void {
71
+ conn.join('lobby')
72
+ }
73
+
74
+ @OnMessage('room.join')
75
+ join(@Conn() conn: WsConnection, @Payload(JoinRoom) input: JoinRoom): { members: number } {
76
+ conn.join(input.room)
77
+ return { members: this.ws.of(ChatGateway).members(input.room).size } // the reply
78
+ }
79
+
80
+ @OnMessage('message.send')
81
+ send(@Conn() conn: WsConnection, @Payload(SendMessage) input: SendMessage): void {
82
+ if (!conn.rooms.has(input.room)) throw new WsException('FORBIDDEN', `Join "${input.room}" first`)
83
+ this.ws.of<ChatEvents>(ChatGateway).to(input.room).emit('message.new', input) // encoded once
84
+ }
85
+
86
+ @OnDisconnect()
87
+ disconnected(): void {}
88
+ }
89
+ ```
90
+
91
+ | Decorator | Target | Description |
92
+ | --------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------- |
93
+ | `@Gateway(path?)` / `@Gateway(options)` | class | A provider handling the connections of `path` (default `/`), joined to the `HttpPlugin` prefix |
94
+ | `@OnConnect()` | method | Runs once the handshake succeeded, before any message. Throwing closes the connection |
95
+ | `@OnMessage(event?)` | method | Handles an event; without event, the default handler (unknown events, every frame of a raw gateway) |
96
+ | `@OnDisconnect()` | method | Runs once per connection whose `@OnConnect` completed, after its running handlers |
97
+ | `@Payload(schema?)` | parameter | The message `data` (`events`) or the frame (`raw`), validated by a Standard Schema |
98
+ | `@Conn()` | parameter | The `WsConnection` |
99
+ | `@Ctx()` | parameter | The `WsMessageContext` of the message |
100
+ | `@UseHandshakeGuard(...guards)` | class | HTTP guards (`CanActivate`) of the handshake, after the `HttpPlugin` global guards |
101
+ | `@UseMessageGuard(...guards)` | class, method | `WsGuard`s run before each handler (plugin, class, then method) |
102
+ | `createRouteDecorator` decorators | class, method | Class: handshake and messages; method: its messages (nearest wins) |
103
+
104
+ `@Gateway` options (static): `path`, `scope` (`'singleton'` or `'request'`: one instance per
105
+ connection, which may inject `WsConnection`), `protocol` (`'events'` or `'raw'`), `protocols`
106
+ (accepted subprotocols; a client offering none of them is refused with `400`), `maxPayload`
107
+ (`'64kb'`), `concurrency` (messages of a connection handled at once, default `1`: in order),
108
+ `rateLimit` (`{ messages, intervalMs, onLimit?: 'reject' | 'close' }`) and `backpressure`
109
+ (`{ limit?, onLimit?: 'close' | 'drop' }`).
110
+
111
+ Handlers may be `async`; their return value is the reply of a message that carried an `id`.
112
+ Handlers are inherited from abstract base classes. Core AOP (`@Logged`, `@Traced`, `@Intercept`)
113
+ applies to handlers.
114
+
115
+ ## Wire protocol (`events` gateways)
116
+
117
+ | Direction | Frame | Meaning |
118
+ | --------------- | ------------------------------------------------------------------------------ | ----------------------------------------- |
119
+ | client → server | `{ "event": "message.send", "data": {...} }` | fire and forget |
120
+ | client → server | `{ "event": "room.join", "data": {...}, "id": 7 }` | a request: one reply |
121
+ | server → client | `{ "id": 7, "data": {...} }` / `{ "id": 7, "error": { code, message } }` | reply |
122
+ | server → client | `{ "event": "message.new", "data": {...} }` | server event (`emit`, broadcasts) |
123
+ | server → client | `{ "event": "$error", "data": { "event": ..., "code": ..., "message": ... } }` | failure of a message without `id` |
124
+ | client ↔ server | `{ "event": "$ping", "id": 1 }` → `{ "id": 1, "data": { "t": ... } }` | application ping (never reaches handlers) |
125
+
126
+ Error codes: `BAD_FRAME`, `UNSUPPORTED_FRAME`, `BAD_REQUEST` (validation, `details: [{ path,
127
+ message }]`), `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `CONFLICT`, `UNPROCESSABLE`, `RATE_LIMITED`,
128
+ `UNKNOWN_EVENT`, `TIMEOUT`, `INTERNAL` (message hidden unless `exposeErrors`). A `WsException(code,
129
+ message?, details?, { close? })` answers its code; an `HttpException` thrown by a service is mapped
130
+ from its status. In `@OnConnect`, a `WsException` closes with its `close` code (default `1008`), an
131
+ `HttpException` with `4000 + status`, anything else with `1011`.
132
+
133
+ `raw` gateways have no envelope: the default handler receives each frame (`string` or
134
+ `Uint8Array`), `conn.send()` sends frames as they are.
135
+
136
+ ## Connections, rooms and broadcasts
137
+
138
+ `WsConnection<TData, Events>`: `id`, `gateway`, `requestId`, `handshake` (the `HttpContext`),
139
+ `params`, `protocol`, `data` (per-connection state), `rooms`, `readyState`, `bufferedAmount`,
140
+ `signal`, `closeInfo` (set before `@OnDisconnect`), `emit(event, data)`, `send(frame)`,
141
+ `join(room)`, `leave(room)`, `to(room)` (the other members), `drained()`, `close(code?, reason?)`,
142
+ `terminate()`. `emit` and `send` return `'sent'`, `'queued'` or `'dropped'`.
143
+
144
+ `WsServer` (injectable): `of(GatewayClass | '/path')` returns a lazy `GatewayChannel` (`path`,
145
+ `size`, `connection(id)`, `connections()`, `members(room)`, `rooms()`, `to(room)`, `except(...)`,
146
+ `emit`, `send`) and `connections` counts every connection. A broadcast encodes its frame once;
147
+ each recipient applies its own backpressure policy. Rooms are per gateway and per process.
148
+
149
+ ## Plugin options
150
+
151
+ `WebSocketPlugin.configure(options | (container) => options)` (name `websocket`):
152
+
153
+ | Option | Default | Description |
154
+ | -------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
155
+ | `transport` | `'auto'` | `wsTransport()` (`@znight/websocket/ws`) or `bunTransport()` (`@znight/websocket/bun`) |
156
+ | `origins` | `'same-origin'` | `'*'`, exact origins and anchored `RegExp`s, or `(origin, ctx) => boolean`; checked first (`403`) |
157
+ | `maxPayload` | `'1mb'` | Largest inbound message; gateways may lower it (`1009`) |
158
+ | `maxConnections` | `Infinity` | Open connections per process (handshakes in flight count); above, `503` with `retry-after: 1` |
159
+ | `maxPendingMessages` | `32` | Inbound queue per connection before reading pauses |
160
+ | `backpressure` | `{ limit: '1mb', onLimit: 'close' }` | Outbound buffer per connection: close with `1013`, or `'drop'` frames |
161
+ | `heartbeat` | `{ intervalMs: 30_000 }` | `false` disables it; silent connections are terminated after two intervals |
162
+ | `handshakeTimeoutMs` | `10_000` | `408` for handshakes whose guards did not answer |
163
+ | `handlerTimeoutMs` | `30_000` | Per message and `@OnConnect`: `TIMEOUT`, `ctx.signal` aborted |
164
+ | `closeTimeoutMs` | `2_000` | Wait for the client's close frame before terminating |
165
+ | `messageGuards` | `[]` | Global `WsGuard`s (classes or instances) |
166
+ | `exceptionMappers` | `[]` | `WsExceptionMapper`s: `map(error, ctx) → WsException \| undefined` |
167
+ | `exposeErrors` | `false` | Send the message of unexpected errors |
168
+ | `shutdown` | `{ code: 1001, reason: 'Server shutting down', timeoutMs: 3_000 }` | Graceful shutdown: close frames, then terminate |
169
+
170
+ The instance exposes `server` (`WsServer`), `transport`, `gateways`, `connections`, `stats()`
171
+ (`{ connections, opened, closed, messagesIn, messagesOut, bytesIn, bytesOut, dropped,
172
+ slowConsumerCloses }`), `connect(path, options?)` and `initRoots()`.
173
+
174
+ ## Testing
175
+
176
+ ```ts
177
+ import { TestApp, WsHandshakeError } from '@znight/testing'
178
+ import { HttpPlugin } from '@znight/http'
179
+ import { WebSocketPlugin } from '@znight/websocket'
180
+
181
+ await using app = await TestApp.create({
182
+ metadata,
183
+ plugins: [HttpPlugin.configure({ port: 0 }), WebSocketPlugin.configure()],
184
+ })
185
+ await using alice = await app.connect('/chat', { protocols: ['chat.v1'] }) // in memory, real handshake
186
+ expect(await alice.request('room.join', { room: 'general' })).toEqual({ members: 1 })
187
+ await expect(app.connect('/chat', { origin: 'https://evil.example' })).rejects.toBeInstanceOf(WsHandshakeError)
188
+ ```
189
+
190
+ `connectWebSocket(url, { headers?, protocols? })` of `@znight/testing` gives the same client over
191
+ the network. For unit tests, `@znight/websocket/testing` provides fakes:
192
+
193
+ ```ts
194
+ import { createTestConnection, createTestServer } from '@znight/websocket/testing'
195
+
196
+ const server = createTestServer({ paths: [[ChatGateway, '/chat']] }) // records broadcasts
197
+ const conn = createTestConnection({ data: { user: 'alice' }, rooms: ['general'] }) // records emitted frames
198
+ ```
199
+
200
+ ## Compiler contract
201
+
202
+ The compiler extension (`@znight/websocket/compiler`) generates `metadata.extensions.websocket`
203
+ (gateways sorted by token, handlers in declaration order, bases first) and imports from
204
+ `@znight/websocket`:
205
+
206
+ - `validatePayload(schema, value)`: Standard Schema validation of `@Payload(schema)`; returns the
207
+ schema output (synchronously for synchronous schemas) or throws `WsValidationException`
208
+ (`BAD_REQUEST`, `details: [{ path, message }]`). Stable.
209
+ - the types `WsConnection` and `WsMessageContext` of the generated `invoke` functions.
210
+
211
+ ```ts
212
+ ['invoke']: async (g: ChatGateway, m: WsMessageContext) =>
213
+ g.join(m.connection as never, await validatePayload(JoinRoom, m.data)),
214
+ ```
215
+
216
+ Diagnostics: ZNIGHT050 (invalid gateway: options, duplicate events, reserved `$` events, several
217
+ `@OnConnect`, undecorated parameters, `@Payload` outside `@OnMessage`...), ZNIGHT005 (decorator
218
+ misuse, parameter decorators imported from another package), ZNIGHT007 (paths), ZNIGHT017
219
+ (`@Payload()` without schema on a structured type), ZNIGHT003 (a singleton gateway injecting
220
+ `WsConnection`), ZNIGHT023 (required route decisions). At boot: ZNIGHT050 (options, metadata) and
221
+ ZNIGHT051 (no `HttpPlugin`, no upgrade support, `ws` missing, mismatched transport).
222
+
223
+ ## Limitations
224
+
225
+ - `conn.handshake` keeps the native request of the handshake.
226
+ - Rooms are per process: broadcast across instances through `@znight/messaging`.
227
+ - No HTTP/2 or HTTP/3 WebSockets, no Socket.IO protocol.
package/dist/bun.d.mts ADDED
@@ -0,0 +1,4 @@
1
+ import { o as WebSocketTransport } from "./transport-xnJqvB93.mjs";
2
+ /** The Bun transport: completes the `bun` upgrades of `Bun.serve()`. */
3
+ export declare function bunTransport(): WebSocketTransport;
4
+ //# sourceMappingURL=bun.d.mts.map
package/dist/bun.mjs ADDED
@@ -0,0 +1,112 @@
1
+ import { ErrorCode, ZnightError } from "@znight/core";
2
+ // src/transports/bun.ts
3
+ /** One Bun connection; bound before `server.upgrade()`, since `open` may run inside it. */
4
+ var BunTransportSocket = class {
5
+ #events;
6
+ #registry;
7
+ #ws;
8
+ #closed = false;
9
+ constructor(events, registry) {
10
+ this.#events = events;
11
+ this.#registry = registry;
12
+ }
13
+ get bufferedAmount() {
14
+ return this.#ws?.getBufferedAmount() ?? 0;
15
+ }
16
+ get extensions() {
17
+ return this.#ws?.extensions ?? "";
18
+ }
19
+ send(frame, _binary) {
20
+ if (this.#ws === void 0 || this.#closed) return "dropped";
21
+ const result = this.#ws.send(frame);
22
+ if (result > 0) return "sent";
23
+ return result === -1 ? "queued" : "dropped";
24
+ }
25
+ ping() {
26
+ this.#ws?.ping();
27
+ }
28
+ pause() {
29
+ if (this.#ws?.pause === void 0) return false;
30
+ this.#ws.pause();
31
+ return true;
32
+ }
33
+ resume() {
34
+ this.#ws?.resume?.();
35
+ }
36
+ close(code, reason) {
37
+ this.#ws?.close(code, reason);
38
+ }
39
+ terminate() {
40
+ this.#ws?.terminate();
41
+ }
42
+ onOpen(ws) {
43
+ this.#ws = ws;
44
+ this.#registry.add(this);
45
+ }
46
+ onMessage(message) {
47
+ this.#events.message(message, typeof message !== "string");
48
+ }
49
+ onClose(code, reason) {
50
+ if (this.#closed) return;
51
+ this.#closed = true;
52
+ this.#registry.delete(this);
53
+ this.#events.close(code, reason);
54
+ }
55
+ onDrain() {
56
+ this.#events.drain();
57
+ }
58
+ onPong() {
59
+ this.#events.pong();
60
+ }
61
+ };
62
+ /** The Bun transport: completes the `bun` upgrades of `Bun.serve()`. */
63
+ function bunTransport() {
64
+ let settings = {
65
+ maxPayload: 1048576,
66
+ backpressureLimit: 1048576,
67
+ heartbeatMs: 0
68
+ };
69
+ const sockets = /* @__PURE__ */ new Set();
70
+ return {
71
+ name: "bun",
72
+ accepts: "bun",
73
+ configure(next) {
74
+ settings = next;
75
+ },
76
+ bunWebSocket() {
77
+ return {
78
+ maxPayloadLength: settings.maxPayload,
79
+ backpressureLimit: settings.backpressureLimit,
80
+ closeOnBackpressureLimit: false,
81
+ sendPings: settings.heartbeatMs > 0,
82
+ idleTimeout: settings.heartbeatMs > 0 ? Math.min(960, Math.ceil(settings.heartbeatMs * 2 / 1e3)) : 0
83
+ };
84
+ },
85
+ accept(native, init, events) {
86
+ if (native.kind !== "bun") throw new ZnightError(ErrorCode.WebSocketTransportUnavailable, `bunTransport() completes Bun.serve() upgrades, not ${native.kind} upgrades`, { hints: ["Use wsTransport() with the node:http adapters"] });
87
+ const socket = new BunTransportSocket(events, sockets);
88
+ const handlers = {
89
+ open: (ws) => socket.onOpen(ws),
90
+ message: (_ws, message) => socket.onMessage(message),
91
+ close: (_ws, code, reason) => socket.onClose(code, reason),
92
+ drain: () => socket.onDrain(),
93
+ pong: () => socket.onPong()
94
+ };
95
+ const headers = {};
96
+ for (const [name, value] of Object.entries(init.headers)) headers[name] = typeof value === "string" ? value : value.join(", ");
97
+ if (init.protocol !== "") headers["sec-websocket-protocol"] = init.protocol;
98
+ if (!native.server.upgrade(native.request, {
99
+ data: handlers,
100
+ headers
101
+ })) throw new Error("Bun.serve() refused the WebSocket upgrade");
102
+ return socket;
103
+ },
104
+ close() {
105
+ for (const socket of [...sockets]) socket.terminate();
106
+ return Promise.resolve();
107
+ }
108
+ };
109
+ }
110
+ export { bunTransport };
111
+
112
+ //# sourceMappingURL=bun.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bun.mjs","names":[],"sources":["../src/transports/bun.ts"],"sourcesContent":["/**\n * The Bun transport (RFC 0003 §8.3): completes the upgrades of `Bun.serve()` (`honoAdapter()` on\n * Bun) with Bun's built-in WebSocket server. No import: it only uses the server it is given.\n *\n * ```ts\n * import { bunTransport } from '@znight/websocket/bun'\n * ```\n */\nimport type { BunSocketHandlers, BunUpgrade, MemoryUpgrade, NodeUpgrade } from '@znight/http'\nimport type {\n AcceptInit,\n SendStatus,\n SocketEvents,\n TransportSettings,\n TransportSocket,\n WebSocketTransport,\n} from '../transport.ts'\n\nimport { ErrorCode, ZnightError } from '@znight/core'\n\n/** The part of Bun's `ServerWebSocket` the transport uses. */\ninterface BunSocket {\n send(data: string | Uint8Array): number\n getBufferedAmount(): number\n readonly extensions?: string\n ping(): void\n pause?(): void\n resume?(): void\n close(code: number, reason: string): void\n terminate(): void\n}\n\n/** The part of Bun's `Server` the transport uses. */\ninterface BunServer {\n upgrade(request: Request, options: { data: BunSocketHandlers; headers?: Record<string, string> }): boolean\n}\n\n/** One Bun connection; bound before `server.upgrade()`, since `open` may run inside it. */\nclass BunTransportSocket implements TransportSocket {\n readonly #events: SocketEvents\n readonly #registry: Set<BunTransportSocket>\n #ws: BunSocket | undefined\n #closed = false\n\n constructor(events: SocketEvents, registry: Set<BunTransportSocket>) {\n this.#events = events\n this.#registry = registry\n }\n\n get bufferedAmount(): number {\n return this.#ws?.getBufferedAmount() ?? 0\n }\n\n get extensions(): string {\n return this.#ws?.extensions ?? ''\n }\n\n send(frame: string | Uint8Array, _binary: boolean): SendStatus {\n if (this.#ws === undefined || this.#closed) return 'dropped'\n const result = this.#ws.send(frame)\n if (result > 0) return 'sent'\n return result === -1 ? 'queued' : 'dropped'\n }\n\n ping(): void {\n this.#ws?.ping()\n }\n\n pause(): boolean {\n if (this.#ws?.pause === undefined) return false\n this.#ws.pause()\n return true\n }\n\n resume(): void {\n this.#ws?.resume?.()\n }\n\n close(code: number, reason: string): void {\n this.#ws?.close(code, reason)\n }\n\n terminate(): void {\n this.#ws?.terminate()\n }\n\n onOpen(ws: unknown): void {\n this.#ws = ws as BunSocket\n this.#registry.add(this)\n }\n\n onMessage(message: string | Uint8Array): void {\n this.#events.message(message, typeof message !== 'string')\n }\n\n onClose(code: number, reason: string): void {\n if (this.#closed) return\n this.#closed = true\n this.#registry.delete(this)\n this.#events.close(code, reason)\n }\n\n onDrain(): void {\n this.#events.drain()\n }\n\n onPong(): void {\n this.#events.pong()\n }\n}\n\n/** The Bun transport: completes the `bun` upgrades of `Bun.serve()`. */\nexport function bunTransport(): WebSocketTransport {\n let settings: TransportSettings = { maxPayload: 1024 * 1024, backpressureLimit: 1024 * 1024, heartbeatMs: 0 }\n const sockets = new Set<BunTransportSocket>()\n return {\n name: 'bun',\n accepts: 'bun',\n configure(next: TransportSettings): void {\n settings = next\n },\n bunWebSocket(): Readonly<Record<string, unknown>> {\n return {\n maxPayloadLength: settings.maxPayload,\n backpressureLimit: settings.backpressureLimit,\n closeOnBackpressureLimit: false,\n sendPings: settings.heartbeatMs > 0,\n idleTimeout: settings.heartbeatMs > 0 ? Math.min(960, Math.ceil((settings.heartbeatMs * 2) / 1000)) : 0,\n }\n },\n accept(native: NodeUpgrade | BunUpgrade | MemoryUpgrade, init: AcceptInit, events: SocketEvents): TransportSocket {\n if (native.kind !== 'bun') {\n throw new ZnightError(\n ErrorCode.WebSocketTransportUnavailable,\n `bunTransport() completes Bun.serve() upgrades, not ${native.kind} upgrades`,\n { hints: ['Use wsTransport() with the node:http adapters'] },\n )\n }\n const socket = new BunTransportSocket(events, sockets)\n const handlers: BunSocketHandlers = {\n open: ws => socket.onOpen(ws),\n message: (_ws, message) => socket.onMessage(message),\n close: (_ws, code, reason) => socket.onClose(code, reason),\n drain: () => socket.onDrain(),\n pong: () => socket.onPong(),\n }\n const headers: Record<string, string> = {}\n for (const [name, value] of Object.entries(init.headers)) {\n headers[name] = typeof value === 'string' ? value : value.join(', ')\n }\n if (init.protocol !== '') headers['sec-websocket-protocol'] = init.protocol\n const server = native.server as BunServer\n if (!server.upgrade(native.request, { data: handlers, headers })) {\n throw new Error('Bun.serve() refused the WebSocket upgrade')\n }\n return socket\n },\n close(): Promise<void> {\n for (const socket of [...sockets]) socket.terminate()\n return Promise.resolve()\n },\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import { Token } from "@znight/core";
2
+ import { MetaEntry } from "@znight/http/compiler";
3
+ import { ClassRef, CompilerExtension, Location, ValueRef } from "@znight/compiler";
4
+ /** A parameter of a handler, as the generated `invoke` passes it. */
5
+ type WsParamData = {
6
+ readonly kind: "payload";
7
+ } | {
8
+ readonly kind: "validated";
9
+ readonly schema: ValueRef;
10
+ } | {
11
+ readonly kind: "connection";
12
+ } | {
13
+ readonly kind: "context";
14
+ } | {
15
+ readonly kind: "undefined";
16
+ readonly optional: boolean;
17
+ };
18
+ /** `@OnConnect` or `@OnDisconnect`. */
19
+ interface LifecycleData {
20
+ readonly method: string;
21
+ readonly params: readonly WsParamData[];
22
+ readonly declaredBy: string;
23
+ readonly location: Location;
24
+ }
25
+ /** One `@OnMessage` handler (a method decorated several times gives one entry per event). */
26
+ interface HandlerData {
27
+ readonly method: string;
28
+ /** `undefined`: the default handler. */
29
+ readonly event: string | undefined;
30
+ /** Route decorators of the method. */
31
+ readonly meta: readonly MetaEntry[];
32
+ /** `@UseMessageGuard` of the method. */
33
+ readonly guards: readonly ClassRef[];
34
+ readonly params: readonly WsParamData[];
35
+ readonly declaredBy: string;
36
+ readonly location: Location;
37
+ }
38
+ /** Static options of `@Gateway`. */
39
+ interface GatewayOptionsData {
40
+ readonly protocol: "events" | "raw";
41
+ readonly protocols?: readonly string[];
42
+ readonly maxPayload?: number;
43
+ readonly concurrency?: number;
44
+ readonly rateLimit?: {
45
+ readonly messages: number;
46
+ readonly intervalMs: number;
47
+ readonly onLimit: "reject" | "close";
48
+ };
49
+ readonly backpressure?: {
50
+ readonly limit?: number;
51
+ readonly onLimit?: "close" | "drop";
52
+ };
53
+ }
54
+ /** A `@Gateway` class. */
55
+ interface GatewayData {
56
+ readonly kind: "gateway";
57
+ readonly token: Token;
58
+ readonly identity: string;
59
+ readonly displayName: string;
60
+ /** Normalised path; `undefined` when invalid (reported). */
61
+ readonly path: string | undefined;
62
+ readonly options: GatewayOptionsData;
63
+ /** Route decorators of the class. */
64
+ readonly meta: readonly MetaEntry[];
65
+ readonly handshakeGuards: readonly ClassRef[];
66
+ readonly messageGuards: readonly ClassRef[];
67
+ readonly onConnect: LifecycleData | undefined;
68
+ readonly onDisconnect: LifecycleData | undefined;
69
+ readonly handlers: readonly HandlerData[];
70
+ /** Identities of the class and its analysable bases (nearest first). */
71
+ readonly chain: readonly string[];
72
+ readonly location: Location;
73
+ }
74
+ /** A class carrying websocket decorators without `@Gateway` (a base class, or a mistake). */
75
+ interface MemberClassData {
76
+ readonly kind: "members";
77
+ readonly identity: string;
78
+ readonly displayName: string;
79
+ readonly abstract: boolean;
80
+ readonly decorators: readonly {
81
+ readonly name: string;
82
+ readonly location: Location;
83
+ }[];
84
+ }
85
+ /** Everything the extension stores. */
86
+ type WebSocketData = GatewayData | MemberClassData;
87
+ /** The `@znight/websocket` compiler extension. */
88
+ export declare const webSocketCompilerExtension: CompilerExtension<WebSocketData>;
89
+ export { type GatewayData, type GatewayOptionsData, type HandlerData, type LifecycleData, type MemberClassData, type WebSocketData, type WsParamData, webSocketCompilerExtension as default };
90
+ //# sourceMappingURL=compiler.d.mts.map