Skip to content
/ scoped Public

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

License

Notifications You must be signed in to change notification settings

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