muddle/core/modules.ts
2026-10-23 08:53:07 -04:00

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);
}
}