@velajs/vela 1.9.0 → 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/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,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
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accumulates `{ rooms, exceptRooms, exceptIds }` across a fluent chain and, on
|
|
3
|
+
* `.emit()`, produces a serializable `BroadcastCommand` handed to the active
|
|
4
|
+
* `SyncDriver`. The driver decides local-vs-remote delivery.
|
|
5
|
+
*/ export class BroadcastOperatorImpl {
|
|
6
|
+
driver;
|
|
7
|
+
rooms;
|
|
8
|
+
exceptRooms;
|
|
9
|
+
exceptIds;
|
|
10
|
+
constructor(driver, rooms = new Set(), exceptRooms = new Set(), exceptIds = new Set()){
|
|
11
|
+
this.driver = driver;
|
|
12
|
+
this.rooms = rooms;
|
|
13
|
+
this.exceptRooms = exceptRooms;
|
|
14
|
+
this.exceptIds = exceptIds;
|
|
15
|
+
}
|
|
16
|
+
to(room) {
|
|
17
|
+
this.rooms.add(room);
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
in(room) {
|
|
21
|
+
return this.to(room);
|
|
22
|
+
}
|
|
23
|
+
except(room) {
|
|
24
|
+
this.exceptRooms.add(room);
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
emit(event, data) {
|
|
28
|
+
const cmd = {
|
|
29
|
+
rooms: [
|
|
30
|
+
...this.rooms
|
|
31
|
+
],
|
|
32
|
+
exceptRooms: this.exceptRooms.size ? [
|
|
33
|
+
...this.exceptRooms
|
|
34
|
+
] : undefined,
|
|
35
|
+
exceptIds: this.exceptIds.size ? [
|
|
36
|
+
...this.exceptIds
|
|
37
|
+
] : undefined,
|
|
38
|
+
frame: JSON.stringify({
|
|
39
|
+
event,
|
|
40
|
+
data
|
|
41
|
+
})
|
|
42
|
+
};
|
|
43
|
+
return this.driver.dispatch(cmd);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The `@WebSocketServer()`-injected handle. Server-origin broadcasts never
|
|
48
|
+
* pre-exclude anyone (a client-origin `client.to()` seeds `exceptIds` with the
|
|
49
|
+
* sender — that lives in the transport's client, not here).
|
|
50
|
+
*/ export class WsServerImpl {
|
|
51
|
+
driver;
|
|
52
|
+
constructor(driver){
|
|
53
|
+
this.driver = driver;
|
|
54
|
+
}
|
|
55
|
+
emit(event, data) {
|
|
56
|
+
// Empty `rooms` means GLOBAL.
|
|
57
|
+
return this.driver.dispatch({
|
|
58
|
+
rooms: [],
|
|
59
|
+
frame: JSON.stringify({
|
|
60
|
+
event,
|
|
61
|
+
data
|
|
62
|
+
})
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
to(room) {
|
|
66
|
+
return new BroadcastOperatorImpl(this.driver).to(room);
|
|
67
|
+
}
|
|
68
|
+
in(room) {
|
|
69
|
+
return this.to(room);
|
|
70
|
+
}
|
|
71
|
+
except(room) {
|
|
72
|
+
return new BroadcastOperatorImpl(this.driver).except(room);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { BroadcastCommand, WsClient } from './websocket.types';
|
|
2
|
+
/**
|
|
3
|
+
* Owns LOCAL room membership and local fan-out for the connections a single
|
|
4
|
+
* node/isolate holds. The Cloudflare transport backs this with hibernation
|
|
5
|
+
* tags + attachment; node/bun/deno back it with the in-memory implementation
|
|
6
|
+
* below. Always edge-safe (no `node:*`).
|
|
7
|
+
*/
|
|
8
|
+
export interface RoomRegistry {
|
|
9
|
+
/** Register a connection (called on connect) so global broadcasts can reach it. */
|
|
10
|
+
register(client: WsClient): void;
|
|
11
|
+
join(client: WsClient, room: string): void | Promise<void>;
|
|
12
|
+
leave(client: WsClient, room: string): void | Promise<void>;
|
|
13
|
+
/** Remove a connection and all its room memberships (called on disconnect). */
|
|
14
|
+
leaveAll(client: WsClient): void | Promise<void>;
|
|
15
|
+
/** Deliver a command to the sockets THIS node holds, applying every exclusion. */
|
|
16
|
+
deliverLocal(cmd: BroadcastCommand): void | Promise<void>;
|
|
17
|
+
localIdsInRoom(room: string): string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Pluggable cross-instance transport for broadcast commands. `local()` is the
|
|
21
|
+
* zero-config single-instance default; `durableObject()` (Cloudflare) and
|
|
22
|
+
* `redis()` (node) are additive and chosen at the composition root.
|
|
23
|
+
*/
|
|
24
|
+
export interface SyncDriver {
|
|
25
|
+
readonly kind: string;
|
|
26
|
+
/** Wire the local registry so the driver can deliver to locally-held sockets. */
|
|
27
|
+
bind(registry: RoomRegistry): void;
|
|
28
|
+
/** Route a command so every node that may hold a matching socket delivers it. */
|
|
29
|
+
dispatch(cmd: BroadcastCommand): void | Promise<void>;
|
|
30
|
+
countRoom?(room: string): Promise<number>;
|
|
31
|
+
start?(): void | Promise<void>;
|
|
32
|
+
stop?(): void | Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/** Single-instance default: deliver straight to the local registry, no cross-node hop. */
|
|
35
|
+
export declare function local(): SyncDriver;
|
|
36
|
+
/** In-memory `RoomRegistry` for node/bun/deno and tests. Edge-safe (plain Maps). */
|
|
37
|
+
export declare class InMemoryRoomRegistry implements RoomRegistry {
|
|
38
|
+
private readonly rooms;
|
|
39
|
+
private readonly clientRooms;
|
|
40
|
+
private readonly clients;
|
|
41
|
+
register(client: WsClient): void;
|
|
42
|
+
join(client: WsClient, room: string): void;
|
|
43
|
+
leave(client: WsClient, room: string): void;
|
|
44
|
+
leaveAll(client: WsClient): void;
|
|
45
|
+
localIdsInRoom(room: string): string[];
|
|
46
|
+
deliverLocal(cmd: BroadcastCommand): void;
|
|
47
|
+
}
|