Lifecycle Events
Rikta provides hooks into key application lifecycle events, allowing you to run initialization logic, cleanup tasks, and respond to application state changes.
Overview
Understanding the application lifecycle is crucial for:
- Initializing resources (database connections, caches)
- Graceful shutdown (closing connections, flushing buffers)
- Health checks (monitoring readiness and liveness)
- Logging and metrics (tracking application state)
Lifecycle Sequence
┌─────────────────────────────────────────────────────────────┐
│ Application Lifecycle │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Rikta.create() called │
│ │ │
│ ▼ │
│ 2. Container initialization │
│ │ │
│ ▼ │
│ 3. Providers registered │
│ │ │
│ ▼ │
│ 4. OnProviderInit hooks called │
│ │ │
│ ▼ │
│ 5. Routes registered │
│ │ │
│ ▼ │
│ 6. app.listen() - Server starts │
│ │ │
│ ▼ │
│ 7. OnApplicationListen hooks called │
│ │ │
│ ▼ │
│ 8. Application running... │
│ │ │
│ ▼ │
│ 9. Shutdown signal received │
│ │ │
│ ▼ │
│ 10. OnApplicationShutdown hooks called │
│ │ │
│ ▼ │
│ 11. Application terminated │
│ │
└─────────────────────────────────────────────────────────────┘
Lifecycle Hooks
OnProviderInit
Implement this interface to run code when the provider is instantiated and dependencies are injected:
import { Injectable, OnProviderInit } from '@riktajs/core';
@Injectable()
export class DatabaseService implements OnProviderInit {
private connection: Connection;
async onProviderInit() {
console.log('Initializing database connection...');
this.connection = await createConnection();
console.log('Database connected!');
}
}
OnApplicationListen
Implement this interface to run code when the application is fully started and ready to accept requests:
import { Injectable, OnApplicationListen } from '@riktajs/core';
@Injectable()
export class MonitoringService implements OnApplicationListen {
async onApplicationListen(address: string) {
console.log(`Application is ready at ${address}!`);
await this.registerWithServiceDiscovery();
await this.startHealthChecks();
}
private async registerWithServiceDiscovery() {
// Register this instance with service discovery
}
private async startHealthChecks() {
// Start periodic health checks
}
}
OnApplicationShutdown
Implement this interface to run code when the application receives a shutdown signal:
import { Injectable, OnApplicationShutdown, Autowired } from '@riktajs/core';
@Injectable()
export class CleanupService implements OnApplicationShutdown {
@Autowired()
private database!: DatabaseService;
@Autowired()
private cache!: CacheService;
async onApplicationShutdown(signal?: string) {
console.log(`Shutting down gracefully (${signal})...`);
// Close database connections
await this.database.disconnect();
// Flush cache
await this.cache.flush();
// Deregister from service discovery
await this.deregisterService();
console.log('Cleanup complete');
}
}
Async Hooks
All lifecycle hooks support async operations:
@Injectable()
export class StartupService implements OnProviderInit {
async onProviderInit() {
await this.loadConfiguration();
await this.warmupCache();
await this.validateConnections();
}
private async loadConfiguration() {
// Load remote configuration
}
private async warmupCache() {
// Pre-populate cache with common data
}
private async validateConnections() {
// Verify external service connections
}
}
Order of Execution
Lifecycle hooks are executed in the order providers are registered:
@Injectable()
export class ServiceA implements OnProviderInit {
onProviderInit() {
console.log('ServiceA initialized'); // Called first
}
}
@Injectable()
export class ServiceB implements OnProviderInit {
@Autowired()
private serviceA!: ServiceA;
onProviderInit() {
console.log('ServiceB initialized'); // Called after ServiceA
}
}
Practical Examples
Database Connection Manager
import { Injectable, OnProviderInit, OnApplicationShutdown } from '@riktajs/core';
@Injectable()
export class DatabaseManager implements OnProviderInit, OnApplicationShutdown {
private pool: ConnectionPool;
async onProviderInit() {
console.log('Creating database connection pool...');
this.pool = await createPool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME,
max: 20,
});
console.log('Database pool created');
}
async onApplicationShutdown() {
console.log('Closing database connections...');
await this.pool.end();
console.log('Database connections closed');
}
getPool(): ConnectionPool {
return this.pool;
}
}
Health Check Service
import { Injectable, OnApplicationListen, Autowired } from '@riktajs/core';
@Injectable()
export class HealthCheckService implements OnApplicationListen {
@Autowired()
private database!: DatabaseManager;
private isReady = false;
async onApplicationListen(address: string) {
this.isReady = true;
console.log(`Health check: Application ready at ${address}`);
}
async checkHealth(): Promise<HealthStatus> {
const dbHealthy = await this.checkDatabase();
return {
status: this.isReady && dbHealthy ? 'healthy' : 'unhealthy',
checks: {
database: dbHealthy,
ready: this.isReady,
},
timestamp: new Date().toISOString(),
};
}
private async checkDatabase(): Promise<boolean> {
try {
await this.database.getPool().query('SELECT 1');
return true;
} catch {
return false;
}
}
}
interface HealthStatus {
status: 'healthy' | 'unhealthy';
checks: Record<string, boolean>;
timestamp: string;
}
Graceful Shutdown Handler
import { Injectable, OnApplicationShutdown, Autowired } from '@riktajs/core';
@Injectable()
export class GracefulShutdown implements OnApplicationShutdown {
private shutdownInProgress = false;
@Autowired()
private database!: DatabaseManager;
@Autowired()
private messageQueue!: MessageQueueService;
async onApplicationShutdown(signal?: string) {
if (this.shutdownInProgress) return;
this.shutdownInProgress = true;
console.log('Graceful shutdown initiated...');
// Stop accepting new work
await this.messageQueue.stopConsuming();
// Wait for in-flight requests (with timeout)
await this.waitForInFlightRequests(30000);
// Flush any pending writes
await this.messageQueue.flushPending();
// Close connections
await this.database.disconnect();
console.log('Graceful shutdown complete');
}
private async waitForInFlightRequests(timeout: number): Promise<void> {
const start = Date.now();
while (this.getInFlightCount() > 0 && Date.now() - start < timeout) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
private getInFlightCount(): number {
// Return count of in-flight requests
return 0;
}
}
Signal Handling
Rikta automatically handles common shutdown signals:
SIGTERM- Standard termination signalSIGINT- Interrupt from keyboard (Ctrl+C)
// Rikta handles this automatically
process.on('SIGTERM', () => {
// OnApplicationShutdown hooks are called
});
process.on('SIGINT', () => {
// OnApplicationShutdown hooks are called
});
Best Practices
1. Keep Hooks Fast
// ✅ Good - quick initialization
async onProviderInit() {
this.config = this.loadConfig();
await this.validateConfig();
}
// ❌ Avoid - slow startup
async onProviderInit() {
await this.loadAllDataFromDatabase(); // Blocks startup
}
2. Handle Errors Gracefully
async onProviderInit() {
try {
await this.connect();
} catch (error) {
console.error('Failed to initialize:', error);
throw error; // Prevent app from starting with broken state
}
}
3. Set Timeouts for Shutdown
async onApplicationShutdown(signal?: string) {
const timeout = setTimeout(() => {
console.error('Shutdown timeout - forcing exit');
process.exit(1);
}, 30000);
await this.cleanup();
clearTimeout(timeout);
}
4. Order Dependencies Correctly
// Database should initialize before services that use it
@Injectable()
export class DatabaseService implements OnProviderInit {
async onProviderInit() { /* Connect first */ }
}
@Injectable()
export class UserService implements OnProviderInit {
@Autowired()
private database!: DatabaseService; // Depends on database
async onProviderInit() { /* Database already connected */ }
}
EventBus
The EventBus provides a flexible pub/sub mechanism for lifecycle and custom events. It works alongside interface-based hooks.
Basic Usage
import { Injectable, Autowired, EventBus } from '@riktajs/core';
@Injectable()
export class MonitoringService {
@Autowired()
private events!: EventBus;
onProviderInit() {
// Subscribe to lifecycle events
this.events.on('app:listen', ({ address, port }) => {
console.log(`Server started at ${address}:${port}`);
});
this.events.on('app:shutdown', ({ signal }) => {
console.log(`Shutdown signal: ${signal}`);
});
}
}
Built-in Events
| Event | Payload | Description |
|---|---|---|
app:discovery | { files: string[] } | After auto-discovery completes |
app:providers | { count: number } | After providers are registered |
provider:init | { provider, name, priority } | When a provider is initialized |
app:routes | { count: number } | After routes are registered |
app:bootstrap | { providerCount: number } | Application bootstrap complete |
app:listen | { address, port } | Server starts listening |
app:shutdown | { signal?: string } | Shutdown initiated |
provider:destroy | { provider, name } | Provider being destroyed |
app:destroy | { uptime: number } | Application fully shut down |
Unsubscribing
The on() method returns an unsubscribe function:
const unsubscribe = this.events.on('app:listen', () => {
console.log('Server started');
});
// Later, to stop listening:
unsubscribe();
One-time Listeners
Use once() to automatically unsubscribe after the first event:
this.events.once('app:listen', ({ address }) => {
console.log(`First start at ${address}`);
// Automatically unsubscribed after this
});
Promise-based Waiting
Use waitFor() to await an event:
async function waitForReady() {
const { address } = await events.waitFor('app:listen');
console.log(`App is ready at ${address}`);
}
Custom Events
You can emit and listen to custom events:
// Emit custom event
await this.events.emit('user:created', { userId: '123', email: '[email protected]' });
// Listen to custom event
this.events.on<{ userId: string; email: string }>('user:created', ({ userId, email }) => {
console.log(`New user: ${email}`);
});
Owner Tracking and Cleanup
EventBus supports owner tracking for automatic cleanup. When registering listeners, you can specify an owner:
// Register with owner tracking
this.events.on('custom:event', handler, 'MyService');
// Remove all listeners for an owner
this.events.removeByOwner('MyService');
When using the @On() decorator, Rikta automatically tracks listeners by provider name and cleans them up during application shutdown. This prevents memory leaks from accumulated listeners.
Debugging EventBus
Useful methods for debugging:
// Get listener count for specific event
const count = events.listenerCount('app:listen');
// Get total listeners across all events
const total = events.totalListenerCount();
// Get all registered owners
const owners = events.getOwners();
// Get listener count by owner
const ownerCount = events.listenerCountByOwner('MyService');
Using @On() Decorator
The @On() decorator provides a declarative way to subscribe to events:
import { Injectable, On } from '@riktajs/core';
@Injectable()
export class LoggingService {
@On('app:listen')
onAppListen({ address }: { address: string }) {
console.log(`Application started at ${address}`);
}
@On('app:shutdown')
onAppShutdown({ signal }: { signal?: string }) {
console.log(`Shutting down: ${signal}`);
}
@On('provider:init')
onProviderInit({ name }: { name: string }) {
console.log(`Provider initialized: ${name}`);
}
}
Listeners registered with @On() are automatically cleaned up when the application shuts down, preventing memory leaks.