A modern & minimalist (105loc) & clean library for scoped dependency injection, designed to work seamlessly with async_hooks
.
This package can be installed from JSR.
# Using npm (with JSR CLI)
npx jsr add @swing/scoped
# Using pnpm
pnpm add jsr:@swing/scoped
# Using yarn
yarn add jsr:@swing/scoped
@swing/scoped
provides a simple yet powerful way to manage dependencies. The core functions are provide
for registering dependencies and inject
for resolving them.
import { provide, inject= } from '@swing/scoped';
// Define a service
class MyService {
greet() {
return 'Hello from MyService!';
}
}
// Provide the service
provide(new MyService());
// Inject and use the service
const service = inject(MyService);
console.log(service.greet()); // Output: Hello from MyService!
// You can also provide simple values
provide('appName', 'My Awesome App');
const appName = inject('appName');
console.log(appName); // Output: My Awesome App
Enables type-safe dependency injection for classes and asynchronous factory functions. For detailed usage, refer to provide
.
The enter
function allows you to create a new scope for dependencies. This is particularly useful for managing dependencies that are specific to a request, a user session, or any other transient context. Dependencies provided within an enter
block are only available within that scope and its nested scopes.
import { provide, inject, enter } from '@swing/scoped';
class RequestContext {
constructor(public requestId: string) {}
}
// Global provision (available everywhere)
provide('globalData', 'This is global');
async function handleRequest(requestId: string) {
await enter(async () => {
// Provide a dependency specific to this request scope
provide(new RequestContext(requestId));
const context = inject(RequestContext);
const globalData = inject('globalData');
console.log(`Request ${context.requestId}: Global Data - ${globalData}`);
await enter(async () => {
// Nested scope can access parent scope's dependencies
const nestedContext = inject(RequestContext);
console.log(` Nested Scope for Request ${nestedContext.requestId}`);
});
});
}
handleRequest('req-123');
handleRequest('req-456');
@swing/scoped
supports lazy
and asynchronous dependency resolution, which is crucial for performance and handling dependencies that require async initialization.
import { provide, inject, lazy } from '@swing/scoped';
class AsyncService {
private data: string | null = null;
async init() {
return new Promise(resolve => {
setTimeout(() => {
this.data = 'Data loaded asynchronously!';
resolve(null);
}, 100);
});
}
getData() {
return this.data;
}
}
// Provide an async service
provide(AsyncService, lazy(async () => {
const service = new AsyncService();
await service.init();
return service;
}));
async function runApp() {
console.log('Before async service injection...');
const service = await inject(AsyncService); // Await the async injection
console.log(service.getData());
console.log('After async service injection.');
}
runApp();
The bindable
function allows you to create unique, type-safe keys for your dependencies. This ensures that when you provide
or inject
a value using such a key, TypeScript can enforce the correct type, preventing common type-related errors at compile time.
import { provide, inject, bindable } from '@swing/scoped';
// Define a bindable key with a specific type
const UserId = bindable<string>('userId');
// This would cause a TypeError at compile time:
// provide(UserId, 123); // ❌ Compile-time error: Argument of type 'number' is not assignable to parameter of type 'string'.
provide(UserId, "12345"); // ✅ Correct type
const userId = inject(UserId); // userId is inferred as string
console.log(userId); // Output: 12345
The Scoped
object provides a convenient shortcut for IDE auto-import and access to core functions like inject
(aliased as of
), injectAsync
(aliased as async
), provide
(aliased as bind
), and enter
. This allows for a more concise and fluent API when working with dependencies.
import { Scoped } from '@swing/scoped';
// Using Scoped.bind (alias for provide)
Scoped.bind('configValue', { theme: 'dark', language: 'en' });
// Using Scoped.of (alias for inject)
const config = Scoped.of('configValue');
console.log(config.theme); // Output: dark
class UserService {
config = Scoped.of('configValue')
getUserName() {
return 'John Doe';
}
}
// Using Scoped.bind with a class
Scoped.bind(new UserService());
// Using Scoped.of with a class
const userService = Scoped.of(UserService);
console.log(userService.getUserName()); // Output: John Doe
// Using Scoped.async (alias for injectAsync)
Scoped.bind('myAsyncValue', Scoped.lazy(() => Promise.resolve('async_data')));
async function getAsyncData() {
const data = await Scoped.async('myAsyncValue');
console.log(data); // Output: async_data
}
getAsyncData();
// Using Scoped.enter (alias for enter)
let enteredValue: number | undefined;
Scoped.enter(() => {
Scoped.bind('scopedValue', 456);
enteredValue = Scoped.of('scopedValue');
});
console.log(enteredValue); // Output: 456
console.log(Scoped.of('scopedValue')); // Output: undefined (Not visible outside the scope)
For more structured dependency registration, especially with classes, you can use the register
function. This is useful when you want to define how a class should be instantiated and then inject it by its class type.
import { provide, inject, register } from '@swing/scoped';
class Logger {
log(message: string): void {
console.log(`[ConsoleLogger] ${message}`);
}
}
class DatabaseService {
logger = inject(Logger)
constructor() {}
saveData(data: string) {
this.logger.log(`Saving data: ${data}`);
}
}
// Register DatabaseService and Logger
register(Logger, DatabaseService);
// Inject DatabaseService, and it will automatically get Logger
const dbService = inject(DatabaseService);
dbService.saveData('Important record');
@swing/scoped
can be easily integrated with web frameworks like Hono to manage request-scoped dependencies.
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { provide, inject, enter } from '@swing/scoped';
class RequestIdService {
constructor(public id: string) {}
}
const app = new Hono();
// Hono middleware for scoped dependency injection
app.use('*', async (c, next) => {
const requestId = Math.random().toString(36).substring(2, 9);
await enter(async () => {
provide(RequestIdService, () => new RequestIdService(requestId));
await next();
});
});
app.get('/', (c) => {
const requestIdService = inject(RequestIdService);
return c.text(`Hello from Hono! Request ID: ${requestIdService.id}`);
});
serve(app, () => {
console.log('Server listening on http://localhost:3000');
});
This project is licensed under the MIT License - see the LICENSE file for details.