@smounters/imperium 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/core/app-tokens.d.ts +5 -0
- package/dist/core/app-tokens.js +4 -0
- package/dist/core/application.d.ts +34 -0
- package/dist/core/application.js +252 -0
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.js +1 -0
- package/dist/core/container.d.ts +76 -0
- package/dist/core/container.js +687 -0
- package/dist/core/errors.d.ts +31 -0
- package/dist/core/errors.js +169 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +4 -0
- package/dist/core/logger.d.ts +7 -0
- package/dist/core/logger.js +12 -0
- package/dist/core/reflector.d.ts +9 -0
- package/dist/core/reflector.js +39 -0
- package/dist/core/server.d.ts +10 -0
- package/dist/core/server.js +296 -0
- package/dist/core/types.d.ts +25 -0
- package/dist/core/types.js +1 -0
- package/dist/decorators/di.decorators.d.ts +10 -0
- package/dist/decorators/di.decorators.js +15 -0
- package/dist/decorators/filters.decorators.d.ts +6 -0
- package/dist/decorators/filters.decorators.js +16 -0
- package/dist/decorators/guards.decorators.d.ts +4 -0
- package/dist/decorators/guards.decorators.js +8 -0
- package/dist/decorators/http.decorators.d.ts +16 -0
- package/dist/decorators/http.decorators.js +60 -0
- package/dist/decorators/index.d.ts +8 -0
- package/dist/decorators/index.js +8 -0
- package/dist/decorators/interceptors.decorators.d.ts +4 -0
- package/dist/decorators/interceptors.decorators.js +8 -0
- package/dist/decorators/metadata.decorators.d.ts +4 -0
- package/dist/decorators/metadata.decorators.js +22 -0
- package/dist/decorators/pipes.decorators.d.ts +4 -0
- package/dist/decorators/pipes.decorators.js +8 -0
- package/dist/decorators/rpc.decorators.d.ts +11 -0
- package/dist/decorators/rpc.decorators.js +50 -0
- package/dist/http/adapter.d.ts +4 -0
- package/dist/http/adapter.js +199 -0
- package/dist/http/index.d.ts +3 -0
- package/dist/http/index.js +3 -0
- package/dist/http/router-builder.d.ts +4 -0
- package/dist/http/router-builder.js +30 -0
- package/dist/http/utils.d.ts +6 -0
- package/dist/http/utils.js +46 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pipes/index.d.ts +1 -0
- package/dist/pipes/index.js +1 -0
- package/dist/pipes/zod.pipe.d.ts +7 -0
- package/dist/pipes/zod.pipe.js +8 -0
- package/dist/rpc/adapter.d.ts +7 -0
- package/dist/rpc/adapter.js +186 -0
- package/dist/rpc/index.d.ts +3 -0
- package/dist/rpc/index.js +3 -0
- package/dist/rpc/router-builder.d.ts +4 -0
- package/dist/rpc/router-builder.js +28 -0
- package/dist/rpc/utils.d.ts +6 -0
- package/dist/rpc/utils.js +46 -0
- package/dist/services/config.service.d.ts +10 -0
- package/dist/services/config.service.js +41 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +2 -0
- package/dist/services/logger.service.d.ts +30 -0
- package/dist/services/logger.service.js +52 -0
- package/dist/types.d.ts +167 -0
- package/dist/types.js +1 -0
- package/dist/validation/app-config.d.ts +30 -0
- package/dist/validation/app-config.js +25 -0
- package/dist/validation/common.d.ts +8 -0
- package/dist/validation/common.js +57 -0
- package/dist/validation/index.d.ts +2 -0
- package/dist/validation/index.js +2 -0
- package/package.json +98 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@smounters/imperium` are documented in this file.
|
|
4
|
+
|
|
5
|
+
## 0.1.0 - 2026-02-22
|
|
6
|
+
|
|
7
|
+
- Initial public package setup.
|
|
8
|
+
- Unified HTTP + ConnectRPC runtime on Fastify.
|
|
9
|
+
- NestJS-inspired module/decorator/DI API.
|
|
10
|
+
- Typed `Application` bootstrap with pre-start `configureConfig` and `configureLogger`.
|
|
11
|
+
- Multi provider intent metadata (`multi`) and array resolution via `InjectAll` / `resolveAll`.
|
|
12
|
+
- VitePress documentation and GitHub Pages workflow.
|
|
13
|
+
- npm publish workflow with `pnpm`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CryppEX
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# @smounters/imperium
|
|
2
|
+
|
|
3
|
+
`@smounters/imperium` is **inspired by NestJS** and provides a modular DI-first runtime for TypeScript services using Fastify + ConnectRPC.
|
|
4
|
+
|
|
5
|
+
It is designed for teams that want Nest-like architecture but with explicit control over runtime wiring and exported API surface.
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
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
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm add @smounters/imperium reflect-metadata tsyringe fastify @connectrpc/connect @connectrpc/connect-fastify zod
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
TypeScript requirements:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"compilerOptions": {
|
|
40
|
+
"experimentalDecorators": true,
|
|
41
|
+
"emitDecoratorMetadata": true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Entry point must import metadata:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import "reflect-metadata";
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import "reflect-metadata";
|
|
56
|
+
|
|
57
|
+
import { Application } from "@smounters/imperium/core";
|
|
58
|
+
import { Body, HttpController, Injectable, Module, Post } from "@smounters/imperium/decorators";
|
|
59
|
+
|
|
60
|
+
@Injectable()
|
|
61
|
+
class AuthService {
|
|
62
|
+
signIn(email: string) {
|
|
63
|
+
return { ok: true, email };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@HttpController("/auth")
|
|
68
|
+
class AuthHttpController {
|
|
69
|
+
constructor(private readonly authService: AuthService) {}
|
|
70
|
+
|
|
71
|
+
@Post("/sign-in")
|
|
72
|
+
signIn(@Body("email") email: string) {
|
|
73
|
+
return this.authService.signIn(email);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Module({
|
|
78
|
+
providers: [AuthService],
|
|
79
|
+
httpControllers: [AuthHttpController],
|
|
80
|
+
})
|
|
81
|
+
class AppModule {}
|
|
82
|
+
|
|
83
|
+
const app = new Application(AppModule, {
|
|
84
|
+
host: "0.0.0.0",
|
|
85
|
+
accessLogs: true,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await app.start({ port: 3000 });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Recommended Bootstrap Flow
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { Application } from "@smounters/imperium/core";
|
|
95
|
+
import { ConfigService, LoggerService } from "@smounters/imperium/services";
|
|
96
|
+
import { z } from "zod";
|
|
97
|
+
|
|
98
|
+
const appConfigSchema = z.object({
|
|
99
|
+
APP_PORT: z.coerce.number().default(8000),
|
|
100
|
+
APP_GLOBAL_PREFIX: z.string().default(""),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
type AppConfig = z.infer<typeof appConfigSchema>;
|
|
104
|
+
|
|
105
|
+
const app = new Application(AppModule, {
|
|
106
|
+
host: "0.0.0.0",
|
|
107
|
+
accessLogs: true,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
app.configureConfig(appConfigSchema, process.env);
|
|
111
|
+
|
|
112
|
+
const config = app.resolve(ConfigService<AppConfig>).getAll();
|
|
113
|
+
|
|
114
|
+
app.configureLogger({
|
|
115
|
+
name: "backend",
|
|
116
|
+
minLevel: 3,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await app.start({
|
|
120
|
+
port: config.APP_PORT,
|
|
121
|
+
prefix: config.APP_GLOBAL_PREFIX,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
app.resolve(LoggerService).info({ event: "app.started", port: config.APP_PORT });
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Multi Providers
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { InjectAll, Injectable, Module } from "@smounters/imperium/decorators";
|
|
131
|
+
|
|
132
|
+
const AML_RULES = Symbol("AML_RULES");
|
|
133
|
+
|
|
134
|
+
@Module({
|
|
135
|
+
providers: [
|
|
136
|
+
{ provide: AML_RULES, multi: true, useClass: SanctionsRule },
|
|
137
|
+
{ provide: AML_RULES, multi: true, useClass: MixerRule },
|
|
138
|
+
{ provide: AML_RULES, multi: true, useClass: FreshAddressRule },
|
|
139
|
+
],
|
|
140
|
+
exports: [AML_RULES],
|
|
141
|
+
})
|
|
142
|
+
class AmlModule {}
|
|
143
|
+
|
|
144
|
+
@Injectable()
|
|
145
|
+
class AmlEngine {
|
|
146
|
+
constructor(@InjectAll(AML_RULES) private readonly rules: AmlRule[]) {}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Manual resolution is also available:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const rules = app.resolveAll<AmlRule>(AML_RULES);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## HTTP and RPC
|
|
157
|
+
|
|
158
|
+
Use decorators from `@smounters/imperium/decorators`:
|
|
159
|
+
|
|
160
|
+
- HTTP: `HttpController`, `Get`, `Post`, `Put`, `Patch`, `Delete`, `Body`, `Query`, `Param`, `Header`, `Req`, `Res`
|
|
161
|
+
- RPC: `RpcService`, `RpcMethod`, `RpcData`, `RpcContext`, `RpcHeaders`, `RpcHeader`
|
|
162
|
+
|
|
163
|
+
Imperium auto-detects registered HTTP/RPC handlers and serves both protocols from one server.
|
|
164
|
+
|
|
165
|
+
## Services
|
|
166
|
+
|
|
167
|
+
From `@smounters/imperium/services`:
|
|
168
|
+
|
|
169
|
+
- `ConfigService`
|
|
170
|
+
- `LoggerService`
|
|
171
|
+
|
|
172
|
+
## Pipes and Validation
|
|
173
|
+
|
|
174
|
+
- `ZodPipe` from `@smounters/imperium/pipes`
|
|
175
|
+
- validation helpers from `@smounters/imperium/validation`
|
|
176
|
+
- `booleanSchema`
|
|
177
|
+
- `numberSchema`
|
|
178
|
+
- `nativeEnumSchema`
|
|
179
|
+
- `stringArraySchema`
|
|
180
|
+
- `enumArraySchema`
|
|
181
|
+
|
|
182
|
+
## Error Classes
|
|
183
|
+
|
|
184
|
+
From `@smounters/imperium/core`:
|
|
185
|
+
|
|
186
|
+
- `HttpException`
|
|
187
|
+
- `BadRequestException`
|
|
188
|
+
- `UnauthorizedException`
|
|
189
|
+
- `ForbiddenException`
|
|
190
|
+
- `NotFoundException`
|
|
191
|
+
- `InternalServerErrorException`
|
|
192
|
+
|
|
193
|
+
## Documentation
|
|
194
|
+
|
|
195
|
+
Full docs (VitePress) are located in:
|
|
196
|
+
|
|
197
|
+
- `docs`
|
|
198
|
+
|
|
199
|
+
Local docs commands:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
pnpm run docs:dev
|
|
203
|
+
pnpm run docs:build
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
GitHub Pages deployment is configured via `.github/workflows/publish.yml`.
|
|
207
|
+
|
|
208
|
+
## Publish to npm
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pnpm install
|
|
212
|
+
pnpm run typecheck
|
|
213
|
+
pnpm run build
|
|
214
|
+
pnpm publish --access public --no-git-checks
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Automated npm publishing workflow:
|
|
218
|
+
|
|
219
|
+
- `.github/workflows/publish.yml`
|
|
220
|
+
|
|
221
|
+
Required secret:
|
|
222
|
+
|
|
223
|
+
- `NPM_TOKEN`
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ExceptionFilterLike, GuardLike, InjectionToken, InterceptorLike, PipeLike } from "../types";
|
|
2
|
+
export declare const APP_GUARD: InjectionToken<GuardLike>;
|
|
3
|
+
export declare const APP_INTERCEPTOR: InjectionToken<InterceptorLike>;
|
|
4
|
+
export declare const APP_PIPE: InjectionToken<PipeLike>;
|
|
5
|
+
export declare const APP_FILTER: InjectionToken<ExceptionFilterLike>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import type { InjectionToken, LoggerOptions, ModuleImport, ServerOptions } from "../types";
|
|
3
|
+
import type { ZodType, output } from "zod";
|
|
4
|
+
import { AppContainer } from "./container";
|
|
5
|
+
export declare class Application {
|
|
6
|
+
private static readonly DEFAULT_SHUTDOWN_SIGNALS;
|
|
7
|
+
private static readonly DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
8
|
+
private readonly moduleImport;
|
|
9
|
+
private readonly di;
|
|
10
|
+
private options;
|
|
11
|
+
private readonly signalHandlers;
|
|
12
|
+
private moduleLoaded;
|
|
13
|
+
private loggerConfiguredExplicitly;
|
|
14
|
+
private server;
|
|
15
|
+
private startPromise;
|
|
16
|
+
private shutdownInProgress;
|
|
17
|
+
constructor(moduleClass: ModuleImport, options?: ServerOptions);
|
|
18
|
+
getContainer(): AppContainer;
|
|
19
|
+
resolve<T>(token: InjectionToken<T>): T;
|
|
20
|
+
resolveAll<T>(token: InjectionToken<T>): T[];
|
|
21
|
+
getServerOptions(): Readonly<ServerOptions>;
|
|
22
|
+
setServerOptions(options: ServerOptions): this;
|
|
23
|
+
configureLogger(options?: LoggerOptions): this;
|
|
24
|
+
configureConfig<TSchema extends ZodType>(schema: TSchema, source?: unknown): output<TSchema>;
|
|
25
|
+
getServer(): FastifyInstance;
|
|
26
|
+
start(options?: ServerOptions): Promise<FastifyInstance>;
|
|
27
|
+
close(signal?: string): Promise<void>;
|
|
28
|
+
private resolveGracefulShutdownOptions;
|
|
29
|
+
private installSignalHandlers;
|
|
30
|
+
private removeSignalHandlers;
|
|
31
|
+
private handleShutdownSignal;
|
|
32
|
+
private assertConfigurable;
|
|
33
|
+
private ensureModuleLoaded;
|
|
34
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { AppContainer } from "./container";
|
|
2
|
+
import { startServer } from "./server";
|
|
3
|
+
export class Application {
|
|
4
|
+
static { this.DEFAULT_SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM"]; }
|
|
5
|
+
static { this.DEFAULT_SHUTDOWN_TIMEOUT_MS = 15_000; }
|
|
6
|
+
constructor(moduleClass, options = {}) {
|
|
7
|
+
this.signalHandlers = new Map();
|
|
8
|
+
this.moduleLoaded = false;
|
|
9
|
+
this.loggerConfiguredExplicitly = false;
|
|
10
|
+
this.server = null;
|
|
11
|
+
this.startPromise = null;
|
|
12
|
+
this.shutdownInProgress = false;
|
|
13
|
+
this.moduleImport = moduleClass;
|
|
14
|
+
this.di = new AppContainer();
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
getContainer() {
|
|
18
|
+
return this.di;
|
|
19
|
+
}
|
|
20
|
+
resolve(token) {
|
|
21
|
+
try {
|
|
22
|
+
return this.di.resolve(token);
|
|
23
|
+
}
|
|
24
|
+
catch (initialError) {
|
|
25
|
+
if (!this.moduleLoaded) {
|
|
26
|
+
this.ensureModuleLoaded();
|
|
27
|
+
return this.di.resolve(token);
|
|
28
|
+
}
|
|
29
|
+
throw initialError;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
resolveAll(token) {
|
|
33
|
+
try {
|
|
34
|
+
return this.di.resolveAll(token);
|
|
35
|
+
}
|
|
36
|
+
catch (initialError) {
|
|
37
|
+
if (!this.moduleLoaded) {
|
|
38
|
+
this.ensureModuleLoaded();
|
|
39
|
+
return this.di.resolveAll(token);
|
|
40
|
+
}
|
|
41
|
+
throw initialError;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
getServerOptions() {
|
|
45
|
+
return { ...this.options };
|
|
46
|
+
}
|
|
47
|
+
setServerOptions(options) {
|
|
48
|
+
this.assertConfigurable("set server options");
|
|
49
|
+
this.options = {
|
|
50
|
+
...this.options,
|
|
51
|
+
...options,
|
|
52
|
+
};
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
configureLogger(options) {
|
|
56
|
+
this.assertConfigurable("configure logger");
|
|
57
|
+
this.di.configureLogger(options);
|
|
58
|
+
this.loggerConfiguredExplicitly = true;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
configureConfig(schema, source = process.env) {
|
|
62
|
+
this.assertConfigurable("configure config");
|
|
63
|
+
return this.di.configureConfig(schema, source);
|
|
64
|
+
}
|
|
65
|
+
getServer() {
|
|
66
|
+
if (!this.server) {
|
|
67
|
+
throw new Error("Application is not started");
|
|
68
|
+
}
|
|
69
|
+
return this.server;
|
|
70
|
+
}
|
|
71
|
+
async start(options = {}) {
|
|
72
|
+
if (this.server) {
|
|
73
|
+
return this.server;
|
|
74
|
+
}
|
|
75
|
+
if (this.startPromise) {
|
|
76
|
+
return this.startPromise;
|
|
77
|
+
}
|
|
78
|
+
const resolvedOptions = {
|
|
79
|
+
...this.options,
|
|
80
|
+
...options,
|
|
81
|
+
};
|
|
82
|
+
this.options = resolvedOptions;
|
|
83
|
+
const startOptions = this.loggerConfiguredExplicitly && resolvedOptions.loggerOptions !== undefined
|
|
84
|
+
? {
|
|
85
|
+
...resolvedOptions,
|
|
86
|
+
loggerOptions: undefined,
|
|
87
|
+
}
|
|
88
|
+
: resolvedOptions;
|
|
89
|
+
this.startPromise = (async () => {
|
|
90
|
+
this.ensureModuleLoaded();
|
|
91
|
+
const server = await startServer(this.di, startOptions);
|
|
92
|
+
this.server = server;
|
|
93
|
+
this.installSignalHandlers(startOptions);
|
|
94
|
+
return server;
|
|
95
|
+
})();
|
|
96
|
+
try {
|
|
97
|
+
return await this.startPromise;
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
this.startPromise = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async close(signal = "application.close") {
|
|
104
|
+
if (this.startPromise && !this.server) {
|
|
105
|
+
try {
|
|
106
|
+
await this.startPromise;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Ignore startup failures here: startServer() already performs cleanup.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (this.server) {
|
|
113
|
+
const server = this.server;
|
|
114
|
+
try {
|
|
115
|
+
await server.close();
|
|
116
|
+
this.server = null;
|
|
117
|
+
this.removeSignalHandlers();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
this.server = server;
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await this.di.close(signal);
|
|
126
|
+
this.removeSignalHandlers();
|
|
127
|
+
}
|
|
128
|
+
resolveGracefulShutdownOptions(serverOptions) {
|
|
129
|
+
const raw = serverOptions.gracefulShutdown;
|
|
130
|
+
if (raw === false) {
|
|
131
|
+
return {
|
|
132
|
+
enabled: false,
|
|
133
|
+
signals: Application.DEFAULT_SHUTDOWN_SIGNALS,
|
|
134
|
+
timeoutMs: Application.DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
135
|
+
forceExitOnFailure: false,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (raw === undefined || raw === true) {
|
|
139
|
+
return {
|
|
140
|
+
enabled: true,
|
|
141
|
+
signals: Application.DEFAULT_SHUTDOWN_SIGNALS,
|
|
142
|
+
timeoutMs: Application.DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
143
|
+
forceExitOnFailure: false,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const options = raw;
|
|
147
|
+
const timeoutMs = typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
|
|
148
|
+
? options.timeoutMs
|
|
149
|
+
: Application.DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
150
|
+
return {
|
|
151
|
+
enabled: options.enabled ?? true,
|
|
152
|
+
signals: options.signals?.length ? options.signals : Application.DEFAULT_SHUTDOWN_SIGNALS,
|
|
153
|
+
timeoutMs,
|
|
154
|
+
forceExitOnFailure: options.forceExitOnFailure ?? false,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
installSignalHandlers(serverOptions) {
|
|
158
|
+
if (this.signalHandlers.size > 0) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const shutdown = this.resolveGracefulShutdownOptions(serverOptions);
|
|
162
|
+
if (!shutdown.enabled) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const signal of shutdown.signals) {
|
|
166
|
+
const handler = () => {
|
|
167
|
+
void this.handleShutdownSignal(signal, shutdown.timeoutMs, shutdown.forceExitOnFailure);
|
|
168
|
+
};
|
|
169
|
+
process.on(signal, handler);
|
|
170
|
+
this.signalHandlers.set(signal, handler);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
removeSignalHandlers() {
|
|
174
|
+
if (this.signalHandlers.size === 0) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const [signal, handler] of this.signalHandlers) {
|
|
178
|
+
if (typeof process.off === "function") {
|
|
179
|
+
process.off(signal, handler);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
process.removeListener(signal, handler);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
this.signalHandlers.clear();
|
|
186
|
+
}
|
|
187
|
+
async handleShutdownSignal(signal, timeoutMs, forceExitOnFailure) {
|
|
188
|
+
if (this.shutdownInProgress) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.shutdownInProgress = true;
|
|
192
|
+
const logger = this.di.getLogger();
|
|
193
|
+
let timeoutId;
|
|
194
|
+
let shutdownFailed = false;
|
|
195
|
+
logger.info({
|
|
196
|
+
type: "shutdown",
|
|
197
|
+
stage: "received",
|
|
198
|
+
signal,
|
|
199
|
+
timeoutMs,
|
|
200
|
+
forceExitOnFailure,
|
|
201
|
+
});
|
|
202
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
203
|
+
timeoutId = setTimeout(() => {
|
|
204
|
+
reject(new Error(`Graceful shutdown timed out after ${timeoutMs}ms`));
|
|
205
|
+
}, timeoutMs);
|
|
206
|
+
if (typeof timeoutId.unref === "function") {
|
|
207
|
+
timeoutId.unref();
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
try {
|
|
211
|
+
await Promise.race([this.close(`signal:${signal}`), timeoutPromise]);
|
|
212
|
+
logger.info({
|
|
213
|
+
type: "shutdown",
|
|
214
|
+
stage: "completed",
|
|
215
|
+
signal,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
shutdownFailed = true;
|
|
220
|
+
logger.error({
|
|
221
|
+
type: "shutdown",
|
|
222
|
+
stage: "failed",
|
|
223
|
+
signal,
|
|
224
|
+
forceExitOnFailure,
|
|
225
|
+
}, error);
|
|
226
|
+
process.exitCode = 1;
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
if (timeoutId) {
|
|
230
|
+
clearTimeout(timeoutId);
|
|
231
|
+
}
|
|
232
|
+
if (shutdownFailed) {
|
|
233
|
+
if (forceExitOnFailure) {
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
this.shutdownInProgress = false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
assertConfigurable(action) {
|
|
241
|
+
if (this.server || this.startPromise) {
|
|
242
|
+
throw new Error(`Cannot ${action} after application start`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
ensureModuleLoaded() {
|
|
246
|
+
if (this.moduleLoaded) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
this.di.loadModule(this.moduleImport);
|
|
250
|
+
this.moduleLoaded = true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CONFIG_TOKEN: unique symbol;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CONFIG_TOKEN = Symbol("app:config");
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { type DependencyContainer } from "tsyringe";
|
|
3
|
+
import type { Constructor, ExceptionFilterLike, GuardLike, InjectionToken, InterceptorLike, LoggerOptions, ModuleImport, PipeLike } from "../types";
|
|
4
|
+
import type { ZodType, output } from "zod";
|
|
5
|
+
import { type AppLogger } from "./logger";
|
|
6
|
+
export declare class AppContainer {
|
|
7
|
+
private readonly root;
|
|
8
|
+
private readonly loadedModules;
|
|
9
|
+
private readonly dynamicModuleSignatures;
|
|
10
|
+
private readonly globalExportOwners;
|
|
11
|
+
private readonly loadingModules;
|
|
12
|
+
private readonly rpcControllerSet;
|
|
13
|
+
private readonly httpControllerSet;
|
|
14
|
+
private readonly rpcControllerModules;
|
|
15
|
+
private readonly httpControllerModules;
|
|
16
|
+
private readonly lifecycleTargets;
|
|
17
|
+
private readonly requestScopeOwners;
|
|
18
|
+
private readonly requestScopeLinkedScopes;
|
|
19
|
+
private readonly requestContextStorage;
|
|
20
|
+
private rootModuleRef;
|
|
21
|
+
private controllers;
|
|
22
|
+
private httpControllers;
|
|
23
|
+
private lifecycleInstances;
|
|
24
|
+
private initialized;
|
|
25
|
+
private closed;
|
|
26
|
+
private exposeInternalErrors;
|
|
27
|
+
private globalGuards;
|
|
28
|
+
private globalInterceptors;
|
|
29
|
+
private globalPipes;
|
|
30
|
+
private globalFilters;
|
|
31
|
+
constructor();
|
|
32
|
+
private addLifecycleTarget;
|
|
33
|
+
private getLifecycleInstances;
|
|
34
|
+
private normalizeModuleImport;
|
|
35
|
+
private readModuleMetadata;
|
|
36
|
+
private mergeModuleMetadata;
|
|
37
|
+
private collectProviderTokens;
|
|
38
|
+
private linkImportedExports;
|
|
39
|
+
private resolveAppEnhancerInstance;
|
|
40
|
+
private registerAppEnhancerProvider;
|
|
41
|
+
private registerProvider;
|
|
42
|
+
private resolveModuleExports;
|
|
43
|
+
private registerGlobalExports;
|
|
44
|
+
private resolveControllerModule;
|
|
45
|
+
private getOrCreateLinkedScopes;
|
|
46
|
+
private getModuleScopeForRequest;
|
|
47
|
+
private resolveTokenFromModuleScope;
|
|
48
|
+
private assertRuntimeConfigMutable;
|
|
49
|
+
private loadModuleRef;
|
|
50
|
+
loadModule(moduleImport: ModuleImport): void;
|
|
51
|
+
resolve<T>(token: InjectionToken<T>): T;
|
|
52
|
+
resolveAll<T>(token: InjectionToken<T>): T[];
|
|
53
|
+
setLogger(logger: AppLogger): void;
|
|
54
|
+
configureLogger(options?: LoggerOptions): AppLogger;
|
|
55
|
+
getLogger(): AppLogger;
|
|
56
|
+
setExposeInternalErrors(value: boolean): void;
|
|
57
|
+
shouldExposeInternalErrors(): boolean;
|
|
58
|
+
setConfig<TConfig extends Record<string, unknown>>(config: TConfig): void;
|
|
59
|
+
configureConfig<TSchema extends ZodType>(schema: TSchema, source?: unknown): output<TSchema>;
|
|
60
|
+
getConfig<TConfig extends Record<string, unknown> = Record<string, unknown>>(): Readonly<TConfig>;
|
|
61
|
+
init(): Promise<void>;
|
|
62
|
+
close(signal?: string): Promise<void>;
|
|
63
|
+
runInRequestScope<T>(scope: DependencyContainer, execute: () => Promise<T> | T): Promise<T>;
|
|
64
|
+
disposeRequestScope(scope: DependencyContainer): Promise<void>;
|
|
65
|
+
createRequestScope(controller?: Constructor): DependencyContainer;
|
|
66
|
+
getControllers(): Constructor[];
|
|
67
|
+
getHttpControllers(): Constructor[];
|
|
68
|
+
setGlobalGuards(guards: GuardLike[]): void;
|
|
69
|
+
getGlobalGuards(): GuardLike[];
|
|
70
|
+
setGlobalInterceptors(inters: InterceptorLike[]): void;
|
|
71
|
+
getGlobalInterceptors(): InterceptorLike[];
|
|
72
|
+
setGlobalPipes(pipes: PipeLike[]): void;
|
|
73
|
+
getGlobalPipes(): PipeLike[];
|
|
74
|
+
setGlobalFilters(filters: ExceptionFilterLike[]): void;
|
|
75
|
+
getGlobalFilters(): ExceptionFilterLike[];
|
|
76
|
+
}
|