Skip to content

adevday/scoped

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@swing/scoped

A modern & minimalist (105loc) & clean library for scoped dependency injection, designed to work seamlessly with async_hooks.

Installation

This package can be installed from JSR.

Node.js / Bun / Deno

# Using npm (with JSR CLI) npx jsr add @swing/scoped # Using pnpm pnpm add jsr:@swing/scoped # Using yarn yarn add jsr:@swing/scoped 

Quick Start

@swing/scoped provides a simple yet powerful way to manage dependencies. The core functions are provide for registering dependencies and inject for resolving them.

provide and inject

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.

Scoped Dependencies

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');

Lazy & Async Injection

@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();

Type-Safe Bindable Keys

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

Shortcut Scoped

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)

Recipes

Class-based IOC using register

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');

Hono Middleware

@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'); });

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

A modern & minimalist (less than 100loc) & clean library for scoped dependency injection, designed to work seamlessly with `async_hooks`.

Topics

Resources

License

Stars

Watchers

Forks

Contributors