@ayka/dibox
v1.1.0
Published
A type-safe dependency injection container with lazy loading and caching
Downloads
214
Maintainers
Readme
@ayka/dibox
Overview
dibox
is a lightweight, type-safe dependency injection container for TypeScript/JavaScript applications. It provides a simple yet powerful way to manage dependencies with features like lazy loading, immutable API, and automatic circular dependency detection.
Key Features
- 🎯 Type Safety: Full TypeScript support with automatic type inference
- 🦥 Lazy Loading: Dependencies are only initialized when needed
- 🔄 Immutable API: Prevents side effects and makes state management predictable
- 🪶 Zero Dependencies: Lightweight and focused on core DI functionality
- 🎮 Easy API: Simple and intuitive API for managing dependencies
- 🔍 Circular Dependency Detection: Automatically detects and reports circular dependencies
- 🔌 Framework Agnostic: Works with any JavaScript/TypeScript project
Perfect For
- Modular Applications: Organize and manage complex dependency graphs
- Testing: Easily mock dependencies for unit and integration tests
Table of Contents
API Reference
For detailed API documentation, see:
- API Documentation - Complete API reference with types and examples
- Box Class - Core container class documentation
- Test Examples - Comprehensive examples of all features in test form
Usage
Basic Usage
Create a dependency container using makeBox()
and define your dependencies:
import * as DI from '@ayka/dibox';
// Create a box with dependencies
const box = DI.makeBox()
.set('config', () => ({ apiUrl: 'https://api.example.com' }))
.set('api', (box) => new ApiClient(box.get('config').apiUrl))
.set('users', (box) => box.get('api').getUsers());
// Access dependencies - they are lazily loaded and cached
const users = box.get('users'); // API call happens here
const sameUsers = box.get('users'); // Returns cached value
Adding Dependencies
You can add new dependencies using set()
or patch()
:
import * as DI from '@ayka/dibox';
// Add a single dependency
const boxWithLogger = box.set('logger', () => new Logger());
// Add multiple dependencies
const boxWithMore = box.patch({
cache: () => new Cache(),
db: (box) => new Database(box.get('config')),
});
Accessing Dependencies
There are multiple ways to access dependencies:
// Using get() - cached access
const api = box.get('api');
// Using load() - always creates fresh instance (like transient in NestJS or Awilix)
const freshApi = box.load('api');
// Using proxy - convenient property access
const { config, api } = box.proxy;
// Convert entire box to plain object
const allDeps = box.toJS();
Type Safety
The container is fully type-safe:
const box = makeBox({
name: () => 'Alice',
age: () => 30,
});
// Types are inferred automatically
const name = box.get('name'); // type: string
const age = box.get('age'); // type: number
// TypeScript errors on invalid keys
box.get('invalid'); // Error: Argument of type '"invalid"' is not assignable...
Circular Dependency Detection
dibox automatically detects and reports circular dependencies at runtime. A circular dependency occurs when two or more dependencies depend on each other in a cycle.
import * as DI from '@ayka/dibox';
// This will throw CircularDependencyError
const box = DI.makeBox()
.set('chicken', (box) => box.get('egg'))
.set('egg', (box) => box.get('chicken'));
// Error: Circular dependency detected for key: 'chicken'.
// Unresolved keys: chicken, egg
Common Patterns to Resolve Circular Dependencies
1. Use a Shared Configuration
import * as DI from '@ayka/dibox';
// ❌ Circular dependency
const badBox = DI.makeBox()
.set('userService', (box) => new UserService(box.get('authService')))
.set('authService', (box) => new AuthService(box.get('userService')));
// ✅ Share configuration instead
const goodBox = DI.makeBox()
.set('config', () => ({
userApi: 'https://api.example.com/users',
authApi: 'https://api.example.com/auth',
}))
.set('userService', (box) => new UserService(box.get('config')))
.set('authService', (box) => new AuthService(box.get('config')));
2. Use Interface Segregation
import * as DI from '@ayka/dibox';
// ❌ Circular dependency
const badBox = DI.makeBox()
.set('orderProcessor', (box) => new OrderProcessor(box.get('inventory')))
.set('inventory', (box) => new Inventory(box.get('orderProcessor')));
// ✅ Split into smaller, focused interfaces
const goodBox = DI.makeBox()
.set('inventoryReader', () => new InventoryReader())
.set(
'orderProcessor',
(box) => new OrderProcessor(box.get('inventoryReader')),
)
.set(
'inventoryWriter',
(box) => new InventoryWriter(box.get('orderProcessor')),
);
The CircularDependencyError
includes helpful information to debug the cycle:
- The key that triggered the error
- The list of unresolved keys in the dependency chain
- A clear error message explaining the issue
try {
box.get('chicken');
} catch (error) {
if (error instanceof DI.CircularDependencyError) {
console.log(error.message); // Circular dependency detected...
console.log(error.key); // 'chicken'
console.log(error.unresolvedKeys); // Set { 'chicken', 'egg' }
}
}
Advanced Features
Merging Containers
const box1 = makeBox({ foo: () => 'bar' });
const box2 = makeBox({ baz: () => 123 });
const merged = box1.merge(box2);
Cache Control
// Clear specific cached value
box.clearCache('users');
// Clear all cached values
box.resetCache();
// Create new instance with empty cache
const fresh = box.clone();
Preloading Dependencies
// Preload specific dependencies
box.preload(['config', 'api']);
// Preload all dependencies
box.preload(true);
For more examples and detailed API documentation, see the API Documentation.
Proxy Access
The box provides a convenient proxy interface that allows you to access dependencies using property syntax:
import * as DI from '@ayka/dibox';
const box = DI.makeBox({
config: () => ({ apiUrl: 'https://api.example.com' }),
api: (box) => new ApiClient(box.get('config').apiUrl),
users: (box) => box.get('api').getUsers(),
});
// Instead of box.get('config')
const { config, api, users } = box.proxy;
// Types are preserved
const apiUrl = config.apiUrl; // type: string
The proxy maintains all the same behaviors as using get()
:
- Lazy loading: Values are only initialized when accessed
- Caching: Values are cached after first access
- Type safety: Full TypeScript type inference
You can mix and match proxy access with regular methods:
// These are equivalent:
const api1 = box.get('api');
const api2 = box.proxy.api;
// Proxy access works with cache clearing too
box.clearCache('api');
const freshApi = box.proxy.api; // Creates new instance
Note that while proxy access is convenient, it doesn't support all box features:
- Can't use
load()
through proxy (always uses cached values) - Can't check if a key exists
- Can't clear cache through proxy
For these operations, you'll need to use the regular box methods.
Mutating Dependencies
The mutate
method allows you to modify a single dependency in the current box instance. Unlike patch()
or set()
, which create a new box, mutate()
directly alters the existing box.
import * as DI from '@ayka/dibox';
// Create a box with initial dependencies
const box = DI.makeBox({
count: () => 0,
name: () => 'Alice',
});
// Mutate the 'count' dependency to always return 1
box.mutate('count', () => 1);
// Access the mutated dependency
console.log(box.get('count')); // Outputs: 1
// Mutate 'name' to include a greeting
box.mutate('name', (box) => `Hello, ${box.get('name')}!`);
// Access the mutated 'name' dependency
console.log(box.get('name')); // Outputs: Hello, Alice!
Receipts
Overriding Dependencies for Testing
const box = makeBox({
config: () => ({ apiUrl: 'https://api.example.com' }),
api: () => new ApiClient(),
});
const fn = (box: typeof box) => {};
class TestApiClient extends ApiClient {}
const testBox = box.set('api', () => {
const url = box.get('config').apiUrl;
return new TestApiClient(url);
});
fn(testBox);
Async Dependency
const prebox = makeBox({
config: () => ({ postgresUrl: 'postgres://localhost:5432' }),
});
const postgres = await postgresClient(prebox.get('config'));
const box = prebox.set('postgres', () => postgres);
Contributing
Contributions are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request.
License
This project is licensed under the MIT License.