Dependencies are registered and built recursively from the entry assemblage. The lifecycle follows a specific order.
onRegister(Static) - Called when assemblage is registeredconstructor- Instance is built with dependencies injectedonInit- Called when dependency tree is ready (bottom-up)onInited- Called after allonInithooks complete (bottom-up, then entry point)onDispose- Called during cleanup (top-down: entry point first, then dependencies)
Called when the assemblage is registered. Other dependencies may or may not be registered yet.
Receives: Base configuration from @Assemblage() definition, not runtime configuration.
@Assemblage()
class MyService implements AbstractAssemblage {
static onRegister(context: AssemblerContext, configuration: Record<string, any>) {
console.log('Service registered');
// configuration = base config from @Assemblage() definition
}
}Use cases:
- Register global values
- Setup class-level resources
- Validate configuration
The instance is built with all dependencies injected. Each dependency is resolved according to its singleton/transient setting.
@Assemblage({ inject: [[LoggerService]] })
class MyApp implements AbstractAssemblage {
constructor(private logger: LoggerService) {
// Dependencies are ready to use
this.logger.log('App constructed');
}
}Use cases:
- Store injected dependencies
- Initialize instance properties
- Simple synchronous setup
Called when the entire dependency tree is ready. Executed bottom-up (dependencies first, entry point last).
@Assemblage()
class MyService implements AbstractAssemblage {
async onInit(context: AssemblerContext, configuration: Record<string, any>) {
console.log('Service initialized');
// Perform async initialization here
}
}Configuration parameter contains:
- For the entry point: Configuration passed to
Assembler.build(EntryPoint, config) - For dependencies: Their base configuration from
@Assemblage({ inject: [..., config] })
Use cases:
- Async initialization (database connections, API calls)
- Setup that requires other dependencies to be ready
- Event listener registration
Called after all onInit hooks have completed across the entire dependency tree. Executed in reverse order for dependencies (B → A), then the entry point last.
This hook is useful when you need to perform actions that depend on the complete initialization of all services.
@Assemblage()
class MyService implements AbstractAssemblage {
async onInited(context: AssemblerContext, configuration: Record<string, any>) {
console.log('All services are now initialized');
// Perform post-initialization tasks here
}
}Execution order:
- Dependencies'
onInit: A → B → (entry point C) - Dependencies'
onInited: B → A → (entry point C)
Configuration parameter contains:
- For the entry point: Configuration passed to
Assembler.build(EntryPoint, config) - For dependencies: Their base configuration from
@Assemblage({ inject: [..., config] })
Use cases:
- Final setup that requires all services to be fully initialized
- Cross-service validation or health checks
- Triggering initial events after complete system startup
- Starting background tasks that depend on the full system
Called when disposing the assembler. Use for cleanup (closing connections, releasing resources).
Disposal happens in reverse dependency order - dependencies are disposed before their dependents.
@Assemblage()
class DatabaseService implements AbstractAssemblage {
async onDispose(context: AssemblerContext, configuration: Record<string, any>) {
await this.connection.close();
console.log('Database connection closed');
}
}Configuration parameter contains:
- For the entry point: Configuration passed to
Assembler.build(EntryPoint, config) - For dependencies: Their base configuration from
@Assemblage({ inject: [..., config] })
Use cases:
- Close database connections
- Release file handles
- Cleanup timers/intervals
- Unregister event listeners
@Dispose() to call it:
@Assemblage()
class App implements AbstractAssemblage {
constructor(@Dispose() public dispose: () => void) {}
}
const app = Assembler.build(App);
await app.dispose(); // Calls onDispose on all assemblagesSee the warning about @Dispose() - it disposes the entire container.
@Assemblage()
class ChildService implements AbstractAssemblage {
static onRegister() { console.log('1. Child registered'); }
constructor() { console.log('3. Child constructed'); }
onInit() { console.log('5. Child initialized'); }
onInited() { console.log('7. Child inited'); }
onDispose() { console.log('10. Child disposed'); }
}
@Assemblage({ inject: [[ChildService]] })
class ParentService implements AbstractAssemblage {
static onRegister() { console.log('2. Parent registered'); }
constructor(
child: ChildService,
@Dispose() public dispose: () => void
) {
console.log('4. Parent constructed');
}
onInit() { console.log('6. Parent initialized'); }
onInited() { console.log('8. Parent inited'); }
onDispose() { console.log('9. Parent disposed'); }
}
const app = Assembler.build(ParentService);
// Output: 1-8 (registration, construction, initialization, post-initialization)
await app.dispose();
// Output: 10, 9 (disposal: child first, then parent)With multiple dependencies (A, B) and an entry point (C):
@Assemblage()
class ServiceA {
onInit() { console.log('A initialized'); }
onInited() { console.log('A inited'); }
}
@Assemblage()
class ServiceB {
onInit() { console.log('B initialized'); }
onInited() { console.log('B inited'); }
}
@Assemblage({ inject: [[ServiceA], [ServiceB]] })
class ServiceC {
onInit() { console.log('C initialized'); }
onInited() { console.log('C inited'); }
}
// Execution order:
// A initialized → B initialized → C initialized
// B inited → A inited → C inited- Registration & Construction: Top-down order (entry point registers dependencies first)
- Initialization (
onInit): Bottom-up order (dependencies A → B → entry point C) - Post-initialization (
onInited): Reverse order for dependencies (B → A), then entry point (C) - Disposal (
onDispose): Top-down order (entry point first, then dependencies) - Using
dispose(): Must be injected via@Dispose()decorator in the entry point constructor
Both onInit, onInited, and onDispose can be async:
@Assemblage()
class DatabaseService implements AbstractAssemblage {
async onInit() {
await this.connect();
}
async onInited() {
await this.runHealthCheck();
}
async onDispose() {
await this.disconnect();
}
}The DI container will await these hooks before continuing.
If a lifecycle hook throws an error, the build/disposal process stops:
@Assemblage()
class FailingService implements AbstractAssemblage {
onInit() {
throw new Error('Initialization failed');
}
}
try {
const app = Assembler.build(FailingService);
} catch (error) {
console.error('Build failed:', error);
}- Dependency Injection - Understand dependency resolution
- Parameter Decorators - Inject context and configuration
- Events - Setup event listeners in
onInit - Benchmarks - Lifecycle hooks performance
- Advanced Examples - See lifecycle hooks in action