@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.
Files changed (115) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/__tests__/workers-als/entry-als.d.ts +9 -0
  3. package/dist/application.d.ts +10 -0
  4. package/dist/application.js +25 -0
  5. package/dist/cache/cache.module.d.ts +5 -7
  6. package/dist/cache/cache.module.js +44 -66
  7. package/dist/cache/cache.types.d.ts +26 -0
  8. package/dist/cache/cache.types.js +1 -1
  9. package/dist/cache/index.d.ts +2 -1
  10. package/dist/cache/index.js +1 -0
  11. package/dist/cache/tiered-cache.store.d.ts +19 -0
  12. package/dist/cache/tiered-cache.store.js +40 -0
  13. package/dist/config/config.module.d.ts +6 -5
  14. package/dist/config/config.module.js +46 -49
  15. package/dist/container/container.d.ts +33 -0
  16. package/dist/container/container.js +156 -7
  17. package/dist/container/disposable.d.ts +5 -0
  18. package/dist/container/disposable.js +33 -0
  19. package/dist/container/types.d.ts +8 -0
  20. package/dist/factory/bootstrap.js +10 -0
  21. package/dist/fetch/fetch.module.d.ts +5 -6
  22. package/dist/fetch/fetch.module.js +9 -42
  23. package/dist/http/ambient.d.ts +17 -0
  24. package/dist/http/ambient.js +44 -0
  25. package/dist/http/execution-context.js +4 -1
  26. package/dist/http/index.d.ts +1 -0
  27. package/dist/http/index.js +1 -0
  28. package/dist/http/route.manager.d.ts +19 -0
  29. package/dist/http/route.manager.js +84 -0
  30. package/dist/i18n/deep-merge.d.ts +2 -0
  31. package/dist/i18n/deep-merge.js +15 -0
  32. package/dist/i18n/i18n.middleware.d.ts +15 -0
  33. package/dist/i18n/i18n.middleware.js +94 -0
  34. package/dist/i18n/i18n.module.d.ts +13 -0
  35. package/dist/i18n/i18n.module.js +61 -0
  36. package/dist/i18n/i18n.options.d.ts +30 -0
  37. package/dist/i18n/i18n.options.js +16 -0
  38. package/dist/i18n/i18n.service.d.ts +20 -0
  39. package/dist/i18n/i18n.service.js +49 -0
  40. package/dist/i18n/i18n.tokens.d.ts +6 -0
  41. package/dist/i18n/i18n.tokens.js +3 -0
  42. package/dist/i18n/i18n.types.d.ts +6 -0
  43. package/dist/i18n/i18n.types.js +1 -0
  44. package/dist/i18n/index.d.ts +10 -0
  45. package/dist/i18n/index.js +8 -0
  46. package/dist/i18n/message-loader.service.d.ts +21 -0
  47. package/dist/i18n/message-loader.service.js +90 -0
  48. package/dist/i18n/message-registry.d.ts +16 -0
  49. package/dist/i18n/message-registry.js +49 -0
  50. package/dist/index.d.ts +7 -4
  51. package/dist/index.js +6 -2
  52. package/dist/module/configurable-module.builder.d.ts +44 -0
  53. package/dist/module/configurable-module.builder.js +177 -0
  54. package/dist/module/configurable-module.types.d.ts +93 -0
  55. package/dist/module/configurable-module.types.js +5 -0
  56. package/dist/module/index.d.ts +2 -0
  57. package/dist/module/index.js +1 -0
  58. package/dist/openapi/document.js +20 -2
  59. package/dist/openapi/types.d.ts +54 -0
  60. package/dist/pipeline/types.d.ts +21 -4
  61. package/dist/registry/metadata.registry.d.ts +16 -16
  62. package/dist/registry/metadata.registry.js +90 -32
  63. package/dist/seeder/index.d.ts +5 -0
  64. package/dist/seeder/index.js +4 -0
  65. package/dist/seeder/seeder.decorator.d.ts +7 -0
  66. package/dist/seeder/seeder.decorator.js +15 -0
  67. package/dist/seeder/seeder.module.d.ts +18 -0
  68. package/dist/seeder/seeder.module.js +40 -0
  69. package/dist/seeder/seeder.registry.d.ts +23 -0
  70. package/dist/seeder/seeder.registry.js +83 -0
  71. package/dist/seeder/seeder.tokens.d.ts +2 -0
  72. package/dist/seeder/seeder.tokens.js +1 -0
  73. package/dist/seeder/seeder.types.d.ts +20 -0
  74. package/dist/seeder/seeder.types.js +1 -0
  75. package/dist/storage/index.d.ts +4 -0
  76. package/dist/storage/index.js +2 -0
  77. package/dist/storage/path-template.d.ts +9 -0
  78. package/dist/storage/path-template.js +20 -0
  79. package/dist/storage/signed-url.d.ts +12 -0
  80. package/dist/storage/signed-url.js +55 -0
  81. package/dist/storage/storage.types.d.ts +41 -0
  82. package/dist/storage/storage.types.js +5 -0
  83. package/dist/throttler/throttler.module.d.ts +5 -8
  84. package/dist/throttler/throttler.module.js +27 -55
  85. package/dist/websocket/index.d.ts +12 -0
  86. package/dist/websocket/index.js +16 -0
  87. package/dist/websocket/websocket.decorators.d.ts +33 -0
  88. package/dist/websocket/websocket.decorators.js +58 -0
  89. package/dist/websocket/websocket.module.d.ts +18 -0
  90. package/dist/websocket/websocket.module.js +52 -0
  91. package/dist/websocket/websocket.tokens.d.ts +22 -0
  92. package/dist/websocket/websocket.tokens.js +20 -0
  93. package/dist/websocket/websocket.types.d.ts +86 -0
  94. package/dist/websocket/websocket.types.js +4 -0
  95. package/dist/websocket/ws-argument-resolver.d.ts +13 -0
  96. package/dist/websocket/ws-argument-resolver.js +40 -0
  97. package/dist/websocket/ws-dispatcher.d.ts +33 -0
  98. package/dist/websocket/ws-dispatcher.js +253 -0
  99. package/dist/websocket/ws-exception.d.ts +16 -0
  100. package/dist/websocket/ws-exception.js +32 -0
  101. package/dist/websocket/ws-execution-context.d.ts +9 -0
  102. package/dist/websocket/ws-execution-context.js +24 -0
  103. package/dist/websocket/ws-server.d.ts +31 -0
  104. package/dist/websocket/ws-server.js +74 -0
  105. package/dist/websocket/ws-sync.d.ts +47 -0
  106. package/dist/websocket/ws-sync.js +74 -0
  107. package/dist/websocket-node/index.d.ts +4 -0
  108. package/dist/websocket-node/index.js +5 -0
  109. package/dist/websocket-node/node-ws-client.d.ts +24 -0
  110. package/dist/websocket-node/node-ws-client.js +49 -0
  111. package/dist/websocket-node/redis-sync.d.ts +33 -0
  112. package/dist/websocket-node/redis-sync.js +56 -0
  113. package/dist/websocket-node/register-gateways.d.ts +20 -0
  114. package/dist/websocket-node/register-gateways.js +61 -0
  115. package/package.json +31 -1
@@ -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
+ }
@@ -0,0 +1,74 @@
1
+ /** Single-instance default: deliver straight to the local registry, no cross-node hop. */ export function local() {
2
+ let registry;
3
+ return {
4
+ kind: 'local',
5
+ bind (r) {
6
+ registry = r;
7
+ },
8
+ dispatch (cmd) {
9
+ return registry?.deliverLocal(cmd);
10
+ }
11
+ };
12
+ }
13
+ /** In-memory `RoomRegistry` for node/bun/deno and tests. Edge-safe (plain Maps). */ export class InMemoryRoomRegistry {
14
+ rooms = new Map();
15
+ clientRooms = new Map();
16
+ clients = new Map();
17
+ register(client) {
18
+ this.clients.set(client.id, client);
19
+ }
20
+ join(client, room) {
21
+ this.register(client);
22
+ let members = this.rooms.get(room);
23
+ if (!members) {
24
+ members = new Set();
25
+ this.rooms.set(room, members);
26
+ }
27
+ members.add(client);
28
+ let joined = this.clientRooms.get(client.id);
29
+ if (!joined) {
30
+ joined = new Set();
31
+ this.clientRooms.set(client.id, joined);
32
+ }
33
+ joined.add(room);
34
+ }
35
+ leave(client, room) {
36
+ const members = this.rooms.get(room);
37
+ members?.delete(client);
38
+ if (members && members.size === 0) this.rooms.delete(room);
39
+ this.clientRooms.get(client.id)?.delete(room);
40
+ }
41
+ leaveAll(client) {
42
+ for (const room of this.clientRooms.get(client.id) ?? []){
43
+ const members = this.rooms.get(room);
44
+ members?.delete(client);
45
+ if (members && members.size === 0) this.rooms.delete(room);
46
+ }
47
+ this.clientRooms.delete(client.id);
48
+ this.clients.delete(client.id);
49
+ }
50
+ localIdsInRoom(room) {
51
+ return [
52
+ ...this.rooms.get(room) ?? []
53
+ ].map((c)=>c.id);
54
+ }
55
+ deliverLocal(cmd) {
56
+ const excludeIds = new Set(cmd.exceptIds ?? []);
57
+ const excludeRooms = cmd.exceptRooms ?? [];
58
+ // Empty `rooms` => GLOBAL: every registered connection. Otherwise the union
59
+ // of the targeted rooms' members (deduped per connection).
60
+ const candidates = cmd.rooms.length === 0 ? new Set(this.clients.values()) : (()=>{
61
+ const set = new Set();
62
+ for (const room of cmd.rooms){
63
+ for (const client of this.rooms.get(room) ?? [])set.add(client);
64
+ }
65
+ return set;
66
+ })();
67
+ for (const client of candidates){
68
+ if (excludeIds.has(client.id)) continue;
69
+ const joined = this.clientRooms.get(client.id);
70
+ if (excludeRooms.some((room)=>joined?.has(room))) continue;
71
+ client.sendRaw(cmd.frame);
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,4 @@
1
+ export { NodeWsClient } from './node-ws-client';
2
+ export { registerWebSocketGateways } from './register-gateways';
3
+ export { redis } from './redis-sync';
4
+ export type { RedisPubSubClient, RedisSyncOptions } from './redis-sync';
@@ -0,0 +1,5 @@
1
+ // Node / Bun / Deno WebSocket adapter for the Vela WebSocketModule.
2
+ // Transport uses Hono's own `upgradeWebSocket` (no Durable Object hibernation).
3
+ export { NodeWsClient } from "./node-ws-client.js";
4
+ export { registerWebSocketGateways } from "./register-gateways.js";
5
+ export { redis } from "./redis-sync.js";
@@ -0,0 +1,24 @@
1
+ import type { WSContext } from 'hono/ws';
2
+ import type { RoomRegistry, WsClient } from '../websocket/index';
3
+ /**
4
+ * Core `WsClient` over Hono's `WSContext` (node/bun/deno — no hibernation).
5
+ * Room membership is mirrored into the shared `RoomRegistry` so broadcasts from
6
+ * the `WsServer` reach this connection.
7
+ */
8
+ export declare class NodeWsClient<TData extends Record<string, unknown> = Record<string, unknown>> implements WsClient<TData> {
9
+ private readonly ws;
10
+ private readonly registry;
11
+ readonly path: string;
12
+ readonly id: string;
13
+ data: TData;
14
+ private readonly _rooms;
15
+ constructor(ws: WSContext, registry: RoomRegistry, path: string);
16
+ get rooms(): ReadonlySet<string>;
17
+ get raw(): unknown;
18
+ send(event: string, data?: unknown, id?: string): void;
19
+ sendRaw(payload: string): void;
20
+ join(room: string): void | Promise<void>;
21
+ leave(room: string): void | Promise<void>;
22
+ commit(): void;
23
+ close(code?: number, reason?: string): void;
24
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Core `WsClient` over Hono's `WSContext` (node/bun/deno — no hibernation).
3
+ * Room membership is mirrored into the shared `RoomRegistry` so broadcasts from
4
+ * the `WsServer` reach this connection.
5
+ */ export class NodeWsClient {
6
+ ws;
7
+ registry;
8
+ path;
9
+ id = crypto.randomUUID();
10
+ data = {};
11
+ _rooms = new Set();
12
+ constructor(ws, registry, path){
13
+ this.ws = ws;
14
+ this.registry = registry;
15
+ this.path = path;
16
+ }
17
+ get rooms() {
18
+ return this._rooms;
19
+ }
20
+ get raw() {
21
+ return this.ws;
22
+ }
23
+ send(event, data, id) {
24
+ this.ws.send(JSON.stringify(id !== undefined ? {
25
+ id,
26
+ event,
27
+ data
28
+ } : {
29
+ event,
30
+ data
31
+ }));
32
+ }
33
+ sendRaw(payload) {
34
+ this.ws.send(payload);
35
+ }
36
+ join(room) {
37
+ this._rooms.add(room);
38
+ return this.registry.join(this, room);
39
+ }
40
+ leave(room) {
41
+ this._rooms.delete(room);
42
+ return this.registry.leave(this, room);
43
+ }
44
+ // In-memory registry mutates eagerly; nothing deferred to persist.
45
+ commit() {}
46
+ close(code, reason) {
47
+ this.ws.close(code, reason);
48
+ }
49
+ }
@@ -0,0 +1,33 @@
1
+ import type { SyncDriver } from '../websocket/index';
2
+ /**
3
+ * Minimal Redis pub/sub surface (satisfied by `ioredis` / `node-redis`). Pub and
4
+ * sub MUST be separate connections — a subscribed Redis connection cannot publish.
5
+ * `off`/`removeListener`/`unsubscribe` are optional and used only by `stop()`.
6
+ */
7
+ export interface RedisPubSubClient {
8
+ publish(channel: string, message: string): unknown;
9
+ subscribe(channel: string): unknown;
10
+ on(event: 'message', listener: (channel: string, message: string) => void): unknown;
11
+ off?(event: 'message', listener: (channel: string, message: string) => void): unknown;
12
+ removeListener?(event: 'message', listener: (channel: string, message: string) => void): unknown;
13
+ unsubscribe?(channel: string): unknown;
14
+ }
15
+ export interface RedisSyncOptions {
16
+ /** Connection used to publish broadcast commands. */
17
+ pub: RedisPubSubClient;
18
+ /** Separate connection used to subscribe (kept in subscriber mode). */
19
+ sub: RedisPubSubClient;
20
+ /** Channel prefix. Default `vela:ws`. */
21
+ prefix?: string;
22
+ }
23
+ /**
24
+ * Cross-instance sync driver for node/bun via Redis pub/sub. Local-first: every
25
+ * `dispatch` delivers to this instance's sockets immediately, then fans the
26
+ * command out to peers on a single broadcast channel; each peer's registry
27
+ * filters delivery to its own local room members. The publishing instance drops
28
+ * its own echo via an `origin` stamp.
29
+ *
30
+ * Guarantees: at-most-once, no ordering across publishers, no replay — a fan-out
31
+ * bus, not a log. Pass it to `WebSocketModule.forRoot({ sync: redis({ pub, sub }) })`.
32
+ */
33
+ export declare function redis(options: RedisSyncOptions): SyncDriver;
@@ -0,0 +1,56 @@
1
+ const swallow = (op)=>{
2
+ // Redis client calls return promises that reject on transport failure; a
3
+ // fan-out miss must never become an unhandled rejection that crashes the app.
4
+ void Promise.resolve(op).catch(()=>{});
5
+ };
6
+ /**
7
+ * Cross-instance sync driver for node/bun via Redis pub/sub. Local-first: every
8
+ * `dispatch` delivers to this instance's sockets immediately, then fans the
9
+ * command out to peers on a single broadcast channel; each peer's registry
10
+ * filters delivery to its own local room members. The publishing instance drops
11
+ * its own echo via an `origin` stamp.
12
+ *
13
+ * Guarantees: at-most-once, no ordering across publishers, no replay — a fan-out
14
+ * bus, not a log. Pass it to `WebSocketModule.forRoot({ sync: redis({ pub, sub }) })`.
15
+ */ export function redis(options) {
16
+ const prefix = options.prefix ?? 'vela:ws';
17
+ const channel = `${prefix}:broadcast`;
18
+ const origin = crypto.randomUUID();
19
+ let registry;
20
+ let bound = false;
21
+ const onMessage = (ch, message)=>{
22
+ if (ch !== channel) return;
23
+ let cmd;
24
+ try {
25
+ cmd = JSON.parse(message);
26
+ } catch {
27
+ return;
28
+ }
29
+ if (cmd.origin === origin) return; // our own publish — already delivered locally
30
+ swallow(registry?.deliverLocal(cmd));
31
+ };
32
+ return {
33
+ kind: 'redis',
34
+ bind (reg) {
35
+ registry = reg;
36
+ if (bound) return; // idempotent — never stack duplicate 'message' listeners on re-bind
37
+ bound = true;
38
+ swallow(options.sub.subscribe(channel));
39
+ options.sub.on('message', onMessage);
40
+ },
41
+ dispatch (cmd) {
42
+ swallow(registry?.deliverLocal(cmd));
43
+ swallow(options.pub.publish(channel, JSON.stringify({
44
+ ...cmd,
45
+ origin
46
+ })));
47
+ },
48
+ stop () {
49
+ if (!bound) return;
50
+ bound = false;
51
+ options.sub.off?.('message', onMessage);
52
+ options.sub.removeListener?.('message', onMessage);
53
+ swallow(options.sub.unsubscribe?.(channel));
54
+ }
55
+ };
56
+ }
@@ -0,0 +1,20 @@
1
+ import type { UpgradeWebSocket } from 'hono/ws';
2
+ import type { VelaApplication } from '../application';
3
+ /**
4
+ * Registers every `@WebSocketGateway` on the app's Hono instance using the
5
+ * runtime's Hono `upgradeWebSocket` factory — the same call works on node
6
+ * (`@hono/node-ws` / `@hono/node-server` v2), Bun (`hono/bun`), and Deno
7
+ * (`hono/deno`). Each connection auto-joins the room from a `:id` route param
8
+ * (or the route path), mirroring the Cloudflare DO-per-room model.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // Node
13
+ * const app = await VelaFactory.create(AppModule);
14
+ * const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app: app.getHonoApp() });
15
+ * registerWebSocketGateways(app, upgradeWebSocket);
16
+ * const server = serve({ fetch: app.fetch, port: 3000 });
17
+ * injectWebSocket(server);
18
+ * ```
19
+ */
20
+ export declare function registerWebSocketGateways(app: VelaApplication, upgradeWebSocket: UpgradeWebSocket): void;
@@ -0,0 +1,61 @@
1
+ import { WsDispatcher, WS_ROOM_REGISTRY } from "../websocket/index.js";
2
+ import { NodeWsClient } from "./node-ws-client.js";
3
+ function toText(data) {
4
+ if (typeof data === 'string') return data;
5
+ if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
6
+ // Blob / SharedArrayBuffer — binary is out of scope for the JSON envelope protocol.
7
+ return undefined;
8
+ }
9
+ /**
10
+ * Registers every `@WebSocketGateway` on the app's Hono instance using the
11
+ * runtime's Hono `upgradeWebSocket` factory — the same call works on node
12
+ * (`@hono/node-ws` / `@hono/node-server` v2), Bun (`hono/bun`), and Deno
13
+ * (`hono/deno`). Each connection auto-joins the room from a `:id` route param
14
+ * (or the route path), mirroring the Cloudflare DO-per-room model.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * // Node
19
+ * const app = await VelaFactory.create(AppModule);
20
+ * const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app: app.getHonoApp() });
21
+ * registerWebSocketGateways(app, upgradeWebSocket);
22
+ * const server = serve({ fetch: app.fetch, port: 3000 });
23
+ * injectWebSocket(server);
24
+ * ```
25
+ */ export function registerWebSocketGateways(app, upgradeWebSocket) {
26
+ const hono = app.getHonoApp();
27
+ const dispatcher = app.get(WsDispatcher);
28
+ const registry = app.get(WS_ROOM_REGISTRY);
29
+ for (const path of dispatcher.gatewayPaths){
30
+ hono.get(path, upgradeWebSocket((c)=>{
31
+ const roomId = c.req.param('id') ?? path;
32
+ let client;
33
+ // Connection-setup barrier: messages queue behind join + handleConnection
34
+ // so an auth check in handleConnection runs before any message dispatches.
35
+ let ready = Promise.resolve();
36
+ const onSetupError = (err)=>{
37
+ void dispatcher.handleError(path, client, err);
38
+ };
39
+ return {
40
+ onOpen: (_evt, ws)=>{
41
+ client = new NodeWsClient(ws, registry, path);
42
+ registry.register(client);
43
+ ready = Promise.resolve(client.join(roomId)).then(()=>dispatcher.handleOpen(path, client)).catch(onSetupError);
44
+ },
45
+ onMessage: (evt)=>{
46
+ const text = toText(evt.data);
47
+ if (text === undefined) return;
48
+ void ready.then(()=>dispatcher.dispatchMessage(path, client, text)).catch(onSetupError);
49
+ },
50
+ onClose: (evt)=>{
51
+ const code = evt.code || 1000;
52
+ const reason = evt.reason || '';
53
+ void Promise.resolve(dispatcher.handleClose(path, client, code, reason)).catch(onSetupError).finally(()=>registry.leaveAll(client));
54
+ },
55
+ onError: (evt)=>{
56
+ void dispatcher.handleError(path, client, evt);
57
+ }
58
+ };
59
+ }));
60
+ }
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/vela",
3
- "version": "1.8.8",
3
+ "version": "1.10.0",
4
4
  "description": "NestJS-compatible framework for edge runtimes, powered by Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,6 +21,26 @@
21
21
  "./schedule-node": {
22
22
  "types": "./dist/schedule-node/index.d.ts",
23
23
  "import": "./dist/schedule-node/index.js"
24
+ },
25
+ "./websocket": {
26
+ "types": "./dist/websocket/index.d.ts",
27
+ "import": "./dist/websocket/index.js"
28
+ },
29
+ "./websocket-node": {
30
+ "types": "./dist/websocket-node/index.d.ts",
31
+ "import": "./dist/websocket-node/index.js"
32
+ },
33
+ "./i18n": {
34
+ "types": "./dist/i18n/index.d.ts",
35
+ "import": "./dist/i18n/index.js"
36
+ },
37
+ "./storage": {
38
+ "types": "./dist/storage/index.d.ts",
39
+ "import": "./dist/storage/index.js"
40
+ },
41
+ "./seeder": {
42
+ "types": "./dist/seeder/index.d.ts",
43
+ "import": "./dist/seeder/index.js"
24
44
  }
25
45
  },
26
46
  "files": [
@@ -62,6 +82,14 @@
62
82
  "dependencies": {
63
83
  "hono": "^4.12.26"
64
84
  },
85
+ "peerDependencies": {
86
+ "intl-messageformat": ">=10"
87
+ },
88
+ "peerDependenciesMeta": {
89
+ "intl-messageformat": {
90
+ "optional": true
91
+ }
92
+ },
65
93
  "devDependencies": {
66
94
  "@cloudflare/vitest-pool-workers": "^0.16.18",
67
95
  "@swc/cli": "^0.8.1",
@@ -69,6 +97,7 @@
69
97
  "@types/bun": "latest",
70
98
  "@vitest/runner": "^4.1.9",
71
99
  "@vitest/snapshot": "^4.1.9",
100
+ "intl-messageformat": "^11.2.9",
72
101
  "typescript": "^6.0.3",
73
102
  "unplugin-swc": "^1.5.9",
74
103
  "vitest": "^4.1.9",
@@ -78,6 +107,7 @@
78
107
  "build": "rm -rf dist && swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly",
79
108
  "test": "vitest run",
80
109
  "test:workers": "vitest run --config vitest.config.workers.ts",
110
+ "test:workers:als": "vitest run --config vitest.config.workers-als.ts",
81
111
  "typecheck": "tsc --noEmit"
82
112
  }
83
113
  }