Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/container/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ServiceIdentifier, Metadata> = new Map();
Expand All @@ -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;
Expand All @@ -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<T>(metadata: Metadata<T>) {
if (metadata.scope === 'singleton' && !this.isDefault()) {
ContainerRegistry.defaultContainer.register(metadata);
Expand All @@ -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<T>(id: ServiceIdentifier<T>, 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) => {
Expand All @@ -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<T>(id: ServiceIdentifier<T>): T {
if (this.bindingMap.has(id)) {
return this.bindingMap.get(id) as T;
Expand Down Expand Up @@ -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);

Expand Down
11 changes: 11 additions & 0 deletions src/decorators/inject.ts
Original file line number Diff line number Diff line change
@@ -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<T>(dependency: ServiceIdentifier<T>) {
return function (_: undefined, context: ClassFieldDecoratorContext) {
const injections = (context.metadata[INJECTION_KEY] ??= []) as InjectionMetadata[];
Expand Down
11 changes: 11 additions & 0 deletions src/decorators/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
22 changes: 22 additions & 0 deletions src/tokens/token.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
/**
* 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()`;
}
Expand Down
6 changes: 6 additions & 0 deletions src/types/constructable.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* A class constructor that can be instantiated with `new`.
*/
export type Constructable<T = object, Args extends unknown[] = never[]> = new (...args: Args) => T;

/**
* An abstract class constructor used for typing class-based service identifiers.
*/
export type AbstractConstructable<T = object, Args extends unknown[] = never[]> = abstract new (...args: Args) => T;
34 changes: 34 additions & 0 deletions src/types/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T = unknown> {
/**
* The identifier used to resolve the service.
*/
id: ServiceIdentifier;

/**
* The class instantiated for this service.
*/
Class: Constructable<T>;

/**
* 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;
}
13 changes: 13 additions & 0 deletions src/types/injection.ts
Original file line number Diff line number Diff line change
@@ -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');
16 changes: 16 additions & 0 deletions src/types/service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
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<T = unknown, Args extends unknown[] = never[]> =
| Constructable<T, Args>
| AbstractConstructable<T, Args>
| CallableFunction
| string
| Token<T>;

/**
* 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;
}
Loading