A powerful and lightweight inversion of control container for JavaScript and Node.js apps powered by TypeScript.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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.
const bindingScopeValues: {
Singleton: "Singleton";
Transient: "Transient";
Request: "Request";
};
type BindingScope = keyof typeof bindingScopeValues;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); // trueNew 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); // falseSingle 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 instanceExecuted 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 });
}
}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");
}
}interface OnActivation<T> {
(context: ResolutionContext, injectable: T): T | Promise<T>;
}
interface BindOnFluentSyntax<T> {
onActivation(handler: OnActivation<T>): BindInFluentSyntax<T>;
}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;
});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 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);
});@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");
}
}@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();
}
}
}@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;
});