@elsikora/cladi
Version:
Zero-dependency TypeScript DI toolkit with typed tokens and scoped lifecycles.
631 lines (628 loc) • 30.7 kB
JavaScript
import { EDiContainerCaptiveDependencyPolicy } from '../../../domain/enum/captive-dependency-policy.enum.js';
import { EDependencyLifecycle } from '../../../domain/enum/dependency-lifecycle.enum.js';
import { EDiContainerDuplicateProviderPolicy } from '../../../domain/enum/di-container-duplicate-provider-policy.enum.js';
import '../../../domain/enum/logger-log-level.enum.js';
import '../../../domain/enum/provider-type.enum.js';
import { BaseError } from '../base/error.class.js';
import { CacheCoordinator } from './cache/coordinator.class.js';
import { DisposalCoordinator } from './disposal/coordinator.class.js';
import { DisposalManager } from './disposal/manager.class.js';
import { DisposerCoordinator } from './disposer/coordinator.class.js';
import { ResolutionEngine } from './engine/resolution.class.js';
import { DependencyGraphCoordinator } from './graph/coordinator.class.js';
import { ResolveInterceptorDispatcher } from './interceptor/dispatcher.class.js';
import { LookupCoordinator } from './lookup/coordinator.class.js';
import { RegistrationCoordinator } from './registration/coordinator.class.js';
import { ResolutionCoordinator } from './resolution/coordinator.class.js';
import { ConsoleLoggerService } from '../../service/console-logger.service.js';
import { toError } from '../../utility/to-error.utility.js';
/**
* Advanced DI container with scoped and async resolution support.
*/
class DIContainer {
id;
get isLocked() {
return this.isScopeLocked;
}
ASYNC_RESOLUTION_DRAIN_TIMEOUT_MS;
CACHE_COORDINATOR;
CAPTIVE_DEPENDENCY_POLICY;
CHILD_SCOPES;
DEPENDENCY_GRAPH_COORDINATOR;
DISPOSAL_COORDINATOR;
DISPOSAL_MANAGER;
disposePromise;
DISPOSER_COORDINATOR;
DISPOSERS;
DISPOSERS_BY_TOKEN;
DUPLICATE_PROVIDER_POLICY;
isDisposed;
isDisposing;
isScopeLocked;
LOGGER;
LOOKUP_COORDINATOR;
PARENT;
PENDING_PROVIDER_CLEANUPS;
PROVIDERS;
REGISTRATION_COORDINATOR;
RESOLUTION_COORDINATOR;
RESOLUTION_ENGINE;
RESOLVE_INTERCEPTOR_DISPATCHER;
RESOLVE_INTERCEPTORS;
ROOT;
ROOT_SCOPE_COUNTER;
ROOT_SINGLETON_CACHE;
SCOPED_CACHE;
constructor(options = {}) {
this.PARENT = options.parent;
this.ROOT = options.root ?? this;
this.ROOT_SCOPE_COUNTER = this.ROOT === this ? { value: 0 } : this.ROOT.ROOT_SCOPE_COUNTER;
const normalizedScopeName = options.scopeName?.trim();
this.id = normalizedScopeName && normalizedScopeName.length > 0 ? normalizedScopeName : `scope-${String(++this.ROOT_SCOPE_COUNTER.value)}`;
this.LOGGER = options.logger ?? options.parent?.LOGGER ?? new ConsoleLoggerService();
this.ASYNC_RESOLUTION_DRAIN_TIMEOUT_MS = options.asyncResolutionDrainTimeoutMs ?? options.parent?.ASYNC_RESOLUTION_DRAIN_TIMEOUT_MS;
this.CAPTIVE_DEPENDENCY_POLICY = options.captiveDependencyPolicy ?? options.parent?.CAPTIVE_DEPENDENCY_POLICY ?? EDiContainerCaptiveDependencyPolicy.WARN;
this.DUPLICATE_PROVIDER_POLICY = options.duplicateProviderPolicy ?? options.parent?.DUPLICATE_PROVIDER_POLICY ?? EDiContainerDuplicateProviderPolicy.OVERWRITE;
this.PROVIDERS = new Map();
this.ROOT_SINGLETON_CACHE = this.ROOT === this ? new Map() : this.ROOT.ROOT_SINGLETON_CACHE;
this.SCOPED_CACHE = new Map();
this.CACHE_COORDINATOR = this.ROOT === this ? new CacheCoordinator() : this.ROOT.CACHE_COORDINATOR;
this.LOOKUP_COORDINATOR = this.ROOT === this ? new LookupCoordinator() : this.ROOT.LOOKUP_COORDINATOR;
this.DEPENDENCY_GRAPH_COORDINATOR = new DependencyGraphCoordinator({
assertKey: (dependencyKey) => this.assertToken(dependencyKey),
captiveDependencyPolicy: this.CAPTIVE_DEPENDENCY_POLICY,
describeKey: (dependencyKeySymbol) => this.describeToken(dependencyKeySymbol),
getChildScopes: (scopeNode) => [...scopeNode.CHILD_SCOPES],
getCurrentScopeId: () => this.id,
getParent: (scopeNode) => scopeNode.PARENT,
getRegistrationsByToken: (scopeNode) => scopeNode.PROVIDERS,
getRegistrationsForScope: (scopeNode, dependencyKeySymbol) => scopeNode.PROVIDERS.get(dependencyKeySymbol),
getScopeId: (scopeNode) => scopeNode.id,
logger: this.LOGGER,
lookupCoordinator: this.LOOKUP_COORDINATOR,
});
this.DISPOSER_COORDINATOR = new DisposerCoordinator({
logger: this.LOGGER,
stringifyKey: (dependencyKeySymbol) => this.describeToken(dependencyKeySymbol),
toError,
});
this.CHILD_SCOPES = new Set();
this.DISPOSERS = [];
this.DISPOSERS_BY_TOKEN = new Map();
this.PENDING_PROVIDER_CLEANUPS = new Map();
this.RESOLVE_INTERCEPTORS = options.resolveInterceptors ? Object.freeze([...options.resolveInterceptors]) : (options.parent?.RESOLVE_INTERCEPTORS ?? Object.freeze([]));
this.RESOLVE_INTERCEPTOR_DISPATCHER = new ResolveInterceptorDispatcher({
logger: this.LOGGER,
resolveInterceptors: this.RESOLVE_INTERCEPTORS,
stringifyKey: (dependencyKeySymbol) => this.describeToken(dependencyKeySymbol),
toError,
});
this.disposePromise = undefined;
this.isDisposed = false;
this.isDisposing = false;
this.isScopeLocked = false;
this.DISPOSAL_MANAGER = new DisposalManager({
disposeInternal: async () => {
await this.disposeInternal();
},
getAsyncResolutionDrainTimeoutMs: () => this.ASYNC_RESOLUTION_DRAIN_TIMEOUT_MS,
getDisposePromise: () => this.disposePromise,
isDisposed: () => this.isDisposed,
isDisposing: () => this.isDisposing,
setDisposePromise: (disposePromise) => {
this.disposePromise = disposePromise;
},
setIsDisposing: (isDisposing) => {
this.isDisposing = isDisposing;
},
});
this.DISPOSAL_COORDINATOR = new DisposalCoordinator({
clearChildScopes: () => {
this.CHILD_SCOPES.clear();
},
clearDisposers: () => {
this.DISPOSERS.length = 0;
},
clearDisposersByToken: () => {
this.DISPOSERS_BY_TOKEN.clear();
},
clearProviders: () => {
this.PROVIDERS.clear();
},
clearRootSingletonCacheIfRootScope: () => {
if (this === this.ROOT) {
this.ROOT_SINGLETON_CACHE.clear();
}
},
clearScopedCache: () => {
this.SCOPED_CACHE.clear();
},
detachFromParent: () => {
this.PARENT?.CHILD_SCOPES.delete(this);
},
getChildScopesInReverse: () => [...this.CHILD_SCOPES.values()].reverse(),
getDisposersInReverse: () => [...this.DISPOSERS].reverse(),
getScopeId: () => this.id,
releaseSingletonCacheForScopeProviders: () => {
this.CACHE_COORDINATOR.removeSingletonCacheForOwnerRegistrations(this.ROOT_SINGLETON_CACHE, this, this.PROVIDERS);
},
toError,
waitForInFlightAsyncResolutions: async () => {
await this.DISPOSAL_MANAGER.waitForInFlightAsyncResolutions();
},
});
this.REGISTRATION_COORDINATOR = new RegistrationCoordinator({
assertKey: (dependencyKey) => this.assertToken(dependencyKey),
describeKey: (dependencyKeySymbol) => this.describeToken(dependencyKeySymbol),
duplicateProviderPolicy: this.DUPLICATE_PROVIDER_POLICY,
getProviderRegistrationCount: () => this.getProviderRegistrationCount(),
getRegistrationsForProviderKey: (providerKey) => this.PROVIDERS.get(providerKey) ?? [],
getScopeId: () => this.id,
logger: this.LOGGER,
onAfterSingleBindingRegistered: (providerKey, existingRegistrations, registration) => {
const cacheKeysToInvalidate = existingRegistrations.length > 0 ? this.collectCacheKeys(existingRegistrations) : [registration.cacheKey];
this.CACHE_COORDINATOR.removeSingletonCacheForOwnerCacheKeys(this.ROOT_SINGLETON_CACHE, this, cacheKeysToInvalidate);
this.CACHE_COORDINATOR.clearScopedCachesRecursivelyForCacheKeys(cacheKeysToInvalidate, providerKey, this, (scopeNode) => [...scopeNode.CHILD_SCOPES], (scopeNode) => scopeNode.SCOPED_CACHE, (scopeNode, dependencyKey) => scopeNode.PROVIDERS.has(dependencyKey));
},
onBeforeOverwrite: (providerKey, existingRegistrations) => {
if (!this.hasTrackedDisposersForTokenRecursively(this, providerKey)) {
return;
}
const cleanupPromise = this.DISPOSER_COORDINATOR.disposeCachedInstancesForTokenRecursivelyAsync(providerKey, this, {
deleteProviderDisposersByToken: (scopeNode, dependencyKeySymbol) => {
scopeNode.DISPOSERS_BY_TOKEN.delete(dependencyKeySymbol);
},
getChildScopes: (scopeNode) => [...scopeNode.CHILD_SCOPES],
getProviderDisposersByToken: (scopeNode, dependencyKeySymbol) => scopeNode.DISPOSERS_BY_TOKEN.get(dependencyKeySymbol),
hasLocalProvider: (scopeNode, dependencyKeySymbol) => scopeNode.PROVIDERS.has(dependencyKeySymbol),
removeTrackedDisposer: (scopeNode, disposer) => {
const disposerIndex = scopeNode.DISPOSERS.indexOf(disposer);
if (disposerIndex !== -1) {
scopeNode.DISPOSERS.splice(disposerIndex, 1);
}
},
});
if (!this.hasAsyncCleanupHooks(existingRegistrations)) {
void cleanupPromise.catch((error) => {
this.logProviderCleanupFailure(providerKey, error);
});
return;
}
this.queueProviderCleanup(providerKey, cleanupPromise);
},
setRegistrationsForProviderKey: (providerKey, registrations) => {
this.PROVIDERS.set(providerKey, registrations);
},
});
this.RESOLUTION_COORDINATOR = new ResolutionCoordinator({
assertKey: (dependencyKey) => this.assertToken(dependencyKey),
assertSingleBindingLookup: (tokenSymbol, lookup) => this.DEPENDENCY_GRAPH_COORDINATOR.assertSingleBindingLookup(tokenSymbol, lookup),
describeKey: (dependencyKeySymbol) => this.describeToken(dependencyKeySymbol),
ensureActive: () => {
this.ensureActive();
},
findProvider: (requestScope, dependencyKeySymbol) => this.DEPENDENCY_GRAPH_COORDINATOR.findProvider(requestScope, dependencyKeySymbol),
getScopedCacheForLifecycle: (lifecycle, requestScope, providerOwner) => this.CACHE_COORDINATOR.getScopedCacheForLifecycle(lifecycle, requestScope.SCOPED_CACHE, this.ROOT_SINGLETON_CACHE, providerOwner),
isProviderCleanupPending: (ownerScope, dependencyKeySymbol) => ownerScope.isProviderCleanupPending(dependencyKeySymbol),
isRegistrationCurrent: (ownerScope, dependencyKeySymbol, registration) => this.isRegistrationCurrent(ownerScope, dependencyKeySymbol, registration),
registerDisposer: (disposerScope, provider, instance, dependencyKeySymbol, registrationOptions) => this.DISPOSER_COORDINATOR.registerDisposer(disposerScope, provider, instance, dependencyKeySymbol, {
getProviderDisposersByToken: (scopeNode, tokenSymbol) => scopeNode.DISPOSERS_BY_TOKEN.get(tokenSymbol),
pushDisposer: (scopeNode, disposer) => {
scopeNode.DISPOSERS.push(disposer);
},
setProviderDisposersByToken: (scopeNode, tokenSymbol, providerDisposers) => {
scopeNode.DISPOSERS_BY_TOKEN.set(tokenSymbol, providerDisposers);
},
}, registrationOptions),
validateCaptiveDependency: (parentRegistration, dependencyToken, resolutionScope) => {
this.DEPENDENCY_GRAPH_COORDINATOR.validateCaptiveDependency(parentRegistration, dependencyToken, resolutionScope);
},
waitForProviderCleanup: async (ownerScope, dependencyKeySymbol) => {
await ownerScope.waitForProviderCleanup(dependencyKeySymbol);
},
});
this.RESOLUTION_ENGINE = new ResolutionEngine({
assertKey: (dependencyKey) => this.assertToken(dependencyKey),
ensureActive: () => {
this.ensureActive();
},
ensureNotDisposing: () => {
this.ensureNotDisposing();
},
getScopeId: () => this.id,
notifyOnError: (tokenSymbol, isAsync, isResolveAll, isOptional, scopeId, error) => {
this.RESOLVE_INTERCEPTOR_DISPATCHER.notifyOnError(tokenSymbol, isAsync, isResolveAll, isOptional, scopeId, error);
},
notifyOnStart: (tokenSymbol, isAsync, isResolveAll, isOptional, scopeId) => {
this.RESOLVE_INTERCEPTOR_DISPATCHER.notifyOnStart(tokenSymbol, isAsync, isResolveAll, isOptional, scopeId);
},
notifyOnSuccess: (tokenSymbol, isAsync, isResolveAll, isOptional, scopeId, result) => {
this.RESOLVE_INTERCEPTOR_DISPATCHER.notifyOnSuccess(tokenSymbol, isAsync, isResolveAll, isOptional, scopeId, result);
},
onAsyncResolutionEnd: () => {
this.DISPOSAL_MANAGER.onAsyncResolutionEnd();
},
onAsyncResolutionStart: () => {
this.DISPOSAL_MANAGER.onAsyncResolutionStart();
},
resolveAllAsyncInternal: async (tokenSymbol, path, visitedTokens) => await this.RESOLUTION_COORDINATOR.resolveAllAsyncInternal(tokenSymbol, this, path, visitedTokens),
resolveAllSyncInternal: (tokenSymbol, path, visitedTokens) => this.RESOLUTION_COORDINATOR.resolveAllSyncInternal(tokenSymbol, this, path, visitedTokens),
resolveAsyncInternal: async (tokenSymbol, path, visitedTokens) => await this.RESOLUTION_COORDINATOR.resolveAsyncInternal(tokenSymbol, this, path, visitedTokens),
resolveSyncInternal: (tokenSymbol, path, visitedTokens) => this.RESOLUTION_COORDINATOR.resolveSyncInternal(tokenSymbol, this, path, visitedTokens),
});
if (this.PARENT) {
this.PARENT.CHILD_SCOPES.add(this);
}
}
async bootstrap(tokens) {
this.ensureNotDisposing();
this.ensureActive();
const bootstrapTokens = tokens ?? this.getSingletonProviderTokens();
for (const bootstrapDependency of bootstrapTokens) {
const key = this.assertToken(bootstrapDependency);
const lookup = this.DEPENDENCY_GRAPH_COORDINATOR.findProvider(this, key);
const registrations = lookup.registrations ?? (lookup.registration ? [lookup.registration] : []);
if (registrations.length === 0) {
throw new BaseError("Token not found in container", {
code: "TOKEN_NOT_FOUND",
context: {
lookupPath: lookup.lookupPath,
token: this.describeToken(key),
},
source: "DIContainer",
});
}
const hasMultiBindingRegistration = registrations.some((registration) => registration.isMultiBinding);
if (hasMultiBindingRegistration) {
await this.resolveAllAsync(bootstrapDependency);
continue;
}
await this.resolveAsync(bootstrapDependency);
}
}
createScope(name, options) {
this.ensureNotDisposing();
this.ensureActive();
return new DIContainer({
asyncResolutionDrainTimeoutMs: options?.asyncResolutionDrainTimeoutMs,
captiveDependencyPolicy: options?.captiveDependencyPolicy,
duplicateProviderPolicy: options?.duplicateProviderPolicy,
logger: this.LOGGER,
parent: this,
resolveInterceptors: options?.resolveInterceptors,
root: this.ROOT,
scopeName: name,
});
}
async dispose() {
await this.DISPOSAL_MANAGER.dispose();
}
explain(token) {
this.ensureNotDisposing();
this.ensureActive();
const key = this.assertToken(token);
const lookup = this.DEPENDENCY_GRAPH_COORDINATOR.findProvider(this, key);
const registration = lookup.registration;
if (!registration) {
return {
dependencies: [],
hasRootSingletonCache: this.CACHE_COORDINATOR.hasSingletonCacheForAnyOwner(this.ROOT_SINGLETON_CACHE, key, (ownerScope, dependencyKey) => ownerScope.PROVIDERS.get(dependencyKey)),
hasScopeCache: this.CACHE_COORDINATOR.hasScopedCacheForLookup(lookup, this.SCOPED_CACHE, key),
isFound: false,
lookupPath: lookup.lookupPath,
scopeId: this.id,
token: this.describeToken(key),
};
}
return {
dependencies: this.DEPENDENCY_GRAPH_COORDINATOR.getDependencyTokens(registration).map((dependencyToken) => this.describeToken(this.assertToken(dependencyToken))),
hasRootSingletonCache: this.CACHE_COORDINATOR.hasSingletonCacheForOwner(this.ROOT_SINGLETON_CACHE, lookup.owner, lookup.registrations),
hasScopeCache: this.CACHE_COORDINATOR.hasScopedCacheForLookup(lookup, this.SCOPED_CACHE, key),
isAsyncFactory: registration.isAsyncFactory,
isFound: true,
lifecycle: registration.lifecycle,
lookupPath: lookup.lookupPath,
providerType: registration.type,
scopeId: lookup.owner.id,
token: this.describeToken(key),
};
}
exportGraph() {
this.ensureNotDisposing();
this.ensureActive();
const graphNodes = [];
const graphEdges = [];
for (const [dependencyKeySymbol, registrations] of this.PROVIDERS.entries()) {
const keyDescription = this.describeToken(dependencyKeySymbol);
for (const registration of registrations) {
graphNodes.push({
isMultiBinding: registration.isMultiBinding,
lifecycle: registration.lifecycle,
providerType: registration.type,
scopeId: this.id,
token: keyDescription,
});
const dependencyTokens = this.DEPENDENCY_GRAPH_COORDINATOR.getDependencyTokens(registration);
for (const dependencyToken of dependencyTokens) {
graphEdges.push({
from: keyDescription,
scopeId: this.id,
to: this.describeToken(this.assertToken(dependencyToken)),
});
}
}
}
return {
edges: graphEdges,
nodes: graphNodes,
scopeId: this.id,
};
}
getRegisteredTokens() {
this.ensureNotDisposing();
this.ensureActive();
return [...this.PROVIDERS.keys()];
}
has(token) {
this.ensureNotDisposing();
this.ensureActive();
const key = this.assertToken(token);
const lookup = this.DEPENDENCY_GRAPH_COORDINATOR.findProvider(this, key);
return lookup.registration !== undefined;
}
lock() {
this.ensureNotDisposing();
this.ensureActive();
this.isScopeLocked = true;
}
register(providerOrProviders) {
this.ensureNotDisposing();
this.ensureActive();
this.ensureNotLocked();
const providers = Array.isArray(providerOrProviders) ? providerOrProviders : [providerOrProviders];
for (const provider of providers) {
this.REGISTRATION_COORDINATOR.registerProvider(provider);
}
}
resolve(token) {
return this.RESOLUTION_ENGINE.resolve(token);
}
resolveAll(token) {
return this.RESOLUTION_ENGINE.resolveAll(token);
}
async resolveAllAsync(token) {
return await this.RESOLUTION_ENGINE.resolveAllAsync(token);
}
async resolveAsync(token) {
return await this.RESOLUTION_ENGINE.resolveAsync(token);
}
resolveOptional(token) {
return this.RESOLUTION_ENGINE.resolveOptional(token, (tokenSymbol) => this.DEPENDENCY_GRAPH_COORDINATOR.findProvider(this, tokenSymbol).registration !== undefined);
}
async resolveOptionalAsync(token) {
return await this.RESOLUTION_ENGINE.resolveOptionalAsync(token, (tokenSymbol) => this.DEPENDENCY_GRAPH_COORDINATOR.findProvider(this, tokenSymbol).registration !== undefined);
}
snapshot() {
this.ensureNotDisposing();
this.ensureActive();
const dependencyRegistrations = [...this.PROVIDERS.entries()].map(([dependencyKeySymbol, registrations]) => ({
hasMultiBinding: registrations.some((registration) => registration.isMultiBinding),
registrationCount: registrations.length,
token: this.describeToken(dependencyKeySymbol),
}));
return {
childScopeCount: this.CHILD_SCOPES.size,
isDisposed: this.isDisposed,
isLocked: this.isScopeLocked,
parentScopeId: this.PARENT?.id,
providerCount: this.getProviderRegistrationCount(),
rootScopeId: this.ROOT.id,
scopedCacheSize: this.SCOPED_CACHE.size,
scopeId: this.id,
singletonCacheSize: this.CACHE_COORDINATOR.getRootSingletonCacheSize(this.ROOT_SINGLETON_CACHE),
tokenRegistrations: dependencyRegistrations,
tokens: this.getRegisteredTokens().map((token) => this.describeToken(this.assertToken(token))),
};
}
async unregister(token) {
this.ensureNotDisposing();
this.ensureActive();
this.ensureNotLocked();
const key = this.assertToken(token);
const existingRegistrations = this.PROVIDERS.get(key);
if (!existingRegistrations || existingRegistrations.length === 0) {
return false;
}
this.PROVIDERS.delete(key);
const cacheKeysToInvalidate = this.collectCacheKeys(existingRegistrations);
this.CACHE_COORDINATOR.removeSingletonCacheForOwnerCacheKeys(this.ROOT_SINGLETON_CACHE, this, cacheKeysToInvalidate);
this.CACHE_COORDINATOR.clearScopedCachesRecursivelyForCacheKeys(cacheKeysToInvalidate, key, this, (scopeNode) => [...scopeNode.CHILD_SCOPES], (scopeNode) => scopeNode.SCOPED_CACHE, (scopeNode, dependencyKey) => scopeNode.PROVIDERS.has(dependencyKey));
try {
await this.DISPOSER_COORDINATOR.disposeCachedInstancesForTokenRecursivelyAsync(key, this, {
deleteProviderDisposersByToken: (scopeNode, dependencyKeySymbol) => {
scopeNode.DISPOSERS_BY_TOKEN.delete(dependencyKeySymbol);
},
getChildScopes: (scopeNode) => [...scopeNode.CHILD_SCOPES],
getProviderDisposersByToken: (scopeNode, dependencyKeySymbol) => scopeNode.DISPOSERS_BY_TOKEN.get(dependencyKeySymbol),
hasLocalProvider: (scopeNode, dependencyKeySymbol) => scopeNode.PROVIDERS.has(dependencyKeySymbol),
removeTrackedDisposer: (scopeNode, disposer) => {
const disposerIndex = scopeNode.DISPOSERS.indexOf(disposer);
if (disposerIndex !== -1) {
scopeNode.DISPOSERS.splice(disposerIndex, 1);
}
},
});
}
catch (error) {
throw new BaseError("Provider was unregistered with cleanup errors", {
cause: toError(error),
code: "PROVIDER_UNREGISTER_CLEANUP_FAILED",
context: {
scopeId: this.id,
token: this.describeToken(key),
},
source: "DIContainer",
});
}
return true;
}
validate() {
this.ensureNotDisposing();
this.ensureActive();
this.DEPENDENCY_GRAPH_COORDINATOR.validateScopeTree(this);
}
assertToken(token) {
if (typeof token !== "symbol") {
throw new BaseError("Token must be a symbol", {
code: "TOKEN_NOT_SYMBOL",
source: "DIContainer",
});
}
return token;
}
collectCacheKeys(registrations) {
return registrations.map((registration) => registration.cacheKey);
}
describeToken(tokenSymbol) {
return tokenSymbol.description ? `Symbol(${tokenSymbol.description})` : tokenSymbol.toString();
}
async disposeInternal() {
let shouldMarkScopeDisposed = true;
try {
await this.DISPOSAL_COORDINATOR.disposeInternal();
}
catch (error) {
const normalizedError = toError(error);
if (normalizedError instanceof BaseError && normalizedError.code === "SCOPE_DISPOSE_ASYNC_DRAIN_TIMEOUT") {
shouldMarkScopeDisposed = false;
}
throw error;
}
finally {
if (shouldMarkScopeDisposed) {
this.isDisposed = true;
this.PENDING_PROVIDER_CLEANUPS.clear();
}
this.isDisposing = false;
this.disposePromise = undefined;
}
}
ensureActive() {
if (this.isDisposed) {
throw new BaseError("Scope is already disposed", {
code: "SCOPE_DISPOSED",
context: { scopeId: this.id },
source: "DIContainer",
});
}
}
ensureNotDisposing() {
if (this.isDisposing) {
throw new BaseError("Scope is disposing", {
code: "SCOPE_DISPOSING",
context: { scopeId: this.id },
source: "DIContainer",
});
}
}
ensureNotLocked() {
if (this.isScopeLocked) {
throw new BaseError("Scope is locked for registrations", {
code: "SCOPE_LOCKED",
context: { scopeId: this.id },
source: "DIContainer",
});
}
}
getProviderRegistrationCount() {
let totalRegistrations = 0;
for (const registrations of this.PROVIDERS.values()) {
totalRegistrations += registrations.length;
}
return totalRegistrations;
}
getSingletonProviderTokens() {
const singletonTokens = [];
for (const [tokenSymbol, registrations] of this.PROVIDERS.entries()) {
const hasSingletonRegistration = registrations.some((registration) => registration.lifecycle === EDependencyLifecycle.SINGLETON);
if (hasSingletonRegistration) {
singletonTokens.push(tokenSymbol);
}
}
return singletonTokens;
}
hasAsyncCleanupHooks(existingRegistrations) {
return existingRegistrations.some((registration) => {
const onDisposeHook = registration.provider.onDispose;
if (!onDisposeHook) {
return false;
}
const hookConstructor = onDisposeHook.constructor;
if (typeof hookConstructor === "function" && hookConstructor.name === "AsyncFunction") {
return true;
}
return Object.prototype.toString.call(onDisposeHook) === "[object AsyncFunction]";
});
}
hasTrackedDisposersForTokenRecursively(scopeNode, dependencyKeySymbol) {
const localDisposers = scopeNode.DISPOSERS_BY_TOKEN.get(dependencyKeySymbol);
if (localDisposers && localDisposers.length > 0) {
return true;
}
for (const childScope of scopeNode.CHILD_SCOPES) {
if (childScope.PROVIDERS.has(dependencyKeySymbol)) {
continue;
}
if (this.hasTrackedDisposersForTokenRecursively(childScope, dependencyKeySymbol)) {
return true;
}
}
return false;
}
isProviderCleanupPending(dependencyKeySymbol) {
const cleanupState = this.PENDING_PROVIDER_CLEANUPS.get(dependencyKeySymbol);
return cleanupState !== undefined && !cleanupState.isSettled;
}
isRegistrationCurrent(ownerScope, tokenSymbol, registration) {
const currentRegistrations = ownerScope.PROVIDERS.get(tokenSymbol);
return currentRegistrations?.includes(registration) ?? false;
}
logProviderCleanupFailure(dependencyKeySymbol, error) {
this.LOGGER.error("Provider cleanup failed during token overwrite", {
context: {
error: toError(error).message,
scopeId: this.id,
token: this.describeToken(dependencyKeySymbol),
},
source: "DIContainer",
});
}
queueProviderCleanup(dependencyKeySymbol, cleanupPromise) {
const trackedCleanupPromise = cleanupPromise.catch((error) => {
this.logProviderCleanupFailure(dependencyKeySymbol, error);
throw toError(error);
});
const cleanupState = {
isSettled: false,
promise: trackedCleanupPromise,
};
const markCleanupSettled = () => {
cleanupState.isSettled = true;
if (this.PENDING_PROVIDER_CLEANUPS.get(dependencyKeySymbol) === cleanupState) {
this.PENDING_PROVIDER_CLEANUPS.delete(dependencyKeySymbol);
}
};
this.PENDING_PROVIDER_CLEANUPS.set(dependencyKeySymbol, cleanupState);
void trackedCleanupPromise.then(markCleanupSettled, markCleanupSettled);
void trackedCleanupPromise.catch(() => undefined);
}
async waitForProviderCleanup(dependencyKeySymbol) {
const cleanupState = this.PENDING_PROVIDER_CLEANUPS.get(dependencyKeySymbol);
if (!cleanupState) {
return;
}
await cleanupState.promise;
}
}
export { DIContainer };
//# sourceMappingURL=container.class.js.map