@noxfly/noxus 2.3.2 → 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/dist/child.js CHANGED
@@ -963,6 +963,7 @@ var _Router = class _Router {
963
963
  constructor() {
964
964
  __publicField(this, "routes", new RadixTree());
965
965
  __publicField(this, "rootMiddlewares", []);
966
+ __publicField(this, "lazyRoutes", /* @__PURE__ */ new Map());
966
967
  }
967
968
  /**
968
969
  * Registers a controller class with the router.
@@ -1010,6 +1011,24 @@ var _Router = class _Router {
1010
1011
  Logger.log(`Mapped ${controllerClass.name}${controllerGuardsInfo} controller's routes`);
1011
1012
  return this;
1012
1013
  }
1014
+ /**
1015
+ * Registers a lazy route. The module behind this route prefix will only
1016
+ * be imported (and its controllers/services registered in DI) the first
1017
+ * time a request targets this prefix.
1018
+ *
1019
+ * @param pathPrefix - Route prefix (e.g. "auth"). Matched against the first segment of the request path.
1020
+ * @param loadModule - A function that returns a dynamic import promise.
1021
+ */
1022
+ registerLazyRoute(pathPrefix, loadModule) {
1023
+ const normalized = pathPrefix.replace(/^\/+|\/+$/g, "");
1024
+ this.lazyRoutes.set(normalized, {
1025
+ loadModule,
1026
+ loading: null,
1027
+ loaded: false
1028
+ });
1029
+ Logger.log(`Registered lazy route prefix {${normalized}}`);
1030
+ return this;
1031
+ }
1013
1032
  /**
1014
1033
  * Defines a middleware for the root of the application.
1015
1034
  * This method allows you to register a middleware that will be applied to all requests
@@ -1042,7 +1061,7 @@ var _Router = class _Router {
1042
1061
  };
1043
1062
  let isCritical = false;
1044
1063
  try {
1045
- const routeDef = this.findRoute(request);
1064
+ const routeDef = await this.findRoute(request);
1046
1065
  await this.resolveController(request, response, routeDef);
1047
1066
  if (response.status > 400) {
1048
1067
  throw new ResponseException(response.status, response.error);
@@ -1200,16 +1219,62 @@ var _Router = class _Router {
1200
1219
  * @param request - The Request object containing the method and path to search for.
1201
1220
  * @returns The IRouteDefinition for the matched route.
1202
1221
  */
1203
- findRoute(request) {
1222
+ /**
1223
+ * Attempts to find a route definition for the given request.
1224
+ * Returns undefined instead of throwing when the route is not found,
1225
+ * so the caller can try lazy-loading first.
1226
+ */
1227
+ tryFindRoute(request) {
1204
1228
  const matchedRoutes = this.routes.search(request.path);
1205
1229
  if (matchedRoutes?.node === void 0 || matchedRoutes.node.children.length === 0) {
1206
- throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
1230
+ return void 0;
1207
1231
  }
1208
1232
  const routeDef = matchedRoutes.node.findExactChild(request.method);
1209
- if (routeDef?.value === void 0) {
1210
- throw new MethodNotAllowedException(`Method Not Allowed for ${request.method} ${request.path}`);
1233
+ return routeDef?.value;
1234
+ }
1235
+ /**
1236
+ * Finds the route definition for a given request.
1237
+ * If no eagerly-registered route matches, attempts to load a lazy module
1238
+ * whose prefix matches the request path, then retries.
1239
+ */
1240
+ async findRoute(request) {
1241
+ const direct = this.tryFindRoute(request);
1242
+ if (direct) return direct;
1243
+ await this.tryLoadLazyRoute(request.path);
1244
+ const afterLazy = this.tryFindRoute(request);
1245
+ if (afterLazy) return afterLazy;
1246
+ throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
1247
+ }
1248
+ /**
1249
+ * Given a request path, checks whether a lazy route prefix matches
1250
+ * and triggers the dynamic import if it hasn't been loaded yet.
1251
+ */
1252
+ async tryLoadLazyRoute(requestPath) {
1253
+ const firstSegment = requestPath.replace(/^\/+/, "").split("/")[0] ?? "";
1254
+ for (const [prefix, entry] of this.lazyRoutes) {
1255
+ if (entry.loaded) continue;
1256
+ const normalizedPath = requestPath.replace(/^\/+/, "");
1257
+ if (normalizedPath === prefix || normalizedPath.startsWith(prefix + "/") || firstSegment === prefix) {
1258
+ if (!entry.loading) {
1259
+ entry.loading = this.loadLazyModule(prefix, entry);
1260
+ }
1261
+ await entry.loading;
1262
+ return;
1263
+ }
1211
1264
  }
1212
- return routeDef.value;
1265
+ }
1266
+ /**
1267
+ * Dynamically imports a lazy module and registers its decorated classes
1268
+ * (controllers, services) in the DI container using the two-phase strategy.
1269
+ */
1270
+ async loadLazyModule(prefix, entry) {
1271
+ const t0 = performance.now();
1272
+ InjectorExplorer.beginAccumulate();
1273
+ await entry.loadModule();
1274
+ InjectorExplorer.flushAccumulated();
1275
+ entry.loaded = true;
1276
+ const t1 = performance.now();
1277
+ Logger.info(`Lazy-loaded module for prefix {${prefix}} in ${Math.round(t1 - t0)}ms`);
1213
1278
  }
1214
1279
  /**
1215
1280
  * Resolves the controller for a given route definition.
@@ -1330,41 +1395,130 @@ Router = _ts_decorate([
1330
1395
  // src/DI/injector-explorer.ts
1331
1396
  var _InjectorExplorer = class _InjectorExplorer {
1332
1397
  /**
1333
- * Registers the class as injectable.
1334
- * When a class is instantiated, if it has dependencies and those dependencies
1335
- * are listed using this method, they will be injected into the class constructor.
1398
+ * Enqueues a class for deferred registration.
1399
+ * Called by the @Injectable decorator at import time.
1400
+ *
1401
+ * If {@link processPending} has already been called (i.e. after bootstrap)
1402
+ * and accumulation mode is not active, the class is registered immediately
1403
+ * so that late dynamic imports (e.g. middlewares loaded after bootstrap)
1404
+ * work correctly.
1405
+ *
1406
+ * When accumulation mode is active (between {@link beginAccumulate} and
1407
+ * {@link flushAccumulated}), classes are queued instead — preserving the
1408
+ * two-phase binding/resolution guarantee for lazy-loaded modules.
1409
+ */
1410
+ static enqueue(target, lifetime) {
1411
+ if (_InjectorExplorer.processed && !_InjectorExplorer.accumulating) {
1412
+ _InjectorExplorer.registerImmediate(target, lifetime);
1413
+ return;
1414
+ }
1415
+ _InjectorExplorer.pending.push({
1416
+ target,
1417
+ lifetime
1418
+ });
1419
+ }
1420
+ /**
1421
+ * Enters accumulation mode. While active, all decorated classes discovered
1422
+ * via dynamic imports are queued in {@link pending} rather than registered
1423
+ * immediately. Call {@link flushAccumulated} to process them with the
1424
+ * full two-phase (bind-then-resolve) guarantee.
1425
+ */
1426
+ static beginAccumulate() {
1427
+ _InjectorExplorer.accumulating = true;
1428
+ }
1429
+ /**
1430
+ * Exits accumulation mode and processes every class queued since
1431
+ * {@link beginAccumulate} was called. Uses the same two-phase strategy
1432
+ * as {@link processPending} (register all bindings first, then resolve
1433
+ * singletons / controllers) so import ordering within a lazy batch
1434
+ * does not cause resolution failures.
1435
+ */
1436
+ static flushAccumulated() {
1437
+ _InjectorExplorer.accumulating = false;
1438
+ const queue = [
1439
+ ..._InjectorExplorer.pending
1440
+ ];
1441
+ _InjectorExplorer.pending.length = 0;
1442
+ for (const { target, lifetime } of queue) {
1443
+ if (!RootInjector.bindings.has(target)) {
1444
+ RootInjector.bindings.set(target, {
1445
+ implementation: target,
1446
+ lifetime
1447
+ });
1448
+ }
1449
+ }
1450
+ for (const { target, lifetime } of queue) {
1451
+ _InjectorExplorer.processRegistration(target, lifetime);
1452
+ }
1453
+ }
1454
+ /**
1455
+ * Processes all pending registrations in two phases:
1456
+ * 1. Register all bindings (no instantiation) so every dependency is known.
1457
+ * 2. Resolve singletons, register controllers and log module readiness.
1458
+ *
1459
+ * This two-phase approach makes the system resilient to import ordering:
1460
+ * all bindings exist before any singleton is instantiated.
1461
+ */
1462
+ static processPending() {
1463
+ const queue = _InjectorExplorer.pending;
1464
+ for (const { target, lifetime } of queue) {
1465
+ if (!RootInjector.bindings.has(target)) {
1466
+ RootInjector.bindings.set(target, {
1467
+ implementation: target,
1468
+ lifetime
1469
+ });
1470
+ }
1471
+ }
1472
+ for (const { target, lifetime } of queue) {
1473
+ _InjectorExplorer.processRegistration(target, lifetime);
1474
+ }
1475
+ queue.length = 0;
1476
+ _InjectorExplorer.processed = true;
1477
+ }
1478
+ /**
1479
+ * Registers a single class immediately (post-bootstrap path).
1480
+ * Used for classes discovered via late dynamic imports.
1336
1481
  */
1337
- static register(target, lifetime) {
1338
- if (RootInjector.bindings.has(target)) return RootInjector;
1482
+ static registerImmediate(target, lifetime) {
1483
+ if (RootInjector.bindings.has(target)) {
1484
+ return;
1485
+ }
1339
1486
  RootInjector.bindings.set(target, {
1340
1487
  implementation: target,
1341
1488
  lifetime
1342
1489
  });
1490
+ _InjectorExplorer.processRegistration(target, lifetime);
1491
+ }
1492
+ /**
1493
+ * Performs phase-2 work for a single registration: resolve singletons,
1494
+ * register controllers, and log module readiness.
1495
+ */
1496
+ static processRegistration(target, lifetime) {
1343
1497
  if (lifetime === "singleton") {
1344
1498
  RootInjector.resolve(target);
1345
1499
  }
1346
1500
  if (getModuleMetadata(target)) {
1347
1501
  Logger.log(`${target.name} dependencies initialized`);
1348
- return RootInjector;
1502
+ return;
1349
1503
  }
1350
1504
  const controllerMeta = getControllerMetadata(target);
1351
1505
  if (controllerMeta) {
1352
1506
  const router = RootInjector.resolve(Router);
1353
1507
  router?.registerController(target);
1354
- return RootInjector;
1508
+ return;
1355
1509
  }
1356
- const routeMeta = getRouteMetadata(target);
1357
- if (routeMeta) {
1358
- return RootInjector;
1510
+ if (getRouteMetadata(target).length > 0) {
1511
+ return;
1359
1512
  }
1360
1513
  if (getInjectableMetadata(target)) {
1361
1514
  Logger.log(`Registered ${target.name} as ${lifetime}`);
1362
- return RootInjector;
1363
1515
  }
1364
- return RootInjector;
1365
1516
  }
1366
1517
  };
1367
1518
  __name(_InjectorExplorer, "InjectorExplorer");
1519
+ __publicField(_InjectorExplorer, "pending", []);
1520
+ __publicField(_InjectorExplorer, "processed", false);
1521
+ __publicField(_InjectorExplorer, "accumulating", false);
1368
1522
  var InjectorExplorer = _InjectorExplorer;
1369
1523
 
1370
1524
  // src/decorators/injectable.decorator.ts
@@ -1374,7 +1528,7 @@ function Injectable(lifetime = "scope") {
1374
1528
  throw new Error(`@Injectable can only be used on classes, not on ${typeof target}`);
1375
1529
  }
1376
1530
  defineInjectableMetadata(target, lifetime);
1377
- InjectorExplorer.register(target, lifetime);
1531
+ InjectorExplorer.enqueue(target, lifetime);
1378
1532
  };
1379
1533
  }
1380
1534
  __name(Injectable, "Injectable");
package/dist/child.mjs CHANGED
@@ -894,6 +894,7 @@ var _Router = class _Router {
894
894
  constructor() {
895
895
  __publicField(this, "routes", new RadixTree());
896
896
  __publicField(this, "rootMiddlewares", []);
897
+ __publicField(this, "lazyRoutes", /* @__PURE__ */ new Map());
897
898
  }
898
899
  /**
899
900
  * Registers a controller class with the router.
@@ -941,6 +942,24 @@ var _Router = class _Router {
941
942
  Logger.log(`Mapped ${controllerClass.name}${controllerGuardsInfo} controller's routes`);
942
943
  return this;
943
944
  }
945
+ /**
946
+ * Registers a lazy route. The module behind this route prefix will only
947
+ * be imported (and its controllers/services registered in DI) the first
948
+ * time a request targets this prefix.
949
+ *
950
+ * @param pathPrefix - Route prefix (e.g. "auth"). Matched against the first segment of the request path.
951
+ * @param loadModule - A function that returns a dynamic import promise.
952
+ */
953
+ registerLazyRoute(pathPrefix, loadModule) {
954
+ const normalized = pathPrefix.replace(/^\/+|\/+$/g, "");
955
+ this.lazyRoutes.set(normalized, {
956
+ loadModule,
957
+ loading: null,
958
+ loaded: false
959
+ });
960
+ Logger.log(`Registered lazy route prefix {${normalized}}`);
961
+ return this;
962
+ }
944
963
  /**
945
964
  * Defines a middleware for the root of the application.
946
965
  * This method allows you to register a middleware that will be applied to all requests
@@ -973,7 +992,7 @@ var _Router = class _Router {
973
992
  };
974
993
  let isCritical = false;
975
994
  try {
976
- const routeDef = this.findRoute(request);
995
+ const routeDef = await this.findRoute(request);
977
996
  await this.resolveController(request, response, routeDef);
978
997
  if (response.status > 400) {
979
998
  throw new ResponseException(response.status, response.error);
@@ -1131,16 +1150,62 @@ var _Router = class _Router {
1131
1150
  * @param request - The Request object containing the method and path to search for.
1132
1151
  * @returns The IRouteDefinition for the matched route.
1133
1152
  */
1134
- findRoute(request) {
1153
+ /**
1154
+ * Attempts to find a route definition for the given request.
1155
+ * Returns undefined instead of throwing when the route is not found,
1156
+ * so the caller can try lazy-loading first.
1157
+ */
1158
+ tryFindRoute(request) {
1135
1159
  const matchedRoutes = this.routes.search(request.path);
1136
1160
  if (matchedRoutes?.node === void 0 || matchedRoutes.node.children.length === 0) {
1137
- throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
1161
+ return void 0;
1138
1162
  }
1139
1163
  const routeDef = matchedRoutes.node.findExactChild(request.method);
1140
- if (routeDef?.value === void 0) {
1141
- throw new MethodNotAllowedException(`Method Not Allowed for ${request.method} ${request.path}`);
1164
+ return routeDef?.value;
1165
+ }
1166
+ /**
1167
+ * Finds the route definition for a given request.
1168
+ * If no eagerly-registered route matches, attempts to load a lazy module
1169
+ * whose prefix matches the request path, then retries.
1170
+ */
1171
+ async findRoute(request) {
1172
+ const direct = this.tryFindRoute(request);
1173
+ if (direct) return direct;
1174
+ await this.tryLoadLazyRoute(request.path);
1175
+ const afterLazy = this.tryFindRoute(request);
1176
+ if (afterLazy) return afterLazy;
1177
+ throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
1178
+ }
1179
+ /**
1180
+ * Given a request path, checks whether a lazy route prefix matches
1181
+ * and triggers the dynamic import if it hasn't been loaded yet.
1182
+ */
1183
+ async tryLoadLazyRoute(requestPath) {
1184
+ const firstSegment = requestPath.replace(/^\/+/, "").split("/")[0] ?? "";
1185
+ for (const [prefix, entry] of this.lazyRoutes) {
1186
+ if (entry.loaded) continue;
1187
+ const normalizedPath = requestPath.replace(/^\/+/, "");
1188
+ if (normalizedPath === prefix || normalizedPath.startsWith(prefix + "/") || firstSegment === prefix) {
1189
+ if (!entry.loading) {
1190
+ entry.loading = this.loadLazyModule(prefix, entry);
1191
+ }
1192
+ await entry.loading;
1193
+ return;
1194
+ }
1142
1195
  }
1143
- return routeDef.value;
1196
+ }
1197
+ /**
1198
+ * Dynamically imports a lazy module and registers its decorated classes
1199
+ * (controllers, services) in the DI container using the two-phase strategy.
1200
+ */
1201
+ async loadLazyModule(prefix, entry) {
1202
+ const t0 = performance.now();
1203
+ InjectorExplorer.beginAccumulate();
1204
+ await entry.loadModule();
1205
+ InjectorExplorer.flushAccumulated();
1206
+ entry.loaded = true;
1207
+ const t1 = performance.now();
1208
+ Logger.info(`Lazy-loaded module for prefix {${prefix}} in ${Math.round(t1 - t0)}ms`);
1144
1209
  }
1145
1210
  /**
1146
1211
  * Resolves the controller for a given route definition.
@@ -1261,41 +1326,130 @@ Router = _ts_decorate([
1261
1326
  // src/DI/injector-explorer.ts
1262
1327
  var _InjectorExplorer = class _InjectorExplorer {
1263
1328
  /**
1264
- * Registers the class as injectable.
1265
- * When a class is instantiated, if it has dependencies and those dependencies
1266
- * are listed using this method, they will be injected into the class constructor.
1329
+ * Enqueues a class for deferred registration.
1330
+ * Called by the @Injectable decorator at import time.
1331
+ *
1332
+ * If {@link processPending} has already been called (i.e. after bootstrap)
1333
+ * and accumulation mode is not active, the class is registered immediately
1334
+ * so that late dynamic imports (e.g. middlewares loaded after bootstrap)
1335
+ * work correctly.
1336
+ *
1337
+ * When accumulation mode is active (between {@link beginAccumulate} and
1338
+ * {@link flushAccumulated}), classes are queued instead — preserving the
1339
+ * two-phase binding/resolution guarantee for lazy-loaded modules.
1340
+ */
1341
+ static enqueue(target, lifetime) {
1342
+ if (_InjectorExplorer.processed && !_InjectorExplorer.accumulating) {
1343
+ _InjectorExplorer.registerImmediate(target, lifetime);
1344
+ return;
1345
+ }
1346
+ _InjectorExplorer.pending.push({
1347
+ target,
1348
+ lifetime
1349
+ });
1350
+ }
1351
+ /**
1352
+ * Enters accumulation mode. While active, all decorated classes discovered
1353
+ * via dynamic imports are queued in {@link pending} rather than registered
1354
+ * immediately. Call {@link flushAccumulated} to process them with the
1355
+ * full two-phase (bind-then-resolve) guarantee.
1356
+ */
1357
+ static beginAccumulate() {
1358
+ _InjectorExplorer.accumulating = true;
1359
+ }
1360
+ /**
1361
+ * Exits accumulation mode and processes every class queued since
1362
+ * {@link beginAccumulate} was called. Uses the same two-phase strategy
1363
+ * as {@link processPending} (register all bindings first, then resolve
1364
+ * singletons / controllers) so import ordering within a lazy batch
1365
+ * does not cause resolution failures.
1366
+ */
1367
+ static flushAccumulated() {
1368
+ _InjectorExplorer.accumulating = false;
1369
+ const queue = [
1370
+ ..._InjectorExplorer.pending
1371
+ ];
1372
+ _InjectorExplorer.pending.length = 0;
1373
+ for (const { target, lifetime } of queue) {
1374
+ if (!RootInjector.bindings.has(target)) {
1375
+ RootInjector.bindings.set(target, {
1376
+ implementation: target,
1377
+ lifetime
1378
+ });
1379
+ }
1380
+ }
1381
+ for (const { target, lifetime } of queue) {
1382
+ _InjectorExplorer.processRegistration(target, lifetime);
1383
+ }
1384
+ }
1385
+ /**
1386
+ * Processes all pending registrations in two phases:
1387
+ * 1. Register all bindings (no instantiation) so every dependency is known.
1388
+ * 2. Resolve singletons, register controllers and log module readiness.
1389
+ *
1390
+ * This two-phase approach makes the system resilient to import ordering:
1391
+ * all bindings exist before any singleton is instantiated.
1392
+ */
1393
+ static processPending() {
1394
+ const queue = _InjectorExplorer.pending;
1395
+ for (const { target, lifetime } of queue) {
1396
+ if (!RootInjector.bindings.has(target)) {
1397
+ RootInjector.bindings.set(target, {
1398
+ implementation: target,
1399
+ lifetime
1400
+ });
1401
+ }
1402
+ }
1403
+ for (const { target, lifetime } of queue) {
1404
+ _InjectorExplorer.processRegistration(target, lifetime);
1405
+ }
1406
+ queue.length = 0;
1407
+ _InjectorExplorer.processed = true;
1408
+ }
1409
+ /**
1410
+ * Registers a single class immediately (post-bootstrap path).
1411
+ * Used for classes discovered via late dynamic imports.
1267
1412
  */
1268
- static register(target, lifetime) {
1269
- if (RootInjector.bindings.has(target)) return RootInjector;
1413
+ static registerImmediate(target, lifetime) {
1414
+ if (RootInjector.bindings.has(target)) {
1415
+ return;
1416
+ }
1270
1417
  RootInjector.bindings.set(target, {
1271
1418
  implementation: target,
1272
1419
  lifetime
1273
1420
  });
1421
+ _InjectorExplorer.processRegistration(target, lifetime);
1422
+ }
1423
+ /**
1424
+ * Performs phase-2 work for a single registration: resolve singletons,
1425
+ * register controllers, and log module readiness.
1426
+ */
1427
+ static processRegistration(target, lifetime) {
1274
1428
  if (lifetime === "singleton") {
1275
1429
  RootInjector.resolve(target);
1276
1430
  }
1277
1431
  if (getModuleMetadata(target)) {
1278
1432
  Logger.log(`${target.name} dependencies initialized`);
1279
- return RootInjector;
1433
+ return;
1280
1434
  }
1281
1435
  const controllerMeta = getControllerMetadata(target);
1282
1436
  if (controllerMeta) {
1283
1437
  const router = RootInjector.resolve(Router);
1284
1438
  router?.registerController(target);
1285
- return RootInjector;
1439
+ return;
1286
1440
  }
1287
- const routeMeta = getRouteMetadata(target);
1288
- if (routeMeta) {
1289
- return RootInjector;
1441
+ if (getRouteMetadata(target).length > 0) {
1442
+ return;
1290
1443
  }
1291
1444
  if (getInjectableMetadata(target)) {
1292
1445
  Logger.log(`Registered ${target.name} as ${lifetime}`);
1293
- return RootInjector;
1294
1446
  }
1295
- return RootInjector;
1296
1447
  }
1297
1448
  };
1298
1449
  __name(_InjectorExplorer, "InjectorExplorer");
1450
+ __publicField(_InjectorExplorer, "pending", []);
1451
+ __publicField(_InjectorExplorer, "processed", false);
1452
+ __publicField(_InjectorExplorer, "accumulating", false);
1299
1453
  var InjectorExplorer = _InjectorExplorer;
1300
1454
 
1301
1455
  // src/decorators/injectable.decorator.ts
@@ -1305,7 +1459,7 @@ function Injectable(lifetime = "scope") {
1305
1459
  throw new Error(`@Injectable can only be used on classes, not on ${typeof target}`);
1306
1460
  }
1307
1461
  defineInjectableMetadata(target, lifetime);
1308
- InjectorExplorer.register(target, lifetime);
1462
+ InjectorExplorer.enqueue(target, lifetime);
1309
1463
  };
1310
1464
  }
1311
1465
  __name(Injectable, "Injectable");