@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,61 @@
|
|
|
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
|
+
import type { ControllerEntry } from '../di/index.ts';
|
|
13
|
+
import type { Context } from '../http/context.ts';
|
|
14
|
+
import type { FilterStep, GuardStep } from '../http/pipeline.ts';
|
|
15
|
+
/** What a gateway socket carries. */
|
|
16
|
+
export interface GatewaySocketData {
|
|
17
|
+
/** Context of the handshake request: what its guards set is read with `context.get(key)`. */
|
|
18
|
+
readonly context: Context;
|
|
19
|
+
/** @internal */
|
|
20
|
+
readonly gateway: GatewayRuntime;
|
|
21
|
+
}
|
|
22
|
+
/** A connection of a gateway: Bun's `ServerWebSocket`, with `subscribe()`, `publish()`... */
|
|
23
|
+
export type GatewaySocket = Bun.ServerWebSocket<GatewaySocketData>;
|
|
24
|
+
/** `handleConnection(socket)`: called when a client is connected. */
|
|
25
|
+
export interface OnGatewayConnection {
|
|
26
|
+
handleConnection(socket: GatewaySocket): unknown;
|
|
27
|
+
}
|
|
28
|
+
/** `handleDisconnect(socket, code, reason)`: called when a connection is closed. */
|
|
29
|
+
export interface OnGatewayDisconnect {
|
|
30
|
+
handleDisconnect(socket: GatewaySocket, code: number, reason: string): unknown;
|
|
31
|
+
}
|
|
32
|
+
/** The live part of a gateway: its sockets and handlers. */
|
|
33
|
+
export declare class GatewayRuntime {
|
|
34
|
+
#private;
|
|
35
|
+
constructor(instance: object, onError: (error: unknown, req: Request) => void);
|
|
36
|
+
get events(): readonly string[];
|
|
37
|
+
open(socket: GatewaySocket): void;
|
|
38
|
+
message(socket: GatewaySocket, message: string | Buffer): void;
|
|
39
|
+
close(socket: GatewaySocket, code: number, reason: string): void;
|
|
40
|
+
/** Closes every connection, as the application shuts down. */
|
|
41
|
+
closeAll(): void;
|
|
42
|
+
}
|
|
43
|
+
/** Bun's handlers for every gateway of a server: each socket knows its gateway. */
|
|
44
|
+
export declare const websocketHandler: Bun.WebSocketHandler<GatewaySocketData>;
|
|
45
|
+
export interface GatewayParts {
|
|
46
|
+
readonly entry: ControllerEntry;
|
|
47
|
+
readonly path: string;
|
|
48
|
+
readonly guards: readonly GuardStep[];
|
|
49
|
+
readonly filters: readonly FilterStep[];
|
|
50
|
+
readonly metadata: ReadonlyMap<symbol, unknown>;
|
|
51
|
+
readonly onError: (error: unknown, req: Request) => void;
|
|
52
|
+
}
|
|
53
|
+
/** A gateway ready to serve: its native route and its runtime. */
|
|
54
|
+
export interface BuiltGateway {
|
|
55
|
+
readonly path: string;
|
|
56
|
+
readonly runtime: GatewayRuntime;
|
|
57
|
+
/** The native route of the handshake, for `Bun.serve({ routes })`. */
|
|
58
|
+
readonly handle: (req: Bun.BunRequest, server: Bun.Server<GatewaySocketData>) => Response | undefined | Promise<Response | undefined>;
|
|
59
|
+
}
|
|
60
|
+
export declare const buildGateway: (parts: GatewayParts) => BuiltGateway;
|
|
61
|
+
//# sourceMappingURL=gateway.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../src/websocket/gateway.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAErD,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,oBAAoB,CAAA;AAE5D,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAKhE,qCAAqC;AACrC,MAAM,WAAW,iBAAiB;IAChC,6FAA6F;IAC7F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,gBAAgB;IAChB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;CACjC;AAED,6FAA6F;AAC7F,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,eAAe,CAAC,iBAAiB,CAAC,CAAA;AAElE,qEAAqE;AACrE,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAA;CACjD;AAED,oFAAoF;AACpF,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAA;CAC/E;AAoBD,4DAA4D;AAC5D,qBAAa,cAAc;;IAMzB,YAAY,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,EAY5E;IAED,IAAI,MAAM,IAAI,SAAS,MAAM,EAAE,CAE9B;IAED,IAAI,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAGhC;IAED,OAAO,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiB7D;IAED,KAAK,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAG/D;IAED,8DAA8D;IAC9D,QAAQ,IAAI,IAAI,CAIf;CAqBF;AAkBD,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,EAAE,GAAG,CAAC,gBAAgB,CAAC,iBAAiB,CAIpE,CAAA;AAKD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAA;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAA;IACrC,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAA;IACvC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/C,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;CACzD;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;IAChC,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,KAAK,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAA;CACtI;AAED,eAAO,MAAM,YAAY,UAAW,YAAY,KAAG,YA2BlD,CAAA"}
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
import { parseJson } from '../http/body.js';
|
|
13
|
+
import { createPipeline } from '../http/pipeline.js';
|
|
14
|
+
import { methodNames } from '../utils/object.js';
|
|
15
|
+
import { isThenable } from '../utils/promise.js';
|
|
16
|
+
import { getSubscribedEvent } from './decorators.js';
|
|
17
|
+
/** Code of the close frame sent when the application shuts down. */
|
|
18
|
+
const GOING_AWAY = 1001;
|
|
19
|
+
/** Runs `fn`, reporting a synchronous or asynchronous failure. */
|
|
20
|
+
const settle = (fn, report, then) => {
|
|
21
|
+
try {
|
|
22
|
+
const result = fn();
|
|
23
|
+
if (isThenable(result)) {
|
|
24
|
+
Promise.resolve(result).then(then, report);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
then?.(result);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
report(error);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** The live part of a gateway: its sockets and handlers. */
|
|
35
|
+
export class GatewayRuntime {
|
|
36
|
+
#instance;
|
|
37
|
+
#handlers = new Map();
|
|
38
|
+
#sockets = new Set();
|
|
39
|
+
#onError;
|
|
40
|
+
constructor(instance, onError) {
|
|
41
|
+
this.#instance = instance;
|
|
42
|
+
this.#onError = onError;
|
|
43
|
+
for (const name of methodNames(instance)) {
|
|
44
|
+
const method = this.#instance[name];
|
|
45
|
+
const event = getSubscribedEvent(method);
|
|
46
|
+
if (event !== undefined) {
|
|
47
|
+
this.#handlers.set(event, method);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
get events() {
|
|
52
|
+
return [...this.#handlers.keys()];
|
|
53
|
+
}
|
|
54
|
+
open(socket) {
|
|
55
|
+
this.#sockets.add(socket);
|
|
56
|
+
this.#call('handleConnection', socket, [socket]);
|
|
57
|
+
}
|
|
58
|
+
message(socket, message) {
|
|
59
|
+
const frame = typeof message === 'string' ? parseFrame(message) : undefined;
|
|
60
|
+
const handler = frame === undefined ? undefined : this.#handlers.get(frame.event);
|
|
61
|
+
if (handler === undefined) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
settle(() => handler.call(this.#instance, frame.data, socket), (error) => this.#onError(error, socket.data.context.req), (result) => {
|
|
65
|
+
if (result !== undefined) {
|
|
66
|
+
this.#send(socket, result);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
close(socket, code, reason) {
|
|
71
|
+
this.#sockets.delete(socket);
|
|
72
|
+
this.#call('handleDisconnect', socket, [socket, code, reason]);
|
|
73
|
+
}
|
|
74
|
+
/** Closes every connection, as the application shuts down. */
|
|
75
|
+
closeAll() {
|
|
76
|
+
for (const socket of this.#sockets) {
|
|
77
|
+
socket.close(GOING_AWAY, 'Server shutting down');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
#call(hook, socket, args) {
|
|
81
|
+
const fn = this.#instance[hook];
|
|
82
|
+
if (typeof fn === 'function') {
|
|
83
|
+
settle(() => fn.apply(this.#instance, args), (error) => this.#onError(error, socket.data.context.req));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
#send(socket, value) {
|
|
87
|
+
try {
|
|
88
|
+
// JSON.stringify throws on cycles and bigints: an application error, reported.
|
|
89
|
+
socket.send(JSON.stringify(value));
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
this.#onError(error, socket.data.context.req);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const parseFrame = (message) => {
|
|
97
|
+
let frame;
|
|
98
|
+
try {
|
|
99
|
+
frame = parseJson(message);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
if (typeof frame !== 'object' || frame === null || typeof frame.event !== 'string') {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return frame;
|
|
108
|
+
};
|
|
109
|
+
/** Bun's handlers for every gateway of a server: each socket knows its gateway. */
|
|
110
|
+
export const websocketHandler = {
|
|
111
|
+
open: (socket) => socket.data.gateway.open(socket),
|
|
112
|
+
message: (socket, message) => socket.data.gateway.message(socket, message),
|
|
113
|
+
close: (socket, code, reason) => socket.data.gateway.close(socket, code, reason),
|
|
114
|
+
};
|
|
115
|
+
/** Result of the handshake once the connection is upgraded: Bun wants no response then. */
|
|
116
|
+
const UPGRADED = Symbol('upgraded');
|
|
117
|
+
export const buildGateway = (parts) => {
|
|
118
|
+
const runtime = new GatewayRuntime(parts.entry.instance, parts.onError);
|
|
119
|
+
const route = { controller: parts.entry.controller, handler: 'handshake', method: 'GET', path: parts.path, metadata: parts.metadata };
|
|
120
|
+
// The pipeline is built once; the server of each request is found here.
|
|
121
|
+
const servers = new WeakMap();
|
|
122
|
+
const pipeline = createPipeline({
|
|
123
|
+
route,
|
|
124
|
+
guards: parts.guards,
|
|
125
|
+
interceptors: [],
|
|
126
|
+
filters: parts.filters,
|
|
127
|
+
invoke: (ctx) => servers.get(ctx.req).upgrade(ctx.req, { data: { context: ctx, gateway: runtime } })
|
|
128
|
+
? UPGRADED
|
|
129
|
+
: new Response('Expected a WebSocket upgrade', { status: 426, headers: { upgrade: 'websocket' } }),
|
|
130
|
+
respond: (value) => (value === UPGRADED ? undefined : value),
|
|
131
|
+
onError: parts.onError,
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
path: parts.path,
|
|
135
|
+
runtime,
|
|
136
|
+
handle: (req, server) => {
|
|
137
|
+
servers.set(req, server);
|
|
138
|
+
return pipeline(req, {});
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
//# sourceMappingURL=gateway.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway.js","sourceRoot":"","sources":["../../src/websocket/gateway.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* WebSocket gateways, as in `@nestjs/websockets`.
|
|
4
|
+
*/
|
|
5
|
+
export { SubscribeMessage, WebSocketGateway } from './decorators.ts';
|
|
6
|
+
export type { GatewaySocket, GatewaySocketData, OnGatewayConnection, OnGatewayDisconnect } from './gateway.ts';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/websocket/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/websocket/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getsoda/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Class-based web framework for Bun: modules, dependency injection and decorators like Nest, on Bun's native router",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -32,6 +32,14 @@
|
|
|
32
32
|
"types": "./dist/jwt/index.d.ts",
|
|
33
33
|
"default": "./dist/jwt/index.js"
|
|
34
34
|
},
|
|
35
|
+
"./openapi": {
|
|
36
|
+
"types": "./dist/openapi/index.d.ts",
|
|
37
|
+
"default": "./dist/openapi/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./testing": {
|
|
40
|
+
"types": "./dist/testing/index.d.ts",
|
|
41
|
+
"default": "./dist/testing/index.js"
|
|
42
|
+
},
|
|
35
43
|
"./package.json": "./package.json"
|
|
36
44
|
},
|
|
37
45
|
"types": "./dist/index.d.ts",
|
|
@@ -47,5 +55,8 @@
|
|
|
47
55
|
"scripts": {
|
|
48
56
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
49
57
|
"prepublishOnly": "cd ../.. && bun run lint && bun run typecheck && bun run test && cd packages/core && bun run build"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"zod": "4.6.7"
|
|
50
61
|
}
|
|
51
62
|
}
|
package/src/app/app.ts
CHANGED
|
@@ -22,13 +22,15 @@
|
|
|
22
22
|
import { createContainer, provide } from '../di/index.ts'
|
|
23
23
|
import type { Container, ContainerOptions, ModuleImport } from '../di/index.ts'
|
|
24
24
|
import { destroyAll, initializeAll } from '../di/lifecycle.ts'
|
|
25
|
-
import { buildRoutes } from '../http/index.ts'
|
|
25
|
+
import { buildRoutes, ControllerError, ROUTES } from '../http/index.ts'
|
|
26
26
|
import type { ControllerRoutesOptions, MappedController, Middleware } from '../http/index.ts'
|
|
27
27
|
import { errorResponse } from '../http/exceptions.ts'
|
|
28
28
|
import { applyMiddleware, resolveMiddleware } from '../http/middleware.ts'
|
|
29
29
|
import { ConsoleLogger, Logger, SilentLogger } from '../logger/index.ts'
|
|
30
30
|
import { compileRoutes } from '../routing/index.ts'
|
|
31
|
-
import type { CompiledRoutes, CompileOptions } from '../routing/index.ts'
|
|
31
|
+
import type { CompiledRoutes, CompileOptions, NativeHandler } from '../routing/index.ts'
|
|
32
|
+
import { websocketHandler } from '../websocket/gateway.ts'
|
|
33
|
+
import type { BuiltGateway, GatewaySocketData } from '../websocket/gateway.ts'
|
|
32
34
|
import { release, takeOver } from './hot.ts'
|
|
33
35
|
|
|
34
36
|
/**
|
|
@@ -68,16 +70,16 @@ const DEFAULT_SIGNALS: readonly NodeJS.Signals[] = ['SIGTERM', 'SIGINT']
|
|
|
68
70
|
*/
|
|
69
71
|
export interface App {
|
|
70
72
|
readonly container: Container
|
|
71
|
-
/** Routes for `Bun.serve()`, when serving them yourself instead of calling `listen()`. */
|
|
72
|
-
readonly routes:
|
|
73
|
+
/** Routes (and WebSocket handlers) for `Bun.serve()`, when serving them yourself instead of calling `listen()`. */
|
|
74
|
+
readonly routes: ServeRoutes
|
|
73
75
|
/** The server once `listen()` resolved. */
|
|
74
|
-
readonly server: Bun.Server<
|
|
76
|
+
readonly server: Bun.Server<GatewaySocketData> | undefined
|
|
75
77
|
/**
|
|
76
78
|
* Starts serving. Under `bun --hot`, a new version of the application
|
|
77
79
|
* listening on the same port takes the server over from the previous one,
|
|
78
80
|
* which is then destroyed (its server is not stopped: it is shared).
|
|
79
81
|
*/
|
|
80
|
-
listen(options?: number | ListenOptions): Promise<Bun.Server<
|
|
82
|
+
listen(options?: number | ListenOptions): Promise<Bun.Server<GatewaySocketData>>
|
|
81
83
|
/**
|
|
82
84
|
* Graceful shutdown: refuses new requests, waits for those in flight, then
|
|
83
85
|
* runs `onDestroy` hooks. Safe to call more than once, even from a hook.
|
|
@@ -92,27 +94,35 @@ export interface App {
|
|
|
92
94
|
|
|
93
95
|
class Application implements App {
|
|
94
96
|
readonly container: Container
|
|
95
|
-
readonly routes:
|
|
96
|
-
#server: Bun.Server<
|
|
97
|
+
readonly routes: ServeRoutes
|
|
98
|
+
#server: Bun.Server<GatewaySocketData> | undefined
|
|
97
99
|
/** Set when a new version took the server over: it must not be stopped. */
|
|
98
100
|
#handedOver = false
|
|
99
101
|
#closing: Promise<void> | undefined
|
|
100
102
|
readonly #enhancers: readonly unknown[]
|
|
101
103
|
readonly #signals = new Map<NodeJS.Signals, () => void>()
|
|
102
104
|
readonly #logger: Logger
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
readonly #gateways: readonly BuiltGateway[]
|
|
106
|
+
|
|
107
|
+
constructor(
|
|
108
|
+
container: Container,
|
|
109
|
+
routes: ServeRoutes,
|
|
110
|
+
gateways: readonly BuiltGateway[],
|
|
111
|
+
enhancers: readonly unknown[],
|
|
112
|
+
logger: Logger
|
|
113
|
+
) {
|
|
105
114
|
this.container = container
|
|
106
115
|
this.routes = routes
|
|
116
|
+
this.#gateways = gateways
|
|
107
117
|
this.#enhancers = enhancers
|
|
108
118
|
this.#logger = logger
|
|
109
119
|
}
|
|
110
120
|
|
|
111
|
-
get server(): Bun.Server<
|
|
121
|
+
get server(): Bun.Server<GatewaySocketData> | undefined {
|
|
112
122
|
return this.#server
|
|
113
123
|
}
|
|
114
124
|
|
|
115
|
-
async listen(options: number | ListenOptions = {}): Promise<Bun.Server<
|
|
125
|
+
async listen(options: number | ListenOptions = {}): Promise<Bun.Server<GatewaySocketData>> {
|
|
116
126
|
if (this.#closing !== undefined) {
|
|
117
127
|
throw new Error('Cannot listen: the application is closed')
|
|
118
128
|
}
|
|
@@ -191,6 +201,11 @@ class Application implements App {
|
|
|
191
201
|
const server = this.#server
|
|
192
202
|
|
|
193
203
|
try {
|
|
204
|
+
// Open sockets would keep stop() waiting; under --hot, clients reconnect to the new version.
|
|
205
|
+
for (const gateway of this.#gateways) {
|
|
206
|
+
gateway.runtime.closeAll()
|
|
207
|
+
}
|
|
208
|
+
|
|
194
209
|
if (server !== undefined && !this.#handedOver) {
|
|
195
210
|
await server.stop()
|
|
196
211
|
}
|
|
@@ -245,8 +260,37 @@ const destroyEverything = async (enhancers: readonly unknown[], container: Conta
|
|
|
245
260
|
}
|
|
246
261
|
}
|
|
247
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Routes and WebSocket handlers for `Bun.serve()`: the handshakes of gateways
|
|
265
|
+
* are native routes of their own, outside middleware, as an upgrade returns no response.
|
|
266
|
+
*/
|
|
267
|
+
export interface ServeRoutes {
|
|
268
|
+
readonly routes: Record<string, NativeHandler | BuiltGateway['handle']>
|
|
269
|
+
readonly fetch: CompiledRoutes['fetch']
|
|
270
|
+
readonly websocket: Bun.WebSocketHandler<GatewaySocketData>
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const withGateways = (compiled: CompiledRoutes, gateways: readonly BuiltGateway[]): ServeRoutes => {
|
|
274
|
+
const routes: ServeRoutes['routes'] = { ...compiled.routes }
|
|
275
|
+
|
|
276
|
+
for (const gateway of gateways) {
|
|
277
|
+
if (Object.hasOwn(routes, gateway.path)) {
|
|
278
|
+
throw new ControllerError('DUPLICATE_ROUTE', `The WebSocket gateway on ${gateway.path} has the path of another gateway or route`)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
routes[gateway.path] = gateway.handle
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { routes, fetch: compiled.fetch, websocket: websocketHandler }
|
|
285
|
+
}
|
|
286
|
+
|
|
248
287
|
/** What Nest logs at startup: the modules, then each controller and its routes. */
|
|
249
|
-
const logStartup = (
|
|
288
|
+
const logStartup = (
|
|
289
|
+
logger: Logger,
|
|
290
|
+
container: Container,
|
|
291
|
+
controllers: readonly MappedController[],
|
|
292
|
+
gateways: readonly BuiltGateway[]
|
|
293
|
+
): void => {
|
|
250
294
|
const modules = logger.child('Container')
|
|
251
295
|
const routes = logger.child('Routes')
|
|
252
296
|
|
|
@@ -257,10 +301,14 @@ const logStartup = (logger: Logger, container: Container, controllers: readonly
|
|
|
257
301
|
for (const { controller, path, routes: mapped } of controllers) {
|
|
258
302
|
routes.log(`${controller.name} {${path}}:`)
|
|
259
303
|
|
|
260
|
-
for (const
|
|
261
|
-
routes.log(`Mapped {${
|
|
304
|
+
for (const { info } of mapped) {
|
|
305
|
+
routes.log(`Mapped {${info.path}, ${info.method}} route`)
|
|
262
306
|
}
|
|
263
307
|
}
|
|
308
|
+
|
|
309
|
+
for (const gateway of gateways) {
|
|
310
|
+
routes.log(`WebSocket gateway {${gateway.path}}, events: ${gateway.runtime.events.join(', ') || 'none'}`)
|
|
311
|
+
}
|
|
264
312
|
}
|
|
265
313
|
|
|
266
314
|
/**
|
|
@@ -280,9 +328,11 @@ export const createApp = async (root: ModuleImport, options: AppOptions = {}): P
|
|
|
280
328
|
|
|
281
329
|
own.log('Starting application')
|
|
282
330
|
|
|
331
|
+
// Filled once the routes are built; read by what describes them (OpenAPI).
|
|
332
|
+
const registry: { controllers: readonly MappedController[] } = { controllers: [] }
|
|
283
333
|
const container = await createContainer(root, {
|
|
284
334
|
...options,
|
|
285
|
-
providers: [...(options.providers ?? []), provide(Logger, { useValue: logger })],
|
|
335
|
+
providers: [...(options.providers ?? []), provide(Logger, { useValue: logger }), provide(ROUTES, { useValue: registry })],
|
|
286
336
|
})
|
|
287
337
|
const enhancers: unknown[] = []
|
|
288
338
|
|
|
@@ -293,7 +343,8 @@ export const createApp = async (root: ModuleImport, options: AppOptions = {}): P
|
|
|
293
343
|
})
|
|
294
344
|
const built = buildRoutes(container, { ...options, onError })
|
|
295
345
|
|
|
296
|
-
|
|
346
|
+
registry.controllers = built.controllers
|
|
347
|
+
logStartup(logger, container, built.controllers, built.gateways)
|
|
297
348
|
enhancers.push(...built.enhancers)
|
|
298
349
|
await initializeAll(enhancers)
|
|
299
350
|
own.log('Application started')
|
|
@@ -303,7 +354,7 @@ export const createApp = async (root: ModuleImport, options: AppOptions = {}): P
|
|
|
303
354
|
errorResponse(error, (unknown) => onError(unknown, req))
|
|
304
355
|
)
|
|
305
356
|
|
|
306
|
-
return new Application(container, routes, enhancers, own)
|
|
357
|
+
return new Application(container, withGateways(routes, built.gateways), built.gateways, enhancers, own)
|
|
307
358
|
} catch (error) {
|
|
308
359
|
try {
|
|
309
360
|
await destroyEverything(enhancers, container)
|
package/src/app/index.ts
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Configuration typed by a schema and validated at startup.
|
|
4
|
+
*
|
|
5
|
+
* What `@nestjs/config` does with `registerAs()` and a `validationSchema`,
|
|
6
|
+
* typed by the schema instead of read with string keys. Bun already loads
|
|
7
|
+
* `.env`, `.env.local` and `.env.<NODE_ENV>` into `process.env`.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* export const AppConfig = defineConfig('app', z.object({
|
|
11
|
+
* PORT: z.coerce.number().default(3000),
|
|
12
|
+
* JWT_SECRET: z.string().min(32),
|
|
13
|
+
* }))
|
|
14
|
+
*
|
|
15
|
+
* @Module({ imports: [ConfigModule.forRoot({ load: [AppConfig], global: true })] })
|
|
16
|
+
* class AppModule {}
|
|
17
|
+
*
|
|
18
|
+
* class AuthService {
|
|
19
|
+
* readonly #config = inject(AppConfig) // { PORT: number; JWT_SECRET: string }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { InjectionToken } from '../di/token.ts'
|
|
25
|
+
import { formatIssues } from '../http/schema.ts'
|
|
26
|
+
import type { InferOutput, StandardSchemaV1 } from '../http/schema.ts'
|
|
27
|
+
import { hideFrameworkFrames } from '../utils/errors.ts'
|
|
28
|
+
|
|
29
|
+
/** Environment variables, as in `process.env`. */
|
|
30
|
+
export type Environment = Readonly<Record<string, string | undefined>>
|
|
31
|
+
|
|
32
|
+
/** A configuration that does not match its schema; `issues` lists every problem. */
|
|
33
|
+
export class ConfigError extends Error {
|
|
34
|
+
override readonly name = 'ConfigError'
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
readonly config: string,
|
|
38
|
+
readonly issues: readonly string[]
|
|
39
|
+
) {
|
|
40
|
+
super(`Invalid configuration "${config}":\n ${issues.join('\n ')}`)
|
|
41
|
+
hideFrameworkFrames(this)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A configuration, and the DI token of its value: `inject(AppConfig)`.
|
|
47
|
+
* `ConfigModule.forRoot()` loads it at startup; `load()` works without DI.
|
|
48
|
+
*/
|
|
49
|
+
export class Config<T> extends InjectionToken<T> {
|
|
50
|
+
readonly #schema: StandardSchemaV1<unknown, T>
|
|
51
|
+
readonly #source: (env: Environment) => unknown
|
|
52
|
+
|
|
53
|
+
constructor(name: string, schema: StandardSchemaV1<unknown, T>, source: (env: Environment) => unknown) {
|
|
54
|
+
super(name)
|
|
55
|
+
this.#schema = schema
|
|
56
|
+
this.#source = source
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The value read from `env` and validated; throws a {@link ConfigError} listing every issue. */
|
|
60
|
+
async load(env: Environment = process.env): Promise<T> {
|
|
61
|
+
const result = await this.#schema['~standard'].validate(this.#source(env))
|
|
62
|
+
|
|
63
|
+
if (result.issues !== undefined) {
|
|
64
|
+
throw new ConfigError(this.description, formatIssues(result.issues, ''))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result.value
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
override toString(): string {
|
|
71
|
+
return `Config(${this.description})`
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Defines a configuration validated by `schema`. Its input is the environment,
|
|
77
|
+
* or what `source` builds from it, for a nested configuration:
|
|
78
|
+
*
|
|
79
|
+
* ```ts
|
|
80
|
+
* const DatabaseConfig = defineConfig('database', DatabaseSchema, (env) => ({
|
|
81
|
+
* url: env.DATABASE_URL,
|
|
82
|
+
* pool: { max: env.DATABASE_POOL_MAX },
|
|
83
|
+
* }))
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export const defineConfig = <S extends StandardSchemaV1>(
|
|
87
|
+
name: string,
|
|
88
|
+
schema: S,
|
|
89
|
+
source: (env: Environment) => unknown = (env) => env
|
|
90
|
+
): Config<InferOutput<S>> => new Config(name, schema as StandardSchemaV1<unknown, InferOutput<S>>, source)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Configuration typed by a schema and validated at startup.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { Config, ConfigError, defineConfig } from './config.ts'
|
|
7
|
+
export type { Environment } from './config.ts'
|
|
8
|
+
export { ConfigModule } from './module.ts'
|
|
9
|
+
export type { ConfigModuleOptions } from './module.ts'
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* `ConfigModule`: loads configurations at startup, like `ConfigModule.forRoot()` of `@nestjs/config`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Module, provide } from '../di/index.ts'
|
|
7
|
+
import type { DynamicModule } from '../di/index.ts'
|
|
8
|
+
import type { Config, Environment } from './config.ts'
|
|
9
|
+
|
|
10
|
+
export interface ConfigModuleOptions {
|
|
11
|
+
/** Configurations to load and provide. */
|
|
12
|
+
readonly load: readonly Config<unknown>[]
|
|
13
|
+
/** Makes them injectable in every module. Defaults to `false`, as in Nest. */
|
|
14
|
+
readonly global?: boolean
|
|
15
|
+
/** Where variables come from, read when the container starts. Defaults to `process.env`. */
|
|
16
|
+
readonly env?: Environment
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* ```ts
|
|
21
|
+
* @Module({ imports: [ConfigModule.forRoot({ load: [AppConfig, DatabaseConfig], global: true })] })
|
|
22
|
+
* class AppModule {}
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Each configuration is loaded when the container starts; an invalid one
|
|
26
|
+
* fails the startup with a `ConfigError` listing all its issues. Tests
|
|
27
|
+
* replace a value with `overrides: [provide(AppConfig, { useValue })]`, and
|
|
28
|
+
* the replaced configuration is not loaded.
|
|
29
|
+
*/
|
|
30
|
+
@Module({})
|
|
31
|
+
export class ConfigModule {
|
|
32
|
+
static forRoot(options: ConfigModuleOptions): DynamicModule {
|
|
33
|
+
return {
|
|
34
|
+
module: ConfigModule,
|
|
35
|
+
global: options.global ?? false,
|
|
36
|
+
providers: options.load.map((config) => provide(config, { useFactory: () => config.load(options.env) })),
|
|
37
|
+
exports: options.load,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/di/errors.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Errors raised by the dependency injection container.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { hideFrameworkFrames } from '../utils/errors.ts'
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Reason of a `DiError`.
|
|
8
10
|
*
|
|
@@ -42,5 +44,6 @@ export class DiError extends Error {
|
|
|
42
44
|
message: string
|
|
43
45
|
) {
|
|
44
46
|
super(message)
|
|
47
|
+
hideFrameworkFrames(this)
|
|
45
48
|
}
|
|
46
49
|
}
|