@noxfly/noxus 2.4.0 → 2.5.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/src/app.ts CHANGED
@@ -8,6 +8,7 @@ import { app, BrowserWindow, ipcMain, MessageChannelMain } from "electron/main";
8
8
  import { Injectable } from "src/decorators/injectable.decorator";
9
9
  import { IMiddleware } from "src/decorators/middleware.decorator";
10
10
  import { inject } from "src/DI/app-injector";
11
+ import { InjectorExplorer } from "src/DI/injector-explorer";
11
12
  import { IRequest, IResponse, Request } from "src/request";
12
13
  import { NoxSocket } from "src/socket";
13
14
  import { Router } from "src/router";
@@ -173,6 +174,42 @@ export class NoxApp {
173
174
  this.mainWindow = window;
174
175
  }
175
176
 
177
+ /**
178
+ * Registers a lazy-loaded route. The module behind this path prefix
179
+ * will only be dynamically imported when the first IPC request
180
+ * targets this prefix — like Angular's loadChildren.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * noxApp.lazy("auth", () => import("./modules/auth/auth.module.js"));
185
+ * noxApp.lazy("printing", () => import("./modules/printing/printing.module.js"));
186
+ * ```
187
+ *
188
+ * @param pathPrefix - The route prefix (e.g. "auth", "cash-register").
189
+ * @param loadModule - A function returning a dynamic import promise.
190
+ * @returns NoxApp instance for method chaining.
191
+ */
192
+ public lazy(pathPrefix: string, loadModule: () => Promise<unknown>): NoxApp {
193
+ this.router.registerLazyRoute(pathPrefix, loadModule);
194
+ return this;
195
+ }
196
+
197
+ /**
198
+ * Eagerly loads one or more modules with a two-phase DI guarantee.
199
+ * Use this when a service needed at startup lives inside a module
200
+ * (e.g. the Application service depends on LoaderService).
201
+ *
202
+ * All dynamic imports run in parallel; bindings are registered first,
203
+ * then singletons are resolved — safe regardless of import ordering.
204
+ *
205
+ * @param importFns - Functions returning dynamic import promises.
206
+ */
207
+ public async loadModules(importFns: Array<() => Promise<unknown>>): Promise<void> {
208
+ InjectorExplorer.beginAccumulate();
209
+ await Promise.all(importFns.map(fn => fn()));
210
+ InjectorExplorer.flushAccumulated();
211
+ }
212
+
176
213
  /**
177
214
  * Configures the NoxApp instance with the provided application class.
178
215
  * This method allows you to set the application class that will handle lifecycle events.
package/src/bootstrap.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * @author NoxFly
5
5
  */
6
6
 
7
- import { app, BrowserWindow } from "electron/main";
7
+ import { app, BrowserWindow, screen } from "electron/main";
8
8
  import { NoxApp } from "src/app";
9
9
  import { getModuleMetadata } from "src/decorators/module.decorator";
10
10
  import { inject } from "src/DI/app-injector";
@@ -38,8 +38,8 @@ export interface BootstrapOptions {
38
38
  * @return A promise that resolves to the NoxApp instance.
39
39
  * @throws Error if the root module is not decorated with @Module, or if the electron process could not start.
40
40
  */
41
- export async function bootstrapApplication(rootModule: Type<any>, options?: BootstrapOptions): Promise<NoxApp> {
42
- if(!getModuleMetadata(rootModule)) {
41
+ export async function bootstrapApplication(rootModule?: Type<any> | null, options?: BootstrapOptions): Promise<NoxApp> {
42
+ if(rootModule && !getModuleMetadata(rootModule)) {
43
43
  throw new Error(`Root module must be decorated with @Module`);
44
44
  }
45
45
 
@@ -51,6 +51,21 @@ export async function bootstrapApplication(rootModule: Type<any>, options?: Boot
51
51
 
52
52
  if(options?.window) {
53
53
  mainWindow = new BrowserWindow(options.window);
54
+
55
+ mainWindow.once("ready-to-show", () => {
56
+ mainWindow?.show();
57
+ });
58
+
59
+ const primaryDisplay = screen.getPrimaryDisplay();
60
+ const { width, height } = primaryDisplay.workAreaSize;
61
+
62
+ if(options.window.minWidth && options.window.minHeight) {
63
+ mainWindow.setSize(
64
+ Math.min(width, options.window.minWidth),
65
+ Math.min(height, options.window.minHeight),
66
+ true,
67
+ );
68
+ }
54
69
  }
55
70
 
56
71
  // Process all deferred injectable registrations (two-phase: bindings then resolution)
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  export * from './request';
8
+ export * from './preload-bridge';
8
9
  export * from './renderer-events';
9
10
  export * from './renderer-client';
10
11
  export type { HttpMethod, AtomicHttpMethod } from './decorators/method.decorator';
package/src/main.ts CHANGED
@@ -22,12 +22,9 @@ export * from './decorators/injectable.decorator';
22
22
  export * from './decorators/inject.decorator';
23
23
  export * from './decorators/method.decorator';
24
24
  export * from './decorators/module.decorator';
25
- export * from './preload-bridge';
26
25
  export * from './utils/logger';
27
26
  export * from './utils/types';
28
27
  export * from './utils/forward-ref';
29
28
  export * from './request';
30
- export * from './renderer-events';
31
- export * from './renderer-client';
32
29
  export * from './socket';
33
30
 
package/src/router.ts CHANGED
@@ -12,10 +12,28 @@ import { AtomicHttpMethod, getRouteMetadata } from 'src/decorators/method.decora
12
12
  import { getMiddlewaresForController, getMiddlewaresForControllerAction, IMiddleware, NextFunction } from 'src/decorators/middleware.decorator';
13
13
  import { BadRequestException, MethodNotAllowedException, NotFoundException, ResponseException, UnauthorizedException } from 'src/exceptions';
14
14
  import { IBatchRequestItem, IBatchRequestPayload, IBatchResponsePayload, IResponse, Request } from 'src/request';
15
+ import { InjectorExplorer } from 'src/DI/injector-explorer';
15
16
  import { Logger } from 'src/utils/logger';
16
17
  import { RadixTree } from 'src/utils/radix-tree';
17
18
  import { Type } from 'src/utils/types';
18
19
 
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
+ 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>;
29
+ }
30
+
31
+ interface LazyRouteEntry {
32
+ loadModule: () => Promise<unknown>;
33
+ loading: Promise<void> | null;
34
+ loaded: boolean;
35
+ }
36
+
19
37
  const ATOMIC_HTTP_METHODS: ReadonlySet<AtomicHttpMethod> = new Set<AtomicHttpMethod>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
20
38
 
21
39
  function isAtomicHttpMethod(method: unknown): method is AtomicHttpMethod {
@@ -51,6 +69,7 @@ export type ControllerAction = (request: Request, response: IResponse) => any;
51
69
  export class Router {
52
70
  private readonly routes = new RadixTree<IRouteDefinition>();
53
71
  private readonly rootMiddlewares: Type<IMiddleware>[] = [];
72
+ private readonly lazyRoutes = new Map<string, LazyRouteEntry>();
54
73
 
55
74
  /**
56
75
  * Registers a controller class with the router.
@@ -109,6 +128,21 @@ export class Router {
109
128
  return this;
110
129
  }
111
130
 
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 {
140
+ const normalized = pathPrefix.replace(/^\/+|\/+$/g, '');
141
+ this.lazyRoutes.set(normalized, { loadModule, loading: null, loaded: false });
142
+ Logger.log(`Registered lazy route prefix {${normalized}}`);
143
+ return this;
144
+ }
145
+
112
146
  /**
113
147
  * Defines a middleware for the root of the application.
114
148
  * This method allows you to register a middleware that will be applied to all requests
@@ -148,7 +182,7 @@ export class Router {
148
182
  let isCritical: boolean = false;
149
183
 
150
184
  try {
151
- const routeDef = this.findRoute(request);
185
+ const routeDef = await this.findRoute(request);
152
186
  await this.resolveController(request, response, routeDef);
153
187
 
154
188
  if(response.status > 400) {
@@ -352,20 +386,79 @@ export class Router {
352
386
  * @param request - The Request object containing the method and path to search for.
353
387
  * @returns The IRouteDefinition for the matched route.
354
388
  */
355
- private findRoute(request: Request): IRouteDefinition {
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
+ private tryFindRoute(request: Request): IRouteDefinition | undefined {
356
395
  const matchedRoutes = this.routes.search(request.path);
357
396
 
358
397
  if(matchedRoutes?.node === undefined || matchedRoutes.node.children.length === 0) {
359
- throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
398
+ return undefined;
360
399
  }
361
400
 
362
401
  const routeDef = matchedRoutes.node.findExactChild(request.method);
402
+ return routeDef?.value;
403
+ }
404
+
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
+ private async findRoute(request: Request): Promise<IRouteDefinition> {
411
+ // Fast path: route already registered
412
+ const direct = this.tryFindRoute(request);
413
+ if(direct) return direct;
363
414
 
364
- if(routeDef?.value === undefined) {
365
- throw new MethodNotAllowedException(`Method Not Allowed for ${request.method} ${request.path}`);
415
+ // Try lazy route loading
416
+ await this.tryLoadLazyRoute(request.path);
417
+
418
+ // Retry after lazy load
419
+ const afterLazy = this.tryFindRoute(request);
420
+ if(afterLazy) return afterLazy;
421
+
422
+ throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
423
+ }
424
+
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
+ private async tryLoadLazyRoute(requestPath: string): Promise<void> {
430
+ const firstSegment = requestPath.replace(/^\/+/, '').split('/')[0] ?? '';
431
+
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
+ }
441
+ await entry.loading;
442
+ return;
443
+ }
366
444
  }
445
+ }
446
+
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
+ private async loadLazyModule(prefix: string, entry: LazyRouteEntry): Promise<void> {
452
+ const t0 = performance.now();
453
+
454
+ InjectorExplorer.beginAccumulate();
455
+ await entry.loadModule();
456
+ InjectorExplorer.flushAccumulated();
457
+
458
+ entry.loaded = true;
367
459
 
368
- return routeDef.value;
460
+ const t1 = performance.now();
461
+ Logger.info(`Lazy-loaded module for prefix {${prefix}} in ${Math.round(t1 - t0)}ms`);
369
462
  }
370
463
 
371
464
  /**