npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@ayka/dibox

v1.1.0

Published

A type-safe dependency injection container with lazy loading and caching

Downloads

214

Readme

@ayka/dibox

NPM Version NPM License NPM Downloads

npm package minimized gzipped size NPM Unpacked Size

CI CodeQL

Maintainability Test Coverage Mutation testing badge

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:

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.