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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@eslym/container

v2.1.0

Published

A lightweight dependency injection container for TypeScript and JavaScript.

Readme

@eslym/container

A lightweight, type-safe dependency injection container for TypeScript and JavaScript.

Features

  • Type-safe service keys and factory return values
  • Lazy, memoized service resolution
  • Dependency tracking with automatic invalidation
  • Per-container overrides for tests and request-scoped dependencies
  • Sync and async resource disposal
  • Lifecycle hooks for registration, creation, and resolution
  • ESM and CommonJS builds with bundled TypeScript declarations

Installation

npm install @eslym/container

Quick Start

Define the container shape once, then let individual modules extend it through TypeScript declaration merging:

// app.ts
import { ContainerRegistry, type Container } from '@eslym/container';

declare global {
	namespace Service {
		interface AppContainer {}
		type App = Container<AppContainer>;
	}
}

export const AppContainer = new ContainerRegistry<Service.AppContainer>('app');

Register a database and its configuration in a separate module:

// database.ts
import { Database } from 'bun:sqlite';
import { AppContainer } from './app';

declare global {
	namespace Service {
		interface AppContainer {
			'db.config': {
				path: string;
				autoMigrate: boolean;
			};
			db: Database;
		}
	}
}

export function registerDatabaseService() {
	AppContainer.register('db.config', () => ({
		path: process.env.DB_PATH ?? './db.sqlite',
		autoMigrate: process.env.DB_AUTO_MIGRATE !== 'false'
	}));

	AppContainer.register('db', function () {
		const config = this['db.config'];
		const db = new Database(config.path);

		if (config.autoMigrate) {
			// Perform migrations here.
		}

		return db;
	});
	AppContainer.hooks.on('disposed', ({ queueCleanup, values }) => {
		if (!values.db) return;
		queueCleanup(() => values.db!.close());
	});
}

Create a container after registering services and access services as properties:

// main.ts
import { AppContainer } from './app';
import { registerDatabaseService } from './database';

registerDatabaseService();

const app = AppContainer.create();
app.db.query('SELECT 1').get();

Factories are called only when a service is first accessed. Their results are cached for the lifetime of that container, so repeated access to app.db returns the same instance.

Concepts

Registry

ContainerRegistry<T, Params> stores service factories and creates independent containers from them. The registry name is used in diagnostics and must be provided to the constructor.

const registry = new ContainerRegistry<AppContainer>('app');

Register a factory with register. The factory receives the container as this, so it can resolve other services without importing them directly:

registry.register('logger', () => new Logger());
registry.register('users', function () {
	return new UserRepository(this.db, this.logger);
});

Use registerAll to register several factories at once. Each property is checked against the container's service type:

registry.registerAll({
	logger: () => new Logger(),
	users: function () {
		return new UserRepository(this.db, this.logger);
	}
});

registerAll skips entries whose factory is undefined or another falsy value. Registering a key that already exists replaces its factory and invalidates the key in existing containers.

Container

Each call to create returns a new isolated container. The optional arguments are passed to every factory in that container:

type Services = {
	config: { environment: string };
	requestId: string;
};

const registry = new ContainerRegistry<Services, [requestId: string]>('request');
registry.register('config', () => ({ environment: 'test' }));
registry.register('requestId', function (requestId) {
	return requestId;
});

const request = registry.create('req-123');
request.requestId; // 'req-123'

Overrides and Invalidation

Assigning a service replaces its resolved value and invalidates services that depend on it. This is useful for test doubles and request-specific configuration:

const app = AppContainer.create();

app['db.config'] = {
	path: ':memory:',
	autoMigrate: true
};

// A subsequently resolved app.db uses the replacement configuration.

Deleting a service clears its cached value so the factory will run again on the next access. The proxy supports delete container.value at runtime, but TypeScript reports an error when value is required by the container type. Use the controller API when deleting a required service:

app.$.delete('db');

Replacing a factory in the registry also invalidates that service in existing containers. Dependent services are invalidated recursively.

Lifecycle and Disposal

The container does not automatically dispose resolved services. Register cleanup explicitly with the disposed hook and queueCleanup:

AppContainer.hooks.on('disposed', ({ queueCleanup, values }) => {
	if (!values.db) return;
	queueCleanup(() => values.db!.close());
});

Cleanup callbacks can be synchronous or asynchronous. For disposable services, call Symbol.asyncDispose first and fall back to Symbol.dispose when needed:

AppContainer.hooks.on('disposed', ({ queueCleanup, values }) => {
	const resource = values.resource;
	if (!resource) return;

	queueCleanup(() => resource[Symbol.asyncDispose]?.() ?? resource[Symbol.dispose]?.());
});

await using still disposes the container, but services are only cleaned up if a disposed hook queues their cleanup:

await using app = AppContainer.create();

// Use app here. Registered disposed hooks run when the scope ends.

For manual cleanup, call the container's async disposal method:

const app = AppContainer.create();
try {
	app.db;
} finally {
	await app[Symbol.asyncDispose]();
}

Inspection API

The controller is available through container.$:

app.$.has('db'); // Registered or already resolved
app.$.resolved('db'); // Resolved in this container
app.$.keys(); // Registered and resolved keys
app.$.get('db'); // Equivalent to app.db
app.$.set('db', testDb); // Equivalent to app.db = testDb

resolved can also run a callback conditionally:

app.$.resolved('db', (db) => db.close());

Hooks

Registries expose register and created hooks. Containers expose resolving, resolved, invalidated, and disposed hooks through container.$:

const registry = new ContainerRegistry<Services>('app');

registry.hooks.on('register', (event) => {
	console.log('registered', event.key, event.replace);
});

registry.hooks.on('created', (event) => {
	console.log('created with', event.params);
});

const app = registry.create();
app.$.hooks.on('resolved', (event) => {
	console.log('resolved', event.key, event.value);
});

Container lifecycle events are also forwarded to the registry hooks. A listener registered on registry.hooks receives events from every container created by that registry:

registry.hooks.on('resolved', (event) => {
	console.log('resolved in', event.container.$.name, event.key, event.value);
});

registry.hooks.on('disposed', ({ queueCleanup, values }) => {
	const resource = values.resource;
	if (resource) queueCleanup(() => resource[Symbol.asyncDispose]?.());
});

Use container hooks for events from one container, or registry hooks for cross-container observation and cleanup registration. Registry listeners run after listeners registered on the individual container.

hooks.on returns a function that removes the listener. Pass an AbortSignal as the third argument to remove a listener automatically when the signal aborts.

Errors

The package exports these error classes:

  • KeyNotFoundError: a requested service has not been registered
  • CircularDependencyError: factories depend on one another in a cycle
  • ContainerDisposedError: a disposed container is accessed or modified
  • ContainerError and ResolutionError: base classes for container and resolution errors

Ergonomic Type Organization

Keep each service's type augmentation next to its implementation. IDE navigation then leads from app.service to the service module's type and registration code:

// session.ts
declare global {
	namespace Service {
		interface AppContainer {
			session: SessionController;
		}
	}
}

export function registerSessionService() {
	AppContainer.register('session', function () {
		return new SessionController(this.db);
	});
}

Development

This project uses Bun for package scripts and tests.

bun install
bun test
bun run build
bun run lint
bun run format

License

MIT. See LICENSE.