@getsoda/core 0.2.2 → 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 +80 -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.map +1 -1
- package/dist/config/config.js +2 -0
- package/dist/config/config.js.map +1 -1
- 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 +12 -5
- 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 +6 -4
- 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/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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -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 +2 -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 +16 -8
- package/src/http/errors.ts +7 -0
- package/src/http/index.ts +6 -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/sse.ts +120 -0
- package/src/index.ts +1 -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
package/src/routing/errors.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Errors raised by the route compiler.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { hideFrameworkFrames } from '../utils/errors.ts'
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Reason of a `RouteError`.
|
|
8
10
|
*
|
|
@@ -26,5 +28,6 @@ export class RouteError extends Error {
|
|
|
26
28
|
message: string
|
|
27
29
|
) {
|
|
28
30
|
super(message)
|
|
31
|
+
hideFrameworkFrames(this)
|
|
29
32
|
}
|
|
30
33
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Testing an application over HTTP, as Nest's `Test.createTestingModule()`
|
|
4
|
+
* with supertest does, on a real Bun server bound to a free port.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { createTestApp } from '@getsoda/core/testing'
|
|
8
|
+
*
|
|
9
|
+
* const app = await createTestApp(AppModule, {
|
|
10
|
+
* overrides: [provide(UsersRepository, { useValue: new MemoryUsersRepository() })],
|
|
11
|
+
* })
|
|
12
|
+
*
|
|
13
|
+
* afterAll(() => app.close())
|
|
14
|
+
*
|
|
15
|
+
* test('creates a user', async () => {
|
|
16
|
+
* const response = await app.request('/users', { method: 'POST', json: { name: 'Bo' } })
|
|
17
|
+
*
|
|
18
|
+
* expect(response.status).toBe(201)
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* A single service needs no server: `createContainer(Module, { overrides })`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createApp } from '../app/app.ts'
|
|
26
|
+
import type { App, AppOptions } from '../app/app.ts'
|
|
27
|
+
import type { ModuleImport, Token } from '../di/index.ts'
|
|
28
|
+
|
|
29
|
+
export interface TestRequestInit extends RequestInit {
|
|
30
|
+
/** Sent as the JSON body, with `Content-Type: application/json`. */
|
|
31
|
+
readonly json?: unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A started application, closed by `close()` or at the end of an `await using` block. */
|
|
35
|
+
export interface TestApp extends AsyncDisposable {
|
|
36
|
+
readonly app: App
|
|
37
|
+
/** Where the application listens, on a free port of localhost. */
|
|
38
|
+
readonly url: URL
|
|
39
|
+
/** `app.container.get()`: a provider as the root module sees it. */
|
|
40
|
+
get<T>(token: Token<T>): T
|
|
41
|
+
/** Sends a request to `path`, relative to the application. */
|
|
42
|
+
request(path: string, init?: TestRequestInit): Promise<Response>
|
|
43
|
+
close(): Promise<void>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates `root` and serves it on a free port. Options are those of
|
|
48
|
+
* `createApp`: `overrides` replace providers; the logger is silent unless one is given.
|
|
49
|
+
*/
|
|
50
|
+
export const createTestApp = async (root: ModuleImport, options: AppOptions = {}): Promise<TestApp> => {
|
|
51
|
+
const app = await createApp(root, { logger: false, ...options })
|
|
52
|
+
const server = await app.listen({ port: 0, hostname: '127.0.0.1' })
|
|
53
|
+
const close = () => app.close()
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
app,
|
|
57
|
+
url: server.url,
|
|
58
|
+
get: (token) => app.container.get(token),
|
|
59
|
+
request: (path, { json, ...init } = {}) => {
|
|
60
|
+
if (json === undefined) {
|
|
61
|
+
return fetch(new URL(path, server.url), init)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const headers = new Headers(init.headers)
|
|
65
|
+
|
|
66
|
+
headers.set('content-type', 'application/json')
|
|
67
|
+
|
|
68
|
+
return fetch(new URL(path, server.url), { ...init, headers, body: JSON.stringify(json) })
|
|
69
|
+
},
|
|
70
|
+
close,
|
|
71
|
+
[Symbol.asyncDispose]: close,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Error utilities.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
/** Where the framework's own files are: `src/` from sources, `dist/` once built. */
|
|
9
|
+
const FRAMEWORK = fileURLToPath(new URL('../', import.meta.url))
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Removes the frames of the framework from the stack of an error about the
|
|
13
|
+
* application's code (a missing provider, an invalid route): the runtime then
|
|
14
|
+
* points at the application, not at the framework, and the message says the rest.
|
|
15
|
+
*/
|
|
16
|
+
export const hideFrameworkFrames = (error: Error): void => {
|
|
17
|
+
if (error.stack === undefined) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
error.stack = error.stack
|
|
22
|
+
.split('\n')
|
|
23
|
+
.filter((line) => !(line.trimStart().startsWith('at ') && line.includes(FRAMEWORK)))
|
|
24
|
+
.join('\n')
|
|
25
|
+
}
|
package/src/utils/object.ts
CHANGED
|
@@ -12,3 +12,26 @@
|
|
|
12
12
|
* ```
|
|
13
13
|
*/
|
|
14
14
|
export const createNullObject = <T extends object>(): T => Object.create(null) as T
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Public method names along the prototype chain, most derived first, without duplicates.
|
|
18
|
+
*/
|
|
19
|
+
export const methodNames = (instance: object): string[] => {
|
|
20
|
+
const names: string[] = []
|
|
21
|
+
const seen = new Set<string>(['constructor'])
|
|
22
|
+
|
|
23
|
+
for (
|
|
24
|
+
let prototype: object | null = Object.getPrototypeOf(instance);
|
|
25
|
+
prototype !== null && prototype !== Object.prototype;
|
|
26
|
+
prototype = Object.getPrototypeOf(prototype)
|
|
27
|
+
) {
|
|
28
|
+
for (const name of Object.getOwnPropertyNames(prototype)) {
|
|
29
|
+
if (!seen.has(name)) {
|
|
30
|
+
seen.add(name)
|
|
31
|
+
names.push(name)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return names
|
|
37
|
+
}
|
|
@@ -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
|
+
}
|