@apiratorjs/di-container
Version:
A lightweight dependency injection container for JavaScript and TypeScript with powerful features: modular organization with DiModule.create, lazy initialization, automatic circular dependency detection, and multiple service lifecycles (singleton with bot
200 lines • 7.07 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DiConfigurator = void 0;
const async_context_1 = require("@apiratorjs/async-context");
const di_container_1 = require("./di-container");
const utils_1 = require("./utils");
const locking_1 = require("@apiratorjs/locking");
const errors_1 = require("./errors");
const DI_CONTAINER_REQUEST_SCOPE_NAMESPACE = "APIRATORJS_DI_CONTAINER_REQUEST_SCOPE_NAMESPACE";
class DiConfigurator {
constructor() {
this._singletonServices = new Map();
this._singletonServiceFactories = new Map();
this._requestScopeServiceFactories = new Map();
this._transientFactories = new Map();
this._serviceMutexes = new Map();
this._resolutionChains = new Map();
this._registeredModules = new Set();
}
addSingleton(token, factory, options) {
this._singletonServiceFactories.set(token, { factory, options });
return this;
}
async disposeSingletons() {
await Promise.all(Array.from(this._singletonServices.values()).map(async (service) => {
if (service?.onDispose) {
await service.onDispose();
}
}));
this._singletonServices.clear();
}
addScoped(token, factory) {
this._requestScopeServiceFactories.set(token, { factory });
return this;
}
addTransient(token, factory) {
this._transientFactories.set(token, { factory });
return this;
}
addModule(module) {
if (this._registeredModules.has(module)) {
return this;
}
this._registeredModules.add(module);
module.register(this);
return this;
}
async resolve(token) {
this.checkForCircularDependency(token);
const mutex = this.getMutexFor(token);
return await mutex.runExclusive(async () => {
try {
this.addToResolutionChain(token);
return ((await this.tryGetSingleton(token)) ??
(await this.tryGetScoped(token)) ??
(await this.tryGetTransient(token)) ??
(function () {
throw new errors_1.UnregisteredDependencyError(token);
})());
}
finally {
this.removeFromResolutionChain(token);
}
});
}
async runWithNewRequestScope(initialStore, callback) {
return await async_context_1.AsyncContext.withContext(DI_CONTAINER_REQUEST_SCOPE_NAMESPACE, initialStore ?? new async_context_1.AsyncContextStore(), async () => {
try {
return await callback();
}
finally {
await this.disposeScopedServices();
}
});
}
getRequestScopeContext() {
return async_context_1.AsyncContext.getContext(DI_CONTAINER_REQUEST_SCOPE_NAMESPACE);
}
isInRequestScopeContext() {
return !!this.getRequestScopeContext();
}
async build() {
for (const [token, { options }] of this._singletonServiceFactories.entries()) {
if (options?.isLazy) {
continue;
}
await this.tryGetSingleton(token);
}
return new di_container_1.DiContainer(this);
}
// ============================
// Private methods
// ============================
/**
* Lazy initializes and returns a singleton service.
*/
async tryGetSingleton(token) {
if (this._singletonServices.has(token)) {
return this._singletonServices.get(token);
}
const { factory } = this._singletonServiceFactories.get(token) ?? {};
if (!factory) {
return;
}
// Check if service was already initialized by another thread
if (this._singletonServices.has(token)) {
return this._singletonServices.get(token);
}
const service = await factory(this);
if (service?.onConstruct) {
await service.onConstruct();
}
this._singletonServices.set(token, service);
return service;
}
async tryGetScoped(token) {
if (!this._requestScopeServiceFactories.has(token)) {
return;
}
const store = this.getRequestScopeContext();
if (!store) {
throw new errors_1.RequestScopeResolutionError(token);
}
if (store.has(token)) {
return store.get(token);
}
// Check if service was already initialized by another thread
if (store.has(token)) {
return store.get(token);
}
const { factory } = this._requestScopeServiceFactories.get(token) ?? {};
if (!factory) {
return;
}
const service = await factory(this);
if (service?.onConstruct) {
await service.onConstruct();
}
store.set(token, service);
return service;
}
async tryGetTransient(token) {
if (!this._transientFactories.has(token)) {
return;
}
const { factory } = this._transientFactories.get(token) ?? {};
if (!factory) {
return;
}
const service = await factory(this);
if (service?.onConstruct) {
await service.onConstruct();
}
return service;
}
async disposeScopedServices() {
const scope = this.getRequestScopeContext();
if (!scope) {
return;
}
for (const service of scope.values()) {
if (service?.onDispose) {
await service.onDispose();
}
}
}
getMutexFor(token) {
if (!this._serviceMutexes.has(token)) {
this._serviceMutexes.set(token, new locking_1.Mutex());
}
return this._serviceMutexes.get(token);
}
// ============================
// Circular dependency detection
// ============================
getCurrentResolutionChain() {
const currentScope = this.getRequestScopeContext();
if (!this._resolutionChains.has(currentScope)) {
this._resolutionChains.set(currentScope, new Set());
}
return this._resolutionChains.get(currentScope);
}
addToResolutionChain(token) {
this.getCurrentResolutionChain().add(token);
}
removeFromResolutionChain(token) {
this.getCurrentResolutionChain().delete(token);
}
checkForCircularDependency(token) {
const currentChain = this.getCurrentResolutionChain();
if (currentChain.has(token)) {
const chainTokens = Array.from(currentChain);
const startIndex = chainTokens.findIndex((t) => t === token);
const cycle = [...chainTokens.slice(startIndex), token].map((t) => (0, utils_1.tokenToString)(t));
throw new errors_1.CircularDependencyError(token, cycle);
}
}
}
exports.DiConfigurator = DiConfigurator;
//# sourceMappingURL=di-configurator.js.map