@nmtjs/ws-transport 0.15.0-beta.3 → 0.15.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -12,20 +12,20 @@
12
12
  },
13
13
  "devDependencies": {
14
14
  "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.56.0",
15
- "@nmtjs/client": "0.15.0-beta.3",
16
- "@nmtjs/common": "0.15.0-beta.3",
17
- "@nmtjs/gateway": "0.15.0-beta.3",
18
- "@nmtjs/protocol": "0.15.0-beta.3"
15
+ "@nmtjs/client": "0.15.0-beta.4",
16
+ "@nmtjs/common": "0.15.0-beta.4",
17
+ "@nmtjs/protocol": "0.15.0-beta.4",
18
+ "@nmtjs/gateway": "0.15.0-beta.4"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "@types/bun": "^1.3.0",
22
22
  "@types/deno": "^2.3.0",
23
23
  "@types/node": "^24",
24
24
  "uWebSockets.js": "^20.56.0",
25
- "@nmtjs/common": "0.15.0-beta.3",
26
- "@nmtjs/core": "0.15.0-beta.3",
27
- "@nmtjs/gateway": "0.15.0-beta.3",
28
- "@nmtjs/protocol": "0.15.0-beta.3"
25
+ "@nmtjs/core": "0.15.0-beta.4",
26
+ "@nmtjs/gateway": "0.15.0-beta.4",
27
+ "@nmtjs/common": "0.15.0-beta.4",
28
+ "@nmtjs/protocol": "0.15.0-beta.4"
29
29
  },
30
30
  "peerDependenciesMeta": {
31
31
  "@types/bun": {
@@ -48,10 +48,11 @@
48
48
  },
49
49
  "files": [
50
50
  "dist",
51
+ "src",
51
52
  "LICENSE.md",
52
53
  "README.md"
53
54
  ],
54
- "version": "0.15.0-beta.3",
55
+ "version": "0.15.0-beta.4",
55
56
  "scripts": {
56
57
  "clean-build": "rm -rf ./dist",
57
58
  "build": "tsc --declaration --sourcemap",
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './injectables.ts'
2
+ export * from './server.ts'
3
+ export * from './types.ts'
4
+ export * from './utils.ts'
@@ -0,0 +1,6 @@
1
+ import { connectionData as connectionDataInjectable } from '@nmtjs/gateway'
2
+
3
+ import type { WsTransportServerRequest } from './types.ts'
4
+
5
+ export const connectionData =
6
+ connectionDataInjectable.$withType<WsTransportServerRequest>()
@@ -0,0 +1,82 @@
1
+ import type { Transport } from '@nmtjs/gateway'
2
+ import type { ConnectionType } from '@nmtjs/protocol'
3
+ import { ProxyableTransportType } from '@nmtjs/gateway'
4
+ import createAdapter from 'crossws/adapters/bun'
5
+
6
+ import type {
7
+ WsAdapterParams,
8
+ WsAdapterServer,
9
+ WsTransportOptions,
10
+ } from '../types.ts'
11
+ import * as injectables from '../injectables.ts'
12
+ import { createWSTransportWorker } from '../server.ts'
13
+ import {
14
+ InternalServerErrorHttpResponse,
15
+ NotFoundHttpResponse,
16
+ StatusResponse,
17
+ } from '../utils.ts'
18
+
19
+ function adapterFactory(params: WsAdapterParams<'bun'>): WsAdapterServer {
20
+ const adapter = createAdapter({ hooks: params.wsHooks })
21
+
22
+ let server: Bun.Server<any> | null = null
23
+
24
+ function createServer() {
25
+ return globalThis.Bun.serve(
26
+ // @ts-expect-error ts bs
27
+ {
28
+ ...params.runtime?.server,
29
+ unix: params.listen.unix,
30
+ port: params.listen.port,
31
+ hostname: params.listen.hostname,
32
+ reusePort: params.listen.reusePort,
33
+ tls: params.tls
34
+ ? {
35
+ cert: params.tls.cert,
36
+ key: params.tls.key,
37
+ passphrase: params.tls.passphrase,
38
+ }
39
+ : undefined,
40
+ websocket: { ...params.runtime?.ws, ...adapter.websocket },
41
+ routes: { '/healthy': StatusResponse() },
42
+ async fetch(request, server) {
43
+ try {
44
+ if (request.headers.get('upgrade') === 'websocket') {
45
+ return await adapter.handleUpgrade(request, server)
46
+ }
47
+ } catch (err) {
48
+ console.error('Error in WebSocket fetch handler', err)
49
+ return InternalServerErrorHttpResponse()
50
+ }
51
+ return NotFoundHttpResponse()
52
+ },
53
+ },
54
+ )
55
+ }
56
+
57
+ return {
58
+ start: async () => {
59
+ server = createServer()
60
+ return server!.url.href
61
+ },
62
+ stop: async () => {
63
+ if (server) {
64
+ await server.stop()
65
+ server = null
66
+ }
67
+ },
68
+ }
69
+ }
70
+
71
+ export const WsTransport: Transport<
72
+ ConnectionType.Bidirectional,
73
+ WsTransportOptions<'bun'>,
74
+ typeof injectables,
75
+ ProxyableTransportType.WebSocket
76
+ > = {
77
+ proxyable: ProxyableTransportType.WebSocket,
78
+ injectables,
79
+ factory(options) {
80
+ return createWSTransportWorker(adapterFactory, options)
81
+ },
82
+ }
@@ -0,0 +1,125 @@
1
+ import type { Transport } from '@nmtjs/gateway'
2
+ import type { ConnectionType } from '@nmtjs/protocol'
3
+ import { ProxyableTransportType } from '@nmtjs/gateway'
4
+ import createAdapter from 'crossws/adapters/deno'
5
+
6
+ import type {
7
+ WsAdapterParams,
8
+ WsAdapterServer,
9
+ WsTransportOptions,
10
+ } from '../types.ts'
11
+ import * as injectables from '../injectables.ts'
12
+ import { createWSTransportWorker } from '../server.ts'
13
+ import {
14
+ InternalServerErrorHttpResponse,
15
+ NotFoundHttpResponse,
16
+ StatusResponse,
17
+ } from '../utils.ts'
18
+
19
+ type DenoServer = ReturnType<typeof globalThis.Deno.serve>
20
+ interface DenoNetAddr {
21
+ transport: 'tcp' | 'udp'
22
+ hostname: string
23
+ port: number
24
+ }
25
+
26
+ interface DenoUnixAddr {
27
+ transport: 'unix' | 'unixpacket'
28
+ path: string
29
+ }
30
+
31
+ interface DenoVsockAddr {
32
+ transport: 'vsock'
33
+ cid: number
34
+ port: number
35
+ }
36
+
37
+ type DenoAddr = DenoNetAddr | DenoUnixAddr | DenoVsockAddr
38
+
39
+ function adapterFactory(params: WsAdapterParams<'deno'>): WsAdapterServer {
40
+ const adapter = createAdapter({ hooks: params.wsHooks })
41
+
42
+ let server: DenoServer | null = null
43
+
44
+ function createServer() {
45
+ const listenOptions = params.listen.unix
46
+ ? { path: params.listen.unix }
47
+ : {
48
+ port: params.listen.port,
49
+ hostname: params.listen.hostname,
50
+ reusePort: params.listen.reusePort,
51
+ }
52
+ const options = {
53
+ ...listenOptions,
54
+ tls: params.tls
55
+ ? {
56
+ cert: params.tls.cert,
57
+ key: params.tls.key,
58
+ passphrase: params.tls.passphrase,
59
+ }
60
+ : undefined,
61
+ }
62
+
63
+ return new Promise<{ server: DenoServer; addr: DenoAddr }>((resolve) => {
64
+ const server = globalThis.Deno.serve({
65
+ ...params.runtime?.server,
66
+ ...options,
67
+ handler: async (request: Request, info: any) => {
68
+ const url = new URL(request.url)
69
+ if (url.pathname === '/healthy') return StatusResponse()
70
+ try {
71
+ if (request.headers.get('upgrade') === 'websocket') {
72
+ return await adapter.handleUpgrade(request, info as any)
73
+ }
74
+ } catch (err) {
75
+ console.error('Error in WebSocket fetch handler', err)
76
+ return InternalServerErrorHttpResponse()
77
+ }
78
+ return NotFoundHttpResponse()
79
+ },
80
+ onListen(addr: DenoAddr) {
81
+ setTimeout(() => {
82
+ resolve({ server, addr })
83
+ }, 1)
84
+ },
85
+ })
86
+ })
87
+ }
88
+
89
+ return {
90
+ start: async () => {
91
+ const { server: _server, addr } = await createServer()
92
+ server = _server
93
+ const proto = params.tls ? 'https' : 'http'
94
+
95
+ switch (addr.transport) {
96
+ case 'unix':
97
+ return `${proto}+unix://${addr.path}`
98
+ case 'tcp': {
99
+ return `${proto}://${addr.hostname}:${addr.port}`
100
+ }
101
+ default:
102
+ throw new Error(`Unsupported address transport`)
103
+ }
104
+ },
105
+ stop: async () => {
106
+ if (server) {
107
+ await server.shutdown()
108
+ server = null
109
+ }
110
+ },
111
+ }
112
+ }
113
+
114
+ export const WsTransport: Transport<
115
+ ConnectionType.Bidirectional,
116
+ WsTransportOptions<'deno'>,
117
+ typeof injectables,
118
+ ProxyableTransportType.WebSocket
119
+ > = {
120
+ proxyable: ProxyableTransportType.WebSocket,
121
+ injectables,
122
+ factory(options) {
123
+ return createWSTransportWorker(adapterFactory, options)
124
+ },
125
+ }
@@ -0,0 +1,83 @@
1
+ import type { Transport } from '@nmtjs/gateway'
2
+ import type { ConnectionType } from '@nmtjs/protocol'
3
+ import { ProxyableTransportType } from '@nmtjs/gateway'
4
+ import createAdapter from 'crossws/adapters/uws'
5
+
6
+ import type {
7
+ WsAdapterParams,
8
+ WsAdapterServer,
9
+ WsTransportOptions,
10
+ } from '../types.ts'
11
+ import * as injectables from '../injectables.ts'
12
+ import { createWSTransportWorker } from '../server.ts'
13
+ import { StatusResponse } from '../utils.ts'
14
+
15
+ import { App, SSLApp, us_socket_local_port } from 'uWebSockets.js'
16
+
17
+ const statusResponse = StatusResponse()
18
+ const statusResponseBuffer = await statusResponse.arrayBuffer()
19
+
20
+ function adapterFactory(params: WsAdapterParams<'node'>): WsAdapterServer {
21
+ const adapter = createAdapter({ hooks: params.wsHooks })
22
+
23
+ const server = params.tls
24
+ ? SSLApp({
25
+ passphrase: params.tls.passphrase,
26
+ key_file_name: params.tls.key,
27
+ cert_file_name: params.tls.cert,
28
+ })
29
+ : App()
30
+
31
+ server
32
+ .ws('/*', { ...params.runtime?.ws, ...adapter.websocket })
33
+ .get('/healthy', (res) => {
34
+ res.cork(() => {
35
+ res
36
+ .writeStatus(`${statusResponse.status} ${statusResponse.statusText}`)
37
+ .end(statusResponseBuffer)
38
+ })
39
+ })
40
+
41
+ return {
42
+ start: () =>
43
+ new Promise<string>((resolve, reject) => {
44
+ const proto = params.tls ? 'https' : 'http'
45
+ if (params.listen.unix) {
46
+ server.listen_unix((socket) => {
47
+ if (socket) {
48
+ resolve(`${proto}+unix://` + params.listen.unix)
49
+ } else {
50
+ reject(new Error('Failed to start WebSockets server'))
51
+ }
52
+ }, params.listen.unix)
53
+ } else if (typeof params.listen.port === 'number') {
54
+ const hostname = params.listen.hostname || '127.0.0.1'
55
+ server.listen(hostname, params.listen.port, (socket) => {
56
+ if (socket) {
57
+ resolve(`${proto}://${hostname}:${us_socket_local_port(socket)}`)
58
+ } else {
59
+ reject(new Error('Failed to start WebSockets server'))
60
+ }
61
+ })
62
+ } else {
63
+ reject(new Error('Invalid listen parameters'))
64
+ }
65
+ }),
66
+ stop: () => {
67
+ server.close()
68
+ },
69
+ }
70
+ }
71
+
72
+ export const WsTransport: Transport<
73
+ ConnectionType.Bidirectional,
74
+ WsTransportOptions<'node'>,
75
+ typeof injectables,
76
+ ProxyableTransportType.WebSocket
77
+ > = {
78
+ proxyable: ProxyableTransportType.WebSocket,
79
+ injectables,
80
+ factory(options) {
81
+ return createWSTransportWorker(adapterFactory, options)
82
+ },
83
+ }
package/src/server.ts ADDED
@@ -0,0 +1,161 @@
1
+ import type { TransportWorker, TransportWorkerParams } from '@nmtjs/gateway'
2
+ import type { Hooks, Peer } from 'crossws'
3
+ import { ConnectionType, ProtocolVersion } from '@nmtjs/protocol'
4
+ import { defineHooks } from 'crossws'
5
+
6
+ import type {
7
+ WsAdapterParams,
8
+ WsAdapterServer,
9
+ WsAdapterServerFactory,
10
+ WsTransportOptions,
11
+ WsTransportServerRequest,
12
+ } from './types.ts'
13
+ import {
14
+ InternalServerErrorHttpResponse,
15
+ NotFoundHttpResponse,
16
+ } from './utils.ts'
17
+
18
+ export function createWSTransportWorker(
19
+ adapterFactory: WsAdapterServerFactory<any>,
20
+ options: WsTransportOptions,
21
+ ): TransportWorker<ConnectionType.Bidirectional> {
22
+ return new WsTransportServer(adapterFactory, options)
23
+ }
24
+
25
+ export class WsTransportServer
26
+ implements TransportWorker<ConnectionType.Bidirectional>
27
+ {
28
+ #server: WsAdapterServer
29
+ params!: TransportWorkerParams<ConnectionType.Bidirectional>
30
+ clients = new Map<string, Peer>()
31
+
32
+ constructor(
33
+ protected readonly adapterFactory: WsAdapterServerFactory<any>,
34
+ protected readonly options: WsTransportOptions,
35
+ ) {
36
+ this.#server = this.createServer()
37
+ }
38
+
39
+ async start(
40
+ hooks: TransportWorkerParams<ConnectionType.Bidirectional>,
41
+ ): Promise<string> {
42
+ this.params = hooks
43
+ return await this.#server.start()
44
+ }
45
+
46
+ async stop(): Promise<void> {
47
+ for (const peer of this.clients.values()) {
48
+ try {
49
+ peer.close(1001, 'Transport stopped')
50
+ } catch (error) {
51
+ console.error(
52
+ `Failed to close WebSocket connection ${peer.context.connectionId}`,
53
+ error,
54
+ )
55
+ }
56
+ }
57
+ this.clients.clear()
58
+ await this.#server.stop()
59
+ }
60
+
61
+ send(connectionId: string, buffer: ArrayBufferView) {
62
+ const peer = this.clients.get(connectionId)
63
+ if (!peer) return false
64
+
65
+ try {
66
+ const result = peer.send(buffer)
67
+ if (typeof result === 'boolean') return result
68
+ if (typeof result === 'number') return result > 0
69
+ return true
70
+ } catch (error) {
71
+ console.error(
72
+ `Failed to send data over WebSocket connection ${connectionId}`,
73
+ error,
74
+ )
75
+ this.clients.delete(connectionId)
76
+ return false
77
+ }
78
+ }
79
+
80
+ private createWsHooks(): Hooks {
81
+ return defineHooks({
82
+ upgrade: async (req) => {
83
+ const url = new URL(req.url)
84
+
85
+ if (url.pathname !== '/') {
86
+ return NotFoundHttpResponse()
87
+ }
88
+
89
+ const request: WsTransportServerRequest = {
90
+ url,
91
+ headers: req.headers,
92
+ method: req.method,
93
+ }
94
+
95
+ const accept =
96
+ url.searchParams.get('accept') ?? req.headers.get('accept')
97
+ const contentType =
98
+ url.searchParams.get('content-type') ??
99
+ req.headers.get('content-type')
100
+
101
+ try {
102
+ const connection = await this.params.onConnect({
103
+ type: ConnectionType.Bidirectional,
104
+ protocolVersion: ProtocolVersion.v1,
105
+ accept,
106
+ contentType,
107
+ data: request,
108
+ })
109
+
110
+ return { context: { connectionId: connection.id } }
111
+ } catch (error) {
112
+ console.error('Failed to upgrade WebSocket connection', error)
113
+ return InternalServerErrorHttpResponse()
114
+ }
115
+ },
116
+ open: (peer) => {
117
+ const { connectionId } = peer.context
118
+ this.clients.set(connectionId, peer)
119
+ },
120
+ message: async (peer, message) => {
121
+ const data = message.arrayBuffer() as ArrayBuffer
122
+ try {
123
+ await this.params.onMessage({
124
+ connectionId: peer.context.connectionId,
125
+ data,
126
+ })
127
+ } catch (error) {
128
+ console.error(
129
+ `Error while processing message from ${peer.context.connectionId}`,
130
+ error,
131
+ )
132
+ this.clients.delete(peer.context.connectionId)
133
+ peer.close(1011, 'Internal error')
134
+ }
135
+ },
136
+ error: (peer, error) => {
137
+ console.error(
138
+ `WebSocket error on connection ${peer.context.connectionId}`,
139
+ error,
140
+ )
141
+ },
142
+ close: async (peer) => {
143
+ this.clients.delete(peer.context.connectionId)
144
+ try {
145
+ await this.params.onDisconnect(peer.context.connectionId)
146
+ } catch (error) {
147
+ console.error(
148
+ `Failed to dispose WebSocket connection ${peer.context.connectionId}`,
149
+ error,
150
+ )
151
+ }
152
+ },
153
+ }) as Hooks
154
+ }
155
+
156
+ private createServer() {
157
+ const hooks = this.createWsHooks()
158
+ const opts: WsAdapterParams = { ...this.options, wsHooks: hooks }
159
+ return this.adapterFactory(opts)
160
+ }
161
+ }
package/src/types.ts ADDED
@@ -0,0 +1,123 @@
1
+ import type { Async, OneOf } from '@nmtjs/common'
2
+ import type { Hooks } from 'crossws'
3
+
4
+ export type WsTransportServerRequest = {
5
+ url: URL
6
+ method: string
7
+ headers: Headers
8
+ }
9
+
10
+ export type WsTransportPeerContext = { connectionId: string }
11
+
12
+ declare module 'crossws' {
13
+ interface PeerContext extends WsTransportPeerContext {}
14
+ }
15
+
16
+ export type WsTransportOptions<
17
+ R extends keyof WsTransportRuntimes = keyof WsTransportRuntimes,
18
+ > = {
19
+ listen: WsTransportListenOptions
20
+ cors?: WsTransportCorsOptions
21
+ tls?: WsTransportTlsOptions
22
+ runtime?: WsTransportRuntimes[R]
23
+ }
24
+
25
+ export type WsTransportCorsCustomParams = {
26
+ allowMethods?: string[]
27
+ allowHeaders?: string[]
28
+ allowCredentials?: string
29
+ maxAge?: string
30
+ exposeHeaders?: string[]
31
+ requestHeaders?: string[]
32
+ requestMethod?: string
33
+ }
34
+
35
+ export type WsTransportCorsOptions =
36
+ | true
37
+ | string[]
38
+ | WsTransportCorsCustomParams
39
+ | ((
40
+ origin: string,
41
+ request: WsTransportServerRequest,
42
+ ) => boolean | WsTransportCorsCustomParams)
43
+
44
+ export type WsTransportListenOptions = OneOf<
45
+ [{ port: number; hostname?: string; reusePort?: boolean }, { unix: string }]
46
+ >
47
+
48
+ export type WsTransportRuntimeBun = {
49
+ ws?: Partial<
50
+ Pick<
51
+ import('bun').WebSocketHandler<import('crossws').PeerContext>,
52
+ | 'backpressureLimit'
53
+ | 'maxPayloadLength'
54
+ | 'closeOnBackpressureLimit'
55
+ | 'idleTimeout'
56
+ | 'perMessageDeflate'
57
+ | 'sendPings'
58
+ >
59
+ >
60
+ server?: Partial<
61
+ Pick<
62
+ import('bun').ServeOptions,
63
+ 'development' | 'id' | 'maxRequestBodySize' | 'idleTimeout' | 'ipv6Only'
64
+ >
65
+ >
66
+ }
67
+
68
+ export type WsTransportRuntimeNode = {
69
+ ws?: Partial<
70
+ Pick<
71
+ import('uWebSockets.js').WebSocketBehavior<import('crossws').PeerContext>,
72
+ | 'maxBackpressure'
73
+ | 'maxPayloadLength'
74
+ | 'maxLifetime'
75
+ | 'closeOnBackpressureLimit'
76
+ | 'idleTimeout'
77
+ | 'compression'
78
+ | 'sendPingsAutomatically'
79
+ >
80
+ >
81
+ }
82
+
83
+ export type WsTransportRuntimeDeno = { server?: {} }
84
+
85
+ export type WsTransportRuntimes = {
86
+ bun: WsTransportRuntimeBun
87
+ node: WsTransportRuntimeNode
88
+ deno: WsTransportRuntimeDeno
89
+ }
90
+
91
+ export type WsTransportTlsOptions = {
92
+ /**
93
+ * File path or inlined TLS certificate in PEM format (required).
94
+ */
95
+ cert?: string
96
+ /**
97
+ * File path or inlined TLS private key in PEM format (required).
98
+ */
99
+ key?: string
100
+ /**
101
+ * Passphrase for the private key (optional).
102
+ */
103
+ passphrase?: string
104
+ }
105
+
106
+ export type WsAdapterParams<
107
+ R extends keyof WsTransportRuntimes = keyof WsTransportRuntimes,
108
+ > = {
109
+ listen: WsTransportListenOptions
110
+ wsHooks: Hooks
111
+ cors?: WsTransportCorsOptions
112
+ tls?: WsTransportTlsOptions
113
+ runtime?: WsTransportRuntimes[R]
114
+ }
115
+
116
+ export interface WsAdapterServer {
117
+ stop: () => Async<any>
118
+ start: () => Async<string>
119
+ }
120
+
121
+ export type WsAdapterServerFactory<
122
+ R extends keyof WsTransportRuntimes = keyof WsTransportRuntimes,
123
+ > = (params: WsAdapterParams<R>) => WsAdapterServer
package/src/utils.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { ErrorCode } from '@nmtjs/protocol'
2
+ import { ProtocolError } from '@nmtjs/protocol/server'
3
+
4
+ export const InternalError = (message = 'Internal Server Error') =>
5
+ new ProtocolError(ErrorCode.InternalServerError, message)
6
+
7
+ export const NotFoundError = (message = 'Not Found') =>
8
+ new ProtocolError(ErrorCode.NotFound, message)
9
+
10
+ export const ForbiddenError = (message = 'Forbidden') =>
11
+ new ProtocolError(ErrorCode.Forbidden, message)
12
+
13
+ export const RequestTimeoutError = (message = 'Request Timeout') =>
14
+ new ProtocolError(ErrorCode.RequestTimeout, message)
15
+
16
+ export const NotFoundHttpResponse = () =>
17
+ new Response('Not Found', {
18
+ status: 404,
19
+ headers: { 'Content-Type': 'text/plain' },
20
+ })
21
+
22
+ export const InternalServerErrorHttpResponse = () =>
23
+ new Response('Internal Server Error', {
24
+ status: 500,
25
+ headers: { 'Content-Type': 'text/plain' },
26
+ })
27
+
28
+ export const StatusResponse = () => new Response('OK', { status: 200 })