@smounters/imperium 1.0.3 → 1.1.1

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 CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to `@smounters/imperium` are documented in this file.
4
4
 
5
+ ## 1.1.0 - 2026-03-29
6
+
7
+ ### Added
8
+ - **Server Streaming RPC** — `async function*` handlers via ConnectRPC. Delivered as SSE (Server-Sent Events) in browsers without WebSocket.
9
+ - **`@RpcAbortSignal()` decorator** — injects `AbortSignal` from `HandlerContext.signal` for detecting client disconnects in streaming handlers.
10
+ - **WebSocket Gateway** — real-time bidirectional communication via `@fastify/websocket` (optional peer dependency).
11
+ - `@WsGateway(path)` — marks a class as a WebSocket gateway, registers on the specified path.
12
+ - `@WsHandler(messageType)` — routes incoming JSON messages by `type` field.
13
+ - `@WsConnection()`, `@WsMessage()`, `@WsRequest()` — parameter decorators for handler methods.
14
+ - Lifecycle hooks: `onConnection(socket, req)`, `onDisconnect(socket)`.
15
+ - Guards execute at connection time (upgrade request).
16
+ - **`./ws` subpath export** — `@smounters/imperium/ws` exposes `registerWsGateways`, `WsGatewayLifecycle`, and related types.
17
+ - `BaseContext` extended with `type: "ws"`, `switchToWs()`, and `WsArgumentsHost` interface.
18
+
19
+ ### Changed
20
+ - `ContextType` is now `"http" | "rpc" | "ws"` (was `"http" | "rpc"`).
21
+ - `RpcParamSource` extended with `"abort_signal"`.
22
+ - RPC router builder no longer throws on `server_streaming` methods; only `client_streaming` and `bidi_streaming` are unsupported.
23
+ - `WsArgumentsHost` methods return `| undefined` for type safety across context switches (no unsafe casts).
24
+ - Peer dependency `@fastify/websocket ^11.0.0` added as optional.
25
+
26
+ ### Updated
27
+ - `@types/node` 24.x → 25.x.
28
+ - `@typescript-eslint/*` 8.56 → 8.57.
29
+ - `eslint` 10.0 → 10.1.
30
+ - `fastify` 5.7 → 5.8.
31
+
5
32
  ## 1.0.3 - 2026-03-20
6
33
 
7
34
  ### Fixed
package/README.md CHANGED
@@ -1,38 +1,24 @@
1
1
  # @smounters/imperium
2
2
 
3
- `@smounters/imperium` is **inspired by NestJS** and provides a modular DI-first runtime for TypeScript services using Fastify + ConnectRPC.
3
+ NestJS-inspired modular DI framework for TypeScript services. Unified HTTP + ConnectRPC + WebSocket server on a single Fastify instance.
4
4
 
5
- It is designed for teams that want Nest-like architecture but with explicit control over runtime wiring and exported API surface.
5
+ ## Features
6
6
 
7
- ## Key Features
7
+ - **Module system** with `@Module`, `@Injectable`, guards, pipes, interceptors, filters
8
+ - **HTTP controllers** with `@HttpController`, `@Get`, `@Post`, `@Body`, `@Query`, `@Param`
9
+ - **ConnectRPC** with `@RpcService`, `@RpcMethod` (unary + server streaming)
10
+ - **WebSocket gateway** with `@WsGateway`, `@WsHandler`, message routing, lifecycle hooks
11
+ - **Request-scoped DI** via AsyncLocalStorage (tsyringe-based)
12
+ - **Typed config** via Zod + `ConfigService`
13
+ - **Structured logging** via tslog + `LoggerService`
8
14
 
9
- - Nest-like modules and decorators (`@Module`, `@Injectable`, guards/pipes/interceptors/filters).
10
- - Unified HTTP + RPC server on one Fastify instance.
11
- - Request-scoped handler execution.
12
- - Typed runtime config via `zod` + `ConfigService`.
13
- - Built-in `LoggerService` (tslog-based).
14
- - Global enhancer tokens (`APP_GUARD`, `APP_PIPE`, `APP_INTERCEPTOR`, `APP_FILTER`).
15
- - Multi-provider array injection (`InjectAll`) and manual array resolution (`resolveAll`).
16
-
17
- ## Public Imports
18
-
19
- Root import is intentionally disabled.
20
-
21
- Use subpaths only:
22
-
23
- - `@smounters/imperium/core`
24
- - `@smounters/imperium/decorators`
25
- - `@smounters/imperium/services`
26
- - `@smounters/imperium/pipes`
27
- - `@smounters/imperium/validation`
28
-
29
- ## Installation
15
+ ## Install
30
16
 
31
17
  ```bash
32
- pnpm add @smounters/imperium reflect-metadata tsyringe fastify @connectrpc/connect @connectrpc/connect-fastify zod
18
+ pnpm add @smounters/imperium reflect-metadata tsyringe fastify @connectrpc/connect @connectrpc/connect-fastify zod tslog
33
19
  ```
34
20
 
35
- TypeScript requirements:
21
+ TypeScript config requires:
36
22
 
37
23
  ```json
38
24
  {
@@ -43,190 +29,78 @@ TypeScript requirements:
43
29
  }
44
30
  ```
45
31
 
46
- Entry point must import metadata:
47
-
48
- ```ts
49
- import "reflect-metadata";
50
- ```
51
-
52
32
  ## Quick Start
53
33
 
54
34
  ```ts
55
35
  import "reflect-metadata";
56
-
57
36
  import { Application } from "@smounters/imperium/core";
58
37
  import { Body, HttpController, Injectable, Module, Post } from "@smounters/imperium/decorators";
59
38
 
60
39
  @Injectable()
61
- class AuthService {
62
- signIn(email: string) {
63
- return { ok: true, email };
40
+ class GreetService {
41
+ greet(name: string) {
42
+ return { message: `Hello, ${name}` };
64
43
  }
65
44
  }
66
45
 
67
- @HttpController("/auth")
68
- class AuthHttpController {
69
- constructor(private readonly authService: AuthService) {}
46
+ @HttpController("/api")
47
+ class ApiController {
48
+ constructor(private readonly greetService: GreetService) {}
70
49
 
71
- @Post("/sign-in")
72
- signIn(@Body("email") email: string) {
73
- return this.authService.signIn(email);
50
+ @Post("/greet")
51
+ greet(@Body("name") name: string) {
52
+ return this.greetService.greet(name);
74
53
  }
75
54
  }
76
55
 
77
56
  @Module({
78
- providers: [AuthService],
79
- httpControllers: [AuthHttpController],
57
+ providers: [GreetService],
58
+ httpControllers: [ApiController],
80
59
  })
81
60
  class AppModule {}
82
61
 
83
- const app = new Application(AppModule, {
84
- host: "0.0.0.0",
85
- accessLogs: true,
86
- });
87
-
88
- await app.start({ port: 3000 });
62
+ await new Application(AppModule).start({ port: 3000 });
89
63
  ```
90
64
 
91
- ## Recommended Bootstrap Flow
65
+ ## WebSocket
92
66
 
93
67
  ```ts
94
- import { Application } from "@smounters/imperium/core";
95
- import { ConfigService, LoggerService } from "@smounters/imperium/services";
96
- import { appConfigSchema, type AppConfig } from "@smounters/imperium/validation";
97
-
98
- const app = new Application(AppModule, {
99
- host: "0.0.0.0",
100
- accessLogs: true,
101
- });
102
-
103
- app.configureConfig(appConfigSchema, process.env);
68
+ import { WsGateway, WsHandler, WsConnection, WsMessage } from "@smounters/imperium/decorators";
69
+ import type { WsGatewayLifecycle } from "@smounters/imperium/ws";
70
+ import type { WebSocket } from "@fastify/websocket";
104
71
 
105
- const config = app.resolve(ConfigService<AppConfig>).getAll();
72
+ @WsGateway("/ws")
73
+ class EventsGateway implements WsGatewayLifecycle {
74
+ private clients = new Set<WebSocket>();
106
75
 
107
- app.configureLogger({
108
- name: "backend",
109
- minLevel: 3,
110
- });
76
+ onConnection(socket: WebSocket) { this.clients.add(socket); }
77
+ onDisconnect(socket: WebSocket) { this.clients.delete(socket); }
111
78
 
112
- await app.start({
113
- port: config.APP_PORT,
114
- prefix: config.APP_GLOBAL_PREFIX,
115
- });
116
-
117
- app.resolve(LoggerService).info({ event: "app.started", port: config.APP_PORT });
79
+ @WsHandler("ping")
80
+ onPing(@WsConnection() ws: WebSocket) {
81
+ ws.send(JSON.stringify({ type: "pong" }));
82
+ }
83
+ }
118
84
  ```
119
85
 
120
- If your app needs extra config fields, extend the exported base schema:
121
-
122
- ```ts
123
- import { appConfigSchema } from "@smounters/imperium/validation";
124
- import { z } from "zod";
86
+ Requires optional peer dependency: `pnpm add @fastify/websocket`
125
87
 
126
- const projectConfigSchema = appConfigSchema.extend({
127
- REDIS_URL: z.url(),
128
- });
129
- ```
88
+ ## Import Paths
130
89
 
131
- ## Multi Providers
90
+ No root import. Use subpaths:
132
91
 
133
92
  ```ts
134
- import { InjectAll, Injectable, Module } from "@smounters/imperium/decorators";
135
-
136
- const AML_RULES = Symbol("AML_RULES");
137
-
138
- @Module({
139
- providers: [
140
- { provide: AML_RULES, multi: true, useClass: SanctionsRule },
141
- { provide: AML_RULES, multi: true, useClass: MixerRule },
142
- { provide: AML_RULES, multi: true, useClass: FreshAddressRule },
143
- ],
144
- exports: [AML_RULES],
145
- })
146
- class AmlModule {}
147
-
148
- @Injectable()
149
- class AmlEngine {
150
- constructor(@InjectAll(AML_RULES) private readonly rules: AmlRule[]) {}
151
- }
152
- ```
153
-
154
- Manual resolution is also available:
155
-
156
- ```ts
157
- const rules = app.resolveAll<AmlRule>(AML_RULES);
93
+ import { Application } from "@smounters/imperium/core";
94
+ import { Module, Injectable, HttpController, Get } from "@smounters/imperium/decorators";
95
+ import { ConfigService, LoggerService } from "@smounters/imperium/services";
96
+ import { ZodPipe } from "@smounters/imperium/pipes";
97
+ import { appConfigSchema } from "@smounters/imperium/validation";
98
+ import { registerWsGateways } from "@smounters/imperium/ws";
158
99
  ```
159
100
 
160
- ## HTTP and RPC
161
-
162
- Use decorators from `@smounters/imperium/decorators`:
163
-
164
- - HTTP: `HttpController`, `Get`, `Post`, `Put`, `Patch`, `Delete`, `Body`, `Query`, `Param`, `Header`, `Req`, `Res`
165
- - RPC: `RpcService`, `RpcMethod`, `RpcData`, `RpcContext`, `RpcHeaders`, `RpcHeader`
166
-
167
- Imperium auto-detects registered HTTP/RPC handlers and serves both protocols from one server.
168
-
169
- ## Services
170
-
171
- From `@smounters/imperium/services`:
172
-
173
- - `ConfigService`
174
- - `LoggerService`
175
-
176
- ## Pipes and Validation
177
-
178
- - `ZodPipe` from `@smounters/imperium/pipes`
179
- - validation helpers from `@smounters/imperium/validation`
180
- - `appConfigSchema`
181
- - `AppConfig`
182
- - `booleanSchema`
183
- - `numberSchema`
184
- - `nativeEnumSchema`
185
- - `stringArraySchema`
186
- - `enumArraySchema`
187
-
188
- ## Error Classes
189
-
190
- From `@smounters/imperium/core`:
191
-
192
- - `HttpException`
193
- - `BadRequestException`
194
- - `UnauthorizedException`
195
- - `ForbiddenException`
196
- - `NotFoundException`
197
- - `InternalServerErrorException`
198
-
199
101
  ## Documentation
200
102
 
201
- Full docs (VitePress) are located in:
202
-
203
- - `docs`
204
-
205
- Local docs commands:
206
-
207
- ```bash
208
- pnpm run docs:dev
209
- pnpm run docs:build
210
- ```
211
-
212
- GitHub Pages deployment is configured via `.github/workflows/publish.yml`.
213
-
214
- ## Publish to npm
215
-
216
- ```bash
217
- pnpm install
218
- pnpm run typecheck
219
- pnpm run build
220
- pnpm publish --access public --no-git-checks
221
- ```
222
-
223
- Automated npm publishing workflow:
224
-
225
- - `.github/workflows/publish.yml`
226
-
227
- Required secret:
228
-
229
- - `NPM_TOKEN`
103
+ Full guide and API reference: **[smounters.github.io/imperium](https://smounters.github.io/imperium/)**
230
104
 
231
105
  ## License
232
106
 
@@ -11,8 +11,10 @@ export declare class AppContainer {
11
11
  private readonly loadingModules;
12
12
  private readonly rpcControllerSet;
13
13
  private readonly httpControllerSet;
14
+ private readonly wsGatewaySet;
14
15
  private readonly rpcControllerModules;
15
16
  private readonly httpControllerModules;
17
+ private readonly wsGatewayModules;
16
18
  private readonly lifecycleTargets;
17
19
  private readonly requestScopeOwners;
18
20
  private readonly requestScopeLinkedScopes;
@@ -20,6 +22,7 @@ export declare class AppContainer {
20
22
  private rootModuleRef;
21
23
  private controllers;
22
24
  private httpControllers;
25
+ private wsGateways;
23
26
  private lifecycleInstances;
24
27
  private initialized;
25
28
  private closed;
@@ -65,6 +68,7 @@ export declare class AppContainer {
65
68
  createRequestScope(controller?: Constructor): DependencyContainer;
66
69
  getControllers(): Constructor[];
67
70
  getHttpControllers(): Constructor[];
71
+ getWsGateways(): Constructor[];
68
72
  setGlobalGuards(guards: GuardLike[]): void;
69
73
  getGlobalGuards(): GuardLike[];
70
74
  setGlobalInterceptors(inters: InterceptorLike[]): void;
@@ -2,6 +2,7 @@ import "reflect-metadata";
2
2
  import { container, Lifecycle } from "tsyringe";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
4
  import { MODULE_KEY } from "../decorators/di.decorators";
5
+ import { WS_GATEWAY_KEY } from "../decorators/ws.decorators";
5
6
  import { ConfigService, LoggerService } from "../services";
6
7
  import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from "./app-tokens";
7
8
  import { CONFIG_TOKEN } from "./config";
@@ -121,8 +122,10 @@ export class AppContainer {
121
122
  this.loadingModules = new Set();
122
123
  this.rpcControllerSet = new Set();
123
124
  this.httpControllerSet = new Set();
125
+ this.wsGatewaySet = new Set();
124
126
  this.rpcControllerModules = new Map();
125
127
  this.httpControllerModules = new Map();
128
+ this.wsGatewayModules = new Map();
126
129
  this.lifecycleTargets = [];
127
130
  this.requestScopeOwners = new WeakMap();
128
131
  this.requestScopeLinkedScopes = new WeakMap();
@@ -130,6 +133,7 @@ export class AppContainer {
130
133
  this.rootModuleRef = null;
131
134
  this.controllers = [];
132
135
  this.httpControllers = [];
136
+ this.wsGateways = [];
133
137
  this.lifecycleInstances = null;
134
138
  this.initialized = false;
135
139
  this.closed = false;
@@ -225,7 +229,7 @@ export class AppContainer {
225
229
  return provider.useValue;
226
230
  }
227
231
  if ("useFactory" in provider) {
228
- return provider.useFactory(moduleRef.container);
232
+ return (provider).useFactory(moduleRef.container);
229
233
  }
230
234
  if ("useExisting" in provider) {
231
235
  return moduleRef.container.resolve(provider.useExisting);
@@ -277,7 +281,7 @@ export class AppContainer {
277
281
  }
278
282
  if ("useFactory" in provider) {
279
283
  moduleRef.container.register(provider.provide, {
280
- useFactory: (dc) => provider.useFactory(dc),
284
+ useFactory: (dc) => (provider).useFactory(dc),
281
285
  });
282
286
  return;
283
287
  }
@@ -324,7 +328,7 @@ export class AppContainer {
324
328
  }
325
329
  }
326
330
  resolveControllerModule(controller) {
327
- return this.httpControllerModules.get(controller) ?? this.rpcControllerModules.get(controller);
331
+ return this.httpControllerModules.get(controller) ?? this.rpcControllerModules.get(controller) ?? this.wsGatewayModules.get(controller);
328
332
  }
329
333
  getOrCreateLinkedScopes(originScope) {
330
334
  const existing = this.requestScopeLinkedScopes.get(originScope);
@@ -433,6 +437,17 @@ export class AppContainer {
433
437
  this.httpControllerModules.set(ctrl, moduleRef);
434
438
  this.httpControllers.push(ctrl);
435
439
  }
440
+ // Detect WS gateways among providers
441
+ for (const provider of meta.providers) {
442
+ const providerClass = typeof provider === "function" ? provider : undefined;
443
+ if (providerClass && Reflect.hasMetadata(WS_GATEWAY_KEY, providerClass)) {
444
+ if (!this.wsGatewaySet.has(providerClass)) {
445
+ this.wsGatewaySet.add(providerClass);
446
+ this.wsGatewayModules.set(providerClass, moduleRef);
447
+ this.wsGateways.push(providerClass);
448
+ }
449
+ }
450
+ }
436
451
  moduleRef.exportedTokens = this.resolveModuleExports(moduleRef);
437
452
  if (moduleRef.meta.global) {
438
453
  this.registerGlobalExports(moduleRef);
@@ -452,9 +467,7 @@ export class AppContainer {
452
467
  }
453
468
  loadModule(moduleImport) {
454
469
  const moduleRef = this.loadModuleRef(moduleImport);
455
- if (!this.rootModuleRef) {
456
- this.rootModuleRef = moduleRef;
457
- }
470
+ this.rootModuleRef ??= moduleRef;
458
471
  }
459
472
  resolve(token) {
460
473
  if (this.root.isRegistered(token, false)) {
@@ -660,6 +673,9 @@ export class AppContainer {
660
673
  getHttpControllers() {
661
674
  return [...this.httpControllers];
662
675
  }
676
+ getWsGateways() {
677
+ return [...this.wsGateways];
678
+ }
663
679
  setGlobalGuards(guards) {
664
680
  this.globalGuards = [...guards];
665
681
  }
@@ -6,6 +6,7 @@ import { HTTP_ROUTES_KEY } from "../decorators/http.decorators";
6
6
  import { RPC_METHODS_KEY, RPC_SERVICE_KEY } from "../decorators/rpc.decorators";
7
7
  import { registerHttpRoutes } from "../http";
8
8
  import { buildConnectRoutes } from "../rpc";
9
+ import { registerWsGateways } from "../ws";
9
10
  import { AppContainer } from "./container";
10
11
  function normalizePrefix(prefix) {
11
12
  if (!prefix) {
@@ -151,8 +152,9 @@ export async function startServer(diOrOptions, options) {
151
152
  const healthConfig = resolveHealth(health);
152
153
  const http = hasRegisteredHttpHandlers(di);
153
154
  const rpc = hasRegisteredRpcHandlers(di);
154
- if (!http && !rpc && !healthConfig.enabled) {
155
- throw new Error("No handlers found for HTTP or RPC. Register controllers in @Module().");
155
+ const ws = di.getWsGateways().length > 0;
156
+ if (!http && !rpc && !ws && !healthConfig.enabled) {
157
+ throw new Error("No handlers found for HTTP, RPC, or WebSocket. Register controllers in @Module().");
156
158
  }
157
159
  let server;
158
160
  try {
@@ -177,9 +179,10 @@ export async function startServer(diOrOptions, options) {
177
179
  const corsConfig = resolveCors(cors);
178
180
  const effectiveHttpPrefix = mergePrefixes(prefix, httpPrefix);
179
181
  const effectiveRpcPrefix = mergePrefixes(prefix, rpcPrefix);
180
- server.addHook("onRequest", async (req) => {
182
+ server.addHook("onRequest", (req, _reply, done) => {
181
183
  const request = req;
182
184
  request.requestStartAt = Date.now();
185
+ done();
183
186
  });
184
187
  server.addHook("onResponse", async (req, reply) => {
185
188
  const request = req;
@@ -254,6 +257,9 @@ export async function startServer(diOrOptions, options) {
254
257
  routes: buildConnectRoutes(di),
255
258
  });
256
259
  }
260
+ if (ws) {
261
+ await registerWsGateways(server, di);
262
+ }
257
263
  await server.listen({ port: listenPort, host });
258
264
  return server;
259
265
  }
@@ -11,7 +11,7 @@ export interface HttpParamMeta {
11
11
  source: HttpParamSource;
12
12
  key?: string;
13
13
  }
14
- export type RpcParamSource = "data" | "context" | "headers" | "header";
14
+ export type RpcParamSource = "data" | "context" | "headers" | "header" | "abort_signal";
15
15
  export interface RpcParamMeta {
16
16
  index: number;
17
17
  source: RpcParamSource;
@@ -5,4 +5,5 @@ export { Body, Delete, Get, Header, HttpController, Param, Patch, Post, Put, Que
5
5
  export { UseInterceptors } from "./interceptors.decorators";
6
6
  export { SetMetadata } from "./metadata.decorators";
7
7
  export { UsePipes } from "./pipes.decorators";
8
- export { RpcContext, RpcData, RpcHeader, RpcHeaders, RpcMethod, RpcService } from "./rpc.decorators";
8
+ export { RpcAbortSignal, RpcContext, RpcData, RpcHeader, RpcHeaders, RpcMethod, RpcService } from "./rpc.decorators";
9
+ export { WsConnection, WsGateway, WsHandler, WsMessage, WsRequest } from "./ws.decorators";
@@ -5,4 +5,5 @@ export { Body, Delete, Get, Header, HttpController, Param, Patch, Post, Put, Que
5
5
  export { UseInterceptors } from "./interceptors.decorators";
6
6
  export { SetMetadata } from "./metadata.decorators";
7
7
  export { UsePipes } from "./pipes.decorators";
8
- export { RpcContext, RpcData, RpcHeader, RpcHeaders, RpcMethod, RpcService } from "./rpc.decorators";
8
+ export { RpcAbortSignal, RpcContext, RpcData, RpcHeader, RpcHeaders, RpcMethod, RpcService } from "./rpc.decorators";
9
+ export { WsConnection, WsGateway, WsHandler, WsMessage, WsRequest } from "./ws.decorators";
@@ -9,3 +9,4 @@ export declare const RpcData: (key?: string) => ParameterDecorator;
9
9
  export declare const RpcContext: () => ParameterDecorator;
10
10
  export declare const RpcHeaders: () => ParameterDecorator;
11
11
  export declare const RpcHeader: (key: string) => ParameterDecorator;
12
+ export declare const RpcAbortSignal: () => ParameterDecorator;
@@ -48,3 +48,4 @@ export const RpcData = (key) => rpcParamDecorator("data", key);
48
48
  export const RpcContext = () => rpcParamDecorator("context");
49
49
  export const RpcHeaders = () => rpcParamDecorator("headers");
50
50
  export const RpcHeader = (key) => rpcParamDecorator("header", key);
51
+ export const RpcAbortSignal = () => rpcParamDecorator("abort_signal");
@@ -0,0 +1,9 @@
1
+ import "reflect-metadata";
2
+ export declare const WS_GATEWAY_KEY: unique symbol;
3
+ export declare const WS_HANDLERS_KEY: unique symbol;
4
+ export declare const WS_PARAMS_KEY: unique symbol;
5
+ export declare function WsGateway(path?: string): ClassDecorator;
6
+ export declare function WsHandler(messageType: string): MethodDecorator;
7
+ export declare const WsConnection: () => ParameterDecorator;
8
+ export declare const WsMessage: () => ParameterDecorator;
9
+ export declare const WsRequest: () => ParameterDecorator;
@@ -0,0 +1,43 @@
1
+ import "reflect-metadata";
2
+ import { Injectable } from "./di.decorators";
3
+ export const WS_GATEWAY_KEY = Symbol("ws:gateway");
4
+ export const WS_HANDLERS_KEY = Symbol("ws:handlers");
5
+ export const WS_PARAMS_KEY = Symbol("ws:params");
6
+ export function WsGateway(path = "/ws") {
7
+ return (target) => {
8
+ Injectable()(target);
9
+ const meta = { path };
10
+ Reflect.defineMetadata(WS_GATEWAY_KEY, meta, target);
11
+ };
12
+ }
13
+ export function WsHandler(messageType) {
14
+ return (target, propertyKey) => {
15
+ const handlers = Reflect.getMetadata(WS_HANDLERS_KEY, target.constructor) ?? [];
16
+ handlers.push({ messageType, handlerName: propertyKey });
17
+ Reflect.defineMetadata(WS_HANDLERS_KEY, handlers, target.constructor);
18
+ };
19
+ }
20
+ function getMetaTarget(target, propertyKey) {
21
+ if (propertyKey === undefined) {
22
+ return target;
23
+ }
24
+ const value = target[propertyKey];
25
+ if (typeof value !== "object" && typeof value !== "function") {
26
+ throw new Error("Decorator target is not an object");
27
+ }
28
+ return value;
29
+ }
30
+ function setParamMeta(target, propertyKey, meta) {
31
+ const metaTarget = getMetaTarget(target, propertyKey);
32
+ const existing = Reflect.getMetadata(WS_PARAMS_KEY, metaTarget);
33
+ const next = existing ? [...existing, meta] : [meta];
34
+ Reflect.defineMetadata(WS_PARAMS_KEY, next, metaTarget);
35
+ }
36
+ function wsParamDecorator(source) {
37
+ return (target, propertyKey, index) => {
38
+ setParamMeta(target, propertyKey ?? undefined, { index, source });
39
+ };
40
+ }
41
+ export const WsConnection = () => wsParamDecorator("connection");
42
+ export const WsMessage = () => wsParamDecorator("message");
43
+ export const WsRequest = () => wsParamDecorator("request");
@@ -135,6 +135,11 @@ export function createHttpHandler(app, controller, methodName) {
135
135
  getData: () => undefined,
136
136
  getContext: () => undefined,
137
137
  }),
138
+ switchToWs: () => ({
139
+ getSocket: () => undefined,
140
+ getRequest: () => undefined,
141
+ getMessage: () => undefined,
142
+ }),
138
143
  };
139
144
  try {
140
145
  const instance = requestScope.resolve(controller);
@@ -159,7 +164,7 @@ export function createHttpHandler(app, controller, methodName) {
159
164
  transformedArgs[index] = value;
160
165
  }
161
166
  const interceptors = collectInterceptorsForHttp(controller, methodName, app.getGlobalInterceptors()).map((interceptorLike) => resolveEnhancer(requestScope, interceptorLike));
162
- const controllerHandler = (instance[methodName].bind(instance));
167
+ const controllerHandler = instance[methodName].bind(instance);
163
168
  let idx = -1;
164
169
  const next = async () => {
165
170
  idx++;
@@ -50,6 +50,9 @@ function buildRpcArgs(payload, context, handler) {
50
50
  case "header":
51
51
  args[meta.index] = meta.key ? context.requestHeader.get(meta.key) : undefined;
52
52
  break;
53
+ case "abort_signal":
54
+ args[meta.index] = context.signal;
55
+ break;
53
56
  }
54
57
  }
55
58
  return args;
@@ -124,6 +127,11 @@ export function createRpcHandler(app, controller, methodName) {
124
127
  getData: () => req,
125
128
  getContext: () => context,
126
129
  }),
130
+ switchToWs: () => ({
131
+ getSocket: () => undefined,
132
+ getRequest: () => undefined,
133
+ getMessage: () => undefined,
134
+ }),
127
135
  };
128
136
  try {
129
137
  const instance = scope.resolve(controller);
@@ -1,3 +1,4 @@
1
1
  export * from "./adapter";
2
+ export * from "./streaming-adapter";
2
3
  export * from "./router-builder";
3
4
  export * from "./utils";
package/dist/rpc/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./adapter";
2
+ export * from "./streaming-adapter";
2
3
  export * from "./router-builder";
3
4
  export * from "./utils";
@@ -1,15 +1,7 @@
1
1
  import "reflect-metadata";
2
2
  import { RPC_METHODS_KEY, RPC_SERVICE_KEY } from "../decorators/rpc.decorators";
3
3
  import { createRpcHandler } from "./adapter";
4
- function assertUnaryRpcMethod(controller, handlerName, method) {
5
- if (method.methodKind === "unary") {
6
- return;
7
- }
8
- const serviceName = method.parent.name;
9
- const rpcName = method.name;
10
- throw new Error(`Streaming RPC methods are not supported: ${serviceName}.${rpcName} (${method.methodKind}) ` +
11
- `at ${controller.name}.${handlerName}. Only unary methods are supported.`);
12
- }
4
+ import { createStreamingRpcHandler } from "./streaming-adapter";
13
5
  export function buildConnectRoutes(di) {
14
6
  return (router) => {
15
7
  for (const ctrl of di.getControllers()) {
@@ -20,8 +12,18 @@ export function buildConnectRoutes(di) {
20
12
  }
21
13
  const methods = Reflect.getMetadata(RPC_METHODS_KEY, controller) ?? [];
22
14
  for (const { method, handlerName } of methods) {
23
- assertUnaryRpcMethod(controller, handlerName, method);
24
- router.rpc(method, createRpcHandler(di, controller, handlerName));
15
+ if (method.methodKind === "unary") {
16
+ router.rpc(method, createRpcHandler(di, controller, handlerName));
17
+ }
18
+ else if (method.methodKind === "server_streaming") {
19
+ router.rpc(method, createStreamingRpcHandler(di, controller, handlerName));
20
+ }
21
+ else {
22
+ const serviceName = method.parent.name;
23
+ const rpcName = method.name;
24
+ throw new Error(`Client streaming and bidi streaming are not supported: ${serviceName}.${rpcName} (${method.methodKind}) ` +
25
+ `at ${controller.name}.${handlerName}. Use unary or server_streaming.`);
26
+ }
25
27
  }
26
28
  }
27
29
  };
@@ -0,0 +1,5 @@
1
+ import type { HandlerContext } from "@connectrpc/connect";
2
+ import "reflect-metadata";
3
+ import type { AppContainer } from "../core/container";
4
+ import type { Constructor } from "../types";
5
+ export declare function createStreamingRpcHandler<TController extends Record<string, unknown>>(app: AppContainer, controller: Constructor<TController>, methodName: keyof TController & string): (req: unknown, context: HandlerContext) => AsyncIterable<unknown>;
@@ -0,0 +1,186 @@
1
+ import "reflect-metadata";
2
+ import { ForbiddenException, toConnectError } from "../core/errors";
3
+ import { CATCH_EXCEPTIONS_KEY } from "../decorators/filters.decorators";
4
+ import { RPC_PARAMS_KEY } from "../decorators/rpc.decorators";
5
+ import { LoggerService } from "../services";
6
+ import { collectFiltersForRpc, collectGuardsForRpc, collectPipesForRpc } from "./utils";
7
+ function logRpcError(app, scope, details, error) {
8
+ try {
9
+ scope.resolve(LoggerService).error(details, error);
10
+ return;
11
+ }
12
+ catch {
13
+ // fallback below
14
+ }
15
+ try {
16
+ app.getLogger().error(details, error);
17
+ return;
18
+ }
19
+ catch {
20
+ // final fallback
21
+ }
22
+ console.error("[imperium] rpc_streaming_error", details, error);
23
+ }
24
+ function getPayloadValue(payload, key) {
25
+ if (!key) {
26
+ return payload;
27
+ }
28
+ if (!payload || typeof payload !== "object") {
29
+ return undefined;
30
+ }
31
+ return payload[key];
32
+ }
33
+ function buildRpcArgs(payload, context, handler) {
34
+ const metas = Reflect.getMetadata(RPC_PARAMS_KEY, handler) ?? [];
35
+ if (!metas.length) {
36
+ return [payload];
37
+ }
38
+ const args = [];
39
+ for (const meta of metas) {
40
+ switch (meta.source) {
41
+ case "data":
42
+ args[meta.index] = getPayloadValue(payload, meta.key);
43
+ break;
44
+ case "context":
45
+ args[meta.index] = context;
46
+ break;
47
+ case "headers":
48
+ args[meta.index] = context.requestHeader;
49
+ break;
50
+ case "header":
51
+ args[meta.index] = meta.key ? context.requestHeader.get(meta.key) : undefined;
52
+ break;
53
+ case "abort_signal":
54
+ args[meta.index] = context.signal;
55
+ break;
56
+ }
57
+ }
58
+ return args;
59
+ }
60
+ function resolveEnhancer(scope, enhancer) {
61
+ if (typeof enhancer === "function") {
62
+ return scope.resolve(enhancer);
63
+ }
64
+ return enhancer;
65
+ }
66
+ function canHandleException(error, filter) {
67
+ const exceptions = Reflect.getMetadata(CATCH_EXCEPTIONS_KEY, filter.constructor);
68
+ if (!exceptions || exceptions.length === 0) {
69
+ return true;
70
+ }
71
+ if (!(error instanceof Error)) {
72
+ return false;
73
+ }
74
+ return exceptions.some((ExceptionType) => error instanceof ExceptionType);
75
+ }
76
+ async function runRpcFilters(error, filters, ctx) {
77
+ let currentError = error;
78
+ for (const filter of filters) {
79
+ if (!canHandleException(currentError, filter)) {
80
+ continue;
81
+ }
82
+ try {
83
+ const result = await filter.catch(currentError, ctx);
84
+ if (result !== undefined) {
85
+ return {
86
+ handled: true,
87
+ error: currentError,
88
+ result,
89
+ };
90
+ }
91
+ }
92
+ catch (nextError) {
93
+ currentError = nextError;
94
+ }
95
+ }
96
+ return {
97
+ handled: false,
98
+ error: currentError,
99
+ };
100
+ }
101
+ export function createStreamingRpcHandler(app, controller, methodName) {
102
+ return async function* (req, context) {
103
+ const scope = app.createRequestScope(controller);
104
+ const handler = controller.prototype[methodName];
105
+ const ctx = {
106
+ type: "rpc",
107
+ method: methodName,
108
+ headers: context.requestHeader,
109
+ rpc: {
110
+ data: req,
111
+ context,
112
+ },
113
+ controller,
114
+ handler,
115
+ getType: () => "rpc",
116
+ getClass: () => controller,
117
+ getHandler: () => handler,
118
+ switchToHttp: () => ({
119
+ getRequest: () => undefined,
120
+ getResponse: () => undefined,
121
+ }),
122
+ switchToRpc: () => ({
123
+ getData: () => req,
124
+ getContext: () => context,
125
+ }),
126
+ switchToWs: () => ({
127
+ getSocket: () => undefined,
128
+ getRequest: () => undefined,
129
+ getMessage: () => undefined,
130
+ }),
131
+ };
132
+ try {
133
+ await app.runInRequestScope(scope, async () => {
134
+ // Resolve instance inside request scope
135
+ });
136
+ // We need to run guards/pipes inside the scope, then yield outside of runInRequestScope
137
+ // because async generators cannot be fully consumed inside a single runInRequestScope call.
138
+ // Instead, we set up the scope context and run guards/pipes, then yield from the generator.
139
+ const instance = scope.resolve(controller);
140
+ const guards = collectGuardsForRpc(controller, methodName, app.getGlobalGuards()).map((guardLike) => resolveEnhancer(scope, guardLike));
141
+ for (const guard of guards) {
142
+ const ok = await guard.canActivate(ctx);
143
+ if (!ok) {
144
+ throw new ForbiddenException();
145
+ }
146
+ }
147
+ let payload = req;
148
+ const pipes = collectPipesForRpc(controller, methodName, app.getGlobalPipes()).map((pipeLike) => resolveEnhancer(scope, pipeLike));
149
+ for (const pipe of pipes) {
150
+ payload = await pipe.transform(payload, ctx);
151
+ }
152
+ const args = buildRpcArgs(payload, context, handler);
153
+ const generator = instance[methodName].apply(instance, args);
154
+ yield* generator;
155
+ }
156
+ catch (error) {
157
+ logRpcError(app, scope, {
158
+ type: "rpc_streaming_error",
159
+ controller: controller.name,
160
+ handler: methodName,
161
+ procedure: context.method.name,
162
+ }, error);
163
+ const filters = collectFiltersForRpc(controller, methodName, app.getGlobalFilters()).map((filterLike) => resolveEnhancer(scope, filterLike));
164
+ const filtered = await runRpcFilters(error, filters, ctx);
165
+ if (filtered.handled) {
166
+ return;
167
+ }
168
+ throw toConnectError(filtered.error, {
169
+ exposeInternalErrors: app.shouldExposeInternalErrors(),
170
+ });
171
+ }
172
+ finally {
173
+ try {
174
+ await app.disposeRequestScope(scope);
175
+ }
176
+ catch (disposeError) {
177
+ logRpcError(app, scope, {
178
+ type: "rpc_streaming_scope_dispose_error",
179
+ controller: controller.name,
180
+ handler: methodName,
181
+ procedure: context.method.name,
182
+ }, disposeError);
183
+ }
184
+ }
185
+ };
186
+ }
package/dist/types.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { ISettingsParam } from "tslog";
3
3
  import type { DependencyContainer, RegistrationOptions, InjectionToken as TsyringeInjectionToken } from "tsyringe";
4
4
  import type { ZodType } from "zod";
5
5
  export type Constructor<T = unknown> = new (...args: any[]) => T;
6
- export type ContextType = "http" | "rpc";
6
+ export type ContextType = "http" | "rpc" | "ws";
7
7
  export type LoggerOptions = ISettingsParam<Record<string, unknown>>;
8
8
  export type InjectionToken<T = unknown> = TsyringeInjectionToken<T>;
9
9
  export type MetadataKey = string | symbol;
@@ -39,8 +39,7 @@ interface BaseModuleMeta {
39
39
  exports?: InjectionToken[];
40
40
  global?: boolean;
41
41
  }
42
- export interface ModuleMeta extends BaseModuleMeta {
43
- }
42
+ export type ModuleMeta = BaseModuleMeta;
44
43
  export interface DynamicModule extends BaseModuleMeta {
45
44
  module: Constructor;
46
45
  id?: string;
@@ -69,6 +68,11 @@ export interface RpcArgumentsHost {
69
68
  getData<T = unknown>(): T | undefined;
70
69
  getContext<T = unknown>(): T | undefined;
71
70
  }
71
+ export interface WsArgumentsHost {
72
+ getSocket(): unknown;
73
+ getRequest(): FastifyRequest | undefined;
74
+ getMessage(): unknown;
75
+ }
72
76
  export interface BaseContext {
73
77
  type: ContextType;
74
78
  method: string;
@@ -82,6 +86,11 @@ export interface BaseContext {
82
86
  data: unknown;
83
87
  context: unknown;
84
88
  };
89
+ ws?: {
90
+ socket: unknown;
91
+ request: FastifyRequest;
92
+ message?: unknown;
93
+ };
85
94
  controller: Constructor;
86
95
  handler: Function;
87
96
  getType(): ContextType;
@@ -89,6 +98,7 @@ export interface BaseContext {
89
98
  getHandler(): Function;
90
99
  switchToHttp(): HttpArgumentsHost;
91
100
  switchToRpc(): RpcArgumentsHost;
101
+ switchToWs(): WsArgumentsHost;
92
102
  }
93
103
  export interface Guard {
94
104
  canActivate(ctx: BaseContext): Promise<boolean> | boolean;
@@ -104,7 +114,7 @@ export interface PipeTransform<TIn = unknown, TOut = unknown> {
104
114
  }
105
115
  export type PipeLike = Constructor<PipeTransform> | PipeTransform;
106
116
  export interface ExceptionFilter<T = unknown> {
107
- catch(exception: T, ctx: BaseContext): Promise<unknown> | unknown;
117
+ catch(exception: T, ctx: BaseContext): unknown;
108
118
  }
109
119
  export type ExceptionFilterLike = Constructor<ExceptionFilter> | ExceptionFilter;
110
120
  export interface ConfigServiceOptions<TSchema extends ZodType = ZodType> {
@@ -113,7 +123,7 @@ export interface ConfigServiceOptions<TSchema extends ZodType = ZodType> {
113
123
  }
114
124
  export interface CorsOptions {
115
125
  enabled?: boolean;
116
- origin?: string | boolean | RegExp | Array<string | boolean | RegExp>;
126
+ origin?: string | boolean | RegExp | (string | boolean | RegExp)[];
117
127
  credentials?: boolean;
118
128
  exposedHeaders?: string | string[];
119
129
  allowedHeaders?: string | string[];
@@ -0,0 +1,6 @@
1
+ import type { FastifyRequest } from "fastify";
2
+ import type { WebSocket } from "@fastify/websocket";
3
+ import "reflect-metadata";
4
+ import type { AppContainer } from "../core/container";
5
+ import type { Constructor } from "../types";
6
+ export declare function handleWsConnection(app: AppContainer, gateway: Constructor, socket: WebSocket, request: FastifyRequest): void;
@@ -0,0 +1,168 @@
1
+ import "reflect-metadata";
2
+ import { GUARDS_KEY } from "../decorators/guards.decorators";
3
+ import { WS_HANDLERS_KEY, WS_PARAMS_KEY } from "../decorators/ws.decorators";
4
+ import { Reflector } from "../core/reflector";
5
+ import { LoggerService } from "../services";
6
+ const reflector = new Reflector();
7
+ function collectGuardsForWs(gateway, app) {
8
+ const classGuards = (reflector.get(GUARDS_KEY, gateway) ?? []);
9
+ return [...app.getGlobalGuards(), ...classGuards];
10
+ }
11
+ function resolveEnhancer(scope, enhancer) {
12
+ if (typeof enhancer === "function") {
13
+ return scope.resolve(enhancer);
14
+ }
15
+ return enhancer;
16
+ }
17
+ function buildWsArgs(socket, request, message, handler) {
18
+ const metas = Reflect.getMetadata(WS_PARAMS_KEY, handler) ?? [];
19
+ if (!metas.length) {
20
+ return [socket, message, request];
21
+ }
22
+ const args = [];
23
+ for (const meta of metas) {
24
+ switch (meta.source) {
25
+ case "connection":
26
+ args[meta.index] = socket;
27
+ break;
28
+ case "message":
29
+ args[meta.index] = message;
30
+ break;
31
+ case "request":
32
+ args[meta.index] = request;
33
+ break;
34
+ }
35
+ }
36
+ return args;
37
+ }
38
+ function buildWsContext(gateway, methodName, handler, socket, request, message) {
39
+ return {
40
+ type: "ws",
41
+ method: methodName,
42
+ controller: gateway,
43
+ handler,
44
+ ws: {
45
+ socket,
46
+ request,
47
+ message,
48
+ },
49
+ getType: () => "ws",
50
+ getClass: () => gateway,
51
+ getHandler: () => handler,
52
+ switchToHttp: () => ({
53
+ getRequest: () => undefined,
54
+ getResponse: () => undefined,
55
+ }),
56
+ switchToRpc: () => ({
57
+ getData: () => undefined,
58
+ getContext: () => undefined,
59
+ }),
60
+ switchToWs: () => ({
61
+ getSocket: () => socket,
62
+ getRequest: () => request,
63
+ getMessage: () => message,
64
+ }),
65
+ };
66
+ }
67
+ function logWsError(app, scope, details, error) {
68
+ try {
69
+ scope.resolve(LoggerService).error(details, error);
70
+ return;
71
+ }
72
+ catch {
73
+ // fallback
74
+ }
75
+ try {
76
+ app.getLogger().error(details, error);
77
+ return;
78
+ }
79
+ catch {
80
+ // final fallback
81
+ }
82
+ console.error("[imperium] ws_error", details, error);
83
+ }
84
+ export function handleWsConnection(app, gateway, socket, request) {
85
+ const scope = app.createRequestScope(gateway);
86
+ const handlers = Reflect.getMetadata(WS_HANDLERS_KEY, gateway) ?? [];
87
+ const runAsync = async () => {
88
+ const instance = scope.resolve(gateway);
89
+ // Run guards at connection time
90
+ const guardLikes = collectGuardsForWs(gateway, app);
91
+ const guards = guardLikes.map((g) => resolveEnhancer(scope, g));
92
+ const ctx = buildWsContext(gateway, "onConnection", gateway, socket, request);
93
+ for (const guard of guards) {
94
+ const ok = await guard.canActivate(ctx);
95
+ if (!ok) {
96
+ socket.close(4403, "Forbidden");
97
+ await app.disposeRequestScope(scope);
98
+ return;
99
+ }
100
+ }
101
+ // Call onConnection lifecycle
102
+ if (typeof instance.onConnection === "function") {
103
+ await instance.onConnection(socket, request);
104
+ }
105
+ // Route messages to handlers
106
+ socket.on("message", (raw) => {
107
+ void (async () => {
108
+ try {
109
+ const msg = JSON.parse(String(raw));
110
+ const handler = handlers.find((h) => h.messageType === msg.type);
111
+ if (!handler) {
112
+ return;
113
+ }
114
+ const handlerFn = instance[handler.handlerName];
115
+ const args = buildWsArgs(socket, request, msg.data, handlerFn);
116
+ await handlerFn.apply(instance, args);
117
+ }
118
+ catch (error) {
119
+ logWsError(app, scope, {
120
+ type: "ws_message_error",
121
+ gateway: gateway.name,
122
+ }, error);
123
+ }
124
+ })();
125
+ });
126
+ // Cleanup on disconnect
127
+ socket.on("close", () => {
128
+ void (async () => {
129
+ try {
130
+ if (typeof instance.onDisconnect === "function") {
131
+ await instance.onDisconnect(socket);
132
+ }
133
+ }
134
+ catch (error) {
135
+ logWsError(app, scope, {
136
+ type: "ws_disconnect_error",
137
+ gateway: gateway.name,
138
+ }, error);
139
+ }
140
+ finally {
141
+ try {
142
+ await app.disposeRequestScope(scope);
143
+ }
144
+ catch (error) {
145
+ logWsError(app, scope, {
146
+ type: "ws_scope_dispose_error",
147
+ gateway: gateway.name,
148
+ }, error);
149
+ }
150
+ }
151
+ })();
152
+ });
153
+ };
154
+ runAsync().catch((error) => {
155
+ logWsError(app, scope, {
156
+ type: "ws_connection_error",
157
+ gateway: gateway.name,
158
+ }, error);
159
+ try {
160
+ socket.close(4500, "Internal Server Error");
161
+ }
162
+ catch {
163
+ // socket may already be closed
164
+ }
165
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
166
+ app.disposeRequestScope(scope).catch(() => { });
167
+ });
168
+ }
@@ -0,0 +1,3 @@
1
+ export { handleWsConnection } from "./adapter";
2
+ export { registerWsGateways } from "./router-builder";
3
+ export type { WsGatewayLifecycle, WsGatewayMeta, WsHandlerMeta, WsParamMeta, WsParamSource } from "./types";
@@ -0,0 +1,2 @@
1
+ export { handleWsConnection } from "./adapter";
2
+ export { registerWsGateways } from "./router-builder";
@@ -0,0 +1,4 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import "reflect-metadata";
3
+ import type { AppContainer } from "../core/container";
4
+ export declare function registerWsGateways(server: FastifyInstance, app: AppContainer): Promise<void>;
@@ -0,0 +1,25 @@
1
+ import "reflect-metadata";
2
+ import { WS_GATEWAY_KEY } from "../decorators/ws.decorators";
3
+ import { handleWsConnection } from "./adapter";
4
+ export async function registerWsGateways(server, app) {
5
+ const gateways = app.getWsGateways();
6
+ if (gateways.length === 0) {
7
+ return;
8
+ }
9
+ // Dynamic import — @fastify/websocket is an optional peer dep
10
+ let fastifyWebsocket;
11
+ try {
12
+ fastifyWebsocket = await import("@fastify/websocket");
13
+ }
14
+ catch {
15
+ throw new Error("WebSocket gateways are registered but @fastify/websocket is not installed. " +
16
+ "Install it: pnpm add @fastify/websocket");
17
+ }
18
+ await server.register(fastifyWebsocket.default);
19
+ for (const gateway of gateways) {
20
+ const meta = Reflect.getMetadata(WS_GATEWAY_KEY, gateway);
21
+ server.get(meta.path, { websocket: true }, (socket, req) => {
22
+ handleWsConnection(app, gateway, socket, req);
23
+ });
24
+ }
25
+ }
@@ -0,0 +1,18 @@
1
+ import type { FastifyRequest } from "fastify";
2
+ import type { WebSocket } from "@fastify/websocket";
3
+ export interface WsHandlerMeta {
4
+ messageType: string;
5
+ handlerName: string;
6
+ }
7
+ export interface WsGatewayMeta {
8
+ path: string;
9
+ }
10
+ export type WsParamSource = "connection" | "message" | "request";
11
+ export interface WsParamMeta {
12
+ index: number;
13
+ source: WsParamSource;
14
+ }
15
+ export interface WsGatewayLifecycle {
16
+ onConnection?(socket: WebSocket, req: FastifyRequest): void | Promise<void>;
17
+ onDisconnect?(socket: WebSocket): void | Promise<void>;
18
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smounters/imperium",
3
- "version": "1.0.3",
3
+ "version": "1.1.1",
4
4
  "description": "NestJS-like modular DI container with unified HTTP + Connect RPC server for TypeScript",
5
5
  "keywords": [
6
6
  "di",
@@ -31,6 +31,10 @@
31
31
  "./validation": {
32
32
  "types": "./dist/validation/index.d.ts",
33
33
  "import": "./dist/validation/index.js"
34
+ },
35
+ "./ws": {
36
+ "types": "./dist/ws/index.d.ts",
37
+ "import": "./dist/ws/index.js"
34
38
  }
35
39
  },
36
40
  "files": [
@@ -60,6 +64,8 @@
60
64
  "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
61
65
  "format": "prettier . --write",
62
66
  "format:check": "prettier . --check",
67
+ "test": "vitest run",
68
+ "test:watch": "vitest",
63
69
  "clean": "rm -rf dist",
64
70
  "docs:dev": "pnpm exec vitepress dev docs",
65
71
  "docs:build": "pnpm exec vitepress build docs",
@@ -71,30 +77,40 @@
71
77
  "@connectrpc/connect": "^2.1.1",
72
78
  "@connectrpc/connect-fastify": "^2.1.1",
73
79
  "@fastify/cors": "^11.2.0",
80
+ "@fastify/websocket": "^11.0.0",
74
81
  "fastify": "^5.7.4",
75
- "tsyringe": "^4.10.0",
76
82
  "reflect-metadata": "^0.2.2",
77
83
  "tslog": "^4.10.2",
84
+ "tsyringe": "^4.10.0",
78
85
  "typescript": "^5.9.3",
79
86
  "zod": "^4.3.6"
80
87
  },
88
+ "peerDependenciesMeta": {
89
+ "@fastify/websocket": {
90
+ "optional": true
91
+ }
92
+ },
81
93
  "devDependencies": {
82
- "@types/node": "^24.5.2",
83
- "@typescript-eslint/eslint-plugin": "^8.56.0",
84
- "@typescript-eslint/parser": "^8.56.0",
85
94
  "@connectrpc/connect": "^2.1.1",
86
95
  "@connectrpc/connect-fastify": "^2.1.1",
96
+ "@eslint/js": "^10.0.1",
87
97
  "@fastify/cors": "^11.2.0",
88
- "eslint": "^10.0.0",
98
+ "@fastify/websocket": "^11.2.0",
99
+ "@types/node": "^25.5.0",
100
+ "@types/ws": "^8.18.1",
101
+ "eslint": "^10.1.0",
89
102
  "eslint-config-prettier": "^10.1.8",
90
- "fastify": "^5.7.4",
103
+ "fastify": "^5.8.4",
91
104
  "prettier": "^3.8.1",
92
105
  "reflect-metadata": "^0.2.2",
93
106
  "tslog": "^4.10.2",
94
107
  "tsyringe": "^4.10.0",
95
108
  "typescript": "^5.9.3",
109
+ "typescript-eslint": "^8.57.2",
110
+ "vite": "^8.0.3",
96
111
  "vitepress": "^1.6.4",
112
+ "vitest": "^4.1.2",
113
+ "ws": "^8.20.0",
97
114
  "zod": "^4.3.6"
98
- },
99
- "dependencies": {}
115
+ }
100
116
  }