Skip to content

Latest commit

 

History

History
277 lines (206 loc) · 6.55 KB

DependencyInjection.md

File metadata and controls

277 lines (206 loc) · 6.55 KB

Dependency Injection

A lightweight IoC container for managing service dependencies in your application.

Overview

The DI system provides:

  • Constructor injection
  • Property injection
  • Singleton and scoped lifetimes
  • Decorator-based registration

Quick Start

import { ContainerService, container } from '@relax.js/core/di';

@ContainerService()
class LoggerService {
    log(message: string) {
        console.log(`[LOG] ${message}`);
    }
}

// Resolve and use
const logger = container.resolve(LoggerService);
logger.log('Hello!');

Registration

Use the @ContainerService decorator to register services:

@ContainerService()
class ConfigService {
    apiUrl = '/api/v1';
}

@ContainerService({ inject: [ConfigService] })
class ApiClient {
    constructor(private config: ConfigService) {}

    fetch(path: string) {
        return fetch(this.config.apiUrl + path);
    }
}

Manual Registration

For cases where you can't use decorators (e.g. registering an existing instance):

import { serviceCollection } from '@relax.js/core/di';

// Register with existing instance
const config = { apiUrl: '/api' };
serviceCollection.register(ConfigService, {
    inject: [],
    instance: config
});

// Register with custom key
serviceCollection.register(CacheService, {
    key: 'primaryCache',
    scope: 'global',
    inject: []
});

Scopes

Global (Singleton)

Same instance returned every time:

@ContainerService({ scope: 'global' })
class DatabaseConnection {
    // ...
}

const db1 = container.resolve(DatabaseConnection);
const db2 = container.resolve(DatabaseConnection);
// db1 === db2

Closest (Scoped)

New instance for each container scope:

@ContainerService({ scope: 'closest' })
class RequestContext {
    // ...
}

Constructor Injection

Dependencies are passed to the constructor in order:

@ContainerService({
    inject: [LoggerService, ConfigService, DatabaseConnection]
})
class UserRepository {
    constructor(
        private logger: LoggerService,
        private config: ConfigService,
        private db: DatabaseConnection
    ) {}
}

Property Injection

Using the @Inject Decorator

The @Inject decorator resolves a dependency and assigns it to a class field. The service is resolved when the instance is created, so the field is available in all methods including connectedCallback for web components.

import { ContainerService, Inject } from '@relax.js/core/di';

@ContainerService({ inject: [] })
class OrderService {
    @Inject(LoggerService)
    private logger!: LoggerService;

    @Inject('primaryCache')  // resolve by string key
    private cache!: CacheService;
}

Using the properties Option

Alternatively, declare property injection in the registration options:

@ContainerService({
    inject: [],
    properties: {
        logger: LoggerService,
        cache: 'primaryCache'  // string key
    }
})
class OrderService {
    logger!: LoggerService;
    cache!: CacheService;
}

Using Services in Web Components

Web components use @Inject to access services. Do not use @ContainerService on components since the browser creates them with zero constructor arguments.

Property Injection with @Inject

import { Inject } from '@relax.js/core/di';

class UserPanel extends HTMLElement {
    @Inject(UserService)
    private userService!: UserService;

    @Inject(LoggerService)
    private logger!: LoggerService;

    connectedCallback() {
        // Injected fields are resolved and ready to use
        const user = this.userService.getCurrentUser();
        this.logger.log('UserPanel connected');
        this.render(user);
    }
}

customElements.define('user-panel', UserPanel);

This works the same way regardless of how the component is created:

// Created by application code
document.createElement('user-panel');

// Created by the browser from HTML
// <user-panel></user-panel>

Setup Order

Register all services before defining custom elements. Services decorated with @ContainerService register themselves when imported, so import those modules first.

// main.ts - application entry point

// 1. Import services (registers them via @ContainerService)
import './services/ApiClient';
import './services/UserService';

// 2. Register any manual services
serviceCollection.register(ConfigService, {
    inject: [],
    instance: { apiUrl: '/api/v1' },
});

// 3. Define custom elements (components can now resolve services)
import { UserPanel } from './components/UserPanel';
customElements.define('user-panel', UserPanel);

Resolving Dependencies

// By class
const service = container.resolve(MyService);

// By string key
const cache = container.resolve<CacheService>('primaryCache');

Error Handling

All DI errors go through the global onError handler, so you can log, display, or suppress them.

Resolution Errors

Thrown when resolve() cannot find a registered service.

Field Description
service The class name or string key that was requested
registeredTypes Array of all registered class names
registeredKeys Array of all registered string keys
RelaxError: Failed to resolve service 'MyService'
  { service: 'MyService', registeredTypes: ['LoggerService', 'ConfigService'], registeredKeys: ['primaryCache'] }

Registration Errors

Thrown during register() or registerByType() to catch configuration mistakes early.

Error Context Cause
Service name collision { service } Two different classes registered with the same class name
Service key already registered { key, existingClass, newClass } A string key is reused for a different class
Instance and inject conflict { service } Both instance and non-empty inject provided (inject is ignored)

Suppressing Errors

import { onError } from '@relax.js/core/utils';

onError((error, ctx) => {
    if (error.context.service === 'OptionalPlugin') {
        ctx.suppress();
        return;
    }
    console.error(error.message, error.context);
});

Best Practices

  1. Register early: Register all services at application startup
  2. Prefer constructor injection: More explicit than property injection
  3. Use global scope for stateless services: Like loggers, config
  4. Use closest scope for request-specific data: Like user context
  5. Avoid circular dependencies: Restructure to use events or mediators