diff --git a/src/decorators.ts b/src/decorators.ts index 0ab2b7d..56c0c64 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -1,8 +1,33 @@ import type { ServiceIdentifier } from './types.js'; +/** + * Metadata key for storing parameter type information for dependency injection + * @internal + */ export const DI_PARAM_TYPES = 'di:paramtypes'; + +/** + * Metadata key for marking classes as injectable + * @internal + */ export const DI_INJECTABLE = 'di:injectable'; +/** + * Decorator for constructor parameters that specifies which service identifier to use for injection + * + * @template T - The type of the service to be injected + * @param serviceType - The service identifier for the dependency to inject + * @returns A parameter decorator function that associates the parameter with the service identifier + * + * @example + * ```typescript + * class MyService { + * constructor( + * @inject(loggerIdentifier) private logger: ILogger + * ) {} + * } + * ``` + */ export function inject(serviceType: ServiceIdentifier): ParameterDecorator { // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types return (target: Object, _: string | symbol | undefined, parameterIndex: number) => { @@ -10,6 +35,19 @@ export function inject(serviceType: ServiceIdentifier): ParameterDecorator }; } +/** + * Decorator that marks a class as injectable, allowing the container to create instances with dependencies + * + * @returns A class decorator function that marks the class as injectable + * + * @example + * ```typescript + * @injectable() + * class MyService { + * constructor() {} + * } + * ``` + */ export function injectable(): ClassDecorator { // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type return (target: Function) => { diff --git a/src/serviceCollection.ts b/src/serviceCollection.ts index a841a45..1753ed0 100644 --- a/src/serviceCollection.ts +++ b/src/serviceCollection.ts @@ -9,10 +9,38 @@ import type { ServiceLifetime, } from './types.js'; +/** + * A collection of service descriptors that can be used to build a service provider. + * + * This class is used to register services with different lifetimes and build a service provider + * that can resolve those services at runtime. + * + * @implements {IServiceCollection} + */ export class ServiceCollection implements IServiceCollection { + /** + * Internal map of service descriptors, keyed by service identifiers + */ private readonly _descriptors: Map, ServiceDescriptor> = new Map(); + /** + * Registers a singleton service with the collection. + * + * Singleton services are created once and shared by all consumers. + * + * @template T - The type of the service to register + * @param serviceType - The service identifier + * @param implementationOrFactory - The implementation class or factory function + * @returns The service collection instance for method chaining + * + * @example + * ```typescript + * services.addSingleton(userServiceIdentifier, UserService); + * // or with a factory: + * services.addSingleton(userServiceIdentifier, (provider) => new UserService(provider.getService(loggerIdentifier))); + * ``` + */ public addSingleton( serviceType: ServiceIdentifier, implementationOrFactory: ServiceFactory | Constructor, @@ -20,6 +48,24 @@ export class ServiceCollection implements IServiceCollection { return this.add(serviceType, implementationOrFactory, 'singleton'); } + /** + * Registers a scoped service with the collection. + * + * Scoped services are created once per scope. This is useful for services that should be + * shared within a request but not across requests. + * + * @template T - The type of the service to register + * @param serviceType - The service identifier + * @param implementationOrFactory - The implementation class or factory function + * @returns The service collection instance for method chaining + * + * @example + * ```typescript + * services.addScoped(userServiceIdentifier, UserService); + * // or with a factory: + * services.addScoped(userServiceIdentifier, (provider) => new UserService(provider.getService(loggerIdentifier))); + * ``` + */ public addScoped( serviceType: ServiceIdentifier, implementationOrFactory: ServiceFactory | Constructor, @@ -27,6 +73,23 @@ export class ServiceCollection implements IServiceCollection { return this.add(serviceType, implementationOrFactory, 'scoped'); } + /** + * Registers a transient service with the collection. + * + * Transient services are created each time they are requested. + * + * @template T - The type of the service to register + * @param serviceType - The service identifier + * @param implementationOrFactory - The implementation class or factory function + * @returns The service collection instance for method chaining + * + * @example + * ```typescript + * services.addTransient(userServiceIdentifier, UserService); + * // or with a factory: + * services.addTransient(userServiceIdentifier, (provider) => new UserService(provider.getService(loggerIdentifier))); + * ``` + */ public addTransient( serviceType: ServiceIdentifier, implementationOrFactory: ServiceFactory | Constructor, @@ -34,10 +97,24 @@ export class ServiceCollection implements IServiceCollection { return this.add(serviceType, implementationOrFactory, 'transient'); } + /** + * Builds a service provider from the registered services. + * + * @returns A new service provider that can resolve the registered services + */ public build(): IServiceProvider { return new ServiceProvider(this._descriptors); } + /** + * Internal method to add a service descriptor to the collection. + * + * @template T - The type of the service to register + * @param serviceType - The service identifier + * @param implementationOrFactory - The implementation class or factory function + * @param lifetime - The service lifetime + * @returns The service collection instance for method chaining + */ private add( serviceType: ServiceIdentifier, implementationOrFactory: ServiceFactory | Constructor, @@ -65,6 +142,12 @@ export class ServiceCollection implements IServiceCollection { return this; } + /** + * Checks if a function is a constructor + * + * @param func - The function to check + * @returns True if the function is a constructor, false otherwise + */ private isConstructor(func: unknown): func is Constructor { return typeof func === 'function' && !!func.prototype && diff --git a/src/serviceProvider.ts b/src/serviceProvider.ts index 07fad58..a6585b0 100644 --- a/src/serviceProvider.ts +++ b/src/serviceProvider.ts @@ -7,23 +7,66 @@ import type { Constructor, } from './types.js'; +/** + * Represents a scope for scoped services. + * + * A service scope provides access to scoped services that are created once per scope. + * + * @implements {IServiceScope} + */ export class ServiceScope implements IServiceScope { + /** + * The service provider associated with this scope + */ public readonly serviceProvider: IServiceProvider; + /** + * Creates a new service scope + * + * @param serviceProvider - The service provider for this scope + */ constructor(serviceProvider: IServiceProvider) { this.serviceProvider = serviceProvider; } + /** + * Disposes the scope and clears any scoped service instances + */ dispose(): void { this.serviceProvider.dispose(); } } +/** + * A provider that can resolve registered services by their service identifier. + * + * The service provider is responsible for creating and managing service instances + * according to their registered lifetime. + * + * @implements {IServiceProvider} + */ export class ServiceProvider implements IServiceProvider { + /** + * Map of service descriptors by service identifier + */ private readonly _descriptors: Map, ServiceDescriptor>; + + /** + * Map of singleton service instances by service identifier + */ private readonly _singletonInstances: Map, unknown> = new Map(); + + /** + * Map of scoped service instances by service identifier + */ private readonly _scopedInstances: Map, unknown> = new Map(); + /** + * Creates a new service provider + * + * @param descriptors - Map of service descriptors + * @param parent - Optional parent service provider to inherit singleton instances from + */ constructor( descriptors: Map, ServiceDescriptor>, parent?: ServiceProvider, @@ -36,6 +79,7 @@ export class ServiceProvider implements IServiceProvider { }); } + // Pre-resolve singleton services for (const descriptor of descriptors.values()) { if (descriptor.lifetime === 'singleton') { this.resolveService(descriptor); @@ -43,6 +87,19 @@ export class ServiceProvider implements IServiceProvider { } } + /** + * Gets a service instance by its service identifier + * + * @template T - The type of the service to resolve + * @param serviceType - The service identifier of the service to resolve + * @returns The resolved service instance + * @throws Error if the service is not registered + * + * @example + * ```typescript + * const userService = serviceProvider.getService(userServiceIdentifier); + * ``` + */ getService(serviceType: ServiceIdentifier): T { const descriptor = this._descriptors.get(serviceType); @@ -53,15 +110,39 @@ export class ServiceProvider implements IServiceProvider { return this.resolveService(descriptor as ServiceDescriptor); } + /** + * Creates a new scope for scoped services + * + * @returns A new service scope + * + * @example + * ```typescript + * const scope = serviceProvider.createScope(); + * const scopedService = scope.serviceProvider.getService(serviceIdentifier); + * // ... use scoped service + * scope.dispose(); + * ``` + */ createScope(): IServiceScope { const scopedProvider = new ServiceProvider(this._descriptors, this); return new ServiceScope(scopedProvider); } + /** + * Disposes the service provider and clears any scoped service instances + */ dispose(): void { this._scopedInstances.clear(); } + /** + * Resolves a service instance from its descriptor + * + * @template T - The type of the service to resolve + * @param descriptor - The service descriptor + * @returns The resolved service instance + * @throws Error if the service lifetime is unknown + */ private resolveService(descriptor: ServiceDescriptor): T { const { serviceType, implementationType, lifetime, factory } = descriptor; @@ -89,6 +170,13 @@ export class ServiceProvider implements IServiceProvider { } } + /** + * Creates an instance of a service class and resolves its dependencies + * + * @template T - The type of the service to create + * @param ctor - The constructor of the service class + * @returns A new instance of the service class with its dependencies resolved + */ private createInstance(ctor: Constructor): T { const paramTypes = Reflect.getMetadata('design:paramtypes', ctor) ?? []; diff --git a/src/types.ts b/src/types.ts index 527d44c..b9279c5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,40 +1,189 @@ +/** + * Represents the possible lifetimes for registered services + * + * - singleton: Created once and shared by all consumers + * - scoped: Created once per scope + * - transient: Created each time they are requested + */ export type ServiceLifetime = 'singleton' | 'scoped' | 'transient'; +/** + * Represents a constructor function for a class + * + * @template T - The type of object the constructor creates + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Constructor = new (...args: any[]) => T; +/** + * A unique identifier for a service type + * + * @template T - The type of the service + */ export type ServiceIdentifier = symbol & { __brand: T }; +/** + * A factory function that creates a service instance + * + * @template T - The type of the service to create + */ export type ServiceFactory = (provider: IServiceProvider) => T; +/** + * Descriptor for a registered service + * + * @template T - The type of the service + */ export type ServiceDescriptor = { + /** + * The service identifier + */ serviceType: ServiceIdentifier; + + /** + * The implementation class constructor + */ implementationType: Constructor; + + /** + * The service lifetime + */ lifetime: ServiceLifetime; + + /** + * Optional factory function to create the service instance + */ factory?: ServiceFactory; }; +/** + * Creates a typed service identifier + * + * @template T - The type of the service + * @returns A unique identifier for the service type + * + * @example + * ```typescript + * interface IUserService { + * getUserById(id: string): Promise; + * } + * + * const userServiceIdentifier = createServiceIdentifier(); + * ``` + */ export function createServiceIdentifier(): ServiceIdentifier { return Symbol() as ServiceIdentifier; } +/** + * Interface for a collection of service descriptors that can be used to build a service provider + */ export interface IServiceCollection { + /** + * Registers a singleton service with the collection + * + * @template T - The type of the service + * @param serviceType - The service identifier + * @param implementationType - The implementation class + * @returns The service collection for method chaining + */ addSingleton(serviceType: ServiceIdentifier, implementationType: Constructor): IServiceCollection; + + /** + * Registers a singleton service with a factory function + * + * @template T - The type of the service + * @param serviceType - The service identifier + * @param factory - A factory function that creates the service instance + * @returns The service collection for method chaining + */ addSingleton(serviceType: ServiceIdentifier, factory: ServiceFactory): IServiceCollection; + + /** + * Registers a scoped service with the collection + * + * @template T - The type of the service + * @param serviceType - The service identifier + * @param implementationType - The implementation class + * @returns The service collection for method chaining + */ addScoped(serviceType: ServiceIdentifier, implementationType: Constructor): IServiceCollection; + + /** + * Registers a scoped service with a factory function + * + * @template T - The type of the service + * @param serviceType - The service identifier + * @param factory - A factory function that creates the service instance + * @returns The service collection for method chaining + */ addScoped(serviceType: ServiceIdentifier, factory: ServiceFactory): IServiceCollection; + + /** + * Registers a transient service with the collection + * + * @template T - The type of the service + * @param serviceType - The service identifier + * @param implementationType - The implementation class + * @returns The service collection for method chaining + */ addTransient(serviceType: ServiceIdentifier, implementationType: Constructor): IServiceCollection; + + /** + * Registers a transient service with a factory function + * + * @template T - The type of the service + * @param serviceType - The service identifier + * @param factory - A factory function that creates the service instance + * @returns The service collection for method chaining + */ addTransient(serviceType: ServiceIdentifier, factory: ServiceFactory): IServiceCollection; + + /** + * Builds a service provider from the registered services + * + * @returns A new service provider instance + */ build(): IServiceProvider; } +/** + * Interface for a provider that can resolve services by their service identifier + */ export interface IServiceProvider { + /** + * Gets a service instance by its service identifier + * + * @template T - The type of the service to resolve + * @param serviceType - The service identifier + * @returns The resolved service instance + */ getService(serviceType: ServiceIdentifier): T; + + /** + * Creates a new scope for scoped services + * + * @returns A new service scope + */ createScope(): IServiceScope; + + /** + * Disposes the service provider and clears any scoped service instances + */ dispose(): void; } +/** + * Interface for a scope that provides access to scoped services + */ export interface IServiceScope { + /** + * The service provider for this scope + */ serviceProvider: IServiceProvider; + + /** + * Disposes the scope and clears any scoped service instances + */ dispose(): void; }