@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 +40 -199
- package/dist/core/container.js +3 -5
- package/dist/core/server.js +2 -1
- package/dist/types.d.ts +5 -6
- package/dist/ws/adapter.js +4 -3
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -1,41 +1,24 @@
|
|
|
1
1
|
# @smounters/imperium
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
NestJS-inspired modular DI framework for TypeScript services. Unified HTTP + ConnectRPC + WebSocket server on a single Fastify instance.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
65
|
-
|
|
66
|
-
return {
|
|
40
|
+
class GreetService {
|
|
41
|
+
greet(name: string) {
|
|
42
|
+
return { message: `Hello, ${name}` };
|
|
67
43
|
}
|
|
68
44
|
}
|
|
69
45
|
|
|
70
|
-
@HttpController("/
|
|
71
|
-
class
|
|
72
|
-
constructor(private readonly
|
|
46
|
+
@HttpController("/api")
|
|
47
|
+
class ApiController {
|
|
48
|
+
constructor(private readonly greetService: GreetService) {}
|
|
73
49
|
|
|
74
|
-
@Post("/
|
|
75
|
-
|
|
76
|
-
return this.
|
|
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: [
|
|
82
|
-
httpControllers: [
|
|
57
|
+
providers: [GreetService],
|
|
58
|
+
httpControllers: [ApiController],
|
|
83
59
|
})
|
|
84
60
|
class AppModule {}
|
|
85
61
|
|
|
86
|
-
|
|
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
|
|
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
|
|
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("
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
226
|
-
- `BadRequestException`
|
|
227
|
-
- `UnauthorizedException`
|
|
228
|
-
- `ForbiddenException`
|
|
229
|
-
- `NotFoundException`
|
|
230
|
-
- `InternalServerErrorException`
|
|
90
|
+
No root import. Use subpaths:
|
|
231
91
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
- `.github/workflows/publish.yml`
|
|
259
|
-
|
|
260
|
-
Required secret:
|
|
101
|
+
## Documentation
|
|
261
102
|
|
|
262
|
-
|
|
103
|
+
Full guide and API reference: **[smounters.github.io/imperium](https://smounters.github.io/imperium/)**
|
|
263
104
|
|
|
264
105
|
## License
|
|
265
106
|
|
package/dist/core/container.js
CHANGED
|
@@ -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
|
-
|
|
471
|
-
this.rootModuleRef = moduleRef;
|
|
472
|
-
}
|
|
470
|
+
this.rootModuleRef ??= moduleRef;
|
|
473
471
|
}
|
|
474
472
|
resolve(token) {
|
|
475
473
|
if (this.root.isRegistered(token, false)) {
|
package/dist/core/server.js
CHANGED
|
@@ -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",
|
|
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
|
|
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
|
|
72
|
+
getSocket(): unknown;
|
|
74
73
|
getRequest(): FastifyRequest | undefined;
|
|
75
|
-
getMessage
|
|
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):
|
|
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 |
|
|
126
|
+
origin?: string | boolean | RegExp | (string | boolean | RegExp)[];
|
|
128
127
|
credentials?: boolean;
|
|
129
128
|
exposedHeaders?: string | string[];
|
|
130
129
|
allowedHeaders?: string | string[];
|
package/dist/ws/adapter.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
"@
|
|
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
|
}
|