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(); 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(); export class ModuleRegistry { private readonly contexts = new Map(); 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 { for (const { context, controller } of this.controllers()) { await context.container.resolveAsync(controller); } } /** Creates the per request container graph. */ createScope(): ScopeGraph { return new ScopeGraph(this); } }