@getsoda/core 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -0
- package/dist/app/app.d.ts +15 -5
- package/dist/app/app.d.ts.map +1 -1
- package/dist/app/app.js +31 -8
- package/dist/app/app.js.map +1 -1
- package/dist/app/index.d.ts +1 -1
- package/dist/app/index.d.ts.map +1 -1
- package/dist/config/config.d.ts +57 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +74 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/module.d.ts +29 -0
- package/dist/config/module.d.ts.map +1 -0
- package/dist/config/module.js +28 -0
- package/dist/config/module.js.map +1 -0
- package/dist/di/errors.d.ts.map +1 -1
- package/dist/di/errors.js +2 -0
- package/dist/di/errors.js.map +1 -1
- package/dist/http/arguments.d.ts +19 -6
- package/dist/http/arguments.d.ts.map +1 -1
- package/dist/http/arguments.js +7 -0
- package/dist/http/arguments.js.map +1 -1
- package/dist/http/cookies.d.ts.map +1 -1
- package/dist/http/cookies.js +2 -0
- package/dist/http/cookies.js.map +1 -1
- package/dist/http/decorators.d.ts +18 -6
- package/dist/http/decorators.d.ts.map +1 -1
- package/dist/http/decorators.js +7 -2
- package/dist/http/decorators.js.map +1 -1
- package/dist/http/errors.d.ts +3 -1
- package/dist/http/errors.d.ts.map +1 -1
- package/dist/http/errors.js +2 -0
- package/dist/http/errors.js.map +1 -1
- package/dist/http/index.d.ts +7 -5
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/index.js +3 -2
- package/dist/http/index.js.map +1 -1
- package/dist/http/parsers.d.ts +4 -0
- package/dist/http/parsers.d.ts.map +1 -1
- package/dist/http/parsers.js +9 -5
- package/dist/http/parsers.js.map +1 -1
- package/dist/http/pipeline.d.ts +4 -5
- package/dist/http/pipeline.d.ts.map +1 -1
- package/dist/http/pipeline.js +2 -2
- package/dist/http/pipeline.js.map +1 -1
- package/dist/http/routes.d.ts +27 -2
- package/dist/http/routes.d.ts.map +1 -1
- package/dist/http/routes.js +34 -20
- package/dist/http/routes.js.map +1 -1
- package/dist/http/schema.d.ts +2 -0
- package/dist/http/schema.d.ts.map +1 -1
- package/dist/http/schema.js +2 -1
- package/dist/http/schema.js.map +1 -1
- package/dist/http/sse.d.ts +39 -0
- package/dist/http/sse.d.ts.map +1 -0
- package/dist/http/sse.js +89 -0
- package/dist/http/sse.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/openapi/decorators.d.ts +41 -0
- package/dist/openapi/decorators.d.ts.map +1 -0
- package/dist/openapi/decorators.js +29 -0
- package/dist/openapi/decorators.js.map +1 -0
- package/dist/openapi/document.d.ts +62 -0
- package/dist/openapi/document.d.ts.map +1 -0
- package/dist/openapi/document.js +135 -0
- package/dist/openapi/document.js.map +1 -0
- package/dist/openapi/index.d.ts +11 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +8 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/openapi/module.d.ts +26 -0
- package/dist/openapi/module.d.ts.map +1 -0
- package/dist/openapi/module.js +73 -0
- package/dist/openapi/module.js.map +1 -0
- package/dist/routing/errors.d.ts.map +1 -1
- package/dist/routing/errors.js +2 -0
- package/dist/routing/errors.js.map +1 -1
- package/dist/testing/index.d.ts +46 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +49 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/utils/errors.d.ts +11 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +22 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/object.d.ts +4 -0
- package/dist/utils/object.d.ts.map +1 -1
- package/dist/utils/object.js +16 -0
- package/dist/utils/object.js.map +1 -1
- package/dist/websocket/decorators.d.ts +31 -0
- package/dist/websocket/decorators.d.ts.map +1 -0
- package/dist/websocket/decorators.js +43 -0
- package/dist/websocket/decorators.js.map +1 -0
- package/dist/websocket/gateway.d.ts +61 -0
- package/dist/websocket/gateway.d.ts.map +1 -0
- package/dist/websocket/gateway.js +142 -0
- package/dist/websocket/gateway.js.map +1 -0
- package/dist/websocket/index.d.ts +7 -0
- package/dist/websocket/index.d.ts.map +1 -0
- package/dist/websocket/index.js +6 -0
- package/dist/websocket/index.js.map +1 -0
- package/package.json +12 -1
- package/src/app/app.ts +69 -18
- package/src/app/index.ts +1 -1
- package/src/config/config.ts +90 -0
- package/src/config/index.ts +9 -0
- package/src/config/module.ts +40 -0
- package/src/di/errors.ts +3 -0
- package/src/http/arguments.ts +31 -6
- package/src/http/cookies.ts +2 -0
- package/src/http/decorators.ts +25 -10
- package/src/http/errors.ts +7 -0
- package/src/http/index.ts +7 -4
- package/src/http/parsers.ts +18 -7
- package/src/http/pipeline.ts +6 -6
- package/src/http/routes.ts +68 -29
- package/src/http/schema.ts +2 -1
- package/src/http/sse.ts +120 -0
- package/src/index.ts +2 -0
- package/src/openapi/decorators.ts +50 -0
- package/src/openapi/document.ts +221 -0
- package/src/openapi/index.ts +11 -0
- package/src/openapi/module.ts +95 -0
- package/src/routing/errors.ts +3 -0
- package/src/testing/index.ts +73 -0
- package/src/utils/errors.ts +25 -0
- package/src/utils/object.ts +23 -0
- package/src/websocket/decorators.ts +55 -0
- package/src/websocket/gateway.ts +213 -0
- package/src/websocket/index.ts +7 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Decorators of WebSocket gateways, as in `@nestjs/websockets`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Constructor } from '../di/index.ts'
|
|
7
|
+
import { ControllerError } from '../http/errors.ts'
|
|
8
|
+
|
|
9
|
+
const gateways = new WeakMap<Constructor<unknown>, string>()
|
|
10
|
+
const events = new WeakMap<object, string>()
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Declares a gateway: WebSocket connections upgraded from `GET path`. List it
|
|
14
|
+
* in the `controllers` of its module: like a controller, it is an entry point
|
|
15
|
+
* the framework creates, and its guards run on the handshake.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* @WebSocketGateway('/chat')
|
|
19
|
+
* @UseGuards(JwtGuard)
|
|
20
|
+
* class ChatGateway {
|
|
21
|
+
* @SubscribeMessage('message')
|
|
22
|
+
* message(data: unknown, socket: GatewaySocket) {
|
|
23
|
+
* return { event: 'message', data }
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const WebSocketGateway =
|
|
29
|
+
(path: string) =>
|
|
30
|
+
(target: Constructor<unknown>, _context: ClassDecoratorContext<Constructor<unknown>>): void => {
|
|
31
|
+
if (!path.startsWith('/') || /[:*?]/.test(path)) {
|
|
32
|
+
throw new ControllerError('INVALID_PATH', `@WebSocketGateway('${path}'): a gateway path is static and starts with "/"`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
gateways.set(target, path)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Handles the frames `{ "event": event, "data": ... }` of a gateway. The
|
|
40
|
+
* method receives `(data, socket)`; what it returns, unless `undefined`, is
|
|
41
|
+
* sent back as JSON.
|
|
42
|
+
*/
|
|
43
|
+
export const SubscribeMessage =
|
|
44
|
+
(event: string) =>
|
|
45
|
+
<This, M extends (this: This, ...args: any[]) => unknown>(method: M, context: ClassMethodDecoratorContext<This, M>): void => {
|
|
46
|
+
if (context.static || context.private) {
|
|
47
|
+
throw new ControllerError('INVALID_HANDLER', `@SubscribeMessage('${event}') must decorate a public instance method`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
events.set(method, event)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const getGatewayPath = (target: Constructor<unknown>): string | undefined => gateways.get(target)
|
|
54
|
+
|
|
55
|
+
export const getSubscribedEvent = (method: object): string | undefined => events.get(method)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Serving a gateway: the handshake goes through the route pipeline (guards
|
|
4
|
+
* and exception filters of the gateway), then frames are dispatched to its
|
|
5
|
+
* `@SubscribeMessage()` methods.
|
|
6
|
+
*
|
|
7
|
+
* The protocol is the one of Nest's `WsAdapter`: text frames holding
|
|
8
|
+
* `{ "event": string, "data": any }`. Frames that are not JSON, not such an
|
|
9
|
+
* object, or name no handled event are dropped silently, as anyone can send
|
|
10
|
+
* them; errors of the gateway's own code are reported.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ControllerEntry } from '../di/index.ts'
|
|
14
|
+
import { parseJson } from '../http/body.ts'
|
|
15
|
+
import type { Context, RouteInfo } from '../http/context.ts'
|
|
16
|
+
import { createPipeline } from '../http/pipeline.ts'
|
|
17
|
+
import type { FilterStep, GuardStep } from '../http/pipeline.ts'
|
|
18
|
+
import { methodNames } from '../utils/object.ts'
|
|
19
|
+
import { isThenable } from '../utils/promise.ts'
|
|
20
|
+
import { getSubscribedEvent } from './decorators.ts'
|
|
21
|
+
|
|
22
|
+
/** What a gateway socket carries. */
|
|
23
|
+
export interface GatewaySocketData {
|
|
24
|
+
/** Context of the handshake request: what its guards set is read with `context.get(key)`. */
|
|
25
|
+
readonly context: Context
|
|
26
|
+
/** @internal */
|
|
27
|
+
readonly gateway: GatewayRuntime
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A connection of a gateway: Bun's `ServerWebSocket`, with `subscribe()`, `publish()`... */
|
|
31
|
+
export type GatewaySocket = Bun.ServerWebSocket<GatewaySocketData>
|
|
32
|
+
|
|
33
|
+
/** `handleConnection(socket)`: called when a client is connected. */
|
|
34
|
+
export interface OnGatewayConnection {
|
|
35
|
+
handleConnection(socket: GatewaySocket): unknown
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `handleDisconnect(socket, code, reason)`: called when a connection is closed. */
|
|
39
|
+
export interface OnGatewayDisconnect {
|
|
40
|
+
handleDisconnect(socket: GatewaySocket, code: number, reason: string): unknown
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Code of the close frame sent when the application shuts down. */
|
|
44
|
+
const GOING_AWAY = 1001
|
|
45
|
+
|
|
46
|
+
/** Runs `fn`, reporting a synchronous or asynchronous failure. */
|
|
47
|
+
const settle = (fn: () => unknown, report: (error: unknown) => void, then?: (value: unknown) => void): void => {
|
|
48
|
+
try {
|
|
49
|
+
const result = fn()
|
|
50
|
+
|
|
51
|
+
if (isThenable(result)) {
|
|
52
|
+
Promise.resolve(result).then(then, report)
|
|
53
|
+
} else {
|
|
54
|
+
then?.(result)
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
report(error)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The live part of a gateway: its sockets and handlers. */
|
|
62
|
+
export class GatewayRuntime {
|
|
63
|
+
readonly #instance: Record<string, unknown>
|
|
64
|
+
readonly #handlers = new Map<string, (data: unknown, socket: GatewaySocket) => unknown>()
|
|
65
|
+
readonly #sockets = new Set<GatewaySocket>()
|
|
66
|
+
readonly #onError: (error: unknown, req: Request) => void
|
|
67
|
+
|
|
68
|
+
constructor(instance: object, onError: (error: unknown, req: Request) => void) {
|
|
69
|
+
this.#instance = instance as Record<string, unknown>
|
|
70
|
+
this.#onError = onError
|
|
71
|
+
|
|
72
|
+
for (const name of methodNames(instance)) {
|
|
73
|
+
const method = this.#instance[name] as (data: unknown, socket: GatewaySocket) => unknown
|
|
74
|
+
const event = getSubscribedEvent(method)
|
|
75
|
+
|
|
76
|
+
if (event !== undefined) {
|
|
77
|
+
this.#handlers.set(event, method)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get events(): readonly string[] {
|
|
83
|
+
return [...this.#handlers.keys()]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
open(socket: GatewaySocket): void {
|
|
87
|
+
this.#sockets.add(socket)
|
|
88
|
+
this.#call('handleConnection', socket, [socket])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
message(socket: GatewaySocket, message: string | Buffer): void {
|
|
92
|
+
const frame = typeof message === 'string' ? parseFrame(message) : undefined
|
|
93
|
+
const handler = frame === undefined ? undefined : this.#handlers.get(frame.event)
|
|
94
|
+
|
|
95
|
+
if (handler === undefined) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
settle(
|
|
100
|
+
() => handler.call(this.#instance, frame!.data, socket),
|
|
101
|
+
(error) => this.#onError(error, socket.data.context.req),
|
|
102
|
+
(result) => {
|
|
103
|
+
if (result !== undefined) {
|
|
104
|
+
this.#send(socket, result)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
close(socket: GatewaySocket, code: number, reason: string): void {
|
|
111
|
+
this.#sockets.delete(socket)
|
|
112
|
+
this.#call('handleDisconnect', socket, [socket, code, reason])
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Closes every connection, as the application shuts down. */
|
|
116
|
+
closeAll(): void {
|
|
117
|
+
for (const socket of this.#sockets) {
|
|
118
|
+
socket.close(GOING_AWAY, 'Server shutting down')
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#call(hook: string, socket: GatewaySocket, args: readonly unknown[]): void {
|
|
123
|
+
const fn = this.#instance[hook]
|
|
124
|
+
|
|
125
|
+
if (typeof fn === 'function') {
|
|
126
|
+
settle(
|
|
127
|
+
() => fn.apply(this.#instance, args),
|
|
128
|
+
(error) => this.#onError(error, socket.data.context.req)
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#send(socket: GatewaySocket, value: unknown): void {
|
|
134
|
+
try {
|
|
135
|
+
// JSON.stringify throws on cycles and bigints: an application error, reported.
|
|
136
|
+
socket.send(JSON.stringify(value))
|
|
137
|
+
} catch (error) {
|
|
138
|
+
this.#onError(error, socket.data.context.req)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const parseFrame = (message: string): { event: string; data: unknown } | undefined => {
|
|
144
|
+
let frame: unknown
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
frame = parseJson(message)
|
|
148
|
+
} catch {
|
|
149
|
+
return undefined
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof frame !== 'object' || frame === null || typeof (frame as { event?: unknown }).event !== 'string') {
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return frame as { event: string; data: unknown }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Bun's handlers for every gateway of a server: each socket knows its gateway. */
|
|
160
|
+
export const websocketHandler: Bun.WebSocketHandler<GatewaySocketData> = {
|
|
161
|
+
open: (socket) => socket.data.gateway.open(socket),
|
|
162
|
+
message: (socket, message) => socket.data.gateway.message(socket, message),
|
|
163
|
+
close: (socket, code, reason) => socket.data.gateway.close(socket, code, reason),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Result of the handshake once the connection is upgraded: Bun wants no response then. */
|
|
167
|
+
const UPGRADED = Symbol('upgraded')
|
|
168
|
+
|
|
169
|
+
export interface GatewayParts {
|
|
170
|
+
readonly entry: ControllerEntry
|
|
171
|
+
readonly path: string
|
|
172
|
+
readonly guards: readonly GuardStep[]
|
|
173
|
+
readonly filters: readonly FilterStep[]
|
|
174
|
+
readonly metadata: ReadonlyMap<symbol, unknown>
|
|
175
|
+
readonly onError: (error: unknown, req: Request) => void
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A gateway ready to serve: its native route and its runtime. */
|
|
179
|
+
export interface BuiltGateway {
|
|
180
|
+
readonly path: string
|
|
181
|
+
readonly runtime: GatewayRuntime
|
|
182
|
+
/** The native route of the handshake, for `Bun.serve({ routes })`. */
|
|
183
|
+
readonly handle: (req: Bun.BunRequest, server: Bun.Server<GatewaySocketData>) => Response | undefined | Promise<Response | undefined>
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const buildGateway = (parts: GatewayParts): BuiltGateway => {
|
|
187
|
+
const runtime = new GatewayRuntime(parts.entry.instance as object, parts.onError)
|
|
188
|
+
const route: RouteInfo = { controller: parts.entry.controller, handler: 'handshake', method: 'GET', path: parts.path, metadata: parts.metadata }
|
|
189
|
+
// The pipeline is built once; the server of each request is found here.
|
|
190
|
+
const servers = new WeakMap<Request, Bun.Server<GatewaySocketData>>()
|
|
191
|
+
const pipeline = createPipeline({
|
|
192
|
+
route,
|
|
193
|
+
guards: parts.guards,
|
|
194
|
+
interceptors: [],
|
|
195
|
+
filters: parts.filters,
|
|
196
|
+
invoke: (ctx) =>
|
|
197
|
+
servers.get(ctx.req)!.upgrade(ctx.req, { data: { context: ctx, gateway: runtime } })
|
|
198
|
+
? UPGRADED
|
|
199
|
+
: new Response('Expected a WebSocket upgrade', { status: 426, headers: { upgrade: 'websocket' } }),
|
|
200
|
+
respond: (value) => (value === UPGRADED ? (undefined as unknown as Response) : (value as Response)),
|
|
201
|
+
onError: parts.onError,
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
path: parts.path,
|
|
206
|
+
runtime,
|
|
207
|
+
handle: (req, server) => {
|
|
208
|
+
servers.set(req, server)
|
|
209
|
+
|
|
210
|
+
return pipeline(req, {})
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
}
|