@smounters/imperium 1.1.0 → 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/README.md CHANGED
@@ -1,41 +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 + WebSocket.
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 + WebSocket server on one Fastify instance.
11
- - Server streaming RPC via `async function*` (delivered as SSE in browsers).
12
- - WebSocket gateway with message routing, lifecycle hooks, and guards at connection time.
13
- - Request-scoped handler execution.
14
- - Typed runtime config via `zod` + `ConfigService`.
15
- - Built-in `LoggerService` (tslog-based).
16
- - Global enhancer tokens (`APP_GUARD`, `APP_PIPE`, `APP_INTERCEPTOR`, `APP_FILTER`).
17
- - Multi-provider array injection (`InjectAll`) and manual array resolution (`resolveAll`).
18
-
19
- ## Public Imports
20
-
21
- Root import is intentionally disabled.
22
-
23
- Use subpaths only:
24
-
25
- - `@smounters/imperium/core`
26
- - `@smounters/imperium/decorators`
27
- - `@smounters/imperium/services`
28
- - `@smounters/imperium/pipes`
29
- - `@smounters/imperium/validation`
30
- - `@smounters/imperium/ws`
31
-
32
- ## Installation
15
+ ## Install
33
16
 
34
17
  ```bash
35
- 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
36
19
  ```
37
20
 
38
- TypeScript requirements:
21
+ TypeScript config requires:
39
22
 
40
23
  ```json
41
24
  {
@@ -46,220 +29,78 @@ TypeScript requirements:
46
29
  }
47
30
  ```
48
31
 
49
- Entry point must import metadata:
50
-
51
- ```ts
52
- import "reflect-metadata";
53
- ```
54
-
55
32
  ## Quick Start
56
33
 
57
34
  ```ts
58
35
  import "reflect-metadata";
59
-
60
36
  import { Application } from "@smounters/imperium/core";
61
37
  import { Body, HttpController, Injectable, Module, Post } from "@smounters/imperium/decorators";
62
38
 
63
39
  @Injectable()
64
- class AuthService {
65
- signIn(email: string) {
66
- return { ok: true, email };
40
+ class GreetService {
41
+ greet(name: string) {
42
+ return { message: `Hello, ${name}` };
67
43
  }
68
44
  }
69
45
 
70
- @HttpController("/auth")
71
- class AuthHttpController {
72
- constructor(private readonly authService: AuthService) {}
46
+ @HttpController("/api")
47
+ class ApiController {
48
+ constructor(private readonly greetService: GreetService) {}
73
49
 
74
- @Post("/sign-in")
75
- signIn(@Body("email") email: string) {
76
- return this.authService.signIn(email);
50
+ @Post("/greet")
51
+ greet(@Body("name") name: string) {
52
+ return this.greetService.greet(name);
77
53
  }
78
54
  }
79
55
 
80
56
  @Module({
81
- providers: [AuthService],
82
- httpControllers: [AuthHttpController],
57
+ providers: [GreetService],
58
+ httpControllers: [ApiController],
83
59
  })
84
60
  class AppModule {}
85
61
 
86
- const app = new Application(AppModule, {
87
- host: "0.0.0.0",
88
- accessLogs: true,
89
- });
90
-
91
- await app.start({ port: 3000 });
92
- ```
93
-
94
- ## Recommended Bootstrap Flow
95
-
96
- ```ts
97
- import { Application } from "@smounters/imperium/core";
98
- import { ConfigService, LoggerService } from "@smounters/imperium/services";
99
- import { appConfigSchema, type AppConfig } from "@smounters/imperium/validation";
100
-
101
- const app = new Application(AppModule, {
102
- host: "0.0.0.0",
103
- accessLogs: true,
104
- });
105
-
106
- app.configureConfig(appConfigSchema, process.env);
107
-
108
- const config = app.resolve(ConfigService<AppConfig>).getAll();
109
-
110
- app.configureLogger({
111
- name: "backend",
112
- minLevel: 3,
113
- });
114
-
115
- await app.start({
116
- port: config.APP_PORT,
117
- prefix: config.APP_GLOBAL_PREFIX,
118
- });
119
-
120
- app.resolve(LoggerService).info({ event: "app.started", port: config.APP_PORT });
62
+ await new Application(AppModule).start({ port: 3000 });
121
63
  ```
122
64
 
123
- If your app needs extra config fields, extend the exported base schema:
124
-
125
- ```ts
126
- import { appConfigSchema } from "@smounters/imperium/validation";
127
- import { z } from "zod";
128
-
129
- const projectConfigSchema = appConfigSchema.extend({
130
- REDIS_URL: z.url(),
131
- });
132
- ```
133
-
134
- ## Multi Providers
135
-
136
- ```ts
137
- import { InjectAll, Injectable, Module } from "@smounters/imperium/decorators";
138
-
139
- const AML_RULES = Symbol("AML_RULES");
140
-
141
- @Module({
142
- providers: [
143
- { provide: AML_RULES, multi: true, useClass: SanctionsRule },
144
- { provide: AML_RULES, multi: true, useClass: MixerRule },
145
- { provide: AML_RULES, multi: true, useClass: FreshAddressRule },
146
- ],
147
- exports: [AML_RULES],
148
- })
149
- class AmlModule {}
150
-
151
- @Injectable()
152
- class AmlEngine {
153
- constructor(@InjectAll(AML_RULES) private readonly rules: AmlRule[]) {}
154
- }
155
- ```
156
-
157
- Manual resolution is also available:
158
-
159
- ```ts
160
- const rules = app.resolveAll<AmlRule>(AML_RULES);
161
- ```
162
-
163
- ## HTTP and RPC
164
-
165
- Use decorators from `@smounters/imperium/decorators`:
166
-
167
- - HTTP: `HttpController`, `Get`, `Post`, `Put`, `Patch`, `Delete`, `Body`, `Query`, `Param`, `Header`, `Req`, `Res`
168
- - RPC: `RpcService`, `RpcMethod`, `RpcData`, `RpcContext`, `RpcHeaders`, `RpcHeader`, `RpcAbortSignal`
169
- - WebSocket: `WsGateway`, `WsHandler`, `WsConnection`, `WsMessage`, `WsRequest`
170
-
171
- Imperium auto-detects registered HTTP/RPC/WebSocket handlers and serves all protocols from one server.
172
-
173
65
  ## WebSocket
174
66
 
175
67
  ```ts
176
- import { WsGateway, WsHandler, WsConnection, WsMessage, Module } from "@smounters/imperium/decorators";
68
+ import { WsGateway, WsHandler, WsConnection, WsMessage } from "@smounters/imperium/decorators";
177
69
  import type { WsGatewayLifecycle } from "@smounters/imperium/ws";
178
70
  import type { WebSocket } from "@fastify/websocket";
179
71
 
180
72
  @WsGateway("/ws")
181
- class ChatGateway implements WsGatewayLifecycle {
73
+ class EventsGateway implements WsGatewayLifecycle {
182
74
  private clients = new Set<WebSocket>();
183
75
 
184
76
  onConnection(socket: WebSocket) { this.clients.add(socket); }
185
77
  onDisconnect(socket: WebSocket) { this.clients.delete(socket); }
186
78
 
187
- @WsHandler("message")
188
- onMessage(@WsConnection() ws: WebSocket, @WsMessage() data: { text: string }) {
189
- const msg = JSON.stringify({ type: "message", data });
190
- for (const client of this.clients) {
191
- if (client.readyState === 1) client.send(msg);
192
- }
79
+ @WsHandler("ping")
80
+ onPing(@WsConnection() ws: WebSocket) {
81
+ ws.send(JSON.stringify({ type: "pong" }));
193
82
  }
194
83
  }
195
-
196
- @Module({ providers: [ChatGateway] })
197
- class AppModule {}
198
84
  ```
199
85
 
200
86
  Requires optional peer dependency: `pnpm add @fastify/websocket`
201
87
 
202
- ## Services
203
-
204
- From `@smounters/imperium/services`:
205
-
206
- - `ConfigService`
207
- - `LoggerService`
208
-
209
- ## Pipes and Validation
210
-
211
- - `ZodPipe` from `@smounters/imperium/pipes`
212
- - validation helpers from `@smounters/imperium/validation`
213
- - `appConfigSchema`
214
- - `AppConfig`
215
- - `booleanSchema`
216
- - `numberSchema`
217
- - `nativeEnumSchema`
218
- - `stringArraySchema`
219
- - `enumArraySchema`
220
-
221
- ## Error Classes
222
-
223
- From `@smounters/imperium/core`:
88
+ ## Import Paths
224
89
 
225
- - `HttpException`
226
- - `BadRequestException`
227
- - `UnauthorizedException`
228
- - `ForbiddenException`
229
- - `NotFoundException`
230
- - `InternalServerErrorException`
90
+ No root import. Use subpaths:
231
91
 
232
- ## Documentation
233
-
234
- Full docs (VitePress) are located in:
235
-
236
- - `docs`
237
-
238
- Local docs commands:
239
-
240
- ```bash
241
- pnpm run docs:dev
242
- pnpm run docs:build
243
- ```
244
-
245
- GitHub Pages deployment is configured via `.github/workflows/publish.yml`.
246
-
247
- ## Publish to npm
248
-
249
- ```bash
250
- pnpm install
251
- pnpm run typecheck
252
- pnpm run build
253
- pnpm publish --access public --no-git-checks
92
+ ```ts
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";
254
99
  ```
255
100
 
256
- Automated npm publishing workflow:
257
-
258
- - `.github/workflows/publish.yml`
259
-
260
- Required secret:
101
+ ## Documentation
261
102
 
262
- - `NPM_TOKEN`
103
+ Full guide and API reference: **[smounters.github.io/imperium](https://smounters.github.io/imperium/)**
263
104
 
264
105
  ## License
265
106
 
@@ -229,7 +229,7 @@ export class AppContainer {
229
229
  return provider.useValue;
230
230
  }
231
231
  if ("useFactory" in provider) {
232
- return provider.useFactory(moduleRef.container);
232
+ return (provider).useFactory(moduleRef.container);
233
233
  }
234
234
  if ("useExisting" in provider) {
235
235
  return moduleRef.container.resolve(provider.useExisting);
@@ -281,7 +281,7 @@ export class AppContainer {
281
281
  }
282
282
  if ("useFactory" in provider) {
283
283
  moduleRef.container.register(provider.provide, {
284
- useFactory: (dc) => provider.useFactory(dc),
284
+ useFactory: (dc) => (provider).useFactory(dc),
285
285
  });
286
286
  return;
287
287
  }
@@ -467,9 +467,7 @@ export class AppContainer {
467
467
  }
468
468
  loadModule(moduleImport) {
469
469
  const moduleRef = this.loadModuleRef(moduleImport);
470
- if (!this.rootModuleRef) {
471
- this.rootModuleRef = moduleRef;
472
- }
470
+ this.rootModuleRef ??= moduleRef;
473
471
  }
474
472
  resolve(token) {
475
473
  if (this.root.isRegistered(token, false)) {
@@ -179,9 +179,10 @@ export async function startServer(diOrOptions, options) {
179
179
  const corsConfig = resolveCors(cors);
180
180
  const effectiveHttpPrefix = mergePrefixes(prefix, httpPrefix);
181
181
  const effectiveRpcPrefix = mergePrefixes(prefix, rpcPrefix);
182
- server.addHook("onRequest", async (req) => {
182
+ server.addHook("onRequest", (req, _reply, done) => {
183
183
  const request = req;
184
184
  request.requestStartAt = Date.now();
185
+ done();
185
186
  });
186
187
  server.addHook("onResponse", async (req, reply) => {
187
188
  const request = req;
package/dist/types.d.ts CHANGED
@@ -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;
@@ -70,9 +69,9 @@ export interface RpcArgumentsHost {
70
69
  getContext<T = unknown>(): T | undefined;
71
70
  }
72
71
  export interface WsArgumentsHost {
73
- getSocket<T = unknown>(): T | undefined;
72
+ getSocket(): unknown;
74
73
  getRequest(): FastifyRequest | undefined;
75
- getMessage<T = unknown>(): T | undefined;
74
+ getMessage(): unknown;
76
75
  }
77
76
  export interface BaseContext {
78
77
  type: ContextType;
@@ -115,7 +114,7 @@ export interface PipeTransform<TIn = unknown, TOut = unknown> {
115
114
  }
116
115
  export type PipeLike = Constructor<PipeTransform> | PipeTransform;
117
116
  export interface ExceptionFilter<T = unknown> {
118
- catch(exception: T, ctx: BaseContext): Promise<unknown> | unknown;
117
+ catch(exception: T, ctx: BaseContext): unknown;
119
118
  }
120
119
  export type ExceptionFilterLike = Constructor<ExceptionFilter> | ExceptionFilter;
121
120
  export interface ConfigServiceOptions<TSchema extends ZodType = ZodType> {
@@ -124,7 +123,7 @@ export interface ConfigServiceOptions<TSchema extends ZodType = ZodType> {
124
123
  }
125
124
  export interface CorsOptions {
126
125
  enabled?: boolean;
127
- origin?: string | boolean | RegExp | Array<string | boolean | RegExp>;
126
+ origin?: string | boolean | RegExp | (string | boolean | RegExp)[];
128
127
  credentials?: boolean;
129
128
  exposedHeaders?: string | string[];
130
129
  allowedHeaders?: string | string[];
@@ -104,9 +104,9 @@ export function handleWsConnection(app, gateway, socket, request) {
104
104
  }
105
105
  // Route messages to handlers
106
106
  socket.on("message", (raw) => {
107
- (async () => {
107
+ void (async () => {
108
108
  try {
109
- const msg = JSON.parse(raw.toString());
109
+ const msg = JSON.parse(String(raw));
110
110
  const handler = handlers.find((h) => h.messageType === msg.type);
111
111
  if (!handler) {
112
112
  return;
@@ -125,7 +125,7 @@ export function handleWsConnection(app, gateway, socket, request) {
125
125
  });
126
126
  // Cleanup on disconnect
127
127
  socket.on("close", () => {
128
- (async () => {
128
+ void (async () => {
129
129
  try {
130
130
  if (typeof instance.onDisconnect === "function") {
131
131
  await instance.onDisconnect(socket);
@@ -162,6 +162,7 @@ export function handleWsConnection(app, gateway, socket, request) {
162
162
  catch {
163
163
  // socket may already be closed
164
164
  }
165
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
165
166
  app.disposeRequestScope(scope).catch(() => { });
166
167
  });
167
168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smounters/imperium",
3
- "version": "1.1.0",
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",
@@ -64,6 +64,8 @@
64
64
  "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
65
65
  "format": "prettier . --write",
66
66
  "format:check": "prettier . --check",
67
+ "test": "vitest run",
68
+ "test:watch": "vitest",
67
69
  "clean": "rm -rf dist",
68
70
  "docs:dev": "pnpm exec vitepress dev docs",
69
71
  "docs:build": "pnpm exec vitepress build docs",
@@ -91,11 +93,11 @@
91
93
  "devDependencies": {
92
94
  "@connectrpc/connect": "^2.1.1",
93
95
  "@connectrpc/connect-fastify": "^2.1.1",
96
+ "@eslint/js": "^10.0.1",
94
97
  "@fastify/cors": "^11.2.0",
95
98
  "@fastify/websocket": "^11.2.0",
96
99
  "@types/node": "^25.5.0",
97
- "@typescript-eslint/eslint-plugin": "^8.57.2",
98
- "@typescript-eslint/parser": "^8.57.2",
100
+ "@types/ws": "^8.18.1",
99
101
  "eslint": "^10.1.0",
100
102
  "eslint-config-prettier": "^10.1.8",
101
103
  "fastify": "^5.8.4",
@@ -104,7 +106,11 @@
104
106
  "tslog": "^4.10.2",
105
107
  "tsyringe": "^4.10.0",
106
108
  "typescript": "^5.9.3",
109
+ "typescript-eslint": "^8.57.2",
110
+ "vite": "^8.0.3",
107
111
  "vitepress": "^1.6.4",
112
+ "vitest": "^4.1.2",
113
+ "ws": "^8.20.0",
108
114
  "zod": "^4.3.6"
109
115
  }
110
116
  }