@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/src/router.ts CHANGED
@@ -4,252 +4,155 @@
4
4
  * @author NoxFly
5
5
  */
6
6
 
7
- import 'reflect-metadata';
8
- import { getControllerMetadata } from 'src/decorators/controller.decorator';
9
- import { getGuardForController, getGuardForControllerAction, IGuard } from 'src/decorators/guards.decorator';
10
- import { Injectable } from 'src/decorators/injectable.decorator';
11
- import { AtomicHttpMethod, getRouteMetadata } from 'src/decorators/method.decorator';
12
- import { getMiddlewaresForController, getMiddlewaresForControllerAction, IMiddleware, NextFunction } from 'src/decorators/middleware.decorator';
13
- import { BadRequestException, MethodNotAllowedException, NotFoundException, ResponseException, UnauthorizedException } from 'src/exceptions';
14
- import { IBatchRequestItem, IBatchRequestPayload, IBatchResponsePayload, IResponse, Request } from 'src/request';
15
- import { InjectorExplorer } from 'src/DI/injector-explorer';
16
- import { Logger } from 'src/utils/logger';
17
- import { RadixTree } from 'src/utils/radix-tree';
18
- import { Type } from 'src/utils/types';
7
+ import { getControllerMetadata } from './decorators/controller.decorator';
8
+ import { Guard } from './decorators/guards.decorator';
9
+ import { Injectable } from './decorators/injectable.decorator';
10
+ import { getRouteMetadata, isAtomicHttpMethod } from './decorators/method.decorator';
11
+ import { Middleware, NextFunction } from './decorators/middleware.decorator';
12
+ import { InjectorExplorer } from './DI/injector-explorer';
13
+ import {
14
+ BadRequestException,
15
+ NotFoundException,
16
+ ResponseException,
17
+ UnauthorizedException
18
+ } from './exceptions';
19
+ import { IBatchRequestItem, IBatchRequestPayload, IBatchResponsePayload, IResponse, Request } from './request';
20
+ import { Logger } from './utils/logger';
21
+ import { RadixTree } from './utils/radix-tree';
22
+ import { Type } from './utils/types';
19
23
 
20
- /**
21
- * A lazy route entry maps a path prefix to a dynamic import function.
22
- * The module is loaded on the first request matching the prefix.
23
- */
24
24
  export interface ILazyRoute {
25
- /** Path prefix (e.g. "auth", "printing"). Matched against the first segment(s) of the request path. */
26
- path: string;
27
- /** Dynamic import function returning the module file. */
28
- loadModule: () => Promise<unknown>;
25
+ load: () => Promise<unknown>;
26
+ guards: Guard[];
27
+ middlewares: Middleware[];
28
+ loading: Promise<void> | null;
29
+ loaded: boolean;
29
30
  }
30
31
 
31
32
  interface LazyRouteEntry {
32
- loadModule: () => Promise<unknown>;
33
+ load: (() => Promise<unknown>) | null;
34
+ guards: Guard[];
35
+ middlewares: Middleware[];
33
36
  loading: Promise<void> | null;
34
37
  loaded: boolean;
35
38
  }
36
39
 
37
- const ATOMIC_HTTP_METHODS: ReadonlySet<AtomicHttpMethod> = new Set<AtomicHttpMethod>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
38
-
39
- function isAtomicHttpMethod(method: unknown): method is AtomicHttpMethod {
40
- return typeof method === 'string' && ATOMIC_HTTP_METHODS.has(method as AtomicHttpMethod);
41
- }
42
-
43
- /**
44
- * IRouteDefinition interface defines the structure of a route in the application.
45
- * It includes the HTTP method, path, controller class, handler method name,
46
- * guards, and middlewares associated with the route.
47
- */
48
40
  export interface IRouteDefinition {
49
41
  method: string;
50
42
  path: string;
51
- controller: Type<any>;
43
+ controller: Type<unknown>;
52
44
  handler: string;
53
- guards: Type<IGuard>[];
54
- middlewares: Type<IMiddleware>[];
45
+ guards: Guard[];
46
+ middlewares: Middleware[];
55
47
  }
56
48
 
57
- /**
58
- * This type defines a function that represents an action in a controller.
59
- * It takes a Request and an IResponse as parameters and returns a value or a Promise.
60
- */
61
- export type ControllerAction = (request: Request, response: IResponse) => any;
62
-
49
+ export type ControllerAction = (request: Request, response: IResponse) => unknown;
63
50
 
64
- /**
65
- * Router class is responsible for managing the application's routing.
66
- * It registers controllers, handles requests, and manages middlewares and guards.
67
- */
68
- @Injectable('singleton')
51
+ @Injectable({ lifetime: 'singleton' })
69
52
  export class Router {
70
53
  private readonly routes = new RadixTree<IRouteDefinition>();
71
- private readonly rootMiddlewares: Type<IMiddleware>[] = [];
54
+ private readonly rootMiddlewares: Middleware[] = [];
72
55
  private readonly lazyRoutes = new Map<string, LazyRouteEntry>();
73
56
 
74
- /**
75
- * Registers a controller class with the router.
76
- * This method extracts the route metadata from the controller class and registers it in the routing tree.
77
- * It also handles the guards and middlewares associated with the controller.
78
- * @param controllerClass - The controller class to register.
79
- */
80
- public registerController(controllerClass: Type<unknown>): Router {
81
- const controllerMeta = getControllerMetadata(controllerClass);
82
-
83
- const controllerGuards = getGuardForController(controllerClass.name);
84
- const controllerMiddlewares = getMiddlewaresForController(controllerClass.name);
57
+ // -------------------------------------------------------------------------
58
+ // Registration
59
+ // -------------------------------------------------------------------------
85
60
 
86
- if(!controllerMeta)
87
- throw new Error(`Missing @Controller decorator on ${controllerClass.name}`);
61
+ public registerController(
62
+ controllerClass: Type<unknown>,
63
+ pathPrefix: string,
64
+ routeGuards: Guard[] = [],
65
+ routeMiddlewares: Middleware[] = [],
66
+ ): this {
67
+ const meta = getControllerMetadata(controllerClass);
88
68
 
89
- const routeMetadata = getRouteMetadata(controllerClass);
69
+ if (!meta) {
70
+ throw new Error(`[Noxus] Missing @Controller decorator on ${controllerClass.name}`);
71
+ }
90
72
 
91
- for(const def of routeMetadata) {
92
- const fullPath = `${controllerMeta.path}/${def.path}`.replace(/\/+/g, '/');
73
+ const routeMeta = getRouteMetadata(controllerClass);
93
74
 
94
- const routeGuards = getGuardForControllerAction(controllerClass.name, def.handler);
95
- const routeMiddlewares = getMiddlewaresForControllerAction(controllerClass.name, def.handler);
75
+ for (const def of routeMeta) {
76
+ const fullPath = `${pathPrefix}/${def.path}`.replace(/\/+/g, '/').replace(/\/$/, '') || '/';
96
77
 
97
- const guards = new Set([...controllerGuards, ...routeGuards]);
98
- const middlewares = new Set([...controllerMiddlewares, ...routeMiddlewares]);
78
+ // Route-level guards/middlewares from defineRoutes() + action-level ones
79
+ const guards = [...new Set([...routeGuards, ...def.guards])];
80
+ const middlewares = [...new Set([...routeMiddlewares, ...def.middlewares])];
99
81
 
100
82
  const routeDef: IRouteDefinition = {
101
83
  method: def.method,
102
84
  path: fullPath,
103
85
  controller: controllerClass,
104
86
  handler: def.handler,
105
- guards: [...guards],
106
- middlewares: [...middlewares],
87
+ guards,
88
+ middlewares,
107
89
  };
108
90
 
109
91
  this.routes.insert(fullPath + '/' + def.method, routeDef);
110
92
 
111
- const hasActionGuards = routeDef.guards.length > 0;
112
-
113
- const actionGuardsInfo = hasActionGuards
114
- ? '<' + routeDef.guards.map(g => g.name).join('|') + '>'
115
- : '';
116
-
117
- Logger.log(`Mapped {${routeDef.method} /${fullPath}}${actionGuardsInfo} route`);
93
+ const guardInfo = guards.length ? `<${guards.map(g => g.name).join('|')}>` : '';
94
+ Logger.log(`Mapped {${def.method} /${fullPath}}${guardInfo} route`);
118
95
  }
119
96
 
120
- const hasCtrlGuards = controllerMeta.guards.length > 0;
121
-
122
- const controllerGuardsInfo = hasCtrlGuards
123
- ? '<' + controllerMeta.guards.map(g => g.name).join('|') + '>'
97
+ const ctrlGuardInfo = routeGuards.length
98
+ ? `<${routeGuards.map(g => g.name).join('|')}>`
124
99
  : '';
125
-
126
- Logger.log(`Mapped ${controllerClass.name}${controllerGuardsInfo} controller's routes`);
100
+ Logger.log(`Mapped ${controllerClass.name}${ctrlGuardInfo} controller's routes`);
127
101
 
128
102
  return this;
129
103
  }
130
104
 
131
- /**
132
- * Registers a lazy route. The module behind this route prefix will only
133
- * be imported (and its controllers/services registered in DI) the first
134
- * time a request targets this prefix.
135
- *
136
- * @param pathPrefix - Route prefix (e.g. "auth"). Matched against the first segment of the request path.
137
- * @param loadModule - A function that returns a dynamic import promise.
138
- */
139
- public registerLazyRoute(pathPrefix: string, loadModule: () => Promise<unknown>): Router {
105
+ public registerLazyRoute(
106
+ pathPrefix: string,
107
+ load: () => Promise<unknown>,
108
+ guards: Guard[] = [],
109
+ middlewares: Middleware[] = [],
110
+ ): this {
140
111
  const normalized = pathPrefix.replace(/^\/+|\/+$/g, '');
141
- this.lazyRoutes.set(normalized, { loadModule, loading: null, loaded: false });
112
+ this.lazyRoutes.set(normalized, { load, guards, middlewares, loading: null, loaded: false });
142
113
  Logger.log(`Registered lazy route prefix {${normalized}}`);
143
114
  return this;
144
115
  }
145
116
 
146
- /**
147
- * Defines a middleware for the root of the application.
148
- * This method allows you to register a middleware that will be applied to all requests
149
- * to the application, regardless of the controller or action.
150
- * @param middleware - The middleware class to register.
151
- */
152
- public defineRootMiddleware(middleware: Type<IMiddleware>): Router {
117
+ public defineRootMiddleware(middleware: Middleware): this {
153
118
  this.rootMiddlewares.push(middleware);
154
119
  return this;
155
120
  }
156
121
 
157
- /**
158
- * Shuts down the message channel for a specific sender ID.
159
- * This method closes the IPC channel for the specified sender ID and
160
- * removes it from the messagePorts map.
161
- * @param channelSenderId - The ID of the sender channel to shut down.
162
- */
163
- public async handle(request: Request): Promise<IResponse> {
164
- if(request.method === 'BATCH') {
165
- return this.handleBatch(request);
166
- }
122
+ // -------------------------------------------------------------------------
123
+ // Request handling
124
+ // -------------------------------------------------------------------------
167
125
 
168
- return this.handleAtomic(request);
126
+ public async handle(request: Request): Promise<IResponse> {
127
+ return request.method === 'BATCH'
128
+ ? this.handleBatch(request)
129
+ : this.handleAtomic(request);
169
130
  }
170
131
 
171
132
  private async handleAtomic(request: Request): Promise<IResponse> {
172
133
  Logger.comment(`> ${request.method} /${request.path}`);
173
-
174
134
  const t0 = performance.now();
175
135
 
176
- const response: IResponse = {
177
- requestId: request.id,
178
- status: 200,
179
- body: null,
180
- };
181
-
182
- let isCritical: boolean = false;
136
+ const response: IResponse = { requestId: request.id, status: 200, body: null };
137
+ let isCritical = false;
183
138
 
184
139
  try {
185
140
  const routeDef = await this.findRoute(request);
186
141
  await this.resolveController(request, response, routeDef);
187
142
 
188
- if(response.status > 400) {
189
- throw new ResponseException(response.status, response.error);
190
- }
143
+ if (response.status >= 400) throw new ResponseException(response.status, response.error);
191
144
  }
192
- catch(error: unknown) {
193
- response.body = undefined;
194
-
195
- if(error instanceof ResponseException) {
196
- response.status = error.status;
197
- response.error = error.message;
198
- response.stack = error.stack;
199
- }
200
- else if(error instanceof Error) {
201
- isCritical = true;
202
- response.status = 500;
203
- response.error = error.message || 'Internal Server Error';
204
- response.stack = error.stack || 'No stack trace available';
205
- }
206
- else {
207
- isCritical = true;
208
- response.status = 500;
209
- response.error = 'Unknown error occurred';
210
- response.stack = 'No stack trace available';
211
- }
145
+ catch (error) {
146
+ this.fillErrorResponse(response, error, (c) => { isCritical = c; });
212
147
  }
213
148
  finally {
214
- const t1 = performance.now();
215
-
216
- const message = `< ${response.status} ${request.method} /${request.path} ${Logger.colors.yellow}${Math.round(t1 - t0)}ms${Logger.colors.initial}`;
217
-
218
- if(response.status < 400) {
219
- Logger.log(message);
220
- }
221
- else if(response.status < 500) {
222
- Logger.warn(message);
223
- }
224
- else {
225
- if(isCritical) {
226
- Logger.critical(message);
227
- }
228
- else {
229
- Logger.error(message);
230
- }
231
- }
232
-
233
- if(response.error !== undefined) {
234
- if(isCritical) {
235
- Logger.critical(response.error);
236
- }
237
- else {
238
- Logger.error(response.error);
239
- }
240
-
241
- if(response.stack !== undefined) {
242
- Logger.errorStack(response.stack);
243
- }
244
- }
245
-
149
+ this.logResponse(request, response, performance.now() - t0, isCritical);
246
150
  return response;
247
151
  }
248
152
  }
249
153
 
250
154
  private async handleBatch(request: Request): Promise<IResponse> {
251
155
  Logger.comment(`> ${request.method} /${request.path}`);
252
-
253
156
  const t0 = performance.now();
254
157
 
255
158
  const response: IResponse<IBatchResponsePayload> = {
@@ -257,338 +160,194 @@ export class Router {
257
160
  status: 200,
258
161
  body: { responses: [] },
259
162
  };
260
-
261
- let isCritical: boolean = false;
163
+ let isCritical = false;
262
164
 
263
165
  try {
264
166
  const payload = this.normalizeBatchPayload(request.body);
265
-
266
- const batchPromises = payload.requests.map((item, index) => {
267
- const subRequestId = item.requestId ?? `${request.id}:${index}`;
268
- const atomicRequest = new Request(request.event, request.senderId, subRequestId, item.method, item.path, item.body);
269
- return this.handleAtomic(atomicRequest);
270
- });
271
-
272
- response.body!.responses = await Promise.all(batchPromises);
167
+ response.body!.responses = await Promise.all(
168
+ payload.requests.map((item, i) => {
169
+ const id = item.requestId ?? `${request.id}:${i}`;
170
+ return this.handleAtomic(new Request(request.event, request.senderId, id, item.method, item.path, item.body));
171
+ }),
172
+ );
273
173
  }
274
- catch(error: unknown) {
275
- response.body = undefined;
276
-
277
- if(error instanceof ResponseException) {
278
- response.status = error.status;
279
- response.error = error.message;
280
- response.stack = error.stack;
281
- }
282
- else if(error instanceof Error) {
283
- isCritical = true;
284
- response.status = 500;
285
- response.error = error.message || 'Internal Server Error';
286
- response.stack = error.stack || 'No stack trace available';
287
- }
288
- else {
289
- isCritical = true;
290
- response.status = 500;
291
- response.error = 'Unknown error occurred';
292
- response.stack = 'No stack trace available';
293
- }
174
+ catch (error) {
175
+ this.fillErrorResponse(response, error, (c) => { isCritical = c; });
294
176
  }
295
177
  finally {
296
- const t1 = performance.now();
297
-
298
- const message = `< ${response.status} ${request.method} /${request.path} ${Logger.colors.yellow}${Math.round(t1 - t0)}ms${Logger.colors.initial}`;
299
-
300
- if(response.status < 400) {
301
- Logger.log(message);
302
- }
303
- else if(response.status < 500) {
304
- Logger.warn(message);
305
- }
306
- else {
307
- if(isCritical) {
308
- Logger.critical(message);
309
- }
310
- else {
311
- Logger.error(message);
312
- }
313
- }
314
-
315
- if(response.error !== undefined) {
316
- if(isCritical) {
317
- Logger.critical(response.error);
318
- }
319
- else {
320
- Logger.error(response.error);
321
- }
322
-
323
- if(response.stack !== undefined) {
324
- Logger.errorStack(response.stack);
325
- }
326
- }
327
-
178
+ this.logResponse(request, response, performance.now() - t0, isCritical);
328
179
  return response;
329
180
  }
330
181
  }
331
182
 
332
- private normalizeBatchPayload(body: unknown): IBatchRequestPayload {
333
- if(body === null || typeof body !== 'object') {
334
- throw new BadRequestException('Batch payload must be an object containing a requests array.');
335
- }
336
-
337
- const possiblePayload = body as Partial<IBatchRequestPayload>;
338
- const { requests } = possiblePayload;
183
+ // -------------------------------------------------------------------------
184
+ // Route resolution
185
+ // -------------------------------------------------------------------------
339
186
 
340
- if(!Array.isArray(requests)) {
341
- throw new BadRequestException('Batch payload must define a requests array.');
342
- }
343
-
344
- const normalizedRequests = requests.map((entry, index) => this.normalizeBatchItem(entry, index));
345
-
346
- return { requests: normalizedRequests };
347
- }
348
-
349
- private normalizeBatchItem(entry: unknown, index: number): IBatchRequestItem {
350
- if(entry === null || typeof entry !== 'object') {
351
- throw new BadRequestException(`Batch request at index ${index} must be an object.`);
352
- }
353
-
354
- const { requestId, path, method, body } = entry as Partial<IBatchRequestItem> & { method?: unknown };
355
-
356
- if(requestId !== undefined && typeof requestId !== 'string') {
357
- throw new BadRequestException(`Batch request at index ${index} has an invalid requestId.`);
358
- }
359
-
360
- if(typeof path !== 'string' || path.length === 0) {
361
- throw new BadRequestException(`Batch request at index ${index} must define a non-empty path.`);
362
- }
363
-
364
- if(typeof method !== 'string') {
365
- throw new BadRequestException(`Batch request at index ${index} must define an HTTP method.`);
366
- }
367
-
368
- const normalizedMethod = method.toUpperCase();
369
-
370
- if(!isAtomicHttpMethod(normalizedMethod)) {
371
- throw new BadRequestException(`Batch request at index ${index} uses the unsupported method ${method}.`);
372
- }
373
-
374
- return {
375
- requestId,
376
- path,
377
- method: normalizedMethod as AtomicHttpMethod,
378
- body,
379
- };
380
- }
381
-
382
- /**
383
- * Finds the route definition for a given request.
384
- * This method searches the routing tree for a matching route based on the request's path and method.
385
- * If no matching route is found, it throws a NotFoundException.
386
- * @param request - The Request object containing the method and path to search for.
387
- * @returns The IRouteDefinition for the matched route.
388
- */
389
- /**
390
- * Attempts to find a route definition for the given request.
391
- * Returns undefined instead of throwing when the route is not found,
392
- * so the caller can try lazy-loading first.
393
- */
394
187
  private tryFindRoute(request: Request): IRouteDefinition | undefined {
395
- const matchedRoutes = this.routes.search(request.path);
396
-
397
- if(matchedRoutes?.node === undefined || matchedRoutes.node.children.length === 0) {
398
- return undefined;
399
- }
400
-
401
- const routeDef = matchedRoutes.node.findExactChild(request.method);
402
- return routeDef?.value;
188
+ const matched = this.routes.search(request.path);
189
+ if (!matched?.node || matched.node.children.length === 0) return undefined;
190
+ return matched.node.findExactChild(request.method)?.value;
403
191
  }
404
192
 
405
- /**
406
- * Finds the route definition for a given request.
407
- * If no eagerly-registered route matches, attempts to load a lazy module
408
- * whose prefix matches the request path, then retries.
409
- */
410
193
  private async findRoute(request: Request): Promise<IRouteDefinition> {
411
- // Fast path: route already registered
412
194
  const direct = this.tryFindRoute(request);
413
- if(direct) return direct;
195
+ if (direct) return direct;
414
196
 
415
- // Try lazy route loading
416
197
  await this.tryLoadLazyRoute(request.path);
417
198
 
418
- // Retry after lazy load
419
199
  const afterLazy = this.tryFindRoute(request);
420
- if(afterLazy) return afterLazy;
200
+ if (afterLazy) return afterLazy;
421
201
 
422
202
  throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
423
203
  }
424
204
 
425
- /**
426
- * Given a request path, checks whether a lazy route prefix matches
427
- * and triggers the dynamic import if it hasn't been loaded yet.
428
- */
429
205
  private async tryLoadLazyRoute(requestPath: string): Promise<void> {
430
206
  const firstSegment = requestPath.replace(/^\/+/, '').split('/')[0] ?? '';
431
207
 
432
- // Check exact first segment, then try multi-segment prefixes
433
- for(const [prefix, entry] of this.lazyRoutes) {
434
- if(entry.loaded) continue;
435
-
436
- const normalizedPath = requestPath.replace(/^\/+/, '');
437
- if(normalizedPath === prefix || normalizedPath.startsWith(prefix + '/') || firstSegment === prefix) {
438
- if(!entry.loading) {
439
- entry.loading = this.loadLazyModule(prefix, entry);
440
- }
208
+ for (const [prefix, entry] of this.lazyRoutes) {
209
+ if (entry.loaded) continue;
210
+ const normalized = requestPath.replace(/^\/+/, '');
211
+ if (normalized === prefix || normalized.startsWith(prefix + '/') || firstSegment === prefix) {
212
+ if (!entry.loading) entry.loading = this.loadLazyModule(prefix, entry);
441
213
  await entry.loading;
442
214
  return;
443
215
  }
444
216
  }
445
217
  }
446
218
 
447
- /**
448
- * Dynamically imports a lazy module and registers its decorated classes
449
- * (controllers, services) in the DI container using the two-phase strategy.
450
- */
451
219
  private async loadLazyModule(prefix: string, entry: LazyRouteEntry): Promise<void> {
452
220
  const t0 = performance.now();
453
-
454
221
  InjectorExplorer.beginAccumulate();
455
- await entry.loadModule();
456
- InjectorExplorer.flushAccumulated();
222
+
223
+ await entry.load?.();
224
+
225
+ entry.loading = null;
226
+ entry.load = null;
227
+
228
+ InjectorExplorer.flushAccumulated(entry.guards, entry.middlewares, prefix);
457
229
 
458
230
  entry.loaded = true;
459
231
 
460
- const t1 = performance.now();
461
- Logger.info(`Lazy-loaded module for prefix {${prefix}} in ${Math.round(t1 - t0)}ms`);
232
+ Logger.info(`Lazy-loaded module for prefix {${prefix}} in ${Math.round(performance.now() - t0)}ms`);
462
233
  }
463
234
 
464
- /**
465
- * Resolves the controller for a given route definition.
466
- * This method creates an instance of the controller class and prepares the request parameters.
467
- * It also runs the request pipeline, which includes executing middlewares and guards.
468
- * @param request - The Request object containing the request data.
469
- * @param response - The IResponse object to populate with the response data.
470
- * @param routeDef - The IRouteDefinition for the matched route.
471
- * @return A Promise that resolves when the controller action has been executed.
472
- * @throws UnauthorizedException if the request is not authorized by the guards.
473
- */
474
- private async resolveController(request: Request, response: IResponse, routeDef: IRouteDefinition): Promise<void> {
475
- const controllerInstance = request.context.resolve(routeDef.controller);
235
+ // -------------------------------------------------------------------------
236
+ // Pipeline
237
+ // -------------------------------------------------------------------------
476
238
 
239
+ private async resolveController(request: Request, response: IResponse, routeDef: IRouteDefinition): Promise<void> {
240
+ const instance = request.context.resolve(routeDef.controller);
477
241
  Object.assign(request.params, this.extractParams(request.path, routeDef.path));
478
-
479
- await this.runRequestPipeline(request, response, routeDef, controllerInstance);
242
+ await this.runPipeline(request, response, routeDef, instance);
480
243
  }
481
244
 
482
- /**
483
- * Runs the request pipeline for a given request.
484
- * This method executes the middlewares and guards associated with the route,
485
- * and finally calls the controller action.
486
- * @param request - The Request object containing the request data.
487
- * @param response - The IResponse object to populate with the response data.
488
- * @param routeDef - The IRouteDefinition for the matched route.
489
- * @param controllerInstance - The instance of the controller class.
490
- * @return A Promise that resolves when the request pipeline has been executed.
491
- * @throws ResponseException if the response status is not successful.
492
- */
493
- private async runRequestPipeline(request: Request, response: IResponse, routeDef: IRouteDefinition, controllerInstance: any): Promise<void> {
245
+ private async runPipeline(
246
+ request: Request,
247
+ response: IResponse,
248
+ routeDef: IRouteDefinition,
249
+ controllerInstance: unknown,
250
+ ): Promise<void> {
494
251
  const middlewares = [...new Set([...this.rootMiddlewares, ...routeDef.middlewares])];
495
-
496
- const middlewareMaxIndex = middlewares.length - 1;
497
- const guardsMaxIndex = middlewareMaxIndex + routeDef.guards.length;
498
-
252
+ const mwMax = middlewares.length - 1;
253
+ const guardMax = mwMax + routeDef.guards.length;
499
254
  let index = -1;
500
255
 
501
256
  const dispatch = async (i: number): Promise<void> => {
502
- if(i <= index)
503
- throw new Error("next() called multiple times");
504
-
257
+ if (i <= index) throw new Error('next() called multiple times');
505
258
  index = i;
506
259
 
507
- // middlewares
508
- if(i <= middlewareMaxIndex) {
509
- const nextFn = dispatch.bind(null, i + 1);
510
- await this.runMiddleware(request, response, nextFn, middlewares[i]!);
511
-
512
- if(response.status >= 400) {
513
- throw new ResponseException(response.status, response.error);
514
- }
515
-
260
+ if (i <= mwMax) {
261
+ await this.runMiddleware(request, response, dispatch.bind(null, i + 1), middlewares[i]!);
262
+ if (response.status >= 400) throw new ResponseException(response.status, response.error);
516
263
  return;
517
264
  }
518
265
 
519
- // guards
520
- if(i <= guardsMaxIndex) {
521
- const guardIndex = i - middlewares.length;
522
- const guardType = routeDef.guards[guardIndex]!;
523
- await this.runGuard(request, guardType);
266
+ if (i <= guardMax) {
267
+ await this.runGuard(request, routeDef.guards[i - middlewares.length]!);
524
268
  await dispatch(i + 1);
525
269
  return;
526
270
  }
527
271
 
528
- // endpoint action
529
- const action = controllerInstance[routeDef.handler] as ControllerAction;
272
+ const action = (controllerInstance as Record<string, ControllerAction>)[routeDef.handler]!;
530
273
  response.body = await action.call(controllerInstance, request, response);
531
-
532
- // avoid parsing error on the renderer if the action just does treatment without returning anything
533
- if(response.body === undefined) {
534
- response.body = {};
535
- }
274
+ if (response.body === undefined) response.body = {};
536
275
  };
537
276
 
538
277
  await dispatch(0);
539
278
  }
540
279
 
541
- /**
542
- * Runs a middleware function in the request pipeline.
543
- * This method creates an instance of the middleware and invokes its `invoke` method,
544
- * passing the request, response, and next function.
545
- * @param request - The Request object containing the request data.
546
- * @param response - The IResponse object to populate with the response data.
547
- * @param next - The NextFunction to call to continue the middleware chain.
548
- * @param middlewareType - The type of the middleware to run.
549
- * @return A Promise that resolves when the middleware has been executed.
550
- */
551
- private async runMiddleware(request: Request, response: IResponse, next: NextFunction, middlewareType: Type<IMiddleware>): Promise<void> {
552
- const middleware = request.context.resolve(middlewareType);
553
- await middleware.invoke(request, response, next);
280
+ private async runMiddleware(request: Request, response: IResponse, next: NextFunction, middleware: Middleware): Promise<void> {
281
+ await middleware(request, response, next);
554
282
  }
555
283
 
556
- /**
557
- * Runs a guard to check if the request is authorized.
558
- * This method creates an instance of the guard and calls its `canActivate` method.
559
- * If the guard returns false, it throws an UnauthorizedException.
560
- * @param request - The Request object containing the request data.
561
- * @param guardType - The type of the guard to run.
562
- * @return A Promise that resolves if the guard allows the request, or throws an UnauthorizedException if not.
563
- * @throws UnauthorizedException if the guard denies access to the request.
564
- */
565
- private async runGuard(request: Request, guardType: Type<IGuard>): Promise<void> {
566
- const guard = request.context.resolve(guardType);
567
- const allowed = await guard.canActivate(request);
568
-
569
- if(!allowed)
284
+ private async runGuard(request: Request, guard: Guard): Promise<void> {
285
+ if (!await guard(request)) {
570
286
  throw new UnauthorizedException(`Unauthorized for ${request.method} ${request.path}`);
287
+ }
571
288
  }
572
289
 
573
- /**
574
- * Extracts parameters from the actual request path based on the template path.
575
- * This method splits the actual path and the template path into segments,
576
- * then maps the segments to parameters based on the template.
577
- * @param actual - The actual request path.
578
- * @param template - The template path to extract parameters from.
579
- * @returns An object containing the extracted parameters.
580
- */
290
+ // -------------------------------------------------------------------------
291
+ // Utilities
292
+ // -------------------------------------------------------------------------
293
+
581
294
  private extractParams(actual: string, template: string): Record<string, string> {
582
295
  const aParts = actual.split('/');
583
296
  const tParts = template.split('/');
584
297
  const params: Record<string, string> = {};
585
-
586
298
  tParts.forEach((part, i) => {
587
- if(part.startsWith(':')) {
588
- params[part.slice(1)] = aParts[i] ?? '';
589
- }
299
+ if (part.startsWith(':')) params[part.slice(1)] = aParts[i] ?? '';
590
300
  });
591
-
592
301
  return params;
593
302
  }
303
+
304
+ private normalizeBatchPayload(body: unknown): IBatchRequestPayload {
305
+ if (body === null || typeof body !== 'object') {
306
+ throw new BadRequestException('Batch payload must be an object containing a requests array.');
307
+ }
308
+ const { requests } = body as Partial<IBatchRequestPayload>;
309
+ if (!Array.isArray(requests)) throw new BadRequestException('Batch payload must define a requests array.');
310
+ return { requests: requests.map((e, i) => this.normalizeBatchItem(e, i)) };
311
+ }
312
+
313
+ private normalizeBatchItem(entry: unknown, index: number): IBatchRequestItem {
314
+ if (entry === null || typeof entry !== 'object') throw new BadRequestException(`Batch request at index ${index} must be an object.`);
315
+ const { requestId, path, method, body } = entry as Partial<IBatchRequestItem> & { method?: unknown };
316
+ if (requestId !== undefined && typeof requestId !== 'string') throw new BadRequestException(`Batch request at index ${index} has an invalid requestId.`);
317
+ if (typeof path !== 'string' || !path.length) throw new BadRequestException(`Batch request at index ${index} must define a non-empty path.`);
318
+ if (typeof method !== 'string') throw new BadRequestException(`Batch request at index ${index} must define an HTTP method.`);
319
+ const normalized = method.toUpperCase();
320
+ if (!isAtomicHttpMethod(normalized)) throw new BadRequestException(`Batch request at index ${index} uses unsupported method ${method}.`);
321
+ return { requestId, path, method: normalized, body };
322
+ }
323
+
324
+ private fillErrorResponse(response: IResponse, error: unknown, setCritical: (v: boolean) => void): void {
325
+ response.body = undefined;
326
+ if (error instanceof ResponseException) {
327
+ response.status = error.status;
328
+ response.error = error.message;
329
+ response.stack = error.stack;
330
+ } else if (error instanceof Error) {
331
+ setCritical(true);
332
+ response.status = 500;
333
+ response.error = error.message || 'Internal Server Error';
334
+ response.stack = error.stack;
335
+ } else {
336
+ setCritical(true);
337
+ response.status = 500;
338
+ response.error = 'Unknown error occurred';
339
+ }
340
+ }
341
+
342
+ private logResponse(request: Request, response: IResponse, ms: number, isCritical: boolean): void {
343
+ const msg = `< ${response.status} ${request.method} /${request.path} ${Logger.colors.yellow}${Math.round(ms)}ms${Logger.colors.initial}`;
344
+ if (response.status < 400) Logger.log(msg);
345
+ else if (response.status < 500) Logger.warn(msg);
346
+ else isCritical ? Logger.critical(msg) : Logger.error(msg);
347
+
348
+ if (response.error) {
349
+ isCritical ? Logger.critical(response.error) : Logger.error(response.error);
350
+ if (response.stack) Logger.errorStack(response.stack);
351
+ }
352
+ }
594
353
  }