@noxfly/noxus 3.0.0-dev.8 → 3.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/README.md CHANGED
@@ -1,686 +1,686 @@
1
- # @noxfly/noxus
2
-
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
-
5
- No dependency on `reflect-metadata` or `emitDecoratorMetadata`.
6
-
7
- ---
8
-
9
- ## Installation
10
-
11
- ```bash
12
- npm install @noxfly/noxus
13
- ```
14
-
15
- In your `tsconfig.json`:
16
- ```json
17
- {
18
- "compilerOptions": {
19
- "experimentalDecorators": true,
20
- "emitDecoratorMetadata": false
21
- }
22
- }
23
- ```
24
-
25
- ---
26
-
27
- ## Concepts
28
-
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 |
38
-
39
- ---
40
-
41
- ## Quick start
42
-
43
- ### 1. Create a service
44
-
45
- ```ts
46
- // services/user.service.ts
47
- import { Injectable } from '@noxfly/noxus/main';
48
-
49
- @Injectable({ lifetime: 'singleton' })
50
- export class UserService {
51
- private users = [{ id: 1, name: 'Alice' }];
52
-
53
- findAll() {
54
- return this.users;
55
- }
56
-
57
- findById(id: number) {
58
- return this.users.find(u => u.id === id);
59
- }
60
- }
61
- ```
62
-
63
- ### 2. Create a controller
64
-
65
- ```ts
66
- // controllers/user.controller.ts
67
- import { Controller, Get, Post, Request } from '@noxfly/noxus/main';
68
- import { UserService } from '../services/user.service';
69
-
70
- @Controller({ path: 'users', deps: [UserService] })
71
- export class UserController {
72
- constructor(private svc: UserService) {}
73
-
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
- }
84
-
85
- @Post('create')
86
- create(req: Request) {
87
- return { created: true, body: req.body };
88
- }
89
- }
90
- ```
91
-
92
- ### 3. Create the application service
93
-
94
- ```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
- }
112
-
113
- async onActivated() {
114
- if (this.wm.count === 0) await this.onReady();
115
- }
116
-
117
- async dispose() {
118
- // cleanup on app close
119
- }
120
- }
121
- ```
122
-
123
- ### 4. Define routes
124
-
125
- ```ts
126
- // routes.ts
127
- import { defineRoutes } from '@noxfly/noxus/main';
128
-
129
- export const routes = defineRoutes([
130
- { path: 'users', load: () => import('./controllers/user.controller.js') },
131
- { path: 'orders', load: () => import('./controllers/order.controller.js') },
132
- ]);
133
- ```
134
-
135
- ### 5. Bootstrap the application
136
-
137
- ```ts
138
- // main.ts
139
- import { bootstrapApplication } from '@noxfly/noxus/main';
140
- import { AppService } from './app.service';
141
- import { routes } from './routes';
142
-
143
- const noxApp = await bootstrapApplication({ routes });
144
-
145
- noxApp
146
- .configure(AppService)
147
- .start();
148
- ```
149
-
150
- ---
151
-
152
- ## Dependency Injection
153
-
154
- ### `@Injectable`
155
-
156
- ```ts
157
- @Injectable({ lifetime: 'singleton', deps: [RepoA, RepoB] })
158
- class MyService {
159
- constructor(private a: RepoA, private b: RepoB) {}
160
- }
161
- ```
162
-
163
- | Option | Type | Default | Description |
164
- | ---------- | --------------------------------------- | --------- | ---------------------------------- |
165
- | `lifetime` | `'singleton' \| 'scope' \| 'transient'` | `'scope'` | Instance lifetime |
166
- | `deps` | `TokenKey[]` | `[]` | Constructor dependencies, in order |
167
-
168
- **Lifetimes:**
169
- - `singleton` — one instance for the entire lifetime of the app
170
- - `scope` — one instance per IPC request
171
- - `transient` — a new instance on every resolution
172
-
173
- ### `token()` — Non-class dependencies
174
-
175
- To inject values that are not classes (strings, interfaces, config objects):
176
-
177
- ```ts
178
- // tokens.ts
179
- import { token } from '@noxfly/noxus/main';
180
-
181
- export const DB_URL = token<string>('DB_URL');
182
- export const APP_CONFIG = token<AppConfig>('APP_CONFIG');
183
- ```
184
-
185
- ```ts
186
- // Declaring the dependency
187
- @Injectable({ deps: [DB_URL, APP_CONFIG] })
188
- class DbService {
189
- constructor(private url: string, private config: AppConfig) {}
190
- }
191
-
192
- // Providing the value in bootstrapApplication
193
- bootstrapApplication({
194
- singletons: [
195
- { token: DB_URL, useValue: process.env.DATABASE_URL! },
196
- { token: APP_CONFIG, useValue: { debug: true } },
197
- ],
198
- });
199
- ```
200
-
201
- ### `inject()` — Manual resolution
202
-
203
- ```ts
204
- import { inject } from '@noxfly/noxus/main';
205
-
206
- const userService = inject(UserService);
207
- ```
208
-
209
- When you decide to inject manually with `inject()`, you may not duplicate the dependency in the `deps` array of `@Injectable`.
210
- The resolve dependency in deps will be ignored in the constructor and will be re-solved at runtime by `inject()`.
211
-
212
- Useful outside a constructor — in callbacks, factories, etc.
213
-
214
- ### `forwardRef()` — Circular dependencies
215
-
216
- ```ts
217
- import { forwardRef } from '@noxfly/noxus/main';
218
-
219
- @Injectable({ deps: [forwardRef(() => ServiceB)] })
220
- class ServiceA {
221
- constructor(private b: ServiceB) {}
222
- }
223
- ```
224
-
225
- ---
226
-
227
- ## Routing
228
-
229
- ### Available HTTP methods
230
-
231
- ```ts
232
- import { Get, Post, Put, Patch, Delete } from '@noxfly/noxus/main';
233
-
234
- @Controller({ path: 'products', deps: [ProductService] })
235
- class ProductController {
236
- constructor(private svc: ProductService) {}
237
-
238
- @Get('list') list(req: Request) { ... }
239
- @Post('create') create(req: Request) { ... }
240
- @Put(':id') replace(req: Request) { ... }
241
- @Patch(':id') update(req: Request) { ... }
242
- @Delete(':id') remove(req: Request) { ... }
243
- }
244
- ```
245
-
246
- ### Route parameters
247
-
248
- ```ts
249
- @Get('category/:categoryId/product/:productId')
250
- getProduct(req: Request) {
251
- const { categoryId, productId } = req.params;
252
- }
253
- ```
254
-
255
- ### Query parameters
256
-
257
- ```ts
258
- // Renderer side:
259
- await client.request({ method: 'GET', path: 'users/list', query: { role: 'admin', page: '1' } });
260
-
261
- // Controller side:
262
- @Get('list')
263
- list(req: Request) {
264
- const role = req.query['role']; // 'admin'
265
- const page = req.query['page']; // '1'
266
- return this.svc.findAll({ role, page: parseInt(page!) });
267
- }
268
- ```
269
-
270
- ### Request body
271
-
272
- ```ts
273
- @Post('create')
274
- create(req: Request) {
275
- const { name, price } = req.body as { name: string; price: number };
276
- }
277
- ```
278
-
279
- ### Response
280
-
281
- The value returned by the handler is automatically placed in `response.body`. To control the status code:
282
-
283
- ```ts
284
- @Post('create')
285
- create(req: Request, res: IResponse) {
286
- res.status = 201;
287
- return { id: 42 };
288
- }
289
- ```
290
-
291
- ---
292
-
293
- ## Route definitions (`defineRoutes`)
294
-
295
- `defineRoutes` is the single source of truth for your routing table. It validates prefixes, detects duplicates and overlapping paths, and supports nested routes:
296
-
297
- ```ts
298
- import { defineRoutes } from '@noxfly/noxus/main';
299
-
300
- export const routes = defineRoutes([
301
- { path: 'users', load: () => import('./modules/users/users.controller.js'), guards: [authGuard] },
302
- { path: 'orders', load: () => import('./modules/orders/orders.controller.js') },
303
- ]);
304
- ```
305
-
306
- ### Nested routes
307
-
308
- Parent routes can omit `load` and only serve as a shared prefix with inherited guards/middlewares:
309
-
310
- ```ts
311
- export const routes = defineRoutes([
312
- {
313
- path: 'admin',
314
- guards: [authGuard, adminGuard],
315
- children: [
316
- { path: 'users', load: () => import('./admin/users.controller.js') },
317
- { path: 'products', load: () => import('./admin/products.controller.js') },
318
- ],
319
- },
320
- ]);
321
- // Produces flat routes: admin/users and admin/products, both inheriting authGuard + adminGuard.
322
- ```
323
-
324
- ## Lazy loading
325
-
326
- This is the core mechanism for keeping startup fast. A lazy controller is never imported until an IPC request targets its prefix.
327
-
328
- Routes declared via `defineRoutes` are lazy by default. You can also register lazy routes manually:
329
-
330
- ```ts
331
- noxApp
332
- .lazy('users', () => import('./modules/users/users.controller.js'))
333
- .lazy('orders', () => import('./modules/orders/orders.controller.js'))
334
- .lazy('printing', () => import('./modules/printing/printing.controller.js'))
335
- .start();
336
- ```
337
-
338
- > **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.
339
-
340
- ### Eager loading (`eagerLoad`)
341
-
342
- For modules whose services are needed before `onReady()`:
343
-
344
- ```ts
345
- bootstrapApplication({
346
- eagerLoad: [
347
- () => import('./modules/auth/auth.controller.js'),
348
- ],
349
- });
350
- ```
351
-
352
- ### Manual loading from NoxApp
353
-
354
- ```ts
355
- await noxApp.load([
356
- () => import('./modules/reporting/reporting.controller.js'),
357
- ]);
358
- ```
359
-
360
- ---
361
-
362
- ## Guards
363
-
364
- A guard is a plain function that decides whether a request can reach its handler.
365
-
366
- ```ts
367
- // guards/auth.guard.ts
368
- import { Guard } from '@noxfly/noxus/main';
369
-
370
- export const authGuard: Guard = async (req) => {
371
- return req.body?.token === 'secret'; // your auth logic
372
- };
373
- ```
374
-
375
- **On an entire controller:**
376
- ```ts
377
- @Controller({ path: 'admin', deps: [AdminService], guards: [authGuard] })
378
- class AdminController { ... }
379
- ```
380
-
381
- **On a specific route:**
382
- ```ts
383
- @Delete(':id', { guards: [authGuard, adminGuard] })
384
- remove(req: Request) { ... }
385
- ```
386
-
387
- Controller guards and route guards are **cumulative** — both run, in the given order.
388
-
389
- ---
390
-
391
- ## Middlewares
392
-
393
- A middleware is a plain function that runs before guards.
394
-
395
- ```ts
396
- // middlewares/log.middleware.ts
397
- import { Middleware } from '@noxfly/noxus/main';
398
-
399
- export const logMiddleware: Middleware = async (req, res, next) => {
400
- console.log(`→ ${req.method} ${req.path}`);
401
- await next();
402
- console.log(`← ${res.status}`);
403
- };
404
- ```
405
-
406
- **Global (all routes):**
407
- ```ts
408
- noxApp.use(logMiddleware);
409
- ```
410
-
411
- **On a controller:**
412
- ```ts
413
- @Controller({ path: 'users', deps: [...], middlewares: [logMiddleware] })
414
- class UserController { ... }
415
- ```
416
-
417
- **On a route:**
418
- ```ts
419
- @Post('upload', { middlewares: [fileSizeMiddleware] })
420
- upload(req: Request) { ... }
421
- ```
422
-
423
- **Execution order:** global middlewares → controller middlewares → route middlewares → guards → handler.
424
-
425
- ---
426
-
427
- ## WindowManager
428
-
429
- Injectable singleton service for managing `BrowserWindow` instances.
430
-
431
- ```ts
432
- @Injectable({ lifetime: 'singleton', deps: [WindowManager] })
433
- class AppService implements IApp {
434
- constructor(private wm: WindowManager) {}
435
-
436
- async onReady() {
437
- // Creates a 600×600 window, animates it to full screen,
438
- // then resolves the promise once the animation is complete.
439
- // loadFile() is therefore always called at the correct size — no viewbox freeze.
440
- const win = await this.wm.createSplash({
441
- webPreferences: { preload: path.join(__dirname, 'preload.js') },
442
- });
443
- win.loadFile('index.html');
444
- }
445
- }
446
- ```
447
-
448
- ### Full API
449
-
450
- ```ts
451
- // Creation
452
- const win = await wm.createSplash(options); // animated main window
453
- const win2 = await wm.create(config, isMain?); // custom window
454
-
455
- // Access
456
- wm.getMain() // main window
457
- wm.getById(id) // by Electron id
458
- wm.getAll() // all open windows
459
- wm.count // number of open windows
460
-
461
- // Actions
462
- wm.close(id) // close a window
463
- wm.closeAll() // close all windows
464
-
465
- // Messaging
466
- wm.send(id, 'channel', ...args) // send a message to one window
467
- wm.broadcast('channel', ...args) // send to all windows
468
- ```
469
-
470
- ### `createSplash` vs `create`
471
-
472
- | | `createSplash` | `create` |
473
- | ------------ | ---------------------- | ----------------------------------- |
474
- | Initial size | 600×600 centered | Whatever you define |
475
- | Animation | Expands to work area | Optional (`expandToWorkArea: true`) |
476
- | `show` | `true` immediately | `false` until `ready-to-show` |
477
- | Use case | Main window at startup | Secondary windows |
478
-
479
- ---
480
-
481
- ## External singletons
482
-
483
- To inject values built outside the DI container (DB connection, third-party SDK):
484
-
485
- ```ts
486
- // main.ts
487
- import { MikroORM } from '@mikro-orm/core';
488
- import { bootstrapApplication } from '@noxfly/noxus/main';
489
-
490
- const orm = await MikroORM.init(ormConfig);
491
-
492
- const noxApp = await bootstrapApplication({
493
- singletons: [
494
- { token: MikroORM, useValue: orm },
495
- ],
496
- });
497
- ```
498
-
499
- These values are then available via injection in any service:
500
-
501
- ```ts
502
- @Injectable({ lifetime: 'singleton', deps: [MikroORM] })
503
- class UserRepository {
504
- constructor(private orm: MikroORM) {}
505
- }
506
- ```
507
-
508
- ---
509
-
510
- ## Renderer side
511
-
512
- ### Preload
513
-
514
- ```ts
515
- // preload.ts
516
- import { exposeNoxusBridge } from '@noxfly/noxus/preload';
517
-
518
- exposeNoxusBridge(); // exposes window.__noxus__ to the renderer
519
- ```
520
-
521
- ### IPC client
522
-
523
- ```ts
524
- // In the renderer (Angular, React, Vue, Vanilla...)
525
- import { NoxRendererClient } from '@noxfly/noxus';
526
-
527
- const client = new NoxRendererClient({
528
- requestTimeout: 10_000, // 10s (default). Set to 0 to disable.
529
- });
530
- await client.setup(); // requests the MessagePort from main
531
-
532
- // Requests
533
- const users = await client.request<User[]>({ method: 'GET', path: 'users/list' });
534
- const user = await client.request<User> ({ method: 'GET', path: 'users/42' });
535
- await client.request({ method: 'GET', path: 'users/list', query: { role: 'admin' } });
536
- await client.request({ method: 'POST', path: 'users/create', body: { name: 'Bob' } });
537
- await client.request({ method: 'PUT', path: 'users/42', body: { name: 'Bob Updated' } });
538
- await client.request({ method: 'DELETE', path: 'users/42' });
539
-
540
- // Per-request timeout override (takes precedence over the global requestTimeout)
541
- const report = await client.request<Report>(
542
- { method: 'GET', path: 'reports/heavy' },
543
- { timeout: 60_000 }, // 60s for this specific request
544
- );
545
- ```
546
-
547
- ### Push events (main → renderer)
548
-
549
- On the main side, via `NoxSocket`:
550
-
551
- ```ts
552
- @Injectable({ lifetime: 'singleton', deps: [NoxSocket] })
553
- class NotificationService {
554
- constructor(private socket: NoxSocket) {}
555
-
556
- notifyAll(message: string) {
557
- this.socket.emit('notification', { message });
558
- }
559
-
560
- notifyOne(senderId: number, message: string) {
561
- this.socket.emitToRenderer(senderId, 'notification', { message });
562
- }
563
- }
564
- ```
565
-
566
- On the renderer side:
567
-
568
- ```ts
569
- const sub = client.events.subscribe<INotification>('notification', (payload) => {
570
- console.log(payload.message);
571
- });
572
-
573
- // Unsubscribe when done
574
- sub.unsubscribe();
575
- ```
576
-
577
- ---
578
-
579
- ## Batch requests
580
-
581
- Multiple IPC requests in a single round-trip:
582
-
583
- ```ts
584
- const results = await client.batch([
585
- { method: 'GET', path: 'users/list', query: { role: 'admin' } },
586
- { method: 'GET', path: 'products/list' },
587
- { method: 'POST', path: 'orders/create', body: { ... } },
588
- ]);
589
- ```
590
-
591
- ---
592
-
593
- ## Exceptions
594
-
595
- Noxus provides an HTTP exception hierarchy to throw from handlers:
596
-
597
- ```ts
598
- import {
599
- BadRequestException, // 400
600
- UnauthorizedException, // 401
601
- ForbiddenException, // 403
602
- NotFoundException, // 404
603
- ConflictException, // 409
604
- InternalServerException, // 500
605
- // ... and all other 4xx/5xx
606
- } from '@noxfly/noxus/main';
607
-
608
- @Get(':id')
609
- getOne(req: Request) {
610
- const user = this.svc.findById(parseInt(req.params['id']!));
611
- if (!user) throw new NotFoundException(`User not found`);
612
- return user;
613
- }
614
- ```
615
-
616
- The exception is automatically caught by the router and translated into a response with the correct HTTP status.
617
-
618
- ---
619
-
620
- ## Recommended project structure
621
-
622
- ```
623
- src/
624
- ├── main.ts ← bootstrapApplication + lazy routes
625
- ├── app.service.ts ← implements IApp
626
- ├── modules/
627
- │ ├── users/
628
- │ │ ├── user.controller.ts
629
- │ │ ├── user.service.ts
630
- │ │ └── user.repository.ts
631
- │ ├── orders/
632
- │ │ ├── order.controller.ts
633
- │ │ └── order.service.ts
634
- │ └── printing/
635
- │ ├── printing.controller.ts
636
- │ └── printing.service.ts
637
- ├── guards/
638
- │ └── auth.guard.ts
639
- ├── middlewares/
640
- │ └── log.middleware.ts
641
- └── tokens.ts ← shared named tokens
642
- ```
643
-
644
- 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.
645
-
646
- ---
647
-
648
- ## Log level
649
-
650
- By default, all framework logs are enabled (`debug` level). Control verbosity via `bootstrapApplication`:
651
-
652
- ```ts
653
- const noxApp = await bootstrapApplication({
654
- logLevel: 'none', // 'debug' | 'info' | 'none'
655
- });
656
- ```
657
-
658
- You can also pass an array of specific levels:
659
-
660
- ```ts
661
- bootstrapApplication({ logLevel: ['warn', 'error', 'critical'] });
662
- ```
663
-
664
- Or change it at runtime:
665
-
666
- ```ts
667
- import { Logger } from '@noxfly/noxus/main';
668
-
669
- Logger.setLogLevel('info');
670
- ```
671
-
672
- ---
673
-
674
- ## Testing
675
-
676
- To reset the DI container between tests (avoids leaking singletons across test suites):
677
-
678
- ```ts
679
- import { resetRootInjector } from '@noxfly/noxus/main';
680
-
681
- afterEach(() => {
682
- resetRootInjector();
683
- });
684
- ```
685
-
686
- This clears all bindings, singletons, and scoped instances from the root injector.
1
+ # @noxfly/noxus
2
+
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
+
5
+ No dependency on `reflect-metadata` or `emitDecoratorMetadata`.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @noxfly/noxus
13
+ ```
14
+
15
+ In your `tsconfig.json`:
16
+ ```json
17
+ {
18
+ "compilerOptions": {
19
+ "experimentalDecorators": true,
20
+ "emitDecoratorMetadata": false
21
+ }
22
+ }
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Concepts
28
+
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 |
38
+
39
+ ---
40
+
41
+ ## Quick start
42
+
43
+ ### 1. Create a service
44
+
45
+ ```ts
46
+ // services/user.service.ts
47
+ import { Injectable } from '@noxfly/noxus/main';
48
+
49
+ @Injectable({ lifetime: 'singleton' })
50
+ export class UserService {
51
+ private users = [{ id: 1, name: 'Alice' }];
52
+
53
+ findAll() {
54
+ return this.users;
55
+ }
56
+
57
+ findById(id: number) {
58
+ return this.users.find(u => u.id === id);
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### 2. Create a controller
64
+
65
+ ```ts
66
+ // controllers/user.controller.ts
67
+ import { Controller, Get, Post, Request } from '@noxfly/noxus/main';
68
+ import { UserService } from '../services/user.service';
69
+
70
+ @Controller({ path: 'users', deps: [UserService] })
71
+ export class UserController {
72
+ constructor(private svc: UserService) {}
73
+
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
+ }
84
+
85
+ @Post('create')
86
+ create(req: Request) {
87
+ return { created: true, body: req.body };
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### 3. Create the application service
93
+
94
+ ```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
+ }
112
+
113
+ async onActivated() {
114
+ if (this.wm.count === 0) await this.onReady();
115
+ }
116
+
117
+ async dispose() {
118
+ // cleanup on app close
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### 4. Define routes
124
+
125
+ ```ts
126
+ // routes.ts
127
+ import { defineRoutes } from '@noxfly/noxus/main';
128
+
129
+ export const routes = defineRoutes([
130
+ { path: 'users', load: () => import('./controllers/user.controller.js') },
131
+ { path: 'orders', load: () => import('./controllers/order.controller.js') },
132
+ ]);
133
+ ```
134
+
135
+ ### 5. Bootstrap the application
136
+
137
+ ```ts
138
+ // main.ts
139
+ import { bootstrapApplication } from '@noxfly/noxus/main';
140
+ import { AppService } from './app.service';
141
+ import { routes } from './routes';
142
+
143
+ const noxApp = await bootstrapApplication({ routes });
144
+
145
+ noxApp
146
+ .configure(AppService)
147
+ .start();
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Dependency Injection
153
+
154
+ ### `@Injectable`
155
+
156
+ ```ts
157
+ @Injectable({ lifetime: 'singleton', deps: [RepoA, RepoB] })
158
+ class MyService {
159
+ constructor(private a: RepoA, private b: RepoB) {}
160
+ }
161
+ ```
162
+
163
+ | Option | Type | Default | Description |
164
+ | ---------- | --------------------------------------- | --------- | ---------------------------------- |
165
+ | `lifetime` | `'singleton' \| 'scope' \| 'transient'` | `'scope'` | Instance lifetime |
166
+ | `deps` | `TokenKey[]` | `[]` | Constructor dependencies, in order |
167
+
168
+ **Lifetimes:**
169
+ - `singleton` — one instance for the entire lifetime of the app
170
+ - `scope` — one instance per IPC request
171
+ - `transient` — a new instance on every resolution
172
+
173
+ ### `token()` — Non-class dependencies
174
+
175
+ To inject values that are not classes (strings, interfaces, config objects):
176
+
177
+ ```ts
178
+ // tokens.ts
179
+ import { token } from '@noxfly/noxus/main';
180
+
181
+ export const DB_URL = token<string>('DB_URL');
182
+ export const APP_CONFIG = token<AppConfig>('APP_CONFIG');
183
+ ```
184
+
185
+ ```ts
186
+ // Declaring the dependency
187
+ @Injectable({ deps: [DB_URL, APP_CONFIG] })
188
+ class DbService {
189
+ constructor(private url: string, private config: AppConfig) {}
190
+ }
191
+
192
+ // Providing the value in bootstrapApplication
193
+ bootstrapApplication({
194
+ singletons: [
195
+ { token: DB_URL, useValue: process.env.DATABASE_URL! },
196
+ { token: APP_CONFIG, useValue: { debug: true } },
197
+ ],
198
+ });
199
+ ```
200
+
201
+ ### `inject()` — Manual resolution
202
+
203
+ ```ts
204
+ import { inject } from '@noxfly/noxus/main';
205
+
206
+ const userService = inject(UserService);
207
+ ```
208
+
209
+ When you decide to inject manually with `inject()`, you may not duplicate the dependency in the `deps` array of `@Injectable`.
210
+ The resolve dependency in deps will be ignored in the constructor and will be re-solved at runtime by `inject()`.
211
+
212
+ Useful outside a constructor — in callbacks, factories, etc.
213
+
214
+ ### `forwardRef()` — Circular dependencies
215
+
216
+ ```ts
217
+ import { forwardRef } from '@noxfly/noxus/main';
218
+
219
+ @Injectable({ deps: [forwardRef(() => ServiceB)] })
220
+ class ServiceA {
221
+ constructor(private b: ServiceB) {}
222
+ }
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Routing
228
+
229
+ ### Available HTTP methods
230
+
231
+ ```ts
232
+ import { Get, Post, Put, Patch, Delete } from '@noxfly/noxus/main';
233
+
234
+ @Controller({ path: 'products', deps: [ProductService] })
235
+ class ProductController {
236
+ constructor(private svc: ProductService) {}
237
+
238
+ @Get('list') list(req: Request) { ... }
239
+ @Post('create') create(req: Request) { ... }
240
+ @Put(':id') replace(req: Request) { ... }
241
+ @Patch(':id') update(req: Request) { ... }
242
+ @Delete(':id') remove(req: Request) { ... }
243
+ }
244
+ ```
245
+
246
+ ### Route parameters
247
+
248
+ ```ts
249
+ @Get('category/:categoryId/product/:productId')
250
+ getProduct(req: Request) {
251
+ const { categoryId, productId } = req.params;
252
+ }
253
+ ```
254
+
255
+ ### Query parameters
256
+
257
+ ```ts
258
+ // Renderer side:
259
+ await client.request({ method: 'GET', path: 'users/list', query: { role: 'admin', page: '1' } });
260
+
261
+ // Controller side:
262
+ @Get('list')
263
+ list(req: Request) {
264
+ const role = req.query['role']; // 'admin'
265
+ const page = req.query['page']; // '1'
266
+ return this.svc.findAll({ role, page: parseInt(page!) });
267
+ }
268
+ ```
269
+
270
+ ### Request body
271
+
272
+ ```ts
273
+ @Post('create')
274
+ create(req: Request) {
275
+ const { name, price } = req.body as { name: string; price: number };
276
+ }
277
+ ```
278
+
279
+ ### Response
280
+
281
+ The value returned by the handler is automatically placed in `response.body`. To control the status code:
282
+
283
+ ```ts
284
+ @Post('create')
285
+ create(req: Request, res: IResponse) {
286
+ res.status = 201;
287
+ return { id: 42 };
288
+ }
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Route definitions (`defineRoutes`)
294
+
295
+ `defineRoutes` is the single source of truth for your routing table. It validates prefixes, detects duplicates and overlapping paths, and supports nested routes:
296
+
297
+ ```ts
298
+ import { defineRoutes } from '@noxfly/noxus/main';
299
+
300
+ export const routes = defineRoutes([
301
+ { path: 'users', load: () => import('./modules/users/users.controller.js'), guards: [authGuard] },
302
+ { path: 'orders', load: () => import('./modules/orders/orders.controller.js') },
303
+ ]);
304
+ ```
305
+
306
+ ### Nested routes
307
+
308
+ Parent routes can omit `load` and only serve as a shared prefix with inherited guards/middlewares:
309
+
310
+ ```ts
311
+ export const routes = defineRoutes([
312
+ {
313
+ path: 'admin',
314
+ guards: [authGuard, adminGuard],
315
+ children: [
316
+ { path: 'users', load: () => import('./admin/users.controller.js') },
317
+ { path: 'products', load: () => import('./admin/products.controller.js') },
318
+ ],
319
+ },
320
+ ]);
321
+ // Produces flat routes: admin/users and admin/products, both inheriting authGuard + adminGuard.
322
+ ```
323
+
324
+ ## Lazy loading
325
+
326
+ This is the core mechanism for keeping startup fast. A lazy controller is never imported until an IPC request targets its prefix.
327
+
328
+ Routes declared via `defineRoutes` are lazy by default. You can also register lazy routes manually:
329
+
330
+ ```ts
331
+ noxApp
332
+ .lazy('users', () => import('./modules/users/users.controller.js'))
333
+ .lazy('orders', () => import('./modules/orders/orders.controller.js'))
334
+ .lazy('printing', () => import('./modules/printing/printing.controller.js'))
335
+ .start();
336
+ ```
337
+
338
+ > **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.
339
+
340
+ ### Eager loading (`eagerLoad`)
341
+
342
+ For modules whose services are needed before `onReady()`:
343
+
344
+ ```ts
345
+ bootstrapApplication({
346
+ eagerLoad: [
347
+ () => import('./modules/auth/auth.controller.js'),
348
+ ],
349
+ });
350
+ ```
351
+
352
+ ### Manual loading from NoxApp
353
+
354
+ ```ts
355
+ await noxApp.load([
356
+ () => import('./modules/reporting/reporting.controller.js'),
357
+ ]);
358
+ ```
359
+
360
+ ---
361
+
362
+ ## Guards
363
+
364
+ A guard is a plain function that decides whether a request can reach its handler.
365
+
366
+ ```ts
367
+ // guards/auth.guard.ts
368
+ import { Guard } from '@noxfly/noxus/main';
369
+
370
+ export const authGuard: Guard = async (req) => {
371
+ return req.body?.token === 'secret'; // your auth logic
372
+ };
373
+ ```
374
+
375
+ **On an entire controller:**
376
+ ```ts
377
+ @Controller({ path: 'admin', deps: [AdminService], guards: [authGuard] })
378
+ class AdminController { ... }
379
+ ```
380
+
381
+ **On a specific route:**
382
+ ```ts
383
+ @Delete(':id', { guards: [authGuard, adminGuard] })
384
+ remove(req: Request) { ... }
385
+ ```
386
+
387
+ Controller guards and route guards are **cumulative** — both run, in the given order.
388
+
389
+ ---
390
+
391
+ ## Middlewares
392
+
393
+ A middleware is a plain function that runs before guards.
394
+
395
+ ```ts
396
+ // middlewares/log.middleware.ts
397
+ import { Middleware } from '@noxfly/noxus/main';
398
+
399
+ export const logMiddleware: Middleware = async (req, res, next) => {
400
+ console.log(`→ ${req.method} ${req.path}`);
401
+ await next();
402
+ console.log(`← ${res.status}`);
403
+ };
404
+ ```
405
+
406
+ **Global (all routes):**
407
+ ```ts
408
+ noxApp.use(logMiddleware);
409
+ ```
410
+
411
+ **On a controller:**
412
+ ```ts
413
+ @Controller({ path: 'users', deps: [...], middlewares: [logMiddleware] })
414
+ class UserController { ... }
415
+ ```
416
+
417
+ **On a route:**
418
+ ```ts
419
+ @Post('upload', { middlewares: [fileSizeMiddleware] })
420
+ upload(req: Request) { ... }
421
+ ```
422
+
423
+ **Execution order:** global middlewares → controller middlewares → route middlewares → guards → handler.
424
+
425
+ ---
426
+
427
+ ## WindowManager
428
+
429
+ Injectable singleton service for managing `BrowserWindow` instances.
430
+
431
+ ```ts
432
+ @Injectable({ lifetime: 'singleton', deps: [WindowManager] })
433
+ class AppService implements IApp {
434
+ constructor(private wm: WindowManager) {}
435
+
436
+ async onReady() {
437
+ // Creates a 600×600 window, animates it to full screen,
438
+ // then resolves the promise once the animation is complete.
439
+ // loadFile() is therefore always called at the correct size — no viewbox freeze.
440
+ const win = await this.wm.createSplash({
441
+ webPreferences: { preload: path.join(__dirname, 'preload.js') },
442
+ });
443
+ win.loadFile('index.html');
444
+ }
445
+ }
446
+ ```
447
+
448
+ ### Full API
449
+
450
+ ```ts
451
+ // Creation
452
+ const win = await wm.createSplash(options); // animated main window
453
+ const win2 = await wm.create(config, isMain?); // custom window
454
+
455
+ // Access
456
+ wm.getMain() // main window
457
+ wm.getById(id) // by Electron id
458
+ wm.getAll() // all open windows
459
+ wm.count // number of open windows
460
+
461
+ // Actions
462
+ wm.close(id) // close a window
463
+ wm.closeAll() // close all windows
464
+
465
+ // Messaging
466
+ wm.send(id, 'channel', ...args) // send a message to one window
467
+ wm.broadcast('channel', ...args) // send to all windows
468
+ ```
469
+
470
+ ### `createSplash` vs `create`
471
+
472
+ | | `createSplash` | `create` |
473
+ | ------------ | ---------------------- | ----------------------------------- |
474
+ | Initial size | 600×600 centered | Whatever you define |
475
+ | Animation | Expands to work area | Optional (`expandToWorkArea: true`) |
476
+ | `show` | `true` immediately | `false` until `ready-to-show` |
477
+ | Use case | Main window at startup | Secondary windows |
478
+
479
+ ---
480
+
481
+ ## External singletons
482
+
483
+ To inject values built outside the DI container (DB connection, third-party SDK):
484
+
485
+ ```ts
486
+ // main.ts
487
+ import { MikroORM } from '@mikro-orm/core';
488
+ import { bootstrapApplication } from '@noxfly/noxus/main';
489
+
490
+ const orm = await MikroORM.init(ormConfig);
491
+
492
+ const noxApp = await bootstrapApplication({
493
+ singletons: [
494
+ { token: MikroORM, useValue: orm },
495
+ ],
496
+ });
497
+ ```
498
+
499
+ These values are then available via injection in any service:
500
+
501
+ ```ts
502
+ @Injectable({ lifetime: 'singleton', deps: [MikroORM] })
503
+ class UserRepository {
504
+ constructor(private orm: MikroORM) {}
505
+ }
506
+ ```
507
+
508
+ ---
509
+
510
+ ## Renderer side
511
+
512
+ ### Preload
513
+
514
+ ```ts
515
+ // preload.ts
516
+ import { exposeNoxusBridge } from '@noxfly/noxus/preload';
517
+
518
+ exposeNoxusBridge(); // exposes window.__noxus__ to the renderer
519
+ ```
520
+
521
+ ### IPC client
522
+
523
+ ```ts
524
+ // In the renderer (Angular, React, Vue, Vanilla...)
525
+ import { NoxRendererClient } from '@noxfly/noxus';
526
+
527
+ const client = new NoxRendererClient({
528
+ requestTimeout: 10_000, // 10s (default). Set to 0 to disable.
529
+ });
530
+ await client.setup(); // requests the MessagePort from main
531
+
532
+ // Requests
533
+ const users = await client.request<User[]>({ method: 'GET', path: 'users/list' });
534
+ const user = await client.request<User> ({ method: 'GET', path: 'users/42' });
535
+ await client.request({ method: 'GET', path: 'users/list', query: { role: 'admin' } });
536
+ await client.request({ method: 'POST', path: 'users/create', body: { name: 'Bob' } });
537
+ await client.request({ method: 'PUT', path: 'users/42', body: { name: 'Bob Updated' } });
538
+ await client.request({ method: 'DELETE', path: 'users/42' });
539
+
540
+ // Per-request timeout override (takes precedence over the global requestTimeout)
541
+ const report = await client.request<Report>(
542
+ { method: 'GET', path: 'reports/heavy' },
543
+ { timeout: 60_000 }, // 60s for this specific request
544
+ );
545
+ ```
546
+
547
+ ### Push events (main → renderer)
548
+
549
+ On the main side, via `NoxSocket`:
550
+
551
+ ```ts
552
+ @Injectable({ lifetime: 'singleton', deps: [NoxSocket] })
553
+ class NotificationService {
554
+ constructor(private socket: NoxSocket) {}
555
+
556
+ notifyAll(message: string) {
557
+ this.socket.emit('notification', { message });
558
+ }
559
+
560
+ notifyOne(senderId: number, message: string) {
561
+ this.socket.emitToRenderer(senderId, 'notification', { message });
562
+ }
563
+ }
564
+ ```
565
+
566
+ On the renderer side:
567
+
568
+ ```ts
569
+ const sub = client.events.subscribe<INotification>('notification', (payload) => {
570
+ console.log(payload.message);
571
+ });
572
+
573
+ // Unsubscribe when done
574
+ sub.unsubscribe();
575
+ ```
576
+
577
+ ---
578
+
579
+ ## Batch requests
580
+
581
+ Multiple IPC requests in a single round-trip:
582
+
583
+ ```ts
584
+ const results = await client.batch([
585
+ { method: 'GET', path: 'users/list', query: { role: 'admin' } },
586
+ { method: 'GET', path: 'products/list' },
587
+ { method: 'POST', path: 'orders/create', body: { ... } },
588
+ ]);
589
+ ```
590
+
591
+ ---
592
+
593
+ ## Exceptions
594
+
595
+ Noxus provides an HTTP exception hierarchy to throw from handlers:
596
+
597
+ ```ts
598
+ import {
599
+ BadRequestException, // 400
600
+ UnauthorizedException, // 401
601
+ ForbiddenException, // 403
602
+ NotFoundException, // 404
603
+ ConflictException, // 409
604
+ InternalServerException, // 500
605
+ // ... and all other 4xx/5xx
606
+ } from '@noxfly/noxus/main';
607
+
608
+ @Get(':id')
609
+ getOne(req: Request) {
610
+ const user = this.svc.findById(parseInt(req.params['id']!));
611
+ if (!user) throw new NotFoundException(`User not found`);
612
+ return user;
613
+ }
614
+ ```
615
+
616
+ The exception is automatically caught by the router and translated into a response with the correct HTTP status.
617
+
618
+ ---
619
+
620
+ ## Recommended project structure
621
+
622
+ ```
623
+ src/
624
+ ├── main.ts ← bootstrapApplication + lazy routes
625
+ ├── app.service.ts ← implements IApp
626
+ ├── modules/
627
+ │ ├── users/
628
+ │ │ ├── user.controller.ts
629
+ │ │ ├── user.service.ts
630
+ │ │ └── user.repository.ts
631
+ │ ├── orders/
632
+ │ │ ├── order.controller.ts
633
+ │ │ └── order.service.ts
634
+ │ └── printing/
635
+ │ ├── printing.controller.ts
636
+ │ └── printing.service.ts
637
+ ├── guards/
638
+ │ └── auth.guard.ts
639
+ ├── middlewares/
640
+ │ └── log.middleware.ts
641
+ └── tokens.ts ← shared named tokens
642
+ ```
643
+
644
+ 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.
645
+
646
+ ---
647
+
648
+ ## Log level
649
+
650
+ By default, all framework logs are enabled (`debug` level). Control verbosity via `bootstrapApplication`:
651
+
652
+ ```ts
653
+ const noxApp = await bootstrapApplication({
654
+ logLevel: 'none', // 'debug' | 'info' | 'none'
655
+ });
656
+ ```
657
+
658
+ You can also pass an array of specific levels:
659
+
660
+ ```ts
661
+ bootstrapApplication({ logLevel: ['warn', 'error', 'critical'] });
662
+ ```
663
+
664
+ Or change it at runtime:
665
+
666
+ ```ts
667
+ import { Logger } from '@noxfly/noxus/main';
668
+
669
+ Logger.setLogLevel('info');
670
+ ```
671
+
672
+ ---
673
+
674
+ ## Testing
675
+
676
+ To reset the DI container between tests (avoids leaking singletons across test suites):
677
+
678
+ ```ts
679
+ import { resetRootInjector } from '@noxfly/noxus/main';
680
+
681
+ afterEach(() => {
682
+ resetRootInjector();
683
+ });
684
+ ```
685
+
686
+ This clears all bindings, singletons, and scoped instances from the root injector.