154 lines
4.3 KiB
TypeScript
154 lines
4.3 KiB
TypeScript
import {
|
|
Container,
|
|
type AbstractType,
|
|
type ProviderDefinition,
|
|
type Token,
|
|
type Type,
|
|
} from "./container.ts";
|
|
import {
|
|
getInjectableOptions,
|
|
getModuleOptions,
|
|
} from "../decorators/Module.ts";
|
|
|
|
export interface ModuleContext {
|
|
type: Type;
|
|
container: Container;
|
|
exports: Token[];
|
|
controllers: Type[];
|
|
imports: ModuleContext[];
|
|
}
|
|
|
|
/** Per request container graph, one scoped container per module. */
|
|
export class ScopeGraph {
|
|
private readonly containers = new Map<ModuleContext, Container>();
|
|
|
|
constructor(private readonly registry: ModuleRegistry) {
|
|
for (const context of registry.all()) {
|
|
const scope = context.container.createScope();
|
|
scopeGraphs.set(scope, this);
|
|
this.containers.set(context, scope);
|
|
}
|
|
}
|
|
|
|
/** Scoped container of a module. */
|
|
containerFor(context: ModuleContext): Container {
|
|
const container = this.containers.get(context);
|
|
if (!container)
|
|
throw new Error(`Module ${context.type.name} is not registered`);
|
|
return container;
|
|
}
|
|
}
|
|
|
|
const scopeGraphs = new WeakMap<Container, ScopeGraph>();
|
|
|
|
export class ModuleRegistry {
|
|
private readonly contexts = new Map<Type, ModuleContext>();
|
|
private readonly order: ModuleContext[] = [];
|
|
|
|
/** Every registered module, in registration order. */
|
|
all(): ModuleContext[] {
|
|
return [...this.order];
|
|
}
|
|
|
|
/** Registers a module and all of its imports. */
|
|
register(moduleType: Type): ModuleContext {
|
|
const existing = this.contexts.get(moduleType);
|
|
if (existing) return existing;
|
|
|
|
const options = getModuleOptions(moduleType);
|
|
const context: ModuleContext = {
|
|
type: moduleType,
|
|
container: new Container(),
|
|
exports: [],
|
|
controllers: [...(options.controllers ?? [])],
|
|
imports: [],
|
|
};
|
|
this.contexts.set(moduleType, context);
|
|
|
|
for (const imported of options.imports ?? []) {
|
|
context.imports.push(this.register(imported));
|
|
}
|
|
|
|
for (const provider of options.providers ?? []) {
|
|
if (typeof provider === "function") {
|
|
const scope = getInjectableOptions(provider).scope;
|
|
context.container.registerClass(provider as AbstractType, { scope });
|
|
} else {
|
|
context.container.registerProvider(provider as ProviderDefinition);
|
|
}
|
|
}
|
|
|
|
for (const controller of context.controllers) {
|
|
context.container.registerClass(controller);
|
|
}
|
|
|
|
context.exports = [...(options.exports ?? [])];
|
|
context.container.fallbackResolver = (token, scope) =>
|
|
this.resolveFromImports(context, token, scope);
|
|
|
|
this.order.push(context);
|
|
return context;
|
|
}
|
|
|
|
/** Resolves a token from the exported providers of the imported modules. */
|
|
resolveFromImports(
|
|
context: ModuleContext,
|
|
token: Token,
|
|
scope?: Container,
|
|
): unknown {
|
|
for (const imported of context.imports) {
|
|
const resolved = this.resolveExported(imported, token, scope);
|
|
if (resolved !== undefined) return resolved;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** Resolves a token that an imported module re-exports. */
|
|
resolveExported(
|
|
context: ModuleContext,
|
|
token: Token,
|
|
scope?: Container,
|
|
): unknown {
|
|
if (context.exports.includes(token)) {
|
|
return context.container.resolve(token, scope);
|
|
}
|
|
for (const exported of context.exports) {
|
|
if (typeof exported !== "function") continue;
|
|
const exportedContext = this.contexts.get(exported as Type);
|
|
if (!exportedContext) continue;
|
|
const resolved = this.resolveExported(exportedContext, token, scope);
|
|
if (resolved !== undefined) return resolved;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** Controllers of every module together with the module they belong to. */
|
|
controllers(): { context: ModuleContext; controller: Type }[] {
|
|
const result: { context: ModuleContext; controller: Type }[] = [];
|
|
for (const context of this.order) {
|
|
for (const controller of context.controllers) {
|
|
result.push({ context, controller });
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** Instantiates every controller, surfacing dependency errors at boot time. */
|
|
bootstrap(): void {
|
|
for (const { context, controller } of this.controllers()) {
|
|
context.container.resolve(controller);
|
|
}
|
|
}
|
|
|
|
/** Instantiates every controller asynchronously. */
|
|
async bootstrapAsync(): Promise<void> {
|
|
for (const { context, controller } of this.controllers()) {
|
|
await context.container.resolveAsync(controller);
|
|
}
|
|
}
|
|
|
|
/** Creates the per request container graph. */
|
|
createScope(): ScopeGraph {
|
|
return new ScopeGraph(this);
|
|
}
|
|
}
|