diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..728218e --- /dev/null +++ b/manifest.json @@ -0,0 +1,6 @@ +{ + "modules": { + "@wessberg/di": ["dist/index"] + }, + "preload": ["@wessberg/di"] +} diff --git a/src/di-container.ts b/src/di-container.ts index 8cd5378..396292a 100644 --- a/src/di-container.ts +++ b/src/di-container.ts @@ -13,6 +13,8 @@ import type { GetOptions, HasOptions, ConstructInstanceOptions, + DIContainerOptions, + IDIContainerMaps, Parent } from "./type.js"; import {isClass, isCustomConstructableService} from "./util.js"; @@ -23,23 +25,25 @@ import {isClass, isCustomConstructableService} from "./util.js"; * @author Frederik Wessberg */ export class DIContainer implements IDIContainer { - get [Symbol.toStringTag]() { - return "DIContainer"; - } + readonly #containerMaps: IDIContainerMaps; /** - * A map between interface names and the services that should be dependency injected - */ - readonly #constructorArguments = new Map(); - /** - * A Map between identifying names for services and their IRegistrationRecords. + * Constructs a new dependency-injection container, optionally using custom container maps (defaults to using Map objects). + * + * @param options - Optional object with options, currently including only `customContainerMaps` to override the + * default Map-based implementation of container maps. */ - readonly #serviceRegistry = new Map>(); + constructor(options?: DIContainerOptions) { + this.#containerMaps = options?.customContainerMaps ?? { + constructorArguments: new Map(), + serviceRegistry: new Map>(), + instances: new Map() + }; + } - /** - * A map between identifying names for services and concrete instances of their implementation. - */ - readonly #instances = new Map(); + get [Symbol.toStringTag]() { + return "DIContainer"; + } /** * Registers a service that will be instantiated once in the application lifecycle. All requests @@ -112,7 +116,7 @@ export class DIContainer implements IDIContainer { if (options == null) { throw new ReferenceError(`1 argument required, but only 0 present. ${DI_COMPILER_ERROR_HINT}`); } - return this.#serviceRegistry.has(options.identifier); + return this.#containerMaps.serviceRegistry.get(options.identifier) != null; } /** @@ -124,9 +128,14 @@ export class DIContainer implements IDIContainer { // Take all of the constructor arguments for the implementation const implementationArguments = "implementation" in options && options.implementation?.[CONSTRUCTOR_ARGUMENTS_SYMBOL] != null ? options.implementation[CONSTRUCTOR_ARGUMENTS_SYMBOL] : []; - this.#constructorArguments.set(options.identifier, implementationArguments); + this.#containerMaps.constructorArguments.set(options.identifier, implementationArguments); + + // Clear cached instance of re-registered singletons + if (this.#hasInstance(options.identifier)) { + this.#setInstance(options.identifier, undefined); + } - this.#serviceRegistry.set( + this.#containerMaps.serviceRegistry.set( options.identifier, "implementation" in options && options.implementation != null ? {...options, kind} : {...options, kind, newExpression: newExpression!} ); @@ -143,7 +152,7 @@ export class DIContainer implements IDIContainer { * Gets the cached instance, if any, associated with the given identifier. */ #getInstance(identifier: string): T | null { - const instance = this.#instances.get(identifier); + const instance = this.#containerMaps.instances.get(identifier); return instance == null ? null : (instance as T); } @@ -151,14 +160,14 @@ export class DIContainer implements IDIContainer { * Gets an IRegistrationRecord associated with the given identifier. */ #getRegistrationRecord(identifier: string): RegistrationRecord | undefined { - return this.#serviceRegistry.get(identifier) as RegistrationRecord | undefined; + return this.#containerMaps.serviceRegistry.get(identifier) as RegistrationRecord | undefined; } /** * Caches the given instance so that it can be retrieved in the future. */ #setInstance(identifier: string, instance: T): T { - this.#instances.set(identifier, instance); + this.#containerMaps.instances.set(identifier, instance); return instance; } @@ -197,7 +206,7 @@ export class DIContainer implements IDIContainer { if (isClass(implementation)) { // Find the arguments for the identifier - const mappedArgs = this.#constructorArguments.get(identifier); + const mappedArgs = this.#containerMaps.constructorArguments.get(identifier); if (mappedArgs == null) { throw new InstantiationError(`Could not find constructor arguments. Have you registered it as a service?`, {identifier, parentChain}); } diff --git a/src/index.ts b/src/index.ts index 30ea186..8522bbd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ -export type {IDIContainer} from "./type.js"; +export type {IDIContainer, ConstructorArgument, IContainerMap, RegistrationRecord} from "./type.js"; export {DIContainer} from "./di-container.js"; export {CONSTRUCTOR_ARGUMENTS_SYMBOL, CONSTRUCTOR_ARGUMENTS_SYMBOL_IDENTIFIER} from "./constant.js"; diff --git a/src/type.ts b/src/type.ts index d7799cc..640db40 100644 --- a/src/type.ts +++ b/src/type.ts @@ -64,3 +64,29 @@ export interface IDIContainer { // @ts-expect-error The 'T' type parameter is required for compile-time reflection, even though it is not part of the signature. has(options?: HasOptions): boolean; } + +export interface IContainerMap { + get(key: K): V | undefined; + set(key: K, value: V): void; +} + +export interface IDIContainerMaps { + /** + * A map between interface names and the services that should be dependency injected + */ + constructorArguments: IContainerMap; + + /** + * A Map between identifying names for services and their IRegistrationRecords. + */ + serviceRegistry: IContainerMap>; + + /** + * A map between identifying names for services and concrete instances of their implementation. + */ + instances: IContainerMap; +} + +export interface DIContainerOptions { + customContainerMaps?: IDIContainerMaps; +}