diff --git a/src/container/container.ts b/src/container/container.ts index dddd68d..df6780e 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -3,7 +3,19 @@ import type { ContainerIdentifier, Metadata, ServiceIdentifier } from '../types' import { EMPTY_VALUE } from '../types'; import { ContainerRegistry } from './registry'; +/** + * Resolves services and stores container-local bindings. + * + * Containers let you resolve registered services, override values for the + * current context, and isolate container-scoped services by container. + */ export class Container { + /** + * The identifier of this container. + * + * The default container uses `'default'`, and named containers use the + * identifier they were created with. + */ public readonly id: ContainerIdentifier; private metadataMap: Map = new Map(); @@ -15,6 +27,15 @@ export class Container { this.id = id; } + /** + * Returns the default container or a named container. + * + * Calling this method with the same identifier always returns the same + * container instance. + * + * @param id The container identifier. Omit this to use the default container. + * @returns The matching container instance. + */ public static of(id: ContainerIdentifier = 'default') { if (id === 'default') { return ContainerRegistry.defaultContainer; @@ -31,6 +52,16 @@ export class Container { return container; } + /** + * Registers service metadata in this container. + * + * This is a low-level API for manual registration when you are not using + * `@Service()`. Registering a `singleton` on a named container stores it in + * the default container so it can be shared across containers. + * + * @param metadata The service metadata to register. + * @returns The current container. + */ public register(metadata: Metadata) { if (metadata.scope === 'singleton' && !this.isDefault()) { ContainerRegistry.defaultContainer.register(metadata); @@ -53,21 +84,55 @@ export class Container { return this; } + /** + * Returns whether this container has a local registration for an identifier. + * + * This only checks registrations stored on the current container and does + * not indicate whether the identifier can be resolved through fallback. + * + * @param id The service identifier to check. + * @returns `true` when the current container has a local registration. + */ public has(id: ServiceIdentifier): boolean { return this.metadataMap.has(id); } + /** + * Binds a concrete value to this container. + * + * Bound values are returned as-is when the same identifier is requested from + * this container. + * + * @param id The service identifier to bind. + * @param value The value to return for that identifier. + * @returns The current container. + */ public set(id: ServiceIdentifier, value: T) { this.bindingMap.set(id, value); return this; } + /** + * Removes a local binding or registration from this container. + * + * @param id The service identifier to remove. + * @returns The current container. + */ public remove(id: ServiceIdentifier) { this.bindingMap.delete(id); this.metadataMap.delete(id); return this; } + /** + * Resets services stored in this container. + * + * Use `'value'` to clear cached instances while keeping registrations, or + * `'service'` to remove local registrations and bindings entirely. + * + * @param strategy The reset strategy to apply. + * @returns The current container. + */ public reset(strategy: 'value' | 'service' = 'value') { if (strategy === 'value') { this.metadataMap.forEach((metadata) => { @@ -83,6 +148,19 @@ export class Container { } } + /** + * Resolves a service from this container. + * + * If the current container does not have a local registration, named + * containers can continue resolution through the default container according + * to the service scope. Services are instantiated with `new Class()` and do + * not support constructor arguments. + * + * @param id The service identifier to resolve. + * @returns The resolved service instance or bound value. + * @throws {ServiceNotFoundError} If no service is registered for the identifier. + * @throws {CircularDependencyError} If the dependency graph contains a cycle. + */ public get(id: ServiceIdentifier): T { if (this.bindingMap.has(id)) { return this.bindingMap.get(id) as T; @@ -155,6 +233,12 @@ export class Container { return this === ContainerRegistry.defaultContainer; } + /** + * Resets a container by its identifier. + * + * @param containerId The container to reset. + * @param options Reset options. + */ public static reset(containerId: ContainerIdentifier, options?: { strategy?: 'value' | 'service' }) { const container = ContainerRegistry.getContainer(containerId); diff --git a/src/decorators/inject.ts b/src/decorators/inject.ts index 56cd51c..d418ead 100644 --- a/src/decorators/inject.ts +++ b/src/decorators/inject.ts @@ -1,5 +1,16 @@ import { INJECTION_KEY, type InjectionMetadata, type ServiceIdentifier } from '../types'; +/** + * Injects a dependency into a class field when the service is resolved. + * + * Use this field decorator on service properties that should be resolved from + * the container that creates the service instance. + * + * The dependency is assigned after the instance is created, so it is not + * available in constructors or field initializers. + * + * @param dependency The service identifier to inject into the decorated field. + */ export function Inject(dependency: ServiceIdentifier) { return function (_: undefined, context: ClassFieldDecoratorContext) { const injections = (context.metadata[INJECTION_KEY] ??= []) as InjectionMetadata[]; diff --git a/src/decorators/service.ts b/src/decorators/service.ts index c3c62a1..be439f0 100644 --- a/src/decorators/service.ts +++ b/src/decorators/service.ts @@ -14,6 +14,17 @@ function normalizeArguments(args?: ServiceIdentifier | ServiceOption): ServiceOp return { id: args }; } +/** + * Registers a class as a service that can be resolved from a container. + * + * By default, the decorated class is registered in the default container, + * uses its own class as the service identifier, and uses the `container` + * scope. + * + * Use this decorator with a custom identifier or `scope` option when you need + * to change how the service is registered or reused. When you provide a + * custom identifier, resolve the service by that identifier. + */ export function Service(): Function; export function Service(id: ServiceIdentifier): Function; export function Service(options?: ServiceOption): Function; diff --git a/src/tokens/token.ts b/src/tokens/token.ts index 4b5c666..809605a 100644 --- a/src/tokens/token.ts +++ b/src/tokens/token.ts @@ -1,11 +1,33 @@ +/** + * Represents a typed service identifier. + * + * Tokens are useful when a dependency should be resolved by an explicit key + * instead of by its class constructor. + */ // oxlint-disable-next-line no-unused-vars export class Token { + /** + * A human-readable name for this token. + * + * This value is useful for debugging and display, but token resolution is + * based on the token instance itself. + */ public name?: string; + /** + * Creates a token with an optional display name. + * + * @param name A human-readable name used for debugging and display. + */ constructor(name?: string) { this.name = name; } + /** + * Returns a readable string representation of this token. + * + * @returns A display string for this token. + */ public toString() { return this.name ? `Token(${this.name})` : `Token()`; } diff --git a/src/types/constructable.ts b/src/types/constructable.ts index e0cd8fe..8210062 100644 --- a/src/types/constructable.ts +++ b/src/types/constructable.ts @@ -1,3 +1,9 @@ +/** + * A class constructor that can be instantiated with `new`. + */ export type Constructable = new (...args: Args) => T; +/** + * An abstract class constructor used for typing class-based service identifiers. + */ export type AbstractConstructable = abstract new (...args: Args) => T; diff --git a/src/types/container.ts b/src/types/container.ts index 8d2f98a..53bd3cb 100644 --- a/src/types/container.ts +++ b/src/types/container.ts @@ -2,15 +2,49 @@ import type { Constructable } from './constructable'; import type { InjectionMetadata } from './injection'; import type { ServiceIdentifier, ServiceScope } from './service'; +/** + * A container identifier used to select the default container or a named container. + */ export type ContainerIdentifier = string | symbol; +/** + * Sentinel value used internally to mark services that have not been instantiated yet. + */ export const EMPTY_VALUE = Symbol.for('EMPTY_VALUE'); +/** + * Service registration metadata stored by a container. + * + * This is primarily used by low-level registration APIs and internal container logic. + */ export interface Metadata { + /** + * The identifier used to resolve the service. + */ id: ServiceIdentifier; + + /** + * The class instantiated for this service. + */ Class: Constructable; + + /** + * The original class or member name, when available. + */ name?: string | symbol; + + /** + * Property injection definitions collected for the service. + */ injections: InjectionMetadata[]; + + /** + * The lifetime used when resolving the service. + */ scope: ServiceScope; + + /** + * The cached service instance, or `EMPTY_VALUE` when no instance is cached. + */ value: T | typeof EMPTY_VALUE; } diff --git a/src/types/injection.ts b/src/types/injection.ts index 8307023..e850a2b 100644 --- a/src/types/injection.ts +++ b/src/types/injection.ts @@ -1,8 +1,21 @@ import type { ServiceIdentifier } from './service'; +/** + * Property injection metadata collected for a service. + */ export interface InjectionMetadata { + /** + * The identifier of the dependency to inject. + */ id: ServiceIdentifier; + + /** + * The class field that receives the resolved dependency. + */ name: string | symbol; } +/** + * Metadata key used internally to store property injection definitions. + */ export const INJECTION_KEY = Symbol.for('navi-di:injections'); diff --git a/src/types/service.ts b/src/types/service.ts index ed11cfe..5ea759c 100644 --- a/src/types/service.ts +++ b/src/types/service.ts @@ -1,6 +1,9 @@ import type { Token } from '../tokens'; import type { AbstractConstructable, Constructable } from './constructable'; +/** + * An identifier that can be used to register or resolve a service. + */ export type ServiceIdentifier = | Constructable | AbstractConstructable @@ -8,9 +11,22 @@ export type ServiceIdentifier = | string | Token; +/** + * The lifetime used when a service is resolved from a container. + */ export type ServiceScope = 'singleton' | 'container' | 'transient'; +/** + * Options for registering a service with `@Service()`. + */ export interface ServiceOption { + /** + * A custom identifier used to resolve the service. + */ id?: ServiceIdentifier; + + /** + * The lifetime used when the service is resolved. + */ scope?: ServiceScope; }