JSPM

@lexms/di-container

1.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 2
  • Score
    100M100P100Q23263F
  • License MIT

A lightweight dependency injection container for TypeScript

Package Exports

  • @lexms/di-container
  • @lexms/di-container/dist/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@lexms/di-container) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

@lexms/di-container

A lightweight, TypeScript-first dependency injection container with support for both synchronous and asynchronous service resolution.

Features

  • ๐Ÿš€ Lightweight: Minimal overhead with no external dependencies (except reflect-metadata for decorators)
  • ๐Ÿ”„ Sync & Async: Support for both synchronous and asynchronous service factories
  • ๐Ÿ›ก๏ธ Type Safe: Full TypeScript support with comprehensive type definitions
  • ๐Ÿ” Lifetime Management: Singleton and transient service lifetimes
  • ๐ŸŽฏ Flexible Tokens: Register services using classes, strings, or symbols
  • ๐Ÿšซ Circular Dependency Detection: Automatic detection and prevention of circular dependencies
  • ๐ŸŽจ Decorator Support: Optional decorator-based dependency injection
  • ๐Ÿงน Automatic Cleanup: Built-in disposal pattern for resource cleanup
  • ๐Ÿ“Š Logging: Optional logging for debugging and monitoring
  • โšก Performance Monitoring: Built-in performance tracking and metrics collection

Installation

pnpm add @lexms/di-container

For decorator support, also install:

pnpm add reflect-metadata

Quick Start

import { DIContainer, LifetimeScope } from '@lexms/di-container';

// Create a container
const container = new DIContainer();

// Define services
class DatabaseService {
  connect() {
    console.log('Connected to database');
  }
}

class UserService {
  constructor(private db: DatabaseService) {}
  
  getUsers() {
    this.db.connect();
    return ['user1', 'user2'];
  }
}

// Register services
container.registerSingleton(DatabaseService, () => new DatabaseService());
container.registerSingleton(UserService, () => 
  new UserService(container.resolve(DatabaseService))
);

// Resolve and use
const userService = container.resolve(UserService);
const users = userService.getUsers();

API Reference

DIContainer

Constructor

const container = new DIContainer(options?: DIContainerOptions);

Options:

  • enableLogging?: boolean - Enable debug logging (default: false)
  • logPrefix?: string - Custom log prefix (default: 'DIContainer')
  • enablePerformanceMonitoring?: boolean - Enable performance tracking (default: false)

Registration Methods

register<T>(token, factory, scope?)

Register a service with explicit lifetime scope.

container.register(MyService, () => new MyService(), LifetimeScope.SINGLETON);
registerSingleton<T>(token, factory)

Register a singleton service (same instance returned on every resolve).

container.registerSingleton(MyService, () => new MyService());
registerTransient<T>(token, factory)

Register a transient service (new instance returned on every resolve).

container.registerTransient(MyService, () => new MyService());
registerInstance<T>(token, instance)

Register an existing instance.

const config = { apiUrl: 'https://api.example.com' };
container.registerInstance('config', config);

Resolution Methods

resolve<T>(token)

Synchronously resolve a service.

const service = container.resolve(MyService);
resolveAsync<T>(token)

Asynchronously resolve a service (required for async factories).

const service = await container.resolveAsync(MyAsyncService);

Utility Methods

has(token): boolean

Check if a service is registered.

if (container.has(MyService)) {
  // Service is registered
}
getRegisteredTokens(): (string | symbol)[]

Get all registered service tokens.

const tokens = container.getRegisteredTokens();
clear(): void

Remove all service registrations.

container.clear();
dispose(): Promise<void>

Dispose the container and call dispose() on all disposable services.

await container.dispose();
getPerformanceStats(): ContainerPerformanceStats

Get comprehensive performance statistics for the container.

const stats = container.getPerformanceStats();
console.log(`Total resolutions: ${stats.totalResolutions}`);
console.log(`Average time: ${stats.averageResolutionTime}ms`);
getServiceMetrics(token?): ServicePerformanceMetrics[]

Get performance metrics for specific services or all services.

// Get metrics for all services
const allMetrics = container.getServiceMetrics();

// Get metrics for specific service
const serviceMetrics = container.getServiceMetrics(MyService);
resetPerformanceStats(): void

Reset all performance statistics.

container.resetPerformanceStats();

Advanced Usage

Async Services

class AsyncDatabaseService {
  async connect() {
    // Async connection logic
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log('Connected to database');
  }
}

// Register with async factory
container.registerSingleton(AsyncDatabaseService, async () => {
  const service = new AsyncDatabaseService();
  await service.connect();
  return service;
});

// Must use resolveAsync for async factories
const dbService = await container.resolveAsync(AsyncDatabaseService);

String and Symbol Tokens

// String tokens
container.registerInstance('apiUrl', 'https://api.example.com');
const apiUrl = container.resolve<string>('apiUrl');

// Symbol tokens
const DATABASE_CONFIG = Symbol('DatabaseConfig');
container.registerInstance(DATABASE_CONFIG, { host: 'localhost', port: 5432 });
const dbConfig = container.resolve<{ host: string; port: number }>(DATABASE_CONFIG);

Performance Monitoring

Enable performance monitoring to track service resolution times and identify bottlenecks.

const container = new DIContainer({ 
  enablePerformanceMonitoring: true,
  enableLogging: true 
});

class FastService {
  process() { return 'fast'; }
}

class SlowService {
  process() {
    // Simulate slow operation
    const start = Date.now();
    while (Date.now() - start < 100) {}
    return 'slow';
  }
}

container.registerSingleton(FastService, () => new FastService());
container.registerTransient(SlowService, () => new SlowService());

// Resolve services multiple times
container.resolve(FastService);
container.resolve(FastService); // Cached singleton
container.resolve(SlowService);
container.resolve(SlowService); // New instance

// Get performance statistics
const stats = container.getPerformanceStats();
console.log(`Total resolutions: ${stats.totalResolutions}`);
console.log(`Average resolution time: ${stats.averageResolutionTime}ms`);
console.log(`Slowest services:`, stats.slowestServices);

// Get service-specific metrics
const metrics = container.getServiceMetrics();
metrics.forEach(metric => {
  console.log(`${metric.token}: ${metric.averageTime}ms avg (${metric.totalResolutions} calls)`);
});

// Reset statistics
container.resetPerformanceStats();

Disposable Services

Services that implement a dispose() method will be automatically disposed when the container is disposed.

class ResourceService {
  private connection: any;
  
  constructor() {
    this.connection = createConnection();
  }
  
  async dispose() {
    await this.connection.close();
    console.log('Resources cleaned up');
  }
}

container.registerSingleton(ResourceService, () => new ResourceService());

// Later...
await container.dispose(); // Automatically calls ResourceService.dispose()

Decorator Support (Optional)

First, import reflect-metadata at the top of your main file:

import 'reflect-metadata';
import { Injectable, Inject, autoRegister } from '@lexms/di-container';

@Injectable
class DatabaseService {
  connect() {
    console.log('Connected to database');
  }
}

@Injectable
class UserService {
  constructor(
    private db: DatabaseService,
    @Inject('config') private config: any
  ) {}
}

// Auto-register decorated classes
autoRegister(DatabaseService);
autoRegister(UserService);

Error Handling

The container provides specific error types for different failure scenarios:

import { 
  ServiceNotFoundError, 
  CircularDependencyError, 
  DIContainerError 
} from '@lexms/di-container';

try {
  const service = container.resolve(UnregisteredService);
} catch (error) {
  if (error instanceof ServiceNotFoundError) {
    console.log('Service not found');
  } else if (error instanceof CircularDependencyError) {
    console.log('Circular dependency detected');
  }
}

Best Practices

1. Use Interface-based Design

interface IUserRepository {
  getUsers(): User[];
}

class DatabaseUserRepository implements IUserRepository {
  getUsers(): User[] {
    // Database implementation
    return [];
  }
}

class MockUserRepository implements IUserRepository {
  getUsers(): User[] {
    // Mock implementation
    return [{ id: 1, name: 'Test User' }];
  }
}

// Register based on environment
const repository = process.env.NODE_ENV === 'test' 
  ? new MockUserRepository()
  : new DatabaseUserRepository();
  
container.registerInstance('IUserRepository', repository);

2. Factory Functions for Complex Dependencies

container.registerSingleton(UserService, () => {
  const repository = container.resolve<IUserRepository>('IUserRepository');
  const logger = container.resolve<ILogger>('ILogger');
  const config = container.resolve<Config>('config');
  
  return new UserService(repository, logger, config);
});

3. Container Composition

class DatabaseModule {
  static register(container: DIContainer) {
    container.registerSingleton(DatabaseService, () => new DatabaseService());
    container.registerSingleton(UserRepository, () => 
      new UserRepository(container.resolve(DatabaseService))
    );
  }
}

class ServiceModule {
  static register(container: DIContainer) {
    container.registerSingleton(UserService, () =>
      new UserService(container.resolve(UserRepository))
    );
  }
}

// Register all modules
DatabaseModule.register(container);
ServiceModule.register(container);

Development

Scripts

# Install dependencies
pnpm install

# Build the project
pnpm run build

# Run tests
pnpm test

# Run tests in watch mode
pnpm run test:watch

# Run linting
pnpm run lint

# Fix linting issues
pnpm run lint:fix

# Clean build outputs
pnpm run clean

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.