@noxfly/noxus
Version:
Simulate lightweight HTTP-like requests between renderer and main process in Electron applications with MessagePort, with structured and modular design.
170 lines (146 loc) • 6.19 kB
text/typescript
/**
* @copyright 2025 NoxFly
* @license MIT
* @author NoxFly
*/
import { getControllerMetadata } from "src/decorators/controller.decorator";
import { getInjectableMetadata } from "src/decorators/injectable.metadata";
import { getRouteMetadata } from "src/decorators/method.decorator";
import { getModuleMetadata } from "src/decorators/module.decorator";
import { Lifetime, RootInjector } from "src/DI/app-injector";
import { Router } from "src/router";
import { Logger } from "src/utils/logger";
import { Type } from "src/utils/types";
interface PendingRegistration {
target: Type<unknown>;
lifetime: Lifetime;
}
/**
* InjectorExplorer is a utility class that explores the dependency injection system at the startup.
* It collects decorated classes during the import phase and defers their actual registration
* and resolution to when {@link processPending} is called by bootstrapApplication.
*/
export class InjectorExplorer {
private static readonly pending: PendingRegistration[] = [];
private static processed = false;
private static accumulating = false;
/**
* Enqueues a class for deferred registration.
* Called by the @Injectable decorator at import time.
*
* If {@link processPending} has already been called (i.e. after bootstrap)
* and accumulation mode is not active, the class is registered immediately
* so that late dynamic imports (e.g. middlewares loaded after bootstrap)
* work correctly.
*
* When accumulation mode is active (between {@link beginAccumulate} and
* {@link flushAccumulated}), classes are queued instead — preserving the
* two-phase binding/resolution guarantee for lazy-loaded modules.
*/
public static enqueue(target: Type<unknown>, lifetime: Lifetime): void {
if(InjectorExplorer.processed && !InjectorExplorer.accumulating) {
InjectorExplorer.registerImmediate(target, lifetime);
return;
}
InjectorExplorer.pending.push({ target, lifetime });
}
/**
* Enters accumulation mode. While active, all decorated classes discovered
* via dynamic imports are queued in {@link pending} rather than registered
* immediately. Call {@link flushAccumulated} to process them with the
* full two-phase (bind-then-resolve) guarantee.
*/
public static beginAccumulate(): void {
InjectorExplorer.accumulating = true;
}
/**
* Exits accumulation mode and processes every class queued since
* {@link beginAccumulate} was called. Uses the same two-phase strategy
* as {@link processPending} (register all bindings first, then resolve
* singletons / controllers) so import ordering within a lazy batch
* does not cause resolution failures.
*/
public static flushAccumulated(): void {
InjectorExplorer.accumulating = false;
const queue = [...InjectorExplorer.pending];
InjectorExplorer.pending.length = 0;
// Phase 1: register all bindings without instantiation
for(const { target, lifetime } of queue) {
if(!RootInjector.bindings.has(target)) {
RootInjector.bindings.set(target, {
implementation: target,
lifetime
});
}
}
// Phase 2: resolve singletons, register controllers, log modules
for(const { target, lifetime } of queue) {
InjectorExplorer.processRegistration(target, lifetime);
}
}
/**
* Processes all pending registrations in two phases:
* 1. Register all bindings (no instantiation) so every dependency is known.
* 2. Resolve singletons, register controllers and log module readiness.
*
* This two-phase approach makes the system resilient to import ordering:
* all bindings exist before any singleton is instantiated.
*/
public static processPending(): void {
const queue = InjectorExplorer.pending;
// Phase 1: register all bindings without instantiation
for(const { target, lifetime } of queue) {
if(!RootInjector.bindings.has(target)) {
RootInjector.bindings.set(target, {
implementation: target,
lifetime
});
}
}
// Phase 2: resolve singletons, register controllers, log modules
for(const { target, lifetime } of queue) {
InjectorExplorer.processRegistration(target, lifetime);
}
queue.length = 0;
InjectorExplorer.processed = true;
}
/**
* Registers a single class immediately (post-bootstrap path).
* Used for classes discovered via late dynamic imports.
*/
private static registerImmediate(target: Type<unknown>, lifetime: Lifetime): void {
if(RootInjector.bindings.has(target)) {
return;
}
RootInjector.bindings.set(target, {
implementation: target,
lifetime
});
InjectorExplorer.processRegistration(target, lifetime);
}
/**
* Performs phase-2 work for a single registration: resolve singletons,
* register controllers, and log module readiness.
*/
private static processRegistration(target: Type<unknown>, lifetime: Lifetime): void {
if(lifetime === 'singleton') {
RootInjector.resolve(target);
}
if(getModuleMetadata(target)) {
Logger.log(`${target.name} dependencies initialized`);
return;
}
const controllerMeta = getControllerMetadata(target);
if(controllerMeta) {
const router = RootInjector.resolve(Router);
router?.registerController(target);
return;
}
if(getRouteMetadata(target).length > 0) {
return;
}
if(getInjectableMetadata(target)) {
Logger.log(`Registered ${target.name} as ${lifetime}`);
}
}
}