|
| 1 | +import type { LifecycleTriggerable } from './builtin' |
| 2 | + |
| 3 | +export type DependencyMap = Record<string, any> |
| 4 | + |
| 5 | +export interface Container { |
| 6 | + providers: Map<string, ProvideOptionObject<any, any>> |
| 7 | + instances: Map<string, any> |
| 8 | + lifecycleHooks: Map<string, LifecycleTriggerable> |
| 9 | + dependencyGraph: Map<string, string[]> |
| 10 | + invocations: InvokeOptionObject<any>[] |
| 11 | +} |
| 12 | + |
| 13 | +export type BuildContext<D extends DependencyMap | undefined = undefined> = { |
| 14 | + container: Container |
| 15 | + name: string |
| 16 | +} & (D extends undefined ? { dependsOn?: unknown } : { dependsOn: D }) |
| 17 | + |
| 18 | +export type ProvideOptionObject<T, D extends DependencyMap | undefined = undefined> = { build: (context: BuildContext<D>) => T | Promise<T> } & (D extends undefined ? { dependsOn?: Record<string, never> } : { dependsOn: { [K in keyof D]: string } }) |
| 19 | +export type ProvideOptionFunc<T, D extends DependencyMap | undefined = undefined> = (context: BuildContext<D>) => T | Promise<T> |
| 20 | +export type ProvideOption<T, D extends DependencyMap | undefined = undefined> = ProvideOptionObject<T, D> | ProvideOptionFunc<T, D> |
| 21 | + |
| 22 | +export type InvokeOptionObject<D extends DependencyMap | undefined = undefined> = { callback: (dependencies: D) => void | Promise<void> } & (D extends undefined ? { dependsOn?: Record<string, never> } : { dependsOn: { [K in keyof D]: string } }) |
| 23 | +export type InvokeOptionFunc<D extends DependencyMap | undefined = undefined> = (dependencies: D) => void | Promise<void> |
| 24 | +export type InvokeOption<D extends DependencyMap | undefined = undefined> = InvokeOptionObject<D> | InvokeOptionFunc<D> |
| 25 | + |
| 26 | +export function createContainer(): Container { |
| 27 | + return { |
| 28 | + providers: new Map(), |
| 29 | + instances: new Map(), |
| 30 | + lifecycleHooks: new Map(), |
| 31 | + dependencyGraph: new Map(), |
| 32 | + invocations: [], |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +export function provide<D extends DependencyMap | undefined, T = any>( |
| 37 | + container: Container, |
| 38 | + name: string, |
| 39 | + option: ProvideOption<T, D>, |
| 40 | +): void { |
| 41 | + const providerObject = typeof option === 'function' |
| 42 | + ? { build: option } as ProvideOptionObject<any, any> |
| 43 | + : option as ProvideOptionObject<any, any> |
| 44 | + |
| 45 | + container.providers.set(name, providerObject) |
| 46 | + |
| 47 | + // Track dependencies for lifecycle ordering |
| 48 | + const dependencies = providerObject.dependsOn ? Object.values(providerObject.dependsOn) : [] |
| 49 | + container.dependencyGraph.set(name, dependencies) |
| 50 | +} |
| 51 | + |
| 52 | +export function invoke<D extends DependencyMap>(container: Container, option: InvokeOption<D>): void { |
| 53 | + if (typeof option === 'function') { |
| 54 | + container.invocations.push({ callback: option } as InvokeOptionObject<any>) |
| 55 | + } |
| 56 | + else { |
| 57 | + container.invocations.push(option as InvokeOptionObject<any>) |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +async function resolveInstance<T>(container: Container, name: string): Promise<T> { |
| 62 | + if (container.instances.has(name)) { |
| 63 | + return container.instances.get(name) as T |
| 64 | + } |
| 65 | + |
| 66 | + const provider = container.providers.get(name) |
| 67 | + if (!provider) { |
| 68 | + throw new Error(`No provider found for '${name}'`) |
| 69 | + } |
| 70 | + |
| 71 | + const resolvedDependencies: Record<string, any> = {} |
| 72 | + let serviceLifecycle: any = null |
| 73 | + |
| 74 | + if (provider.dependsOn) { |
| 75 | + for (const [key, depName] of Object.entries(provider.dependsOn)) { |
| 76 | + if (depName === 'lifecycle') { |
| 77 | + // Create individual lifecycle instance for this service |
| 78 | + const { buildLifecycle } = await import('./builtin') |
| 79 | + serviceLifecycle = buildLifecycle() |
| 80 | + resolvedDependencies[key] = serviceLifecycle |
| 81 | + // Track this service's lifecycle |
| 82 | + container.lifecycleHooks.set(name, serviceLifecycle) |
| 83 | + } |
| 84 | + else { |
| 85 | + resolvedDependencies[key] = await resolveInstance(container, depName) |
| 86 | + } |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + const context: BuildContext<typeof provider.dependsOn> = { |
| 91 | + container, |
| 92 | + dependsOn: resolvedDependencies, |
| 93 | + name, |
| 94 | + } |
| 95 | + |
| 96 | + const instance = await provider.build(context) |
| 97 | + container.instances.set(name, instance) |
| 98 | + |
| 99 | + return instance as T |
| 100 | +} |
| 101 | + |
| 102 | +function topologicalSort(dependencyGraph: Map<string, string[]>): string[] { |
| 103 | + const visited = new Set<string>() |
| 104 | + const visiting = new Set<string>() |
| 105 | + const result: string[] = [] |
| 106 | + |
| 107 | + function visit(node: string) { |
| 108 | + if (visiting.has(node)) { |
| 109 | + throw new Error(`Circular dependency detected involving '${node}'`) |
| 110 | + } |
| 111 | + if (visited.has(node)) { |
| 112 | + return |
| 113 | + } |
| 114 | + |
| 115 | + visiting.add(node) |
| 116 | + const dependencies = dependencyGraph.get(node) || [] |
| 117 | + for (const dep of dependencies) { |
| 118 | + visit(dep) |
| 119 | + } |
| 120 | + visiting.delete(node) |
| 121 | + visited.add(node) |
| 122 | + result.push(node) |
| 123 | + } |
| 124 | + |
| 125 | + for (const node of dependencyGraph.keys()) { |
| 126 | + if (!visited.has(node)) { |
| 127 | + visit(node) |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + return result |
| 132 | +} |
| 133 | + |
| 134 | +export async function startLifecycleHooks(container: Container): Promise<void> { |
| 135 | + const sortedServices = topologicalSort(container.dependencyGraph) |
| 136 | + |
| 137 | + for (const serviceName of sortedServices) { |
| 138 | + const lifecycle = container.lifecycleHooks.get(serviceName) |
| 139 | + if (lifecycle?.emitOnStart) { |
| 140 | + await lifecycle.emitOnStart() |
| 141 | + } |
| 142 | + } |
| 143 | +} |
| 144 | + |
| 145 | +export async function stopLifecycleHooks(container: Container): Promise<void> { |
| 146 | + const sortedServices = topologicalSort(container.dependencyGraph) |
| 147 | + |
| 148 | + // Shutdown in reverse order |
| 149 | + for (const serviceName of sortedServices.reverse()) { |
| 150 | + const lifecycle = container.lifecycleHooks.get(serviceName) |
| 151 | + if (lifecycle?.emitOnStop) { |
| 152 | + await lifecycle.emitOnStop() |
| 153 | + } |
| 154 | + } |
| 155 | +} |
| 156 | + |
| 157 | +export async function start(container: Container): Promise<void> { |
| 158 | + await startLifecycleHooks(container) |
| 159 | + |
| 160 | + for (const invocation of container.invocations) { |
| 161 | + const resolvedDependencies: Record<string, any> = {} |
| 162 | + |
| 163 | + if (invocation.dependsOn) { |
| 164 | + for (const [key, depName] of Object.entries(invocation.dependsOn)) { |
| 165 | + resolvedDependencies[key] = await resolveInstance(container, depName) |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + await invocation.callback(resolvedDependencies) |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +export async function stop(container: Container): Promise<void> { |
| 174 | + await stopLifecycleHooks(container) |
| 175 | +} |
0 commit comments