@noxfly/noxus 2.5.0 → 3.0.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +403 -341
  2. package/dist/app-injector-Bz3Upc0y.d.mts +125 -0
  3. package/dist/app-injector-Bz3Upc0y.d.ts +125 -0
  4. package/dist/child.d.mts +48 -22
  5. package/dist/child.d.ts +48 -22
  6. package/dist/child.js +1111 -1341
  7. package/dist/child.mjs +1087 -1295
  8. package/dist/main.d.mts +301 -309
  9. package/dist/main.d.ts +301 -309
  10. package/dist/main.js +1471 -1650
  11. package/dist/main.mjs +1420 -1570
  12. package/dist/renderer.d.mts +3 -3
  13. package/dist/renderer.d.ts +3 -3
  14. package/dist/renderer.js +109 -135
  15. package/dist/renderer.mjs +109 -135
  16. package/dist/request-BlTtiHbi.d.ts +112 -0
  17. package/dist/request-qJ9EiDZc.d.mts +112 -0
  18. package/package.json +7 -7
  19. package/src/DI/app-injector.ts +95 -106
  20. package/src/DI/injector-explorer.ts +93 -119
  21. package/src/DI/token.ts +53 -0
  22. package/src/app.ts +141 -168
  23. package/src/bootstrap.ts +78 -54
  24. package/src/decorators/controller.decorator.ts +38 -27
  25. package/src/decorators/guards.decorator.ts +5 -64
  26. package/src/decorators/injectable.decorator.ts +68 -15
  27. package/src/decorators/method.decorator.ts +40 -81
  28. package/src/decorators/middleware.decorator.ts +5 -72
  29. package/src/index.ts +2 -0
  30. package/src/main.ts +4 -8
  31. package/src/non-electron-process.ts +0 -1
  32. package/src/preload-bridge.ts +1 -1
  33. package/src/renderer-client.ts +2 -2
  34. package/src/renderer-events.ts +1 -1
  35. package/src/request.ts +3 -3
  36. package/src/router.ts +190 -431
  37. package/src/routes.ts +78 -0
  38. package/src/socket.ts +4 -4
  39. package/src/window/window-manager.ts +255 -0
  40. package/tsconfig.json +5 -10
  41. package/tsup.config.ts +2 -2
  42. package/dist/app-injector-B3MvgV3k.d.mts +0 -95
  43. package/dist/app-injector-B3MvgV3k.d.ts +0 -95
  44. package/dist/request-CdpZ9qZL.d.ts +0 -167
  45. package/dist/request-Dx_5Prte.d.mts +0 -167
  46. package/src/decorators/inject.decorator.ts +0 -24
  47. package/src/decorators/injectable.metadata.ts +0 -15
  48. package/src/decorators/module.decorator.ts +0 -75
package/README.md CHANGED
@@ -1,508 +1,570 @@
1
- # ⚡ Noxus — The NestJS-Inspired Framework Built for Electron
1
+ # @noxfly/noxus
2
2
 
3
- Noxus brings the elegance and power of NestJS-like architecture to Electron applications but with a purpose-built design for IPC and MessagePort communication instead of HTTP.
3
+ Lightweight IPC framework for Electron, inspired by NestJS and Angular. It simulates HTTP-like requests between the renderer and the main process via `MessageChannel` / `MessagePort`, with routing, DI, guards, middlewares, and lazy-loading.
4
4
 
5
- While NestJS is an excellent framework for building web servers, it is not suited for Electron environments where communication between the main process and the renderer is critical.
5
+ No dependency on `reflect-metadata` or `emitDecoratorMetadata`.
6
6
 
7
- Transferring data between these using a local server and using HTTP request would be a waste of resources for the user's target device.
8
-
9
- Noxus fills that gap.
10
-
11
- ✅ Use of decorators
12
-
13
- ✅ Use of dependency injection, with lifetimes management (singleton, scope, transient)
14
-
15
- ✅ Modular architecture with the use of modules, defining a map of controllers and services
16
-
17
- ✅ Automatic and performant controller and route registration with path and method mapping
18
-
19
- ✅ A true request/response model built on top of MessagePort to look like HTTP requests
20
-
21
- ✅ Custom exception handling and unified error responses
22
-
23
- ✅ Decorator-based guard system for route and controller authorization
24
-
25
- ✅ Scoped dependency injection per request context
26
-
27
- ✅ Setup the electron application and communication with your renderer easily and with flexibility
28
-
29
- ✅ TypeScript-first with full type safety and metadata reflection
30
-
31
- ✅ Pluggable logging with color-coded output for different log levels
32
-
33
- <sub>* If you see any issue and you'd like to enhance this framework, feel free to open an issue, fork and do a pull request.</sub>
7
+ ---
34
8
 
35
9
  ## Installation
36
10
 
37
- Install the package in your main process application, and in your renderer as well :
11
+ ```bash
12
+ npm install @noxfly/noxus
13
+ ```
38
14
 
39
- ```sh
40
- npm i @noxfly/noxus
15
+ In your `tsconfig.json`:
16
+ ```json
17
+ {
18
+ "compilerOptions": {
19
+ "experimentalDecorators": true,
20
+ "emitDecoratorMetadata": false
21
+ }
22
+ }
41
23
  ```
42
24
 
43
- > ⚠️ The default entry (`@noxfly/noxus`) only exposes renderer-friendly helpers and types. Import Electron main-process APIs from `@noxfly/noxus/main`.
25
+ ---
44
26
 
45
- ## Basic use
27
+ ## Concepts
46
28
 
47
- When employing "main", we consider this is the electron side of your application.
29
+ | Concept | Role |
30
+ | ------------------------ | ----------------------------------------------------- |
31
+ | **Controller** | Handles a set of IPC routes under a path prefix |
32
+ | **Injectable** | Service injectable into other services or controllers |
33
+ | **Guard** | Protects a route or controller (authorization) |
34
+ | **Middleware** | Runs before guards and the handler |
35
+ | **Token** | Explicit identifier for non-class dependencies |
36
+ | **WindowManager** | Singleton service for managing Electron windows |
37
+ | **bootstrapApplication** | Single entry point of the application |
48
38
 
49
- When employing "renderer", this is the separated renderer side of your application.
39
+ ---
50
40
 
51
- However, you can feel free to keep both merged, this won't change anything, but for further examples, this will be done to show you where the code should go.
41
+ ## Quick start
52
42
 
53
- ### Setup Main Process side
43
+ ### 1. Create a service
54
44
 
55
45
  ```ts
56
- // main/index.ts
57
-
58
- import { bootstrapApplication } from '@noxfly/noxus/main';
59
- import { AppModule } from './modules/app.module.ts';
60
- import { Application } from './modules/app.service.ts';
46
+ // services/user.service.ts
47
+ import { Injectable } from '@noxfly/noxus/main';
61
48
 
62
- async function main(): Promise<void> {
63
- const noxApp = await bootstrapApplication(AppModule);
49
+ @Injectable({ lifetime: 'singleton' })
50
+ export class UserService {
51
+ private users = [{ id: 1, name: 'Alice' }];
64
52
 
65
- noxApp.configure(Application);
53
+ findAll() {
54
+ return this.users;
55
+ }
66
56
 
67
- noxApp.start();
57
+ findById(id: number) {
58
+ return this.users.find(u => u.id === id);
59
+ }
68
60
  }
69
-
70
- main();
71
61
  ```
72
62
 
73
- > ℹ️ Note that you have to specify which service you'd like to see as your application root's service, so the framework can interact with it and setup things on it.
63
+ ### 2. Create a controller
74
64
 
75
65
  ```ts
76
- // main/modules/app.service.ts
66
+ // controllers/user.controller.ts
67
+ import { Controller, Get, Post, Request } from '@noxfly/noxus/main';
68
+ import { UserService } from '../services/user.service';
77
69
 
78
- import { IApp, Injectable, Logger } from '@noxfly/noxus/main';
70
+ @Controller({ path: 'users', deps: [UserService] })
71
+ export class UserController {
72
+ constructor(private svc: UserService) {}
79
73
 
80
- @Injectable('singleton')
81
- export class Application implements IApp {
82
- constructor(
83
- private readonly windowManager: WindowManager, // An Injectable too
84
- ) {}
74
+ @Get('list')
75
+ list(req: Request) {
76
+ return this.svc.findAll();
77
+ }
78
+
79
+ @Get(':id')
80
+ getOne(req: Request) {
81
+ const id = parseInt(req.params['id']!);
82
+ return this.svc.findById(id);
83
+ }
85
84
 
86
- // automatically called by the bootstrapApplication function
87
- // once it is all setup
88
- public async onReady(): Promise<void> {
89
- Logger.info("Application's ready");
90
- this.windowManager.createMain(); // Your custom logic here to create a window
85
+ @Post('create')
86
+ create(req: Request) {
87
+ return { created: true, body: req.body };
91
88
  }
92
89
  }
93
90
  ```
94
91
 
92
+ ### 3. Create the application service
93
+
95
94
  ```ts
96
- // main/modules/app.module.ts
95
+ // app.service.ts
96
+ import { IApp, Injectable, WindowManager } from '@noxfly/noxus/main';
97
+ import path from 'path';
98
+
99
+ @Injectable({ lifetime: 'singleton', deps: [WindowManager] })
100
+ export class AppService implements IApp {
101
+ constructor(private wm: WindowManager) {}
102
+
103
+ async onReady() {
104
+ const win = await this.wm.createSplash({
105
+ webPreferences: {
106
+ preload: path.join(__dirname, 'preload.js'),
107
+ },
108
+ });
109
+
110
+ win.loadFile('index.html');
111
+ }
97
112
 
98
- import { Module } from '@noxfly/noxus/main';
113
+ async onActivated() {
114
+ if (this.wm.count === 0) await this.onReady();
115
+ }
99
116
 
100
- @Module({
101
- imports: [UsersModule], // import modules to be found here
102
- providers: [], // define services that should be found here
103
- controllers: [], // define controllers that this module has to create a route node
104
- })
105
- export class AppModule {}
117
+ async dispose() {
118
+ // cleanup on app close
119
+ }
120
+ }
106
121
  ```
107
122
 
108
- > ℹ️ Note that we do not register Application service in it because it already has been registered when bootstraping the application.
123
+ ### 4. Bootstrap the application
109
124
 
110
125
  ```ts
111
- // main/modules/users/users.module.ts
126
+ // main.ts
127
+ import { bootstrapApplication } from '@noxfly/noxus/main';
128
+ import { AppService } from './app.service';
112
129
 
113
- import { Module } from '@noxfly/noxus/main';
130
+ const noxApp = await bootstrapApplication();
114
131
 
115
- @Module({
116
- providers: [UsersService],
117
- controllers: [UsersController],
118
- })
119
- export class UsersModule {}
132
+ noxApp
133
+ .configure(AppService)
134
+ .lazy('users', () => import('./controllers/user.controller.js'))
135
+ .lazy('orders', () => import('./controllers/order.controller.js'))
136
+ .start();
120
137
  ```
121
138
 
122
- ```ts
123
- // main/modules/users/users.service.ts
139
+ ---
124
140
 
125
- import { Injectable } from '@noxfly/noxus/main';
141
+ ## Dependency Injection
126
142
 
127
- @Injectable()
128
- export class UsersService {
129
- public async findAll(): Promise<User[]> {
130
- // ...
131
- }
143
+ ### `@Injectable`
132
144
 
133
- public async findOneById(id: string): Promise<User> {
134
- // ...
135
- }
145
+ ```ts
146
+ @Injectable({ lifetime: 'singleton', deps: [RepoA, RepoB] })
147
+ class MyService {
148
+ constructor(private a: RepoA, private b: RepoB) {}
136
149
  }
137
150
  ```
138
151
 
139
- > ℹ️ You can specify the lifetime of an injectable passing a value in the decorator, between `singleton`, `scope` or `transient` (default to `scope`).
152
+ | Option | Type | Default | Description |
153
+ | ---------- | --------------------------------------- | --------- | ---------------------------------- |
154
+ | `lifetime` | `'singleton' \| 'scope' \| 'transient'` | `'scope'` | Instance lifetime |
155
+ | `deps` | `TokenKey[]` | `[]` | Constructor dependencies, in order |
140
156
 
141
- ```ts
142
- // main/modules/users/users.controller.ts
157
+ **Lifetimes:**
158
+ - `singleton` — one instance for the entire lifetime of the app
159
+ - `scope` — one instance per IPC request
160
+ - `transient` — a new instance on every resolution
143
161
 
144
- import { Controller, Get } from '@noxfly/noxus/main';
145
- import { UsersService } from './users.service.ts';
162
+ ### `token()` Non-class dependencies
146
163
 
147
- @Controller('users')
148
- export class UsersController {
149
- constructor(
150
- private readonly usersService: UsersService,
151
- ) {}
164
+ To inject values that are not classes (strings, interfaces, config objects):
152
165
 
153
- @Get('all')
154
- public getAll(): Promise<User[]> {
155
- return await this.usersService.findAll();
156
- }
166
+ ```ts
167
+ // tokens.ts
168
+ import { token } from '@noxfly/noxus/main';
157
169
 
158
- @Get('profile/:id')
159
- @Authorize(AuthGuard)
160
- public getProfile(IRequest request, IResponse response): Promise<User | undefined> {
161
- return await this.usersService.findOneById(request.params.id);
162
- }
163
- }
170
+ export const DB_URL = token<string>('DB_URL');
171
+ export const APP_CONFIG = token<AppConfig>('APP_CONFIG');
164
172
  ```
165
173
 
166
- Further upgrades might include new decorators like `@Param()`, `@Body()` etc... like Nest.js offers.
167
-
168
174
  ```ts
169
- // main/guards/auth.guard.ts
170
-
171
- import { IGuard, Injectable, MaybeAsync, Request } from '@noxfly/noxus/main';
172
-
173
- @Injectable()
174
- export class AuthGuard implements IGuard {
175
- constructor(
176
- private readonly authService: AuthService
177
- ) {}
178
-
179
- public async canActivate(IRequest request): MaybeAsync<boolean> {
180
- return this.authService.isAuthenticated();
181
- }
175
+ // Declaring the dependency
176
+ @Injectable({ deps: [DB_URL, APP_CONFIG] })
177
+ class DbService {
178
+ constructor(private url: string, private config: AppConfig) {}
182
179
  }
180
+
181
+ // Providing the value in bootstrapApplication
182
+ bootstrapApplication({
183
+ singletons: [
184
+ { token: DB_URL, useValue: process.env.DATABASE_URL! },
185
+ { token: APP_CONFIG, useValue: { debug: true } },
186
+ ],
187
+ });
183
188
  ```
184
189
 
185
- Here is the output (not with that exact same example) when running the main process :
190
+ ### `inject()` Manual resolution
186
191
 
187
- ![Startup image](./images/screenshot-startup.png)
192
+ ```ts
193
+ import { inject } from '@noxfly/noxus/main';
188
194
 
195
+ const userService = inject(UserService);
196
+ ```
189
197
 
190
- ### Setup Preload
198
+ Useful outside a constructor — in callbacks, factories, etc.
191
199
 
192
- We need some configuration on the preload so the main process can give the renderer a port (MessagePort) to communicate with. Everytime this is requested, a new channel is created, an d the previous is closed.
200
+ ### `forwardRef()` Circular dependencies
193
201
 
194
202
  ```ts
195
- // main/preload.ts
196
-
197
- import { exposeNoxusBridge } from '@noxfly/noxus';
203
+ import { forwardRef } from '@noxfly/noxus/main';
198
204
 
199
- exposeNoxusBridge();
205
+ @Injectable({ deps: [forwardRef(() => ServiceB)] })
206
+ class ServiceA {
207
+ constructor(private b: ServiceB) {}
208
+ }
200
209
  ```
201
210
 
202
- The helper uses `ipcRenderer.send('gimme-my-port')`, waits for the `'port'` response, starts both transferred `MessagePort`s, and forwards them to the renderer with `window.postMessage({ type: 'init-port', ... }, '*', [requestPort, socketPort])`. If you need to customise any channel names or the exposed property, pass `exposeNoxusBridge({ exposeAs: 'customNoxus', requestChannel: 'my-port', responseChannel: 'my-port-ready', initMessageType: 'custom-init' })`.
211
+ ---
203
212
 
204
- > ⚠️ As the Electron documentation warns, never expose the full `ipcRenderer` object. The helper only reveals a minimal `{ requestPort }` surface under `window.noxus` by default.
213
+ ## Routing
205
214
 
215
+ ### Available HTTP methods
206
216
 
207
- ### Setup Renderer
217
+ ```ts
218
+ import { Get, Post, Put, Patch, Delete } from '@noxfly/noxus/main';
208
219
 
209
- Noxus ships with a `NoxRendererClient` helper that performs the renderer bootstrap: it asks the preload bridge for a port, expects two transferable `MessagePort`s (index `0` for request/response and index `1` for socket pushes), wires both, and exposes a promise-based `request`/`batch` API plus the `RendererEventRegistry` instance.
220
+ @Controller({ path: 'products', deps: [ProductService] })
221
+ class ProductController {
222
+ constructor(private svc: ProductService) {}
210
223
 
211
- By default it calls `window.noxus.requestPort()`—the value registered by `exposeNoxusBridge()`—but you can pass a custom bridge through the constructor options if needed.
224
+ @Get('list') list(req: Request) { ... }
225
+ @Post('create') create(req: Request) { ... }
226
+ @Put(':id') replace(req: Request) { ... }
227
+ @Patch(':id') update(req: Request) { ... }
228
+ @Delete(':id') remove(req: Request) { ... }
229
+ }
230
+ ```
212
231
 
213
- Call `await client.setup()` early in your renderer startup (for example inside the first Angular service that needs IPC). Once the promise resolves, `client.request(...)` automatically includes the negotiated `senderId`, and socket events are routed through `client.events`.
232
+ ### Route parameters
214
233
 
215
234
  ```ts
216
- // renderer/services/noxus.service.ts
217
-
218
- import { Injectable } from '@angular/core';
219
- import { from, Observable } from 'rxjs';
220
- import {
221
- IBatchRequestItem,
222
- IBatchResponsePayload,
223
- IRequest,
224
- NoxRendererClient,
225
- } from '@noxfly/noxus';
226
-
227
- @Injectable({ providedIn: 'root' })
228
- export class NoxusService extends NoxRendererClient {
229
- public async init(): Promise<void> {
230
- await this.setup();
231
- }
235
+ @Get('category/:categoryId/product/:productId')
236
+ getProduct(req: Request) {
237
+ const { categoryId, productId } = req.params;
238
+ }
239
+ ```
232
240
 
233
- public request$<T, U = unknown>(request: Omit<IRequest<U>, 'requestId' | 'senderId'>): Observable<T> {
234
- return from(this.request<T, U>(request));
235
- }
241
+ ### Request body
236
242
 
237
- public batch$<T = IBatchResponsePayload>(requests: Omit<IBatchRequestItem<unknown>, 'requestId'>[]): Observable<T> {
238
- return from(this.batch(requests) as Promise<T>);
239
- }
243
+ ```ts
244
+ @Post('create')
245
+ create(req: Request) {
246
+ const { name, price } = req.body as { name: string; price: number };
240
247
  }
248
+ ```
241
249
 
242
- // Somewhere during app bootstrap
243
- await noxusService.init();
250
+ ### Response
244
251
 
245
- // Subscribe to push notifications
246
- const subscription = noxusService.events.subscribe('users.updated', (payload) => {
247
- console.log('Users updated:', payload);
248
- });
249
- ```
252
+ The value returned by the handler is automatically placed in `response.body`. To control the status code:
250
253
 
251
- If you need a custom bridge (for example a different preload shape), pass it via the constructor: `super({ bridge: window.customBridge })`. The class keeps promise-based semantics so frameworks can layer their own reactive wrappers as shown above.
254
+ ```ts
255
+ @Post('create')
256
+ create(req: Request, res: IResponse) {
257
+ res.status = 201;
258
+ return { id: 42 };
259
+ }
260
+ ```
252
261
 
253
- ![Startup image](./images/screenshot-requests.png)
262
+ ---
254
263
 
264
+ ## Lazy loading
255
265
 
256
- ### Error Handling
266
+ This is the core mechanism for keeping startup fast. A lazy controller is never imported until an IPC request targets its prefix.
257
267
 
258
- You have a bunch of `*Exception` classes that are at your disposal to help you have a clean code.
268
+ ```ts
269
+ noxApp
270
+ .lazy('users', () => import('./modules/users/users.controller.js'))
271
+ .lazy('orders', () => import('./modules/orders/orders.controller.js'))
272
+ .lazy('printing', () => import('./modules/printing/printing.controller.js'))
273
+ .start();
274
+ ```
259
275
 
260
- Replace `*` By any of the HTTP status code's name that is available in the list below.
276
+ > **Important:** the `import()` argument must not statically reference heavy modules. If `users.controller.ts` imports `applicationinsights` at the top of the file, the library will be loaded on the first `users/*` request — not at startup.
261
277
 
262
- If you throw any of these exception, the response to the renderer will contains the associated status code.
278
+ ### Eager loading (`eagerLoad`)
263
279
 
264
- You can specify a message in the constructor.
280
+ For modules whose services are needed before `onReady()`:
265
281
 
266
- You throw it as follow :
267
282
  ```ts
268
- throw new UnauthorizedException("Invalid credentials");
269
- throw new BadRequestException("id is missing in the body");
270
- throw new UnavailableException();
271
- // ...
283
+ bootstrapApplication({
284
+ eagerLoad: [
285
+ () => import('./modules/auth/auth.controller.js'),
286
+ ],
287
+ });
272
288
  ```
273
289
 
274
- | status code | class to throw |
275
- | ----------- | -------------------------------------- |
276
- | 400 | BadRequestException |
277
- | 401 | UnauthorizedException |
278
- | 402 | PaymentRequiredException |
279
- | 403 | ForbiddenException |
280
- | 404 | NotFoundException |
281
- | 405 | MethodNotAllowedException |
282
- | 406 | NotAcceptableException |
283
- | 408 | RequestTimeoutException |
284
- | 409 | ConflictException |
285
- | 426 | UpgradeRequiredException |
286
- | 429 | TooManyRequestsException |
287
- | 500 | InternalServerException |
288
- | 501 | NotImplementedException |
289
- | 502 | BadGatewayException |
290
- | 503 | ServiceUnavailableException |
291
- | 504 | GatewayTimeoutException |
292
- | 505 | HttpVersionNotSupportedException |
293
- | 506 | VariantAlsoNegotiatesException |
294
- | 507 | InsufficientStorageException |
295
- | 508 | LoopDetectedException |
296
- | 510 | NotExtendedException |
297
- | 511 | NetworkAuthenticationRequiredException |
298
- | 599 | NetworkConnectTimeoutException |
290
+ ### Manual loading from NoxApp
299
291
 
292
+ ```ts
293
+ await noxApp.load([
294
+ () => import('./modules/reporting/reporting.controller.js'),
295
+ ]);
296
+ ```
300
297
 
301
- ## Advanced
298
+ ---
302
299
 
303
- ### Injection
300
+ ## Guards
304
301
 
305
- You can decide to inject an Injectable without passing by the constructor, as follow :
302
+ A guard is a plain function that decides whether a request can reach its handler.
306
303
 
307
304
  ```ts
308
- import { inject } from '@noxfly/noxus/main';
309
- import { MyClass } from 'src/myclass';
305
+ // guards/auth.guard.ts
306
+ import { Guard } from '@noxfly/noxus/main';
310
307
 
311
- const instance: MyClass = inject(MyClass);
308
+ export const authGuard: Guard = async (req) => {
309
+ return req.body?.token === 'secret'; // your auth logic
310
+ };
312
311
  ```
313
312
 
314
- ### Circular Dependencies
315
-
316
- Noxus solves circular dependencies using a forward reference pattern. When two classes depend on each other, you can use `forwardRef()` combined with either the `@Inject()` decorator or the `inject()` helper to lazily resolve the dependency.
317
-
318
- **Using Constructor Injection (`@Inject`)**
313
+ **On an entire controller:**
314
+ ```ts
315
+ @Controller({ path: 'admin', deps: [AdminService], guards: [authGuard] })
316
+ class AdminController { ... }
317
+ ```
319
318
 
319
+ **On a specific route:**
320
320
  ```ts
321
- import { Injectable, Inject, forwardRef } from '@noxfly/noxus/main';
321
+ @Delete(':id', { guards: [authGuard, adminGuard] })
322
+ remove(req: Request) { ... }
323
+ ```
322
324
 
323
- @Injectable()
324
- class ServiceA {
325
- constructor(
326
- @Inject(forwardRef(() => ServiceB))
327
- private readonly serviceB: ServiceB
328
- ) {}
329
- }
325
+ Controller guards and route guards are **cumulative** — both run, in the given order.
330
326
 
331
- @Injectable()
332
- class ServiceB {
333
- constructor(
334
- private readonly serviceA: ServiceA
335
- ) {}
336
- }
337
- ```
327
+ ---
328
+
329
+ ## Middlewares
338
330
 
339
- **Using Property Injection (`inject`)**
331
+ A middleware is a plain function that runs before guards.
340
332
 
341
333
  ```ts
342
- import { Injectable, inject, forwardRef } from '@noxfly/noxus/main';
334
+ // middlewares/log.middleware.ts
335
+ import { Middleware } from '@noxfly/noxus/main';
336
+
337
+ export const logMiddleware: Middleware = async (req, res, next) => {
338
+ console.log(`→ ${req.method} ${req.path}`);
339
+ await next();
340
+ console.log(`← ${res.status}`);
341
+ };
342
+ ```
343
343
 
344
- @Injectable()
345
- class ServiceA {
346
- // Lazily resolves ServiceB, avoiding infinite recursion during instantiation
347
- private readonly serviceB = inject(forwardRef(() => ServiceB));
348
- }
344
+ **Global (all routes):**
345
+ ```ts
346
+ noxApp.use(logMiddleware);
347
+ ```
349
348
 
350
- @Injectable()
351
- class ServiceB {
352
- private readonly serviceA = inject(ServiceA);
353
- }
349
+ **On a controller:**
350
+ ```ts
351
+ @Controller({ path: 'users', deps: [...], middlewares: [logMiddleware] })
352
+ class UserController { ... }
354
353
  ```
355
354
 
356
- In both cases, a `Proxy` is returned, meaning the actual instance is only resolved when you access a property or method on it.
355
+ **On a route:**
356
+ ```ts
357
+ @Post('upload', { middlewares: [fileSizeMiddleware] })
358
+ upload(req: Request) { ... }
359
+ ```
357
360
 
358
- ### Middlewares
361
+ **Execution order:** global middlewares → controller middlewares → route middlewares → guards → handler.
359
362
 
360
- Declare middlewares as follow :
363
+ ---
361
364
 
362
- ```ts
363
- // renderer/middlewares.ts
365
+ ## WindowManager
364
366
 
365
- import { IMiddleware, Injectable, Request, IResponse, NextFunction } from '@noxfly/noxus/main';
367
+ Injectable singleton service for managing `BrowserWindow` instances.
366
368
 
367
- @Injectable()
368
- export class MiddlewareA implements IMiddleware {
369
- public async invoke(request: Request, response: IResponse, next: NextFunction): Promise<void> {
370
- console.log(`[Middleware A] before next()`);
371
- await next();
372
- console.log(`[Middleware A] after next()`);
369
+ ```ts
370
+ @Injectable({ lifetime: 'singleton', deps: [WindowManager] })
371
+ class AppService implements IApp {
372
+ constructor(private wm: WindowManager) {}
373
+
374
+ async onReady() {
375
+ // Creates a 600×600 window, animates it to full screen,
376
+ // then resolves the promise once the animation is complete.
377
+ // loadFile() is therefore always called at the correct size — no viewbox freeze.
378
+ const win = await this.wm.createSplash({
379
+ webPreferences: { preload: path.join(__dirname, 'preload.js') },
380
+ });
381
+ win.loadFile('index.html');
373
382
  }
374
383
  }
384
+ ```
375
385
 
376
- @Injectable()
377
- export class MiddlewareB implements IMiddleware {
378
- public async invoke(request: Request, response: IResponse, next: NextFunction): Promise<void> {
379
- console.log(`[Middleware B] before next()`);
380
- await next();
381
- console.log(`[Middleware B] after next()`);
382
- }
383
- }
386
+ ### Full API
387
+
388
+ ```ts
389
+ // Creation
390
+ const win = await wm.createSplash(options); // animated main window
391
+ const win2 = await wm.create(config, isMain?); // custom window
392
+
393
+ // Access
394
+ wm.getMain() // main window
395
+ wm.getById(id) // by Electron id
396
+ wm.getAll() // all open windows
397
+ wm.count // number of open windows
398
+
399
+ // Actions
400
+ wm.close(id) // close a window
401
+ wm.closeAll() // close all windows
402
+
403
+ // Messaging
404
+ wm.send(id, 'channel', ...args) // send a message to one window
405
+ wm.broadcast('channel', ...args) // send to all windows
384
406
  ```
385
407
 
386
- It is highly recommended to `await` the call of the `next` function.
408
+ ### `createSplash` vs `create`
387
409
 
388
- Register these by 3 possible ways :
410
+ | | `createSplash` | `create` |
411
+ | ------------ | ---------------------- | ----------------------------------- |
412
+ | Initial size | 600×600 centered | Whatever you define |
413
+ | Animation | Expands to work area | Optional (`expandToWorkArea: true`) |
414
+ | `show` | `true` immediately | `false` until `ready-to-show` |
415
+ | Use case | Main window at startup | Secondary windows |
389
416
 
390
- 1. For a root scope. Will be present for each routes.
417
+ ---
391
418
 
392
- ```ts
393
- const noxApp = bootstrapApplication(AppModule);
419
+ ## External singletons
394
420
 
395
- noxApp.configure(Application);
421
+ To inject values built outside the DI container (DB connection, third-party SDK):
396
422
 
397
- noxApp.use(MiddlewareA);
398
- noxApp.use(MiddlewareB);
423
+ ```ts
424
+ // main.ts
425
+ import { MikroORM } from '@mikro-orm/core';
426
+ import { bootstrapApplication } from '@noxfly/noxus/main';
399
427
 
400
- noxApp.start();
428
+ const orm = await MikroORM.init(ormConfig);
429
+
430
+ const noxApp = await bootstrapApplication({
431
+ singletons: [
432
+ { token: MikroORM, useValue: orm },
433
+ ],
434
+ });
401
435
  ```
402
436
 
403
- 2. Or for a controller or action's scope :
437
+ These values are then available via injection in any service:
404
438
 
405
439
  ```ts
406
- @Controller('user')
407
- @UseMiddlewares([MiddlewareA, MiddlewareB])
408
- export class UserController {
409
- @Get('all')
410
- @UseMiddlewares([MiddlewareA, MiddlewareB])
411
- public getAll(): Promise<void> {
412
- // ...
413
- }
440
+ @Injectable({ lifetime: 'singleton', deps: [MikroORM] })
441
+ class UserRepository {
442
+ constructor(private orm: MikroORM) {}
414
443
  }
415
444
  ```
416
445
 
417
- Note that if, for a given action, it has registered multiples times the same middleware, only the first registration will be saved.
446
+ ---
418
447
 
419
- For instance, registering MiddlewareA for root, on the controller and on the action is useless.
448
+ ## Renderer side
420
449
 
421
- The order of declaration of use of middlewares is important.
450
+ ### Preload
422
451
 
423
- assume we do this :
424
- 1. Use Middleware A for root
425
- 2. Use Middleware B for root just after MiddlewareA
426
- 3. Use Middleware C for controller
427
- 4. Use Middleware D for action
428
- 5. Use AuthGuard on the controller
429
- 6. Use RoleGuard on the action
430
-
431
- Then the executing pipeline will be as follow :
452
+ ```ts
453
+ // preload.ts
454
+ import { createPreloadBridge } from '@noxfly/noxus';
432
455
 
433
- ```r
434
- A -> B -> C -> D -> AuthGuard -> RoleGuard -> [action] -> D -> C -> B -> A.
456
+ createPreloadBridge(); // exposes window.__noxus__ to the renderer
435
457
  ```
436
458
 
437
- if a middleware throw any exception or put the response status higher or equal to 400, the pipeline immediatly stops and the response is returned, weither it is done before or after the call to the next function.
438
-
439
-
440
-
459
+ ### IPC client
441
460
 
461
+ ```ts
462
+ // In the renderer (Angular, React, Vue, Vanilla...)
463
+ import { NoxusClient } from '@noxfly/noxus';
464
+
465
+ const client = new NoxusClient();
466
+ await client.connect();
467
+
468
+ // Requests
469
+ const users = await client.get<User[]>('users/list');
470
+ const user = await client.get<User>('users/42');
471
+ await client.post('users/create', { name: 'Bob' });
472
+ await client.put('users/42', { name: 'Bob Updated' });
473
+ await client.delete('users/42');
474
+ ```
442
475
 
443
- ## Listening to events from the main process
476
+ ### Push events (main renderer)
444
477
 
445
- Starting from v1.2, the main process can push messages to renderer processes without the request/response flow.
478
+ On the main side, via `NoxSocket`:
446
479
 
447
480
  ```ts
448
- // main/users/users.controller.ts
449
- import { Controller, Post, Request, NoxSocket } from '@noxfly/noxus/main';
450
-
451
- @Controller('users')
452
- export class UsersController {
453
- constructor(private readonly socket: NoxSocket) {}
481
+ @Injectable({ lifetime: 'singleton', deps: [NoxSocket] })
482
+ class NotificationService {
483
+ constructor(private socket: NoxSocket) {}
454
484
 
455
- @Post('create')
456
- public async create(request: Request): Promise<void> {
457
- const payload = { nickname: request.body.nickname };
485
+ notifyAll(message: string) {
486
+ this.socket.emit('notification', { message });
487
+ }
458
488
 
459
- this.socket.emitToRenderer(request.event.senderId, 'users:created', payload);
460
- // or broadcast to every connected renderer: this.socket.emit('users:created', payload);
489
+ notifyOne(senderId: number, message: string) {
490
+ this.socket.emitToRenderer(senderId, 'notification', { message });
461
491
  }
462
492
  }
463
493
  ```
464
494
 
465
- On the renderer side, leverage the `RendererEventRegistry` to register and clean up listeners. The registry only handles push events, so it plays nicely with the existing request handling code above.
495
+ On the renderer side:
466
496
 
467
497
  ```ts
468
- import { RendererEventRegistry, RendererEventSubscription } from '@noxfly/noxus';
498
+ client.on('notification', (payload) => {
499
+ console.log(payload.message);
500
+ });
501
+ ```
469
502
 
470
- private readonly events = new RendererEventRegistry();
503
+ ---
471
504
 
472
- constructor() {
473
- // ... after the MessagePort is ready
474
- this.port.onmessage = (event) => {
475
- if(this.events.tryDispatchFromMessageEvent(event)) {
476
- return;
477
- }
505
+ ## Batch requests
478
506
 
479
- this.onMessage(event); // existing request handling
480
- };
481
- }
507
+ Multiple IPC requests in a single round-trip:
482
508
 
483
- public onUsersCreated(): RendererEventSubscription {
484
- return this.events.subscribe('users:created', (payload) => {
485
- // react to the event
486
- });
487
- }
488
-
489
- public teardown(): void {
490
- this.events.clear();
491
- }
509
+ ```ts
510
+ const results = await client.batch([
511
+ { method: 'GET', path: 'users/list' },
512
+ { method: 'GET', path: 'products/list' },
513
+ { method: 'POST', path: 'orders/create', body: { ... } },
514
+ ]);
492
515
  ```
493
516
 
517
+ ---
494
518
 
519
+ ## Exceptions
495
520
 
521
+ Noxus provides an HTTP exception hierarchy to throw from handlers:
496
522
 
497
- ## Contributing
523
+ ```ts
524
+ import {
525
+ BadRequestException, // 400
526
+ UnauthorizedException, // 401
527
+ ForbiddenException, // 403
528
+ NotFoundException, // 404
529
+ ConflictException, // 409
530
+ InternalServerException, // 500
531
+ // ... and all other 4xx/5xx
532
+ } from '@noxfly/noxus/main';
533
+
534
+ @Get(':id')
535
+ getOne(req: Request) {
536
+ const user = this.svc.findById(parseInt(req.params['id']!));
537
+ if (!user) throw new NotFoundException(`User not found`);
538
+ return user;
539
+ }
540
+ ```
498
541
 
499
- 1. Clone the repo
500
- 1. `npm i`
501
- 1. Develop
502
- 1. Push changes (automatically builds)
503
- 1. Create a PR
542
+ The exception is automatically caught by the router and translated into a response with the correct HTTP status.
504
543
 
505
- if you'd like to test your changes locally :
506
- 1. `npm run build`
507
- 1. from an electron project, `npm i ../<path/to/repo>`
544
+ ---
545
+
546
+ ## Recommended project structure
547
+
548
+ ```
549
+ src/
550
+ ├── main.ts ← bootstrapApplication + lazy routes
551
+ ├── app.service.ts ← implements IApp
552
+ ├── modules/
553
+ │ ├── users/
554
+ │ │ ├── user.controller.ts
555
+ │ │ ├── user.service.ts
556
+ │ │ └── user.repository.ts
557
+ │ ├── orders/
558
+ │ │ ├── order.controller.ts
559
+ │ │ └── order.service.ts
560
+ │ └── printing/
561
+ │ ├── printing.controller.ts
562
+ │ └── printing.service.ts
563
+ ├── guards/
564
+ │ └── auth.guard.ts
565
+ ├── middlewares/
566
+ │ └── log.middleware.ts
567
+ └── tokens.ts ← shared named tokens
568
+ ```
508
569
 
570
+ Each `module/` folder is **self-contained** — the controller imports its own services directly, with no central declaration. `main.ts` only knows the lazy loading paths.