CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-inversify

A powerful and lightweight inversion of control container for JavaScript and Node.js apps powered by TypeScript.

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

lifecycle.mddocs/

Lifecycle Management

InversifyJS provides comprehensive lifecycle management features including scope control, activation/deactivation hooks, construction/destruction callbacks, and container event handling. These features enable proper resource management and fine-grained control over service lifetimes.

Lifecycle Scopes

Binding Scope Values

const bindingScopeValues: {
  Singleton: "Singleton";
  Transient: "Transient";
  Request: "Request";
};

type BindingScope = keyof typeof bindingScopeValues;

Singleton Scope

Single instance shared across the entire container lifetime.

@injectable()
class DatabaseConnectionPool {
  private connections: Connection[] = [];
  private maxConnections = 10;
  
  constructor() {
    console.log("Creating connection pool...");
  }
  
  getConnection(): Connection {
    if (this.connections.length < this.maxConnections) {
      const connection = new Connection();
      this.connections.push(connection);
      return connection;
    }
    return this.connections[Math.floor(Math.random() * this.connections.length)];
  }
  
  @preDestroy()
  cleanup() {
    console.log("Closing all connections...");
    this.connections.forEach(conn => conn.close());
  }
}

// Single instance for entire application
container.bind<DatabaseConnectionPool>("ConnectionPool")
  .to(DatabaseConnectionPool)
  .inSingletonScope();

// All requests return same instance
const pool1 = container.get<DatabaseConnectionPool>("ConnectionPool");
const pool2 = container.get<DatabaseConnectionPool>("ConnectionPool");
console.log(pool1 === pool2); // true

Transient Scope

New instance created for every resolution request.

@injectable()
class TaskProcessor {
  private taskId = Math.random().toString(36);
  
  constructor() {
    console.log(`Creating task processor: ${this.taskId}`);
  }
  
  process(data: any) {
    console.log(`Processing with ${this.taskId}`);
    return { processedBy: this.taskId, data };
  }
  
  @preDestroy()
  cleanup() {
    console.log(`Cleaning up processor: ${this.taskId}`);
  }
}

// New instance every time
container.bind<TaskProcessor>("TaskProcessor")
  .to(TaskProcessor)
  .inTransientScope();

const processor1 = container.get<TaskProcessor>("TaskProcessor");
const processor2 = container.get<TaskProcessor>("TaskProcessor");
console.log(processor1 === processor2); // false

Request Scope

Single instance per dependency resolution graph.

@injectable()
class RequestContext {
  private requestId = Math.random().toString(36);
  private startTime = Date.now();
  
  constructor() {
    console.log(`Request context created: ${this.requestId}`);
  }
  
  getRequestId() { return this.requestId; }
  getElapsedTime() { return Date.now() - this.startTime; }
  
  @preDestroy()
  cleanup() {
    console.log(`Request completed in ${this.getElapsedTime()}ms`);
  }
}

@injectable()
class UserService {
  constructor(@inject("RequestContext") private context: RequestContext) {}
  
  getUser(id: string) {
    console.log(`UserService using context: ${this.context.getRequestId()}`);
    return { id, requestId: this.context.getRequestId() };
  }
}

@injectable()
class OrderService {
  constructor(@inject("RequestContext") private context: RequestContext) {}
  
  getOrder(id: string) {
    console.log(`OrderService using context: ${this.context.getRequestId()}`);
    return { id, requestId: this.context.getRequestId() };
  }
}

container.bind<RequestContext>("RequestContext").to(RequestContext).inRequestScope();
container.bind<UserService>("UserService").to(UserService);
container.bind<OrderService>("OrderService").to(OrderService);

// Same context shared within single resolution request
const userService = container.get<UserService>("UserService");
const orderService = container.get<OrderService>("OrderService");
// Both services share the same RequestContext instance

// New context for new resolution request
const userService2 = container.get<UserService>("UserService");
// userService2 gets a different RequestContext instance

Lifecycle Hook Decorators

@postConstruct

Executed after object construction and all dependency injection is complete.

function postConstruct(target: any, propertyKey: string): void;
@injectable()
class EmailService {
  @inject("Config") private config!: IConfig;
  @inject("Logger") private logger!: ILogger;
  
  private smtpClient?: SmtpClient;
  private isInitialized = false;
  
  @postConstruct()
  private async initialize() {
    this.logger.log("Initializing email service...");
    
    this.smtpClient = new SmtpClient({
      host: this.config.smtp.host,
      port: this.config.smtp.port,
      secure: this.config.smtp.secure
    });
    
    await this.smtpClient.connect();
    this.isInitialized = true;
    
    this.logger.log("Email service initialized successfully");
  }
  
  async sendEmail(to: string, subject: string, body: string) {
    if (!this.isInitialized) {
      throw new Error("Email service not initialized");
    }
    
    return this.smtpClient!.sendMail({ to, subject, html: body });
  }
}

@preDestroy

Executed before object destruction or container disposal.

function preDestroy(target: any, propertyKey: string): void;
@injectable()
class FileProcessingService {
  private fileHandles: FileHandle[] = [];
  private tempFiles: string[] = [];
  private processingSessions = new Map<string, ProcessingSession>();
  
  async processFile(filePath: string): Promise<ProcessingResult> {
    const handle = await fs.open(filePath, 'r');
    this.fileHandles.push(handle);
    
    const tempFile = `/tmp/processing_${Date.now()}.tmp`;
    this.tempFiles.push(tempFile);
    
    const sessionId = Math.random().toString(36);
    const session = new ProcessingSession(sessionId);
    this.processingSessions.set(sessionId, session);
    
    // Processing logic...
    return { sessionId, processed: true };
  }
  
  @preDestroy()
  private async cleanup() {
    console.log("Cleaning up file processing service...");
    
    // Close all file handles
    for (const handle of this.fileHandles) {
      try {
        await handle.close();
      } catch (error) {
        console.error("Error closing file handle:", error);
      }
    }
    
    // Remove temporary files
    for (const tempFile of this.tempFiles) {
      try {
        await fs.unlink(tempFile);
      } catch (error) {
        console.error("Error removing temp file:", error);
      }
    }
    
    // Clean up processing sessions
    for (const [sessionId, session] of this.processingSessions) {
      try {
        await session.terminate();
      } catch (error) {
        console.error(`Error terminating session ${sessionId}:`, error);
      }
    }
    
    console.log("File processing service cleanup completed");
  }
}

Activation and Deactivation Hooks

OnActivation Interface

interface OnActivation<T> {
  (context: ResolutionContext, injectable: T): T | Promise<T>;
}

interface BindOnFluentSyntax<T> {
  onActivation(handler: OnActivation<T>): BindInFluentSyntax<T>;
}

Activation Handlers

Called immediately after service instantiation but before returning to requester.

@injectable()
class CacheService {
  private cache = new Map<string, any>();
  
  get(key: string) { return this.cache.get(key); }
  set(key: string, value: any) { this.cache.set(key, value); }
  clear() { this.cache.clear(); }
}

container.bind<CacheService>("CacheService")
  .to(CacheService)
  .inSingletonScope()
  .onActivation((context, cacheService) => {
    console.log("Cache service activated");
    
    // Pre-populate cache with initial data
    cacheService.set("initialized", true);
    cacheService.set("activatedAt", new Date().toISOString());
    
    // Set up periodic cleanup
    setInterval(() => {
      console.log("Performing cache cleanup...");
      // Cleanup logic here
    }, 60000);
    
    return cacheService;
  });

// Async activation handler
container.bind<DatabaseService>("DatabaseService")
  .to(DatabaseService)
  .inSingletonScope()
  .onActivation(async (context, dbService) => {
    console.log("Activating database service...");
    
    await dbService.connect();
    await dbService.runMigrations();
    
    console.log("Database service ready");
    return dbService;
  });

Deactivation Handlers

interface OnDeactivation<T> {
  (injectable: T): void | Promise<void>;
}

interface BindOnFluentSyntax<T> {
  onDeactivation(handler: OnDeactivation<T>): BindInFluentSyntax<T>;
}

Called before service destruction or container disposal.

@injectable()
class WebSocketService {
  private connections = new Set<WebSocket>();
  private heartbeatInterval?: NodeJS.Timeout;
  
  constructor() {
    this.heartbeatInterval = setInterval(() => {
      this.sendHeartbeat();
    }, 30000);
  }
  
  addConnection(ws: WebSocket) {
    this.connections.add(ws);
  }
  
  removeConnection(ws: WebSocket) {
    this.connections.delete(ws);
  }
  
  private sendHeartbeat() {
    for (const ws of this.connections) {
      if (ws.readyState === WebSocket.OPEN) {
        ws.ping();
      }
    }
  }
}

container.bind<WebSocketService>("WebSocketService")
  .to(WebSocketService)
  .inSingletonScope()
  .onDeactivation((wsService) => {
    console.log("Deactivating WebSocket service...");
    
    // Clear heartbeat interval
    if (wsService.heartbeatInterval) {
      clearInterval(wsService.heartbeatInterval);
    }
    
    // Close all connections
    for (const ws of wsService.connections) {
      if (ws.readyState === WebSocket.OPEN) {
        ws.close(1000, "Service shutting down");
      }
    }
    
    console.log("WebSocket service deactivated");
  });

// Async deactivation handler
container.bind<DatabaseService>("DatabaseService")
  .to(DatabaseService)
  .inSingletonScope()
  .onDeactivation(async (dbService) => {
    console.log("Deactivating database service...");
    
    await dbService.flushPendingOperations();
    await dbService.disconnect();
    
    console.log("Database service deactivated");
  });

Container Lifecycle Events

Container Disposal

// Container with lifecycle management
const container = new Container();

// Register services with lifecycle hooks
container.bind<FileService>("FileService")
  .to(FileService)
  .inSingletonScope()
  .onDeactivation(async (service) => {
    await service.closeAllFiles();
  });

container.bind<NetworkService>("NetworkService")
  .to(NetworkService)
  .inSingletonScope()
  .onDeactivation(async (service) => {
    await service.closeConnections();
  });

// Graceful shutdown
process.on('SIGTERM', async () => {
  console.log('Received SIGTERM, shutting down gracefully...');
  
  // This will trigger all deactivation handlers
  await container.unbindAllAsync();
  
  process.exit(0);
});

Advanced Lifecycle Patterns

Circular Dependency Handling

@injectable()
class ServiceA {
  private serviceB?: ServiceB;
  
  @postConstruct()
  initialize() {
    // Safe to access circular dependencies in postConstruct
    this.serviceB = container.get<ServiceB>("ServiceB");
  }
}

@injectable()
class ServiceB {
  private serviceA?: ServiceA;
  
  @postConstruct()
  initialize() {
    this.serviceA = container.get<ServiceA>("ServiceA");
  }
}

Lazy Initialization

@injectable()
class ExpensiveService {
  private _heavyResource?: HeavyResource;
  
  get heavyResource() {
    if (!this._heavyResource) {
      console.log("Lazy loading heavy resource...");
      this._heavyResource = new HeavyResource();
    }
    return this._heavyResource;
  }
  
  @preDestroy()
  cleanup() {
    if (this._heavyResource) {
      this._heavyResource.dispose();
    }
  }
}

Health Check Integration

@injectable()
class HealthCheckService {
  private services = new Map<string, HealthCheckable>();
  
  registerService(name: string, service: HealthCheckable) {
    this.services.set(name, service);
  }
  
  async checkHealth(): Promise<HealthStatus> {
    const results = new Map<string, boolean>();
    
    for (const [name, service] of this.services) {
      try {
        const isHealthy = await service.isHealthy();
        results.set(name, isHealthy);
      } catch (error) {
        results.set(name, false);
      }
    }
    
    return {
      overall: Array.from(results.values()).every(Boolean),
      services: Object.fromEntries(results)
    };
  }
}

// Register services with health checks via activation handlers
container.bind<DatabaseService>("DatabaseService")
  .to(DatabaseService)
  .inSingletonScope()
  .onActivation((context, dbService) => {
    const healthCheck = context.container.get<HealthCheckService>("HealthCheck");
    healthCheck.registerService("database", dbService);
    return dbService;
  });

Best Practices

  1. Use appropriate scopes: Match scope to service purpose and resource usage
  2. Implement @preDestroy: Always clean up resources in preDestroy methods
  3. Avoid heavy work in constructors: Use @postConstruct for complex initialization
  4. Handle async operations: Use async/await in lifecycle hooks when needed
  5. Register health checks: Use activation handlers to integrate with health monitoring
  6. Graceful shutdown: Implement proper container disposal on application exit
  7. Test lifecycle: Verify construction, initialization, and cleanup work correctly
  8. Document dependencies: Make initialization order dependencies clear

docs

binding.md

conditional.md

container.md

decorators.md

index.md

lifecycle.md

modules.md

tile.json