@velajs/vela 1.8.8 → 1.10.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/CHANGELOG.md +20 -0
- package/dist/__tests__/workers-als/entry-als.d.ts +9 -0
- package/dist/application.d.ts +10 -0
- package/dist/application.js +25 -0
- package/dist/cache/cache.module.d.ts +5 -7
- package/dist/cache/cache.module.js +44 -66
- package/dist/cache/cache.types.d.ts +26 -0
- package/dist/cache/cache.types.js +1 -1
- package/dist/cache/index.d.ts +2 -1
- package/dist/cache/index.js +1 -0
- package/dist/cache/tiered-cache.store.d.ts +19 -0
- package/dist/cache/tiered-cache.store.js +40 -0
- package/dist/config/config.module.d.ts +6 -5
- package/dist/config/config.module.js +46 -49
- package/dist/container/container.d.ts +33 -0
- package/dist/container/container.js +156 -7
- package/dist/container/disposable.d.ts +5 -0
- package/dist/container/disposable.js +33 -0
- package/dist/container/types.d.ts +8 -0
- package/dist/factory/bootstrap.js +10 -0
- package/dist/fetch/fetch.module.d.ts +5 -6
- package/dist/fetch/fetch.module.js +9 -42
- package/dist/http/ambient.d.ts +17 -0
- package/dist/http/ambient.js +44 -0
- package/dist/http/execution-context.js +4 -1
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/http/route.manager.d.ts +19 -0
- package/dist/http/route.manager.js +84 -0
- package/dist/i18n/deep-merge.d.ts +2 -0
- package/dist/i18n/deep-merge.js +15 -0
- package/dist/i18n/i18n.middleware.d.ts +15 -0
- package/dist/i18n/i18n.middleware.js +94 -0
- package/dist/i18n/i18n.module.d.ts +13 -0
- package/dist/i18n/i18n.module.js +61 -0
- package/dist/i18n/i18n.options.d.ts +30 -0
- package/dist/i18n/i18n.options.js +16 -0
- package/dist/i18n/i18n.service.d.ts +20 -0
- package/dist/i18n/i18n.service.js +49 -0
- package/dist/i18n/i18n.tokens.d.ts +6 -0
- package/dist/i18n/i18n.tokens.js +3 -0
- package/dist/i18n/i18n.types.d.ts +6 -0
- package/dist/i18n/i18n.types.js +1 -0
- package/dist/i18n/index.d.ts +10 -0
- package/dist/i18n/index.js +8 -0
- package/dist/i18n/message-loader.service.d.ts +21 -0
- package/dist/i18n/message-loader.service.js +90 -0
- package/dist/i18n/message-registry.d.ts +16 -0
- package/dist/i18n/message-registry.js +49 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -2
- package/dist/module/configurable-module.builder.d.ts +44 -0
- package/dist/module/configurable-module.builder.js +177 -0
- package/dist/module/configurable-module.types.d.ts +93 -0
- package/dist/module/configurable-module.types.js +5 -0
- package/dist/module/index.d.ts +2 -0
- package/dist/module/index.js +1 -0
- package/dist/openapi/document.js +20 -2
- package/dist/openapi/types.d.ts +54 -0
- package/dist/pipeline/types.d.ts +21 -4
- package/dist/registry/metadata.registry.d.ts +16 -16
- package/dist/registry/metadata.registry.js +90 -32
- package/dist/seeder/index.d.ts +5 -0
- package/dist/seeder/index.js +4 -0
- package/dist/seeder/seeder.decorator.d.ts +7 -0
- package/dist/seeder/seeder.decorator.js +15 -0
- package/dist/seeder/seeder.module.d.ts +18 -0
- package/dist/seeder/seeder.module.js +40 -0
- package/dist/seeder/seeder.registry.d.ts +23 -0
- package/dist/seeder/seeder.registry.js +83 -0
- package/dist/seeder/seeder.tokens.d.ts +2 -0
- package/dist/seeder/seeder.tokens.js +1 -0
- package/dist/seeder/seeder.types.d.ts +20 -0
- package/dist/seeder/seeder.types.js +1 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/path-template.d.ts +9 -0
- package/dist/storage/path-template.js +20 -0
- package/dist/storage/signed-url.d.ts +12 -0
- package/dist/storage/signed-url.js +55 -0
- package/dist/storage/storage.types.d.ts +41 -0
- package/dist/storage/storage.types.js +5 -0
- package/dist/throttler/throttler.module.d.ts +5 -8
- package/dist/throttler/throttler.module.js +27 -55
- package/dist/websocket/index.d.ts +12 -0
- package/dist/websocket/index.js +16 -0
- package/dist/websocket/websocket.decorators.d.ts +33 -0
- package/dist/websocket/websocket.decorators.js +58 -0
- package/dist/websocket/websocket.module.d.ts +18 -0
- package/dist/websocket/websocket.module.js +52 -0
- package/dist/websocket/websocket.tokens.d.ts +22 -0
- package/dist/websocket/websocket.tokens.js +20 -0
- package/dist/websocket/websocket.types.d.ts +86 -0
- package/dist/websocket/websocket.types.js +4 -0
- package/dist/websocket/ws-argument-resolver.d.ts +13 -0
- package/dist/websocket/ws-argument-resolver.js +40 -0
- package/dist/websocket/ws-dispatcher.d.ts +33 -0
- package/dist/websocket/ws-dispatcher.js +253 -0
- package/dist/websocket/ws-exception.d.ts +16 -0
- package/dist/websocket/ws-exception.js +32 -0
- package/dist/websocket/ws-execution-context.d.ts +9 -0
- package/dist/websocket/ws-execution-context.js +24 -0
- package/dist/websocket/ws-server.d.ts +31 -0
- package/dist/websocket/ws-server.js +74 -0
- package/dist/websocket/ws-sync.d.ts +47 -0
- package/dist/websocket/ws-sync.js +74 -0
- package/dist/websocket-node/index.d.ts +4 -0
- package/dist/websocket-node/index.js +5 -0
- package/dist/websocket-node/node-ws-client.d.ts +24 -0
- package/dist/websocket-node/node-ws-client.js +49 -0
- package/dist/websocket-node/redis-sync.d.ts +33 -0
- package/dist/websocket-node/redis-sync.js +56 -0
- package/dist/websocket-node/register-gateways.d.ts +20 -0
- package/dist/websocket-node/register-gateways.js +61 -0
- package/package.json +31 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { WsDispatcher } from "./ws-dispatcher.js";
|
|
2
|
+
import { WsServerImpl } from "./ws-server.js";
|
|
3
|
+
import { InMemoryRoomRegistry, local } from "./ws-sync.js";
|
|
4
|
+
import { WS_MODULE_OPTIONS, WS_ROOM_REGISTRY, WS_SERVER, WS_SYNC_DRIVER } from "./websocket.tokens.js";
|
|
5
|
+
/**
|
|
6
|
+
* Registers the WebSocket gateway machinery: the message dispatcher (discovers
|
|
7
|
+
* `@WebSocketGateway` classes at bootstrap), the room registry, the sync driver,
|
|
8
|
+
* and the `@WebSocketServer()`-injected server handle. Mirrors
|
|
9
|
+
* `CacheModule.forRoot` / `ScheduleModule.forRoot`.
|
|
10
|
+
*/ // forRoot() carries non-serializable per-call state (a fresh registry/driver/
|
|
11
|
+
// server), so each call is its own module instance. A unique key prevents the
|
|
12
|
+
// module loader from deduping two distinct configurations into one (which would
|
|
13
|
+
// silently discard the second registry/server).
|
|
14
|
+
let instanceCounter = 0;
|
|
15
|
+
export class WebSocketModule {
|
|
16
|
+
static forRoot(options = {}) {
|
|
17
|
+
const registry = options.registry ?? new InMemoryRoomRegistry();
|
|
18
|
+
const driver = options.sync ?? local();
|
|
19
|
+
driver.bind(registry);
|
|
20
|
+
const server = new WsServerImpl(driver);
|
|
21
|
+
const providers = [
|
|
22
|
+
{
|
|
23
|
+
provide: WS_MODULE_OPTIONS,
|
|
24
|
+
useValue: options
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
provide: WS_ROOM_REGISTRY,
|
|
28
|
+
useValue: registry
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
provide: WS_SYNC_DRIVER,
|
|
32
|
+
useValue: driver
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
provide: WS_SERVER,
|
|
36
|
+
useValue: server
|
|
37
|
+
},
|
|
38
|
+
WsDispatcher
|
|
39
|
+
];
|
|
40
|
+
return {
|
|
41
|
+
module: WebSocketModule,
|
|
42
|
+
key: `${driver.kind}#${instanceCounter += 1}`,
|
|
43
|
+
providers,
|
|
44
|
+
exports: [
|
|
45
|
+
WS_SERVER,
|
|
46
|
+
WS_SYNC_DRIVER,
|
|
47
|
+
WS_ROOM_REGISTRY,
|
|
48
|
+
WsDispatcher
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { InjectionToken } from '../container/types';
|
|
2
|
+
import type { RoomRegistry, SyncDriver } from './ws-sync';
|
|
3
|
+
import type { WsServer } from './websocket.types';
|
|
4
|
+
export declare const WS_GATEWAY_METADATA = "vela:ws-gateway";
|
|
5
|
+
export declare const WS_SUBSCRIBE_METADATA = "vela:ws-subscribe";
|
|
6
|
+
export declare const WsParamType: {
|
|
7
|
+
readonly SOCKET: "ws_socket";
|
|
8
|
+
readonly BODY: "ws_body";
|
|
9
|
+
readonly SERVER: "ws_server";
|
|
10
|
+
};
|
|
11
|
+
export type WsParamType = (typeof WsParamType)[keyof typeof WsParamType];
|
|
12
|
+
/**
|
|
13
|
+
* The connected gateway server handle. Injected into gateways/controllers via
|
|
14
|
+
* `@WebSocketServer()` (constructor injection only — the container has no
|
|
15
|
+
* property-injection pass) or `@Inject(WS_SERVER)`.
|
|
16
|
+
*/
|
|
17
|
+
export declare const WS_SERVER: InjectionToken<WsServer>;
|
|
18
|
+
/** The active cross-instance sync driver (`local()` by default). */
|
|
19
|
+
export declare const WS_SYNC_DRIVER: InjectionToken<SyncDriver>;
|
|
20
|
+
/** The local room-membership registry a transport reads/writes. */
|
|
21
|
+
export declare const WS_ROOM_REGISTRY: InjectionToken<RoomRegistry>;
|
|
22
|
+
export declare const WS_MODULE_OPTIONS = "vela:ws-module-options";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { InjectionToken } from "../container/types.js";
|
|
2
|
+
// Free-form metadata keys — same string-token convention as ON_EVENT_METADATA / CRON_METADATA.
|
|
3
|
+
export const WS_GATEWAY_METADATA = 'vela:ws-gateway';
|
|
4
|
+
export const WS_SUBSCRIBE_METADATA = 'vela:ws-subscribe';
|
|
5
|
+
// WebSocket parameter-decorator kinds — parallel to `ParamType` for HTTP, keyed
|
|
6
|
+
// by the WS argument resolver rather than the Hono-bound HTTP one.
|
|
7
|
+
export const WsParamType = {
|
|
8
|
+
SOCKET: 'ws_socket',
|
|
9
|
+
BODY: 'ws_body',
|
|
10
|
+
SERVER: 'ws_server'
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The connected gateway server handle. Injected into gateways/controllers via
|
|
14
|
+
* `@WebSocketServer()` (constructor injection only — the container has no
|
|
15
|
+
* property-injection pass) or `@Inject(WS_SERVER)`.
|
|
16
|
+
*/ export const WS_SERVER = new InjectionToken('WS_SERVER');
|
|
17
|
+
/** The active cross-instance sync driver (`local()` by default). */ export const WS_SYNC_DRIVER = new InjectionToken('WS_SYNC_DRIVER');
|
|
18
|
+
/** The local room-membership registry a transport reads/writes. */ export const WS_ROOM_REGISTRY = new InjectionToken('WS_ROOM_REGISTRY');
|
|
19
|
+
// forRoot() options carrier.
|
|
20
|
+
export const WS_MODULE_OPTIONS = 'vela:ws-module-options';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ExecutionContext, WsArgumentsHost } from '../pipeline/types';
|
|
2
|
+
export type { WsArgumentsHost };
|
|
3
|
+
/** The JSON envelope carried over the raw WebSocket. `id` is an optional client correlation id echoed on the reply. */
|
|
4
|
+
export interface WsMessage<T = unknown> {
|
|
5
|
+
id?: string;
|
|
6
|
+
event: string;
|
|
7
|
+
data?: T;
|
|
8
|
+
}
|
|
9
|
+
/** A handler's structured reply. Returning one frames `{ event, data }` back to the sender. */
|
|
10
|
+
export interface WsResponse<T = unknown> {
|
|
11
|
+
event: string;
|
|
12
|
+
data: T;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A single connected socket, normalized across runtimes. Per-connection state
|
|
16
|
+
* lives in `data`; mutations to `data`/room membership are persisted by
|
|
17
|
+
* `commit()` (on Cloudflare this writes the hibernation attachment).
|
|
18
|
+
*/
|
|
19
|
+
export interface WsClient<TData = Record<string, unknown>> {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly rooms: ReadonlySet<string>;
|
|
22
|
+
data: TData;
|
|
23
|
+
/** Frame and send `{ event, data }` (optionally correlated by `id`) to this socket. */
|
|
24
|
+
send(event: string, data?: unknown, id?: string): void;
|
|
25
|
+
/** Send an already-serialized string. Escape hatch for custom framing. */
|
|
26
|
+
sendRaw(payload: string): void;
|
|
27
|
+
join(room: string): void | Promise<void>;
|
|
28
|
+
leave(room: string): void | Promise<void>;
|
|
29
|
+
/** Persist `data`/room mutations made during a handler. */
|
|
30
|
+
commit(): void | Promise<void>;
|
|
31
|
+
close(code?: number, reason?: string): void;
|
|
32
|
+
/** The native socket (`WSContext` on node/bun/deno, `WebSocket` in a Cloudflare DO). */
|
|
33
|
+
readonly raw: unknown;
|
|
34
|
+
}
|
|
35
|
+
/** Fluent broadcast builder. Terminal `.emit()` produces a `BroadcastCommand`. */
|
|
36
|
+
export interface BroadcastOperator {
|
|
37
|
+
to(room: string): BroadcastOperator;
|
|
38
|
+
in(room: string): BroadcastOperator;
|
|
39
|
+
except(room: string): BroadcastOperator;
|
|
40
|
+
emit(event: string, data?: unknown): void | Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
/** The server handle injected via `@WebSocketServer()`. Server-origin broadcasts never exclude anyone. */
|
|
43
|
+
export interface WsServer {
|
|
44
|
+
emit(event: string, data?: unknown): void | Promise<void>;
|
|
45
|
+
to(room: string): BroadcastOperator;
|
|
46
|
+
in(room: string): BroadcastOperator;
|
|
47
|
+
except(room: string): BroadcastOperator;
|
|
48
|
+
}
|
|
49
|
+
/** An `ExecutionContext` whose transport is a WebSocket gateway. `switchToWs()` is guaranteed present. */
|
|
50
|
+
export interface WsExecutionContext extends ExecutionContext {
|
|
51
|
+
switchToWs(): WsArgumentsHost;
|
|
52
|
+
}
|
|
53
|
+
export interface OnGatewayInit {
|
|
54
|
+
afterInit(server: WsServer): void | Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
export interface OnGatewayConnection {
|
|
57
|
+
handleConnection(client: WsClient): void | Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export interface OnGatewayDisconnect {
|
|
60
|
+
handleDisconnect(client: WsClient): void | Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
export interface WebSocketGatewayOptions {
|
|
63
|
+
/** Route path the upgrade is served on (e.g. `/rooms/:id/ws`). */
|
|
64
|
+
path?: string;
|
|
65
|
+
/** Cloudflare Durable Object binding name that hosts this gateway's sockets. */
|
|
66
|
+
binding?: string;
|
|
67
|
+
}
|
|
68
|
+
/** Stored per `@SubscribeMessage` — a flat class-level list, mirroring `@OnEvent`. */
|
|
69
|
+
export interface SubscribeMessageMetadata {
|
|
70
|
+
event: string;
|
|
71
|
+
methodName: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A fully serializable broadcast instruction. Crosses isolate / Durable-Object /
|
|
75
|
+
* Redis boundaries as JSON, so every sync driver speaks the same command.
|
|
76
|
+
*/
|
|
77
|
+
export interface BroadcastCommand {
|
|
78
|
+
/** Target rooms. An empty array means GLOBAL (every connection). */
|
|
79
|
+
rooms: string[];
|
|
80
|
+
exceptRooms?: string[];
|
|
81
|
+
exceptIds?: string[];
|
|
82
|
+
/** The exact bytes written to each socket: `JSON.stringify({ event, data })`. */
|
|
83
|
+
frame: string;
|
|
84
|
+
/** Origin instance/DO id — lets pub/sub drivers drop their own echo. */
|
|
85
|
+
origin?: string;
|
|
86
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PipeTransform } from '../pipeline/types';
|
|
2
|
+
import type { ParameterMetadata } from '../registry/types';
|
|
3
|
+
import type { WsClient } from './websocket.types';
|
|
4
|
+
/**
|
|
5
|
+
* Builds handler arguments for a gateway message. WS sibling of the HTTP
|
|
6
|
+
* `ArgumentResolver`, but sourced from the envelope (`data`) and the socket
|
|
7
|
+
* (`client`) instead of the Hono `Context`.
|
|
8
|
+
*
|
|
9
|
+
* With no parameter decorators the handler receives `(client, data)` — the
|
|
10
|
+
* NestJS gateway convention. Pipes run only over `@MessageBody()` values, where
|
|
11
|
+
* validation/transformation is meaningful.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveWsArgs(paramMeta: ParameterMetadata[], client: WsClient, data: unknown, pipes: PipeTransform[], paramTypes?: unknown[]): Promise<unknown[]>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { WsParamType } from "./websocket.tokens.js";
|
|
2
|
+
/**
|
|
3
|
+
* Builds handler arguments for a gateway message. WS sibling of the HTTP
|
|
4
|
+
* `ArgumentResolver`, but sourced from the envelope (`data`) and the socket
|
|
5
|
+
* (`client`) instead of the Hono `Context`.
|
|
6
|
+
*
|
|
7
|
+
* With no parameter decorators the handler receives `(client, data)` — the
|
|
8
|
+
* NestJS gateway convention. Pipes run only over `@MessageBody()` values, where
|
|
9
|
+
* validation/transformation is meaningful.
|
|
10
|
+
*/ export async function resolveWsArgs(paramMeta, client, data, pipes, paramTypes) {
|
|
11
|
+
if (paramMeta.length === 0) {
|
|
12
|
+
return [
|
|
13
|
+
client,
|
|
14
|
+
data
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
const maxIndex = paramMeta[paramMeta.length - 1].index;
|
|
18
|
+
const args = new Array(maxIndex + 1).fill(undefined);
|
|
19
|
+
for (const param of paramMeta){
|
|
20
|
+
if (param.type === WsParamType.SOCKET) {
|
|
21
|
+
args[param.index] = client;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (param.type === WsParamType.BODY) {
|
|
25
|
+
let value = data;
|
|
26
|
+
const metadata = {
|
|
27
|
+
type: param.type,
|
|
28
|
+
data: param.name,
|
|
29
|
+
metatype: paramTypes?.[param.index]
|
|
30
|
+
};
|
|
31
|
+
for (const pipe of pipes){
|
|
32
|
+
value = await pipe.transform(value, metadata);
|
|
33
|
+
}
|
|
34
|
+
args[param.index] = value;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
args[param.index] = undefined;
|
|
38
|
+
}
|
|
39
|
+
return args;
|
|
40
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Container } from '../container/container';
|
|
2
|
+
import { RouteManager } from '../http/route.manager';
|
|
3
|
+
import type { OnApplicationBootstrap } from '../lifecycle/index';
|
|
4
|
+
import type { WsClient, WsServer } from './websocket.types';
|
|
5
|
+
/**
|
|
6
|
+
* Discovers `@WebSocketGateway` classes at bootstrap (same `container.getTokens()`
|
|
7
|
+
* scan as `EventEmitterSubscriber` / `ScheduleRegistry`) and routes inbound
|
|
8
|
+
* messages to `@SubscribeMessage` handlers. Transports call `handleOpen`,
|
|
9
|
+
* `dispatchMessage`, `handleClose`, and `handleError`; the guard → pipe →
|
|
10
|
+
* interceptor → filter pipeline (including `APP_*` global components) is reused
|
|
11
|
+
* from `ComponentManager`.
|
|
12
|
+
*/
|
|
13
|
+
export declare class WsDispatcher implements OnApplicationBootstrap {
|
|
14
|
+
private readonly container;
|
|
15
|
+
private readonly server?;
|
|
16
|
+
private readonly routeManager?;
|
|
17
|
+
private readonly gateways;
|
|
18
|
+
constructor(container: Container, server?: WsServer | undefined, routeManager?: RouteManager | undefined);
|
|
19
|
+
/** Paths of every discovered `@WebSocketGateway` — used by transports to register routes. */
|
|
20
|
+
get gatewayPaths(): string[];
|
|
21
|
+
onApplicationBootstrap(): Promise<void>;
|
|
22
|
+
handleOpen(path: string, client: WsClient): Promise<void>;
|
|
23
|
+
handleClose(path: string, client: WsClient, _code: number, _reason: string): Promise<void>;
|
|
24
|
+
handleError(path: string, _client: WsClient, err: unknown): Promise<void>;
|
|
25
|
+
dispatchMessage(path: string, client: WsClient, raw: string | ArrayBuffer): Promise<void>;
|
|
26
|
+
private reply;
|
|
27
|
+
private runFilters;
|
|
28
|
+
private trySend;
|
|
29
|
+
private reportBootstrapError;
|
|
30
|
+
private parse;
|
|
31
|
+
private resolveGateway;
|
|
32
|
+
private buildGatewayEntry;
|
|
33
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
function _ts_metadata(k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
}
|
|
10
|
+
function _ts_param(paramIndex, decorator) {
|
|
11
|
+
return function(target, key) {
|
|
12
|
+
decorator(target, key, paramIndex);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
import { Container } from "../container/container.js";
|
|
16
|
+
import { Inject, Injectable, Optional } from "../container/decorators.js";
|
|
17
|
+
import { instantiateMany } from "../http/instantiate.js";
|
|
18
|
+
import { RouteManager } from "../http/route.manager.js";
|
|
19
|
+
import { ComponentManager } from "../pipeline/component.manager.js";
|
|
20
|
+
import { shouldFilterCatch } from "../pipeline/decorators.js";
|
|
21
|
+
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
22
|
+
import { resolveWsArgs } from "./ws-argument-resolver.js";
|
|
23
|
+
import { buildWsExecutionContext } from "./ws-execution-context.js";
|
|
24
|
+
import { toErrorFrame, WsException } from "./ws-exception.js";
|
|
25
|
+
import { WS_GATEWAY_METADATA, WS_SERVER, WS_SUBSCRIBE_METADATA } from "./websocket.tokens.js";
|
|
26
|
+
function hasAfterInit(x) {
|
|
27
|
+
return typeof x?.afterInit === 'function';
|
|
28
|
+
}
|
|
29
|
+
function hasHandleConnection(x) {
|
|
30
|
+
return typeof x?.handleConnection === 'function';
|
|
31
|
+
}
|
|
32
|
+
function hasHandleDisconnect(x) {
|
|
33
|
+
return typeof x?.handleDisconnect === 'function';
|
|
34
|
+
}
|
|
35
|
+
function isWsResponse(x) {
|
|
36
|
+
return typeof x === 'object' && x !== null && typeof x.event === 'string';
|
|
37
|
+
}
|
|
38
|
+
export class WsDispatcher {
|
|
39
|
+
container;
|
|
40
|
+
server;
|
|
41
|
+
routeManager;
|
|
42
|
+
gateways = new Map();
|
|
43
|
+
constructor(container, server, routeManager){
|
|
44
|
+
this.container = container;
|
|
45
|
+
this.server = server;
|
|
46
|
+
this.routeManager = routeManager;
|
|
47
|
+
}
|
|
48
|
+
/** Paths of every discovered `@WebSocketGateway` — used by transports to register routes. */ get gatewayPaths() {
|
|
49
|
+
return [
|
|
50
|
+
...this.gateways.keys()
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
async onApplicationBootstrap() {
|
|
54
|
+
for (const token of this.container.getTokens()){
|
|
55
|
+
if (typeof token !== 'function') continue;
|
|
56
|
+
const options = MetadataRegistry.getCustomClassMeta(token, WS_GATEWAY_METADATA);
|
|
57
|
+
if (!options) continue;
|
|
58
|
+
const instance = this.resolveGateway(token);
|
|
59
|
+
if (!instance) continue;
|
|
60
|
+
const entry = this.buildGatewayEntry(token, options, instance);
|
|
61
|
+
// Two gateways on the same path (or both defaulting to '') would silently
|
|
62
|
+
// overwrite each other in the routing Map — surface it instead.
|
|
63
|
+
if (this.gateways.has(entry.path)) {
|
|
64
|
+
const msg = `[vela] duplicate @WebSocketGateway path '${entry.path}' ` + `(${token.name}); keeping the first. Give each gateway a distinct path.`;
|
|
65
|
+
if (this.container.getDiagnostics() === 'throw') throw new Error(msg);
|
|
66
|
+
console.warn(msg);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
this.gateways.set(entry.path, entry);
|
|
70
|
+
if (this.server && hasAfterInit(instance)) {
|
|
71
|
+
try {
|
|
72
|
+
await instance.afterInit(this.server);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
this.reportBootstrapError(token.name, err);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async handleOpen(path, client) {
|
|
80
|
+
const entry = this.gateways.get(path);
|
|
81
|
+
if (entry && hasHandleConnection(entry.instance)) {
|
|
82
|
+
await entry.instance.handleConnection(client);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async handleClose(path, client, _code, _reason) {
|
|
86
|
+
const entry = this.gateways.get(path);
|
|
87
|
+
if (entry && hasHandleDisconnect(entry.instance)) {
|
|
88
|
+
await entry.instance.handleDisconnect(client);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async handleError(path, _client, err) {
|
|
92
|
+
if (this.container.getDiagnostics() !== 'silent') {
|
|
93
|
+
console.warn(`[vela] websocket error on gateway ${path}:`, err);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async dispatchMessage(path, client, raw) {
|
|
97
|
+
const entry = this.gateways.get(path);
|
|
98
|
+
if (!entry) return;
|
|
99
|
+
let message;
|
|
100
|
+
try {
|
|
101
|
+
message = this.parse(raw);
|
|
102
|
+
} catch {
|
|
103
|
+
this.trySend(client, 'exception', {
|
|
104
|
+
message: 'Invalid message'
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const handler = entry.handlers.get(message.event);
|
|
109
|
+
if (!handler) return; // unknown event — ignored (NestJS parity)
|
|
110
|
+
const ctx = buildWsExecutionContext(client, message.data, entry.gatewayClass, handler.methodName, message.event);
|
|
111
|
+
// App-wide global components (APP_* provider tokens + imperative
|
|
112
|
+
// app.useGlobalX()) run first, then controller/handler-tier — mirroring the
|
|
113
|
+
// HTTP HandlerExecutor so global guards/pipes/interceptors/filters protect
|
|
114
|
+
// gateway messages too. Read live so late registrations propagate.
|
|
115
|
+
const globals = this.routeManager?.getGlobalComponents();
|
|
116
|
+
const guards = [
|
|
117
|
+
...instantiateMany(globals?.guards ?? [], this.container),
|
|
118
|
+
...instantiateMany(handler.guards, this.container)
|
|
119
|
+
];
|
|
120
|
+
const pipes = [
|
|
121
|
+
...instantiateMany(globals?.pipes ?? [], this.container),
|
|
122
|
+
...instantiateMany(handler.pipes, this.container)
|
|
123
|
+
];
|
|
124
|
+
const interceptors = [
|
|
125
|
+
...instantiateMany(globals?.interceptors ?? [], this.container),
|
|
126
|
+
...instantiateMany(handler.interceptors, this.container)
|
|
127
|
+
];
|
|
128
|
+
// Handler/controller filters first (closest), then global filters last.
|
|
129
|
+
const filters = [
|
|
130
|
+
...instantiateMany(handler.filters, this.container),
|
|
131
|
+
...instantiateMany(globals?.filters ?? [], this.container)
|
|
132
|
+
];
|
|
133
|
+
try {
|
|
134
|
+
for (const guard of guards){
|
|
135
|
+
if (!await guard.canActivate(ctx)) {
|
|
136
|
+
throw new WsException('Forbidden');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const args = await resolveWsArgs(handler.paramMeta, client, message.data, pipes, handler.paramTypes);
|
|
140
|
+
const method = entry.instance[handler.methodName];
|
|
141
|
+
const core = async ()=>method.apply(entry.instance, args);
|
|
142
|
+
const result = await ComponentManager.runInterceptorChain(interceptors, ctx, core);
|
|
143
|
+
this.reply(client, message, result);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
await this.runFilters(client, message, error, filters, ctx);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
reply(client, message, result) {
|
|
149
|
+
if (result === undefined || result === null) return;
|
|
150
|
+
if (isWsResponse(result)) {
|
|
151
|
+
this.trySend(client, result.event, result.data, message.id);
|
|
152
|
+
} else {
|
|
153
|
+
this.trySend(client, message.event, result, message.id);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async runFilters(client, message, error, filters, ctx) {
|
|
157
|
+
for (const filter of filters){
|
|
158
|
+
if (shouldFilterCatch(filter, error)) {
|
|
159
|
+
try {
|
|
160
|
+
const handled = await filter.catch(error, ctx);
|
|
161
|
+
if (isWsResponse(handled)) {
|
|
162
|
+
this.trySend(client, handled.event, handled.data, message.id);
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// A throwing filter falls back to the default error frame (mirrors HandlerExecutor).
|
|
166
|
+
const frame = toErrorFrame(error);
|
|
167
|
+
this.trySend(client, frame.event, frame.data, message.id);
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const errorFrame = toErrorFrame(error);
|
|
173
|
+
this.trySend(client, errorFrame.event, errorFrame.data, message.id);
|
|
174
|
+
}
|
|
175
|
+
// Outbound sends are best-effort: the socket may have closed mid-dispatch.
|
|
176
|
+
trySend(client, event, data, id) {
|
|
177
|
+
try {
|
|
178
|
+
client.send(event, data, id);
|
|
179
|
+
} catch {
|
|
180
|
+
// socket closed — nothing to do
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
reportBootstrapError(name, err) {
|
|
184
|
+
const mode = this.container.getDiagnostics();
|
|
185
|
+
if (mode === 'throw') throw err;
|
|
186
|
+
if (mode === 'log') console.warn(`[vela] websocket gateway ${name} afterInit failed:`, err);
|
|
187
|
+
}
|
|
188
|
+
parse(raw) {
|
|
189
|
+
const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);
|
|
190
|
+
const parsed = JSON.parse(text);
|
|
191
|
+
if (!parsed || typeof parsed !== 'object' || typeof parsed.event !== 'string') {
|
|
192
|
+
throw new Error('Invalid WebSocket message envelope');
|
|
193
|
+
}
|
|
194
|
+
return parsed;
|
|
195
|
+
}
|
|
196
|
+
resolveGateway(token) {
|
|
197
|
+
try {
|
|
198
|
+
return this.container.resolve(token);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
const mode = this.container.getDiagnostics();
|
|
201
|
+
if (mode === 'throw') throw err;
|
|
202
|
+
if (mode === 'log') {
|
|
203
|
+
const name = typeof token === 'function' ? token.name : String(token);
|
|
204
|
+
console.warn(`[vela] websocket gateway discovery: cannot resolve ${name}:`, err);
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
buildGatewayEntry(gatewayClass, options, instance) {
|
|
210
|
+
const ctor = gatewayClass;
|
|
211
|
+
const subs = MetadataRegistry.getCustomClassMeta(ctor, WS_SUBSCRIBE_METADATA) ?? [];
|
|
212
|
+
const allParams = MetadataRegistry.getParameters(ctor);
|
|
213
|
+
const handlers = new Map();
|
|
214
|
+
for (const { event, methodName } of subs){
|
|
215
|
+
const paramMeta = [
|
|
216
|
+
...allParams.get(methodName) ?? []
|
|
217
|
+
].sort((a, b)=>a.index - b.index);
|
|
218
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', gatewayClass.prototype, methodName);
|
|
219
|
+
handlers.set(event, {
|
|
220
|
+
methodName,
|
|
221
|
+
paramMeta,
|
|
222
|
+
paramTypes,
|
|
223
|
+
guards: ComponentManager.getComponents('guard', ctor, methodName),
|
|
224
|
+
pipes: ComponentManager.getComponents('pipe', ctor, methodName),
|
|
225
|
+
interceptors: ComponentManager.getComponents('interceptor', ctor, methodName),
|
|
226
|
+
// Handler → controller → global, so the closest filter runs first (mirrors HandlerExecutor).
|
|
227
|
+
filters: [
|
|
228
|
+
...ComponentManager.getComponents('filter', ctor, methodName)
|
|
229
|
+
].reverse()
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
path: options.path ?? '',
|
|
234
|
+
instance,
|
|
235
|
+
gatewayClass,
|
|
236
|
+
handlers
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
WsDispatcher = _ts_decorate([
|
|
241
|
+
Injectable(),
|
|
242
|
+
_ts_param(0, Inject(Container)),
|
|
243
|
+
_ts_param(1, Optional()),
|
|
244
|
+
_ts_param(1, Inject(WS_SERVER)),
|
|
245
|
+
_ts_param(2, Optional()),
|
|
246
|
+
_ts_param(2, Inject(RouteManager)),
|
|
247
|
+
_ts_metadata("design:type", Function),
|
|
248
|
+
_ts_metadata("design:paramtypes", [
|
|
249
|
+
typeof Container === "undefined" ? Object : Container,
|
|
250
|
+
typeof WsServer === "undefined" ? Object : WsServer,
|
|
251
|
+
typeof RouteManager === "undefined" ? Object : RouteManager
|
|
252
|
+
])
|
|
253
|
+
], WsDispatcher);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket analogue of `HttpException`. Thrown from guards/pipes/handlers to
|
|
3
|
+
* signal a client-facing error; the dispatcher serializes it to an
|
|
4
|
+
* `{ event: 'exception', data }` frame unless a matching exception filter
|
|
5
|
+
* handles it first.
|
|
6
|
+
*/
|
|
7
|
+
export declare class WsException extends Error {
|
|
8
|
+
private readonly err;
|
|
9
|
+
constructor(err: string | Record<string, unknown>);
|
|
10
|
+
getError(): string | Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
/** Default serialization of an uncaught error into the outbound exception frame. */
|
|
13
|
+
export declare function toErrorFrame(error: unknown): {
|
|
14
|
+
event: 'exception';
|
|
15
|
+
data: unknown;
|
|
16
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket analogue of `HttpException`. Thrown from guards/pipes/handlers to
|
|
3
|
+
* signal a client-facing error; the dispatcher serializes it to an
|
|
4
|
+
* `{ event: 'exception', data }` frame unless a matching exception filter
|
|
5
|
+
* handles it first.
|
|
6
|
+
*/ export class WsException extends Error {
|
|
7
|
+
err;
|
|
8
|
+
constructor(err){
|
|
9
|
+
super(typeof err === 'string' ? err : String(err.message ?? 'WsException')), this.err = err;
|
|
10
|
+
this.name = 'WsException';
|
|
11
|
+
}
|
|
12
|
+
getError() {
|
|
13
|
+
return this.err;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Default serialization of an uncaught error into the outbound exception frame. */ export function toErrorFrame(error) {
|
|
17
|
+
if (error instanceof WsException) {
|
|
18
|
+
const e = error.getError();
|
|
19
|
+
return {
|
|
20
|
+
event: 'exception',
|
|
21
|
+
data: typeof e === 'string' ? {
|
|
22
|
+
message: e
|
|
23
|
+
} : e
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
event: 'exception',
|
|
28
|
+
data: {
|
|
29
|
+
message: 'Internal server error'
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Type } from '../container/types';
|
|
2
|
+
import type { WsClient, WsExecutionContext } from './websocket.types';
|
|
3
|
+
/**
|
|
4
|
+
* WebSocket sibling of `buildExecutionContext` (`http/execution-context.ts`).
|
|
5
|
+
* Guards/pipes/interceptors/filters that read `getClass()`/`getHandler()` or
|
|
6
|
+
* call `switchToWs()` reuse unchanged; the HTTP-only accessors throw rather
|
|
7
|
+
* than fabricate a fake `Request`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildWsExecutionContext(client: WsClient, data: unknown, controller: Type, handlerName: string | symbol, pattern: string): WsExecutionContext;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const HTTP_ON_WS = (accessor)=>()=>{
|
|
2
|
+
throw new Error(`${accessor} called on a WebSocket ExecutionContext. This handler runs over a gateway message, not HTTP — use switchToWs().`);
|
|
3
|
+
};
|
|
4
|
+
/**
|
|
5
|
+
* WebSocket sibling of `buildExecutionContext` (`http/execution-context.ts`).
|
|
6
|
+
* Guards/pipes/interceptors/filters that read `getClass()`/`getHandler()` or
|
|
7
|
+
* call `switchToWs()` reuse unchanged; the HTTP-only accessors throw rather
|
|
8
|
+
* than fabricate a fake `Request`.
|
|
9
|
+
*/ export function buildWsExecutionContext(client, data, controller, handlerName, pattern) {
|
|
10
|
+
const host = {
|
|
11
|
+
getClient: ()=>client,
|
|
12
|
+
getData: ()=>data,
|
|
13
|
+
getPattern: ()=>pattern
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
getType: ()=>'ws',
|
|
17
|
+
getClass: ()=>controller,
|
|
18
|
+
getHandler: ()=>handlerName,
|
|
19
|
+
getContext: HTTP_ON_WS('getContext()'),
|
|
20
|
+
getRequest: HTTP_ON_WS('getRequest()'),
|
|
21
|
+
switchToHttp: HTTP_ON_WS('switchToHttp()'),
|
|
22
|
+
switchToWs: ()=>host
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SyncDriver } from './ws-sync';
|
|
2
|
+
import type { BroadcastOperator, WsServer } from './websocket.types';
|
|
3
|
+
/**
|
|
4
|
+
* Accumulates `{ rooms, exceptRooms, exceptIds }` across a fluent chain and, on
|
|
5
|
+
* `.emit()`, produces a serializable `BroadcastCommand` handed to the active
|
|
6
|
+
* `SyncDriver`. The driver decides local-vs-remote delivery.
|
|
7
|
+
*/
|
|
8
|
+
export declare class BroadcastOperatorImpl implements BroadcastOperator {
|
|
9
|
+
private readonly driver;
|
|
10
|
+
private readonly rooms;
|
|
11
|
+
private readonly exceptRooms;
|
|
12
|
+
private readonly exceptIds;
|
|
13
|
+
constructor(driver: SyncDriver, rooms?: Set<string>, exceptRooms?: Set<string>, exceptIds?: Set<string>);
|
|
14
|
+
to(room: string): this;
|
|
15
|
+
in(room: string): this;
|
|
16
|
+
except(room: string): this;
|
|
17
|
+
emit(event: string, data?: unknown): void | Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The `@WebSocketServer()`-injected handle. Server-origin broadcasts never
|
|
21
|
+
* pre-exclude anyone (a client-origin `client.to()` seeds `exceptIds` with the
|
|
22
|
+
* sender — that lives in the transport's client, not here).
|
|
23
|
+
*/
|
|
24
|
+
export declare class WsServerImpl implements WsServer {
|
|
25
|
+
private readonly driver;
|
|
26
|
+
constructor(driver: SyncDriver);
|
|
27
|
+
emit(event: string, data?: unknown): void | Promise<void>;
|
|
28
|
+
to(room: string): BroadcastOperator;
|
|
29
|
+
in(room: string): BroadcastOperator;
|
|
30
|
+
except(room: string): BroadcastOperator;
|
|
31
|
+
}
|