@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.
Files changed (137) hide show
  1. package/README.md +103 -0
  2. package/dist/app/app.d.ts +15 -5
  3. package/dist/app/app.d.ts.map +1 -1
  4. package/dist/app/app.js +31 -8
  5. package/dist/app/app.js.map +1 -1
  6. package/dist/app/index.d.ts +1 -1
  7. package/dist/app/index.d.ts.map +1 -1
  8. package/dist/config/config.d.ts +57 -0
  9. package/dist/config/config.d.ts.map +1 -0
  10. package/dist/config/config.js +74 -0
  11. package/dist/config/config.js.map +1 -0
  12. package/dist/config/index.d.ts +9 -0
  13. package/dist/config/index.d.ts.map +1 -0
  14. package/dist/config/index.js +7 -0
  15. package/dist/config/index.js.map +1 -0
  16. package/dist/config/module.d.ts +29 -0
  17. package/dist/config/module.d.ts.map +1 -0
  18. package/dist/config/module.js +28 -0
  19. package/dist/config/module.js.map +1 -0
  20. package/dist/di/errors.d.ts.map +1 -1
  21. package/dist/di/errors.js +2 -0
  22. package/dist/di/errors.js.map +1 -1
  23. package/dist/http/arguments.d.ts +19 -6
  24. package/dist/http/arguments.d.ts.map +1 -1
  25. package/dist/http/arguments.js +7 -0
  26. package/dist/http/arguments.js.map +1 -1
  27. package/dist/http/cookies.d.ts.map +1 -1
  28. package/dist/http/cookies.js +2 -0
  29. package/dist/http/cookies.js.map +1 -1
  30. package/dist/http/decorators.d.ts +18 -6
  31. package/dist/http/decorators.d.ts.map +1 -1
  32. package/dist/http/decorators.js +7 -2
  33. package/dist/http/decorators.js.map +1 -1
  34. package/dist/http/errors.d.ts +3 -1
  35. package/dist/http/errors.d.ts.map +1 -1
  36. package/dist/http/errors.js +2 -0
  37. package/dist/http/errors.js.map +1 -1
  38. package/dist/http/index.d.ts +7 -5
  39. package/dist/http/index.d.ts.map +1 -1
  40. package/dist/http/index.js +3 -2
  41. package/dist/http/index.js.map +1 -1
  42. package/dist/http/parsers.d.ts +4 -0
  43. package/dist/http/parsers.d.ts.map +1 -1
  44. package/dist/http/parsers.js +9 -5
  45. package/dist/http/parsers.js.map +1 -1
  46. package/dist/http/pipeline.d.ts +4 -5
  47. package/dist/http/pipeline.d.ts.map +1 -1
  48. package/dist/http/pipeline.js +2 -2
  49. package/dist/http/pipeline.js.map +1 -1
  50. package/dist/http/routes.d.ts +27 -2
  51. package/dist/http/routes.d.ts.map +1 -1
  52. package/dist/http/routes.js +34 -20
  53. package/dist/http/routes.js.map +1 -1
  54. package/dist/http/schema.d.ts +2 -0
  55. package/dist/http/schema.d.ts.map +1 -1
  56. package/dist/http/schema.js +2 -1
  57. package/dist/http/schema.js.map +1 -1
  58. package/dist/http/sse.d.ts +39 -0
  59. package/dist/http/sse.d.ts.map +1 -0
  60. package/dist/http/sse.js +89 -0
  61. package/dist/http/sse.js.map +1 -0
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +2 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/openapi/decorators.d.ts +41 -0
  67. package/dist/openapi/decorators.d.ts.map +1 -0
  68. package/dist/openapi/decorators.js +29 -0
  69. package/dist/openapi/decorators.js.map +1 -0
  70. package/dist/openapi/document.d.ts +62 -0
  71. package/dist/openapi/document.d.ts.map +1 -0
  72. package/dist/openapi/document.js +135 -0
  73. package/dist/openapi/document.js.map +1 -0
  74. package/dist/openapi/index.d.ts +11 -0
  75. package/dist/openapi/index.d.ts.map +1 -0
  76. package/dist/openapi/index.js +8 -0
  77. package/dist/openapi/index.js.map +1 -0
  78. package/dist/openapi/module.d.ts +26 -0
  79. package/dist/openapi/module.d.ts.map +1 -0
  80. package/dist/openapi/module.js +73 -0
  81. package/dist/openapi/module.js.map +1 -0
  82. package/dist/routing/errors.d.ts.map +1 -1
  83. package/dist/routing/errors.js +2 -0
  84. package/dist/routing/errors.js.map +1 -1
  85. package/dist/testing/index.d.ts +46 -0
  86. package/dist/testing/index.d.ts.map +1 -0
  87. package/dist/testing/index.js +49 -0
  88. package/dist/testing/index.js.map +1 -0
  89. package/dist/utils/errors.d.ts +11 -0
  90. package/dist/utils/errors.d.ts.map +1 -0
  91. package/dist/utils/errors.js +22 -0
  92. package/dist/utils/errors.js.map +1 -0
  93. package/dist/utils/object.d.ts +4 -0
  94. package/dist/utils/object.d.ts.map +1 -1
  95. package/dist/utils/object.js +16 -0
  96. package/dist/utils/object.js.map +1 -1
  97. package/dist/websocket/decorators.d.ts +31 -0
  98. package/dist/websocket/decorators.d.ts.map +1 -0
  99. package/dist/websocket/decorators.js +43 -0
  100. package/dist/websocket/decorators.js.map +1 -0
  101. package/dist/websocket/gateway.d.ts +61 -0
  102. package/dist/websocket/gateway.d.ts.map +1 -0
  103. package/dist/websocket/gateway.js +142 -0
  104. package/dist/websocket/gateway.js.map +1 -0
  105. package/dist/websocket/index.d.ts +7 -0
  106. package/dist/websocket/index.d.ts.map +1 -0
  107. package/dist/websocket/index.js +6 -0
  108. package/dist/websocket/index.js.map +1 -0
  109. package/package.json +12 -1
  110. package/src/app/app.ts +69 -18
  111. package/src/app/index.ts +1 -1
  112. package/src/config/config.ts +90 -0
  113. package/src/config/index.ts +9 -0
  114. package/src/config/module.ts +40 -0
  115. package/src/di/errors.ts +3 -0
  116. package/src/http/arguments.ts +31 -6
  117. package/src/http/cookies.ts +2 -0
  118. package/src/http/decorators.ts +25 -10
  119. package/src/http/errors.ts +7 -0
  120. package/src/http/index.ts +7 -4
  121. package/src/http/parsers.ts +18 -7
  122. package/src/http/pipeline.ts +6 -6
  123. package/src/http/routes.ts +68 -29
  124. package/src/http/schema.ts +2 -1
  125. package/src/http/sse.ts +120 -0
  126. package/src/index.ts +2 -0
  127. package/src/openapi/decorators.ts +50 -0
  128. package/src/openapi/document.ts +221 -0
  129. package/src/openapi/index.ts +11 -0
  130. package/src/openapi/module.ts +95 -0
  131. package/src/routing/errors.ts +3 -0
  132. package/src/testing/index.ts +73 -0
  133. package/src/utils/errors.ts +25 -0
  134. package/src/utils/object.ts +23 -0
  135. package/src/websocket/decorators.ts +55 -0
  136. package/src/websocket/gateway.ts +213 -0
  137. 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
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @module
3
+ * WebSocket gateways, as in `@nestjs/websockets`.
4
+ */
5
+
6
+ export { SubscribeMessage, WebSocketGateway } from './decorators.ts'
7
+ export type { GatewaySocket, GatewaySocketData, OnGatewayConnection, OnGatewayDisconnect } from './gateway.ts'