@noxfly/noxus
Version:
Simulate lightweight HTTP-like requests between renderer and main process in Electron applications with MessagePort, with structured and modular design.
163 lines (138 loc) • 6.18 kB
text/typescript
/**
* @copyright 2025 NoxFly
* @license MIT
* @author NoxFly
*/
import 'reflect-metadata';
import { INJECT_METADATA_KEY } from 'src/decorators/inject.decorator';
import { InternalServerException } from 'src/exceptions';
import { ForwardReference } from 'src/utils/forward-ref';
import { Type } from 'src/utils/types';
/**
* Represents a lifetime of a binding in the dependency injection system.
* It can be one of the following:
* - 'singleton': The instance is created once and shared across the application.
* - 'scope': The instance is created once per scope (e.g., per request).
* - 'transient': A new instance is created every time it is requested.
*/
export type Lifetime = 'singleton' | 'scope' | 'transient';
/**
* Represents a binding in the dependency injection system.
* It contains the lifetime of the binding, the implementation type, and optionally an instance.
*/
export interface IBinding {
lifetime: Lifetime;
implementation: Type<unknown>;
instance?: InstanceType<Type<unknown>>;
}
/**
* AppInjector is the root dependency injection container.
* It is used to register and resolve dependencies in the application.
* It supports different lifetimes for dependencies:
* This should not be manually instantiated, outside of the framework.
* Use the `RootInjector` instance instead.
*/
export class AppInjector {
public bindings = new Map<Type<unknown>, IBinding>();
public singletons = new Map<Type<unknown>, InstanceType<Type<unknown>>>();
public scoped = new Map<Type<unknown>, InstanceType<Type<unknown>>>();
constructor(
public readonly name: string | null = null,
) {}
/**
* Typically used to create a dependency injection scope
* at the "scope" level (i.e., per-request lifetime).
*
* SHOULD NOT BE USED by anything else than the framework itself.
*/
public createScope(): AppInjector {
const scope = new AppInjector();
scope.bindings = this.bindings; // pass injectable declarations
scope.singletons = this.singletons; // share parent's singletons to avoid recreating them
// do not keep parent's scoped instances
return scope;
}
/**
* Called when resolving a dependency,
* i.e., retrieving the instance of a given class.
*/
public resolve<T>(target: Type<T> | ForwardReference<T>): T {
if (target instanceof ForwardReference) {
return new Proxy({}, {
get: (obj, prop, receiver) => {
const realType = target.forwardRefFn();
const instance = this.resolve(realType) as any;
const value = Reflect.get(instance, prop, receiver);
return typeof value === 'function' ? value.bind(instance) : value;
},
set: (obj, prop, value, receiver) => {
const realType = target.forwardRefFn();
const instance = this.resolve(realType) as any;
return Reflect.set(instance, prop, value, receiver);
},
getPrototypeOf: () => {
const realType = target.forwardRefFn();
return (realType as any).prototype;
}
}) as T;
}
const binding = this.bindings.get(target);
if(!binding) {
if(target === undefined) {
throw new InternalServerException(
"Failed to resolve a dependency injection : Undefined target type.\n"
+ "This might be caused by a circular dependency."
);
}
const name = target.name || "unknown";
throw new InternalServerException(
`Failed to resolve a dependency injection : No binding for type ${name}.\n`
+ `Did you forget to use @Injectable() decorator ?`
);
}
switch(binding.lifetime) {
case 'transient':
return this.instantiate(binding.implementation) as T;
case 'scope': {
if(this.scoped.has(target)) {
return this.scoped.get(target) as T;
}
const instance = this.instantiate(binding.implementation);
this.scoped.set(target, instance);
return instance as T;
}
case 'singleton': {
if(binding.instance === undefined && this.name === 'root') {
binding.instance = this.instantiate(binding.implementation);
this.singletons.set(target, binding.instance);
}
return binding.instance as T;
}
}
}
/**
* Instantiates a class, resolving its dependencies.
*/
private instantiate<T extends Type<unknown>>(target: T): InstanceType<T> {
const paramTypes = Reflect.getMetadata('design:paramtypes', target) || [];
const injectParams = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];
const params = paramTypes.map((paramType: any, index: number) => {
const overrideToken = injectParams[index];
const actualToken = overrideToken !== undefined ? overrideToken : paramType;
return this.resolve(actualToken);
});
return new target(...params) as InstanceType<T>;
}
}
/**
* Injects a type from the dependency injection system.
* This function is used to retrieve an instance of a type that has been registered in the dependency injection system.
* It is typically used in the constructor of a class to inject dependencies.
* @param t - The type to inject.
* @returns An instance of the type.
* @throws If the type is not registered in the dependency injection system.
*/
export function inject<T>(t: Type<T> | ForwardReference<T>): T {
return RootInjector.resolve(t);
}
export const RootInjector = new AppInjector('root');