@nmtjs/ws-transport 0.12.6 → 0.12.7
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 +7 -8
- package/src/http.ts +0 -150
- package/src/index.ts +0 -5
- package/src/injectables.ts +0 -17
- package/src/server.ts +0 -545
- package/src/transport.ts +0 -10
- package/src/types.ts +0 -28
- package/src/utils.ts +0 -126
package/package.json
CHANGED
|
@@ -12,26 +12,25 @@
|
|
|
12
12
|
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.52.0"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"@nmtjs/common": "0.12.
|
|
16
|
-
"@nmtjs/
|
|
17
|
-
"@nmtjs/
|
|
15
|
+
"@nmtjs/common": "0.12.7",
|
|
16
|
+
"@nmtjs/core": "0.12.7",
|
|
17
|
+
"@nmtjs/protocol": "0.12.7"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20",
|
|
21
|
-
"@nmtjs/
|
|
22
|
-
"@nmtjs/
|
|
23
|
-
"@nmtjs/common": "0.12.
|
|
21
|
+
"@nmtjs/client": "0.12.7",
|
|
22
|
+
"@nmtjs/protocol": "0.12.7",
|
|
23
|
+
"@nmtjs/common": "0.12.7"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": ">=20"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
|
-
"src",
|
|
30
29
|
"dist",
|
|
31
30
|
"LICENSE.md",
|
|
32
31
|
"README.md"
|
|
33
32
|
],
|
|
34
|
-
"version": "0.12.
|
|
33
|
+
"version": "0.12.7",
|
|
35
34
|
"scripts": {
|
|
36
35
|
"build": "tsc",
|
|
37
36
|
"type-check": "tsc --noEmit"
|
package/src/http.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { createMetadataKey } from '@nmtjs/core'
|
|
2
|
-
import { ErrorCode } from '@nmtjs/protocol'
|
|
3
|
-
|
|
4
|
-
export enum HttpCode {
|
|
5
|
-
Continue = 100,
|
|
6
|
-
SwitchingProtocols = 101,
|
|
7
|
-
Processing = 102,
|
|
8
|
-
EarlyHints = 103,
|
|
9
|
-
OK = 200,
|
|
10
|
-
Created = 201,
|
|
11
|
-
Accepted = 202,
|
|
12
|
-
NonAuthoritativeInformation = 203,
|
|
13
|
-
NoContent = 204,
|
|
14
|
-
ResetContent = 205,
|
|
15
|
-
PartialContent = 206,
|
|
16
|
-
MultiStatus = 207,
|
|
17
|
-
AlreadyReported = 208,
|
|
18
|
-
IMUsed = 226,
|
|
19
|
-
MultipleChoices = 300,
|
|
20
|
-
MovedPermanently = 301,
|
|
21
|
-
Found = 302,
|
|
22
|
-
SeeOther = 303,
|
|
23
|
-
NotModified = 304,
|
|
24
|
-
UseProxy = 305,
|
|
25
|
-
TemporaryRedirect = 307,
|
|
26
|
-
PermanentRedirect = 308,
|
|
27
|
-
BadRequest = 400,
|
|
28
|
-
Unauthorized = 401,
|
|
29
|
-
PaymentRequired = 402,
|
|
30
|
-
Forbidden = 403,
|
|
31
|
-
NotFound = 404,
|
|
32
|
-
MethodNotAllowed = 405,
|
|
33
|
-
NotAcceptable = 406,
|
|
34
|
-
ProxyAuthenticationRequired = 407,
|
|
35
|
-
RequestTimeout = 408,
|
|
36
|
-
Conflict = 409,
|
|
37
|
-
Gone = 410,
|
|
38
|
-
LengthRequired = 411,
|
|
39
|
-
PreconditionFailed = 412,
|
|
40
|
-
PayloadTooLarge = 413,
|
|
41
|
-
URITooLong = 414,
|
|
42
|
-
UnsupportedMediaType = 415,
|
|
43
|
-
RangeNotSatisfiable = 416,
|
|
44
|
-
ExpectationFailed = 417,
|
|
45
|
-
ImATeapot = 418,
|
|
46
|
-
MisdirectedRequest = 421,
|
|
47
|
-
UnprocessableEntity = 422,
|
|
48
|
-
Locked = 423,
|
|
49
|
-
FailedDependency = 424,
|
|
50
|
-
TooEarly = 425,
|
|
51
|
-
UpgradeRequired = 426,
|
|
52
|
-
PreconditionRequired = 428,
|
|
53
|
-
TooManyRequests = 429,
|
|
54
|
-
RequestHeaderFieldsTooLarge = 431,
|
|
55
|
-
UnavailableForLegalReasons = 451,
|
|
56
|
-
InternalServerError = 500,
|
|
57
|
-
NotImplemented = 501,
|
|
58
|
-
BadGateway = 502,
|
|
59
|
-
ServiceUnavailable = 503,
|
|
60
|
-
GatewayTimeout = 504,
|
|
61
|
-
HTTPVersionNotSupported = 505,
|
|
62
|
-
VariantAlsoNegotiates = 506,
|
|
63
|
-
InsufficientStorage = 507,
|
|
64
|
-
LoopDetected = 508,
|
|
65
|
-
NotExtended = 510,
|
|
66
|
-
NetworkAuthenticationRequired = 511,
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export const HttpStatusText: Record<HttpCode, string> = {
|
|
70
|
-
[HttpCode.Continue]: 'Continue',
|
|
71
|
-
[HttpCode.SwitchingProtocols]: 'Switching Protocols',
|
|
72
|
-
[HttpCode.Processing]: 'Processing',
|
|
73
|
-
[HttpCode.EarlyHints]: 'Early Hints',
|
|
74
|
-
[HttpCode.OK]: 'OK',
|
|
75
|
-
[HttpCode.Created]: 'Created',
|
|
76
|
-
[HttpCode.Accepted]: 'Accepted',
|
|
77
|
-
[HttpCode.NonAuthoritativeInformation]: 'Non-Authoritative Information',
|
|
78
|
-
[HttpCode.NoContent]: 'No Content',
|
|
79
|
-
[HttpCode.ResetContent]: 'Reset Content',
|
|
80
|
-
[HttpCode.PartialContent]: 'Partial Content',
|
|
81
|
-
[HttpCode.MultiStatus]: 'Multi-Status',
|
|
82
|
-
[HttpCode.AlreadyReported]: 'Already Reported',
|
|
83
|
-
[HttpCode.IMUsed]: 'IM Used',
|
|
84
|
-
[HttpCode.MultipleChoices]: 'Multiple Choices',
|
|
85
|
-
[HttpCode.MovedPermanently]: 'Moved Permanently',
|
|
86
|
-
[HttpCode.Found]: 'Found',
|
|
87
|
-
[HttpCode.SeeOther]: 'See Other',
|
|
88
|
-
[HttpCode.NotModified]: 'Not Modified',
|
|
89
|
-
[HttpCode.UseProxy]: 'Use Proxy',
|
|
90
|
-
[HttpCode.TemporaryRedirect]: 'Temporary Redirect',
|
|
91
|
-
[HttpCode.PermanentRedirect]: 'Permanent Redirect',
|
|
92
|
-
[HttpCode.BadRequest]: 'Bad Request',
|
|
93
|
-
[HttpCode.Unauthorized]: 'Unauthorized',
|
|
94
|
-
[HttpCode.PaymentRequired]: 'Payment Required',
|
|
95
|
-
[HttpCode.Forbidden]: 'Forbidden',
|
|
96
|
-
[HttpCode.NotFound]: 'Not Found',
|
|
97
|
-
[HttpCode.MethodNotAllowed]: 'Method Not Allowed',
|
|
98
|
-
[HttpCode.NotAcceptable]: 'Not Acceptable',
|
|
99
|
-
[HttpCode.ProxyAuthenticationRequired]: 'Proxy Authentication Required',
|
|
100
|
-
[HttpCode.RequestTimeout]: 'Request Timeout',
|
|
101
|
-
[HttpCode.Conflict]: 'Conflict',
|
|
102
|
-
[HttpCode.Gone]: 'Gone',
|
|
103
|
-
[HttpCode.LengthRequired]: 'Length Required',
|
|
104
|
-
[HttpCode.PreconditionFailed]: 'Precondition Failed',
|
|
105
|
-
[HttpCode.PayloadTooLarge]: 'Payload Too Large',
|
|
106
|
-
[HttpCode.URITooLong]: 'URI Too Long',
|
|
107
|
-
[HttpCode.UnsupportedMediaType]: 'Unsupported Media Type',
|
|
108
|
-
[HttpCode.RangeNotSatisfiable]: 'Range Not Satisfiable',
|
|
109
|
-
[HttpCode.ExpectationFailed]: 'Expectation Failed',
|
|
110
|
-
[HttpCode.ImATeapot]: "I'm a Teapot",
|
|
111
|
-
[HttpCode.MisdirectedRequest]: 'Misdirected Request',
|
|
112
|
-
[HttpCode.UnprocessableEntity]: 'Unprocessable Entity',
|
|
113
|
-
[HttpCode.Locked]: 'Locked',
|
|
114
|
-
[HttpCode.FailedDependency]: 'Failed Dependency',
|
|
115
|
-
[HttpCode.TooEarly]: 'Too Early',
|
|
116
|
-
[HttpCode.UpgradeRequired]: 'Upgrade Required',
|
|
117
|
-
[HttpCode.PreconditionRequired]: 'Precondition Required',
|
|
118
|
-
[HttpCode.TooManyRequests]: 'Too Many Requests',
|
|
119
|
-
[HttpCode.RequestHeaderFieldsTooLarge]: 'Request Header Fields Too Large',
|
|
120
|
-
[HttpCode.UnavailableForLegalReasons]: 'Unavailable For Legal Reasons',
|
|
121
|
-
[HttpCode.InternalServerError]: 'Internal Server Error',
|
|
122
|
-
[HttpCode.NotImplemented]: 'Not Implemented',
|
|
123
|
-
[HttpCode.BadGateway]: 'Bad Gateway',
|
|
124
|
-
[HttpCode.ServiceUnavailable]: 'Service Unavailable',
|
|
125
|
-
[HttpCode.GatewayTimeout]: 'Gateway Timeout',
|
|
126
|
-
[HttpCode.HTTPVersionNotSupported]: 'HTTP Version Not Supported',
|
|
127
|
-
[HttpCode.VariantAlsoNegotiates]: 'Variant Also Negotiates',
|
|
128
|
-
[HttpCode.InsufficientStorage]: 'Insufficient Storage',
|
|
129
|
-
[HttpCode.LoopDetected]: 'Loop Detected',
|
|
130
|
-
[HttpCode.NotExtended]: 'Not Extended',
|
|
131
|
-
[HttpCode.NetworkAuthenticationRequired]: 'Network Authentication Required',
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export const HttpCodeMap = {
|
|
135
|
-
[ErrorCode.ValidationError]: HttpCode.BadRequest,
|
|
136
|
-
[ErrorCode.BadRequest]: HttpCode.BadRequest,
|
|
137
|
-
[ErrorCode.NotFound]: HttpCode.NotFound,
|
|
138
|
-
[ErrorCode.Forbidden]: HttpCode.Forbidden,
|
|
139
|
-
[ErrorCode.Unauthorized]: HttpCode.Unauthorized,
|
|
140
|
-
[ErrorCode.InternalServerError]: HttpCode.InternalServerError,
|
|
141
|
-
[ErrorCode.NotAcceptable]: HttpCode.NotAcceptable,
|
|
142
|
-
[ErrorCode.RequestTimeout]: HttpCode.RequestTimeout,
|
|
143
|
-
[ErrorCode.GatewayTimeout]: HttpCode.GatewayTimeout,
|
|
144
|
-
[ErrorCode.ServiceUnavailable]: HttpCode.ServiceUnavailable,
|
|
145
|
-
[ErrorCode.ClientRequestError]: HttpCode.BadRequest,
|
|
146
|
-
[ErrorCode.ConnectionError]: HttpCode.NotAcceptable,
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export const AllowedHttpMethod =
|
|
150
|
-
createMetadataKey<Array<'get' | 'post'>>('http:method')
|
package/src/index.ts
DELETED
package/src/injectables.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { createLazyInjectable, type LazyInjectable, Scope } from '@nmtjs/core'
|
|
2
|
-
import { ProtocolInjectables } from '@nmtjs/protocol/server'
|
|
3
|
-
import type { WsUserData } from './types.ts'
|
|
4
|
-
|
|
5
|
-
const connectionData = ProtocolInjectables.connectionData as LazyInjectable<
|
|
6
|
-
WsUserData['request'],
|
|
7
|
-
Scope.Connection
|
|
8
|
-
>
|
|
9
|
-
|
|
10
|
-
const httpResponseHeaders = createLazyInjectable<Headers, Scope.Call>(
|
|
11
|
-
Scope.Call,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
export const WsTransportInjectables = {
|
|
15
|
-
connectionData,
|
|
16
|
-
httpResponseHeaders,
|
|
17
|
-
} as const
|
package/src/server.ts
DELETED
|
@@ -1,545 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
App,
|
|
3
|
-
type HttpRequest,
|
|
4
|
-
type HttpResponse,
|
|
5
|
-
SSLApp,
|
|
6
|
-
type TemplatedApp,
|
|
7
|
-
us_socket_local_port,
|
|
8
|
-
} from 'uWebSockets.js'
|
|
9
|
-
import { randomUUID } from 'node:crypto'
|
|
10
|
-
import { once } from 'node:events'
|
|
11
|
-
import { Duplex, Readable } from 'node:stream'
|
|
12
|
-
import { Scope } from '@nmtjs/core'
|
|
13
|
-
import {
|
|
14
|
-
ClientMessageType,
|
|
15
|
-
decodeNumber,
|
|
16
|
-
ErrorCode,
|
|
17
|
-
ProtocolBlob,
|
|
18
|
-
type ServerMessageType,
|
|
19
|
-
} from '@nmtjs/protocol'
|
|
20
|
-
import {
|
|
21
|
-
Connection,
|
|
22
|
-
getFormat,
|
|
23
|
-
isIterableResult,
|
|
24
|
-
type ProtocolApiCallOptions,
|
|
25
|
-
ProtocolClientStream,
|
|
26
|
-
ProtocolError,
|
|
27
|
-
ProtocolInjectables,
|
|
28
|
-
type Transport,
|
|
29
|
-
type TransportPluginContext,
|
|
30
|
-
UnsupportedContentTypeError,
|
|
31
|
-
UnsupportedFormatError,
|
|
32
|
-
} from '@nmtjs/protocol/server'
|
|
33
|
-
import {
|
|
34
|
-
AllowedHttpMethod,
|
|
35
|
-
HttpCode,
|
|
36
|
-
HttpCodeMap,
|
|
37
|
-
HttpStatusText,
|
|
38
|
-
} from './http.ts'
|
|
39
|
-
import { WsTransportInjectables } from './injectables.ts'
|
|
40
|
-
import type {
|
|
41
|
-
WsConnectionData,
|
|
42
|
-
WsTransportOptions,
|
|
43
|
-
WsTransportSocket,
|
|
44
|
-
WsUserData,
|
|
45
|
-
} from './types.ts'
|
|
46
|
-
import {
|
|
47
|
-
getRequestBody,
|
|
48
|
-
getRequestData,
|
|
49
|
-
readableToArrayBuffer,
|
|
50
|
-
send,
|
|
51
|
-
setHeaders,
|
|
52
|
-
} from './utils.ts'
|
|
53
|
-
|
|
54
|
-
const DEFAULT_ALLOWED_METHODS = ['post'] as ('get' | 'post')[]
|
|
55
|
-
|
|
56
|
-
export class WsTransportServer implements Transport<WsConnectionData> {
|
|
57
|
-
protected server!: TemplatedApp
|
|
58
|
-
protected clients: Map<string, WsTransportSocket> = new Map()
|
|
59
|
-
|
|
60
|
-
constructor(
|
|
61
|
-
protected readonly context: TransportPluginContext,
|
|
62
|
-
protected readonly options: WsTransportOptions,
|
|
63
|
-
) {
|
|
64
|
-
this.server = this.options.tls ? SSLApp(options.tls!) : App()
|
|
65
|
-
this.server
|
|
66
|
-
.options('/*', (res, req) => {
|
|
67
|
-
this.applyCors(res, req)
|
|
68
|
-
res.writeStatus('200 OK')
|
|
69
|
-
res.endWithoutBody()
|
|
70
|
-
})
|
|
71
|
-
.get('/healthy', (res, req) => {
|
|
72
|
-
this.applyCors(res, req)
|
|
73
|
-
res.writeHeader('Content-Type', 'text/plain')
|
|
74
|
-
res.end('OK')
|
|
75
|
-
})
|
|
76
|
-
.ws<WsUserData>('/api', {
|
|
77
|
-
sendPingsAutomatically: true,
|
|
78
|
-
maxPayloadLength: this.options.maxPayloadLength,
|
|
79
|
-
upgrade: async (res, req, socketContext) => {
|
|
80
|
-
const ac = new AbortController()
|
|
81
|
-
|
|
82
|
-
res.onAborted(ac.abort.bind(ac))
|
|
83
|
-
|
|
84
|
-
const requestData = getRequestData(req, res)
|
|
85
|
-
const contentType =
|
|
86
|
-
requestData.query.get('content-type') ||
|
|
87
|
-
requestData.headers.get('content-type')
|
|
88
|
-
const acceptType =
|
|
89
|
-
requestData.query.get('accept') || requestData.headers.get('accept')
|
|
90
|
-
|
|
91
|
-
const connectionId = randomUUID()
|
|
92
|
-
const controller = new AbortController()
|
|
93
|
-
try {
|
|
94
|
-
const { context } = await this.protocol.addConnection(
|
|
95
|
-
this,
|
|
96
|
-
{ id: connectionId, data: { type: 'ws' } },
|
|
97
|
-
{ acceptType, contentType },
|
|
98
|
-
)
|
|
99
|
-
context.container.provide(
|
|
100
|
-
WsTransportInjectables.connectionData,
|
|
101
|
-
requestData,
|
|
102
|
-
)
|
|
103
|
-
context.container.provide(
|
|
104
|
-
ProtocolInjectables.connectionAbortSignal,
|
|
105
|
-
controller.signal,
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
if (!ac.signal.aborted) {
|
|
109
|
-
res.cork(() => {
|
|
110
|
-
res.upgrade(
|
|
111
|
-
{
|
|
112
|
-
id: connectionId,
|
|
113
|
-
request: requestData,
|
|
114
|
-
contentType,
|
|
115
|
-
acceptType,
|
|
116
|
-
backpressure: null,
|
|
117
|
-
context,
|
|
118
|
-
controller,
|
|
119
|
-
} as WsUserData,
|
|
120
|
-
req.getHeader('sec-websocket-key'),
|
|
121
|
-
req.getHeader('sec-websocket-protocol'),
|
|
122
|
-
req.getHeader('sec-websocket-extensions'),
|
|
123
|
-
socketContext,
|
|
124
|
-
)
|
|
125
|
-
})
|
|
126
|
-
}
|
|
127
|
-
} catch (error) {
|
|
128
|
-
this.logger.debug(
|
|
129
|
-
new Error('Failed to upgrade connection', { cause: error }),
|
|
130
|
-
)
|
|
131
|
-
if (!ac.signal.aborted) {
|
|
132
|
-
res.cork(() => {
|
|
133
|
-
res.writeStatus('500 Internal Server Error')
|
|
134
|
-
res.endWithoutBody()
|
|
135
|
-
})
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
open: (ws: WsTransportSocket) => {
|
|
140
|
-
const { id } = ws.getUserData()
|
|
141
|
-
this.logger.debug('Connection %s opened', id)
|
|
142
|
-
this.clients.set(id, ws)
|
|
143
|
-
},
|
|
144
|
-
message: async (ws: WsTransportSocket, buffer) => {
|
|
145
|
-
const messageType = decodeNumber(buffer, 'Uint8')
|
|
146
|
-
if (messageType in this === false) {
|
|
147
|
-
ws.end(1011, 'Unknown message type')
|
|
148
|
-
} else {
|
|
149
|
-
try {
|
|
150
|
-
await this[messageType](
|
|
151
|
-
ws,
|
|
152
|
-
buffer.slice(Uint8Array.BYTES_PER_ELEMENT),
|
|
153
|
-
)
|
|
154
|
-
} catch (error: any) {
|
|
155
|
-
this.logError(error, 'Error while processing message')
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
drain: (ws: WsTransportSocket) => {
|
|
160
|
-
const data = ws.getUserData()
|
|
161
|
-
data.backpressure?.resolve()
|
|
162
|
-
data.backpressure = null
|
|
163
|
-
},
|
|
164
|
-
close: async (ws: WsTransportSocket, code, message) => {
|
|
165
|
-
const { id, controller } = ws.getUserData()
|
|
166
|
-
controller.abort()
|
|
167
|
-
this.logger.debug(
|
|
168
|
-
'Connection %s closed with code %s: %s',
|
|
169
|
-
id,
|
|
170
|
-
code,
|
|
171
|
-
Buffer.from(message).toString(),
|
|
172
|
-
)
|
|
173
|
-
this.clients.delete(id)
|
|
174
|
-
await this.protocol.removeConnection(id)
|
|
175
|
-
},
|
|
176
|
-
})
|
|
177
|
-
.get('/api/:namespace/:procedure', this.httpHandler.bind(this))
|
|
178
|
-
.post('/api/:namespace/:procedure', this.httpHandler.bind(this))
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
send(
|
|
182
|
-
connection: Connection<WsConnectionData>,
|
|
183
|
-
messageType: ServerMessageType,
|
|
184
|
-
buffer: ArrayBuffer,
|
|
185
|
-
) {
|
|
186
|
-
const ws = this.clients.get(connection.id)
|
|
187
|
-
if (ws) send(ws, messageType, buffer)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async start() {
|
|
191
|
-
return new Promise<void>((resolve, reject) => {
|
|
192
|
-
const { hostname = '127.0.0.1', port = 0, unix } = this.options
|
|
193
|
-
if (unix) {
|
|
194
|
-
this.server.listen_unix((socket) => {
|
|
195
|
-
if (socket) {
|
|
196
|
-
this.logger.info('Server started on unix://%s', unix)
|
|
197
|
-
resolve()
|
|
198
|
-
} else {
|
|
199
|
-
reject(new Error('Failed to start WebSockets server'))
|
|
200
|
-
}
|
|
201
|
-
}, unix)
|
|
202
|
-
} else {
|
|
203
|
-
this.server.listen(hostname, port, (socket) => {
|
|
204
|
-
if (socket) {
|
|
205
|
-
this.logger.info(
|
|
206
|
-
'WebSocket Server started on %s:%s',
|
|
207
|
-
hostname,
|
|
208
|
-
us_socket_local_port(socket),
|
|
209
|
-
)
|
|
210
|
-
resolve()
|
|
211
|
-
} else {
|
|
212
|
-
reject(new Error('Failed to start WebSockets server'))
|
|
213
|
-
}
|
|
214
|
-
})
|
|
215
|
-
}
|
|
216
|
-
})
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async stop() {
|
|
220
|
-
this.server.close()
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// TODO: decompose this mess
|
|
224
|
-
protected async httpHandler(res: HttpResponse, req: HttpRequest) {
|
|
225
|
-
this.applyCors(res, req)
|
|
226
|
-
|
|
227
|
-
const controller = new AbortController()
|
|
228
|
-
|
|
229
|
-
res.onAborted(controller.abort.bind(controller))
|
|
230
|
-
|
|
231
|
-
const method = req.getMethod() as 'get' | 'post'
|
|
232
|
-
const namespace = req.getParameter('namespace')
|
|
233
|
-
const procedure = req.getParameter('procedure')
|
|
234
|
-
const requestData = getRequestData(req, res)
|
|
235
|
-
|
|
236
|
-
if (!namespace || !procedure) {
|
|
237
|
-
const status = HttpCode.NotFound
|
|
238
|
-
const text = HttpStatusText[status]
|
|
239
|
-
return void res.cork(() => {
|
|
240
|
-
if (controller.signal.aborted) return
|
|
241
|
-
res.writeStatus(`${status} ${text}`)
|
|
242
|
-
res.endWithoutBody()
|
|
243
|
-
})
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const isBlob = requestData.headers.get('x-neemata-blob') === 'true'
|
|
247
|
-
|
|
248
|
-
const contentType = requestData.headers.get('content-type')
|
|
249
|
-
const acceptType = requestData.headers.get('accept')
|
|
250
|
-
const connectionId = randomUUID()
|
|
251
|
-
const connection = new Connection<WsConnectionData>({
|
|
252
|
-
id: connectionId,
|
|
253
|
-
data: { type: 'http' },
|
|
254
|
-
})
|
|
255
|
-
const responseHeaders = new Headers()
|
|
256
|
-
const container = this.context.container.fork(Scope.Call)
|
|
257
|
-
container.provide(ProtocolInjectables.connection, connection)
|
|
258
|
-
container.provide(
|
|
259
|
-
ProtocolInjectables.connectionAbortSignal,
|
|
260
|
-
controller.signal,
|
|
261
|
-
)
|
|
262
|
-
container.provide(WsTransportInjectables.connectionData, requestData)
|
|
263
|
-
container.provide(
|
|
264
|
-
WsTransportInjectables.httpResponseHeaders,
|
|
265
|
-
responseHeaders,
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
const body = method === 'post' ? getRequestBody(res) : undefined
|
|
269
|
-
|
|
270
|
-
const metadata: ProtocolApiCallOptions['metadata'] = (metadata) => {
|
|
271
|
-
const allowHttpMethod =
|
|
272
|
-
metadata.get(AllowedHttpMethod) ?? DEFAULT_ALLOWED_METHODS
|
|
273
|
-
if (!allowHttpMethod.includes(method)) {
|
|
274
|
-
throw new ProtocolError(ErrorCode.NotFound)
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
let format: ReturnType<typeof getFormat>
|
|
278
|
-
try {
|
|
279
|
-
format = getFormat(this.context.format, {
|
|
280
|
-
acceptType,
|
|
281
|
-
contentType: isBlob ? '*/*' : contentType,
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
let payload: any
|
|
285
|
-
|
|
286
|
-
if (body) {
|
|
287
|
-
if (isBlob) {
|
|
288
|
-
const type = contentType || 'application/octet-stream'
|
|
289
|
-
const contentLength = requestData.headers.get('content-length')
|
|
290
|
-
const size = contentLength
|
|
291
|
-
? Number.parseInt(contentLength)
|
|
292
|
-
: undefined
|
|
293
|
-
const stream = new ProtocolClientStream(-1, { size, type })
|
|
294
|
-
body.pipe(stream)
|
|
295
|
-
payload = stream
|
|
296
|
-
} else {
|
|
297
|
-
const buffer = await readableToArrayBuffer(body)
|
|
298
|
-
if (buffer.byteLength > 0) {
|
|
299
|
-
payload = format.decoder.decode(buffer)
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const result = await this.protocol.call({
|
|
305
|
-
connection,
|
|
306
|
-
namespace,
|
|
307
|
-
procedure,
|
|
308
|
-
payload,
|
|
309
|
-
metadata,
|
|
310
|
-
container,
|
|
311
|
-
signal: controller.signal,
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
if (isIterableResult(result)) {
|
|
315
|
-
res.cork(() => {
|
|
316
|
-
if (controller.signal.aborted) return
|
|
317
|
-
const status = HttpCode.NotImplemented
|
|
318
|
-
const text = HttpStatusText[status]
|
|
319
|
-
res.writeStatus(`${status} ${text}`)
|
|
320
|
-
res.end()
|
|
321
|
-
})
|
|
322
|
-
} else {
|
|
323
|
-
const { output } = result
|
|
324
|
-
|
|
325
|
-
if (output instanceof ProtocolBlob) {
|
|
326
|
-
const { source, metadata } = output
|
|
327
|
-
const { type } = metadata
|
|
328
|
-
|
|
329
|
-
let stream: Readable
|
|
330
|
-
|
|
331
|
-
if (source instanceof ReadableStream) {
|
|
332
|
-
stream = Readable.fromWeb(source as any)
|
|
333
|
-
} else if (source instanceof Readable || source instanceof Duplex) {
|
|
334
|
-
stream = Readable.from(source)
|
|
335
|
-
} else {
|
|
336
|
-
throw new Error('Invalid stream source')
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
res.cork(() => {
|
|
340
|
-
if (controller.signal.aborted) return
|
|
341
|
-
responseHeaders.set('X-Neemata-Blob', 'true')
|
|
342
|
-
responseHeaders.set('Content-Type', type)
|
|
343
|
-
if (metadata.size)
|
|
344
|
-
res.writeHeader('Content-Length', metadata.size.toString())
|
|
345
|
-
setHeaders(res, responseHeaders)
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
controller.signal.addEventListener('abort', () => stream.destroy(), {
|
|
349
|
-
once: true,
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
stream.on('data', (chunk) => {
|
|
353
|
-
if (controller.signal.aborted) return
|
|
354
|
-
const buf = Buffer.from(chunk)
|
|
355
|
-
const ab = buf.buffer.slice(
|
|
356
|
-
buf.byteOffset,
|
|
357
|
-
buf.byteOffset + buf.byteLength,
|
|
358
|
-
)
|
|
359
|
-
const ok = res.write(ab)
|
|
360
|
-
if (!ok) {
|
|
361
|
-
stream.pause()
|
|
362
|
-
res.onWritable(() => {
|
|
363
|
-
stream.resume()
|
|
364
|
-
return true
|
|
365
|
-
})
|
|
366
|
-
}
|
|
367
|
-
})
|
|
368
|
-
await once(stream, 'end')
|
|
369
|
-
if (stream.readableAborted) {
|
|
370
|
-
res.end(undefined, true)
|
|
371
|
-
} else {
|
|
372
|
-
res.end()
|
|
373
|
-
}
|
|
374
|
-
} else {
|
|
375
|
-
res.cork(() => {
|
|
376
|
-
if (controller.signal.aborted) return
|
|
377
|
-
const status = HttpCode.OK
|
|
378
|
-
const text = HttpStatusText[status]
|
|
379
|
-
const buffer = format.encoder.encode(output)
|
|
380
|
-
res.writeStatus(`${status} ${text}`)
|
|
381
|
-
responseHeaders.set('Content-Type', format.encoder.contentType)
|
|
382
|
-
setHeaders(res, responseHeaders)
|
|
383
|
-
res.end(buffer)
|
|
384
|
-
})
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
} catch (error) {
|
|
388
|
-
if (controller.signal.aborted) return
|
|
389
|
-
if (error instanceof UnsupportedFormatError) {
|
|
390
|
-
res.cork(() => {
|
|
391
|
-
if (controller.signal.aborted) return
|
|
392
|
-
const status =
|
|
393
|
-
error instanceof UnsupportedContentTypeError
|
|
394
|
-
? HttpCode.UnsupportedMediaType
|
|
395
|
-
: HttpCode.NotAcceptable
|
|
396
|
-
const text = HttpStatusText[status]
|
|
397
|
-
res.writeStatus(`${status} ${text}`)
|
|
398
|
-
res.end()
|
|
399
|
-
})
|
|
400
|
-
} else if (error instanceof ProtocolError) {
|
|
401
|
-
res.cork(() => {
|
|
402
|
-
if (controller.signal.aborted) return
|
|
403
|
-
const status =
|
|
404
|
-
error.code in HttpCodeMap
|
|
405
|
-
? HttpCodeMap[error.code]
|
|
406
|
-
: HttpCode.InternalServerError
|
|
407
|
-
const text = HttpStatusText[status]
|
|
408
|
-
res.writeStatus(`${status} ${text}`)
|
|
409
|
-
res.end(format!.encoder.encode(error))
|
|
410
|
-
})
|
|
411
|
-
} else {
|
|
412
|
-
this.logError(error, 'Unknown error while processing request')
|
|
413
|
-
res.cork(() => {
|
|
414
|
-
if (controller.signal.aborted) return
|
|
415
|
-
const status = HttpCode.InternalServerError
|
|
416
|
-
const text = HttpStatusText[status]
|
|
417
|
-
const payload = format!.encoder.encode(
|
|
418
|
-
new ProtocolError(
|
|
419
|
-
ErrorCode.InternalServerError,
|
|
420
|
-
'Internal Server Error',
|
|
421
|
-
),
|
|
422
|
-
)
|
|
423
|
-
res.writeStatus(`${status} ${text}`)
|
|
424
|
-
res.end(payload)
|
|
425
|
-
})
|
|
426
|
-
}
|
|
427
|
-
} finally {
|
|
428
|
-
container.dispose().catch((error) => {
|
|
429
|
-
this.logError(error, 'Error while disposing call container')
|
|
430
|
-
})
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
protected get protocol() {
|
|
435
|
-
return this.context.protocol
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
protected get logger() {
|
|
439
|
-
return this.context.logger
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
protected async logError(
|
|
443
|
-
cause: any,
|
|
444
|
-
message = 'Unknown error while processing request',
|
|
445
|
-
) {
|
|
446
|
-
this.logger.error(new Error(message, { cause }))
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
protected applyCors(res: HttpResponse, req: HttpRequest) {
|
|
450
|
-
if (this.options.cors === false) return
|
|
451
|
-
|
|
452
|
-
const origin = req.getHeader('origin')
|
|
453
|
-
if (!origin) return
|
|
454
|
-
|
|
455
|
-
let allowed = false
|
|
456
|
-
|
|
457
|
-
if (this.options.cors === undefined || this.options.cors === true) {
|
|
458
|
-
allowed = true
|
|
459
|
-
} else if (Array.isArray(this.options.cors)) {
|
|
460
|
-
allowed = this.options.cors.includes(origin)
|
|
461
|
-
} else {
|
|
462
|
-
allowed = this.options.cors(origin)
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if (!allowed) return
|
|
466
|
-
|
|
467
|
-
res.writeHeader('Access-Control-Allow-Origin', origin)
|
|
468
|
-
res.writeHeader('Access-Control-Allow-Headers', 'Content-Type')
|
|
469
|
-
res.writeHeader('Access-Control-Allow-Methods', 'GET, POST')
|
|
470
|
-
res.writeHeader('Access-Control-Allow-Credentials', 'true')
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
protected [ClientMessageType.Rpc](
|
|
474
|
-
ws: WsTransportSocket,
|
|
475
|
-
buffer: ArrayBuffer,
|
|
476
|
-
) {
|
|
477
|
-
const { id } = ws.getUserData()
|
|
478
|
-
this.protocol.rpcRaw(id, buffer)
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
protected [ClientMessageType.RpcAbort](
|
|
482
|
-
ws: WsTransportSocket,
|
|
483
|
-
buffer: ArrayBuffer,
|
|
484
|
-
) {
|
|
485
|
-
const { id } = ws.getUserData()
|
|
486
|
-
this.protocol.rpcAbortRaw(id, buffer)
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
protected [ClientMessageType.RpcStreamAbort](
|
|
490
|
-
ws: WsTransportSocket,
|
|
491
|
-
buffer: ArrayBuffer,
|
|
492
|
-
) {
|
|
493
|
-
const { id } = ws.getUserData()
|
|
494
|
-
this.protocol.rpcStreamAbortRaw(id, buffer)
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
protected [ClientMessageType.ClientStreamPush](
|
|
498
|
-
ws: WsTransportSocket,
|
|
499
|
-
buffer: ArrayBuffer,
|
|
500
|
-
) {
|
|
501
|
-
const { id } = ws.getUserData()
|
|
502
|
-
const streamId = decodeNumber(buffer, 'Uint32')
|
|
503
|
-
this.protocol.pushClientStream(
|
|
504
|
-
id,
|
|
505
|
-
streamId,
|
|
506
|
-
buffer.slice(Uint32Array.BYTES_PER_ELEMENT),
|
|
507
|
-
)
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
protected [ClientMessageType.ClientStreamEnd](
|
|
511
|
-
ws: WsTransportSocket,
|
|
512
|
-
buffer: ArrayBuffer,
|
|
513
|
-
) {
|
|
514
|
-
const { id } = ws.getUserData()
|
|
515
|
-
const streamId = decodeNumber(buffer, 'Uint32')
|
|
516
|
-
this.protocol.endClientStream(id, streamId)
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
protected [ClientMessageType.ClientStreamAbort](
|
|
520
|
-
ws: WsTransportSocket,
|
|
521
|
-
buffer: ArrayBuffer,
|
|
522
|
-
) {
|
|
523
|
-
const { id } = ws.getUserData()
|
|
524
|
-
const streamId = decodeNumber(buffer, 'Uint32')
|
|
525
|
-
this.protocol.abortClientStream(id, streamId)
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
protected [ClientMessageType.ServerStreamPull](
|
|
529
|
-
ws: WsTransportSocket,
|
|
530
|
-
buffer: ArrayBuffer,
|
|
531
|
-
) {
|
|
532
|
-
const { id } = ws.getUserData()
|
|
533
|
-
const streamId = decodeNumber(buffer, 'Uint32')
|
|
534
|
-
this.protocol.pullServerStream(id, streamId)
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
protected [ClientMessageType.ServerStreamAbort](
|
|
538
|
-
ws: WsTransportSocket,
|
|
539
|
-
buffer: ArrayBuffer,
|
|
540
|
-
) {
|
|
541
|
-
const { id } = ws.getUserData()
|
|
542
|
-
const streamId = decodeNumber(buffer, 'Uint32')
|
|
543
|
-
this.protocol.abortServerStream(id, streamId)
|
|
544
|
-
}
|
|
545
|
-
}
|
package/src/transport.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { createTransport } from '@nmtjs/protocol/server'
|
|
2
|
-
import { WsTransportServer } from './server.ts'
|
|
3
|
-
import type { WsConnectionData, WsTransportOptions } from './types.ts'
|
|
4
|
-
|
|
5
|
-
export const WsTransport = createTransport<
|
|
6
|
-
WsConnectionData,
|
|
7
|
-
WsTransportOptions
|
|
8
|
-
>('WsTransport', (context, options) => {
|
|
9
|
-
return new WsTransportServer(context, options)
|
|
10
|
-
})
|
package/src/types.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { AppOptions, WebSocket } from 'uWebSockets.js'
|
|
2
|
-
import type { InteractivePromise } from '@nmtjs/common'
|
|
3
|
-
import type { Connection, ConnectionContext } from '@nmtjs/protocol/server'
|
|
4
|
-
import type { RequestData } from './utils.ts'
|
|
5
|
-
|
|
6
|
-
export type WsConnectionData = { type: 'ws' | 'http' }
|
|
7
|
-
|
|
8
|
-
export type WsUserData = {
|
|
9
|
-
id: Connection['id']
|
|
10
|
-
backpressure: InteractivePromise<void> | null
|
|
11
|
-
request: RequestData
|
|
12
|
-
acceptType: string | null
|
|
13
|
-
contentType: string | null
|
|
14
|
-
context: ConnectionContext
|
|
15
|
-
controller: AbortController
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type WsTransportSocket = WebSocket<WsUserData>
|
|
19
|
-
|
|
20
|
-
export type WsTransportOptions = {
|
|
21
|
-
port?: number
|
|
22
|
-
hostname?: string
|
|
23
|
-
unix?: string
|
|
24
|
-
tls?: AppOptions
|
|
25
|
-
cors?: boolean | string[] | ((origin: string) => boolean)
|
|
26
|
-
maxPayloadLength?: number
|
|
27
|
-
maxStreamChunkLength?: number
|
|
28
|
-
}
|
package/src/utils.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import type { HttpRequest, HttpResponse } from 'uWebSockets.js'
|
|
2
|
-
import { PassThrough, type Readable } from 'node:stream'
|
|
3
|
-
import { createPromise } from '@nmtjs/common'
|
|
4
|
-
import { concat, ErrorCode, encodeNumber } from '@nmtjs/protocol'
|
|
5
|
-
import { ProtocolError } from '@nmtjs/protocol/server'
|
|
6
|
-
import type { WsTransportSocket } from './types.ts'
|
|
7
|
-
|
|
8
|
-
export const send = (
|
|
9
|
-
ws: WsTransportSocket,
|
|
10
|
-
type: number,
|
|
11
|
-
...buffers: ArrayBuffer[]
|
|
12
|
-
): boolean | null => {
|
|
13
|
-
const data = ws.getUserData()
|
|
14
|
-
try {
|
|
15
|
-
const buffer = concat(encodeNumber(type, 'Uint8'), ...buffers)
|
|
16
|
-
const result = ws.send(buffer, true)
|
|
17
|
-
if (result === 0) {
|
|
18
|
-
data.backpressure = createPromise()
|
|
19
|
-
return false
|
|
20
|
-
}
|
|
21
|
-
if (result === 2) {
|
|
22
|
-
return null
|
|
23
|
-
}
|
|
24
|
-
return true
|
|
25
|
-
} catch {
|
|
26
|
-
return null
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const toRecord = (input: {
|
|
31
|
-
forEach: (cb: (value, key) => void) => void
|
|
32
|
-
}) => {
|
|
33
|
-
const obj: Record<string, string> = {}
|
|
34
|
-
input.forEach((value, key) => {
|
|
35
|
-
obj[key] = value
|
|
36
|
-
})
|
|
37
|
-
return obj
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export type RequestData = Readonly<{
|
|
41
|
-
url: string
|
|
42
|
-
origin: URL | null
|
|
43
|
-
method: string
|
|
44
|
-
headers: Headers
|
|
45
|
-
querystring: string
|
|
46
|
-
query: URLSearchParams
|
|
47
|
-
remoteAddress: string
|
|
48
|
-
proxiedRemoteAddress: string
|
|
49
|
-
}>
|
|
50
|
-
|
|
51
|
-
export const getRequestData = (
|
|
52
|
-
req: HttpRequest,
|
|
53
|
-
res: HttpResponse,
|
|
54
|
-
): RequestData => {
|
|
55
|
-
const url = req.getUrl()
|
|
56
|
-
const method = req.getMethod()
|
|
57
|
-
const headers = new Headers()
|
|
58
|
-
const querystring = req.getQuery()
|
|
59
|
-
const query = new URLSearchParams(querystring)
|
|
60
|
-
const origin = headers.get('origin')
|
|
61
|
-
const proxiedRemoteAddress = res.getProxiedRemoteAddressAsText()
|
|
62
|
-
const remoteAddress = res.getRemoteAddressAsText()
|
|
63
|
-
|
|
64
|
-
req.forEach((key, value) => headers.append(key, value))
|
|
65
|
-
|
|
66
|
-
return Object.freeze({
|
|
67
|
-
url,
|
|
68
|
-
origin: origin ? new URL(url, origin) : null,
|
|
69
|
-
method,
|
|
70
|
-
headers,
|
|
71
|
-
querystring,
|
|
72
|
-
query,
|
|
73
|
-
remoteAddress: Buffer.from(remoteAddress).toString(),
|
|
74
|
-
proxiedRemoteAddress: Buffer.from(proxiedRemoteAddress).toString(),
|
|
75
|
-
})
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function getRequestBody(res: HttpResponse) {
|
|
79
|
-
const stream = new PassThrough()
|
|
80
|
-
res.onData((chunk, isLast) => {
|
|
81
|
-
stream.write(Buffer.from(chunk))
|
|
82
|
-
if (isLast) stream.end()
|
|
83
|
-
})
|
|
84
|
-
res.onAborted(() => stream.destroy())
|
|
85
|
-
return stream
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function setHeaders(res: HttpResponse, headers: Headers) {
|
|
89
|
-
headers.forEach((value, key) => {
|
|
90
|
-
if (key === 'set-cookie') return
|
|
91
|
-
res.writeHeader(key, value)
|
|
92
|
-
})
|
|
93
|
-
const cookies = headers.getSetCookie()
|
|
94
|
-
if (cookies) {
|
|
95
|
-
for (const cookie of cookies) {
|
|
96
|
-
res.writeHeader('set-cookie', cookie)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function readableToArrayBuffer(stream: Readable): Promise<ArrayBuffer> {
|
|
102
|
-
return new Promise((resolve, reject) => {
|
|
103
|
-
const chunks: Buffer[] = []
|
|
104
|
-
stream.on('data', (chunk) => {
|
|
105
|
-
chunks.push(chunk)
|
|
106
|
-
})
|
|
107
|
-
stream.on('end', () => {
|
|
108
|
-
resolve(Buffer.concat(chunks).buffer)
|
|
109
|
-
})
|
|
110
|
-
stream.on('error', (error) => {
|
|
111
|
-
reject(error)
|
|
112
|
-
})
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export const InternalError = (message = 'Internal Server Error') =>
|
|
117
|
-
new ProtocolError(ErrorCode.InternalServerError, message)
|
|
118
|
-
|
|
119
|
-
export const NotFoundError = (message = 'Not Found') =>
|
|
120
|
-
new ProtocolError(ErrorCode.NotFound, message)
|
|
121
|
-
|
|
122
|
-
export const ForbiddenError = (message = 'Forbidden') =>
|
|
123
|
-
new ProtocolError(ErrorCode.Forbidden, message)
|
|
124
|
-
|
|
125
|
-
export const RequestTimeoutError = (message = 'Request Timeout') =>
|
|
126
|
-
new ProtocolError(ErrorCode.RequestTimeout, message)
|