@fluojs/di
Advanced tools
| export { validateProviderInputs } from './provider-normalization.js'; | ||
| export type { Provider } from './types.js'; | ||
| //# sourceMappingURL=internal.d.ts.map |
| {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"} |
| export { validateProviderInputs } from './provider-normalization.js'; |
| import type { NormalizedProvider, Provider } from './types.js'; | ||
| /** | ||
| * Validates and snapshots a public provider declaration for container registration. | ||
| * | ||
| * @param provider Provider declaration crossing the container registration boundary. | ||
| * @returns An immutable provider record with normalized injection and scope fields. | ||
| */ | ||
| export declare function normalizeProvider(provider: Provider): NormalizedProvider; | ||
| /** | ||
| * Validates provider declarations through the same normalization path used by `Container` registration. | ||
| * | ||
| * @param providers Provider declarations crossing an internal framework integration boundary. | ||
| * @returns The original provider list after every declaration has passed canonical normalization. | ||
| * @internal | ||
| */ | ||
| export declare function validateProviderInputs(providers: Provider[]): Provider[]; | ||
| //# sourceMappingURL=provider-normalization.d.ts.map |
| {"version":3,"file":"provider-normalization.d.ts","sourceRoot":"","sources":["../src/provider-normalization.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAA2B,kBAAkB,EAAiB,QAAQ,EAAE,MAAM,YAAY,CAAC;AA6JvG;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,kBAAkB,CAiFxE;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE,CAMxE"} |
| import { getClassDiMetadata } from '@fluojs/core/internal'; | ||
| import { InvalidProviderError } from './errors.js'; | ||
| import { Scope } from './types.js'; | ||
| function isClassConstructor(value) { | ||
| return isConstructableFunction(value); | ||
| } | ||
| function isProviderObject(value) { | ||
| return typeof value === 'object' && value !== null; | ||
| } | ||
| function isClassType(value) { | ||
| return isConstructableFunction(value); | ||
| } | ||
| function isFactoryFunction(value) { | ||
| return typeof value === 'function'; | ||
| } | ||
| function isTokenResolver(value) { | ||
| return typeof value === 'function'; | ||
| } | ||
| function isConstructableFunction(value) { | ||
| if (typeof value !== 'function') { | ||
| return false; | ||
| } | ||
| try { | ||
| Reflect.construct(Object, [], value); | ||
| return true; | ||
| } catch (error) { | ||
| if (error instanceof TypeError) { | ||
| return false; | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| function isToken(value) { | ||
| return typeof value === 'string' || typeof value === 'symbol' || isConstructableFunction(value); | ||
| } | ||
| function isScope(value) { | ||
| return value === 'singleton' || value === 'request' || value === 'transient'; | ||
| } | ||
| function assertProviderToken(provider) { | ||
| if (!('provide' in provider) || provider.provide == null) { | ||
| throw new InvalidProviderError('Provider object must include a non-null provide token.'); | ||
| } | ||
| } | ||
| function assertProviderStrategy(provider) { | ||
| const strategyCount = Number('useValue' in provider) + Number('useFactory' in provider) + Number('useClass' in provider) + Number('useExisting' in provider); | ||
| if (strategyCount !== 1) { | ||
| throw new InvalidProviderError('Provider object must declare exactly one of useValue, useFactory, useClass, or useExisting.'); | ||
| } | ||
| } | ||
| function assertObjectProvider(provider) { | ||
| assertProviderToken(provider); | ||
| assertProviderStrategy(provider); | ||
| } | ||
| function normalizeProviderScope(scope, providerToken) { | ||
| if (scope === undefined) { | ||
| return undefined; | ||
| } | ||
| if (isScope(scope)) { | ||
| return scope; | ||
| } | ||
| throw new InvalidProviderError('Provider scope must be one of singleton, request, or transient.', { | ||
| token: providerToken, | ||
| scope: String(scope), | ||
| hint: 'Use Scope.DEFAULT, Scope.REQUEST, Scope.TRANSIENT, or the matching string literal.' | ||
| }); | ||
| } | ||
| function normalizeInjectToken(token, providerToken, index) { | ||
| if (typeof token === 'object' && token !== null && '__forwardRef__' in token && token.__forwardRef__ === true) { | ||
| if (!('forwardRef' in token) || !isTokenResolver(token.forwardRef)) { | ||
| throw new InvalidProviderError(`Provider inject forwardRef wrapper at index ${String(index)} must expose a callable forwardRef function.`, { | ||
| token: providerToken | ||
| }); | ||
| } | ||
| return Object.freeze({ | ||
| __forwardRef__: true, | ||
| forwardRef: token.forwardRef | ||
| }); | ||
| } | ||
| if (typeof token === 'object' && token !== null && '__optional__' in token && token.__optional__ === true) { | ||
| if (!('token' in token) || !isToken(token.token)) { | ||
| throw new InvalidProviderError(`Provider inject optional wrapper at index ${String(index)} must contain a valid token.`, { | ||
| token: providerToken | ||
| }); | ||
| } | ||
| return Object.freeze({ | ||
| __optional__: true, | ||
| token: token.token | ||
| }); | ||
| } | ||
| if (isToken(token)) { | ||
| return token; | ||
| } | ||
| throw new InvalidProviderError(`Provider inject entry at index ${String(index)} must be a string, symbol, class, forwardRef(), or optional() token wrapper.`, { | ||
| token: providerToken, | ||
| hint: 'Check that every dependency token is defined, and use forwardRef() for declaration-order cycles.' | ||
| }); | ||
| } | ||
| function normalizeInject(inject, providerToken) { | ||
| if (inject === undefined) { | ||
| return []; | ||
| } | ||
| if (!Array.isArray(inject)) { | ||
| throw new InvalidProviderError('Provider inject must be an array.', { | ||
| token: providerToken, | ||
| hint: 'Pass dependency tokens as inject: [DependencyA, DependencyB].' | ||
| }); | ||
| } | ||
| return inject.map((token, index) => normalizeInjectToken(token, providerToken, index)); | ||
| } | ||
| function freezeNormalizedProvider(provider) { | ||
| return Object.freeze({ | ||
| ...provider, | ||
| inject: Object.freeze([...provider.inject]) | ||
| }); | ||
| } | ||
| /** | ||
| * Validates and snapshots a public provider declaration for container registration. | ||
| * | ||
| * @param provider Provider declaration crossing the container registration boundary. | ||
| * @returns An immutable provider record with normalized injection and scope fields. | ||
| */ | ||
| export function normalizeProvider(provider) { | ||
| if (isClassConstructor(provider)) { | ||
| const metadata = getClassDiMetadata(provider); | ||
| return freezeNormalizedProvider({ | ||
| inject: normalizeInject(metadata?.inject, provider), | ||
| provide: provider, | ||
| scope: normalizeProviderScope(metadata?.scope, provider) ?? Scope.DEFAULT, | ||
| type: 'class', | ||
| useClass: provider | ||
| }); | ||
| } | ||
| if (!isProviderObject(provider)) { | ||
| throw new InvalidProviderError('Unsupported provider type.'); | ||
| } | ||
| const objectProvider = provider; | ||
| assertObjectProvider(objectProvider); | ||
| const explicitScope = normalizeProviderScope(objectProvider.scope, objectProvider.provide); | ||
| if ('useValue' in objectProvider) { | ||
| return freezeNormalizedProvider({ | ||
| inject: [], | ||
| multi: objectProvider.multi, | ||
| provide: objectProvider.provide, | ||
| scope: Scope.DEFAULT, | ||
| type: 'value', | ||
| useValue: objectProvider.useValue | ||
| }); | ||
| } | ||
| if ('useFactory' in objectProvider) { | ||
| if (!isFactoryFunction(objectProvider.useFactory)) { | ||
| throw new InvalidProviderError('Factory provider useFactory must be a function.', { | ||
| token: objectProvider.provide | ||
| }); | ||
| } | ||
| const metadata = objectProvider.resolverClass ? getClassDiMetadata(objectProvider.resolverClass) : undefined; | ||
| return freezeNormalizedProvider({ | ||
| inject: normalizeInject(objectProvider.inject, objectProvider.provide), | ||
| multi: objectProvider.multi, | ||
| provide: objectProvider.provide, | ||
| scope: explicitScope ?? normalizeProviderScope(metadata?.scope, objectProvider.provide) ?? Scope.DEFAULT, | ||
| type: 'factory', | ||
| useFactory: objectProvider.useFactory | ||
| }); | ||
| } | ||
| if ('useClass' in objectProvider) { | ||
| if (!isClassType(objectProvider.useClass)) { | ||
| throw new InvalidProviderError('Class provider useClass must be a constructor.', { | ||
| token: objectProvider.provide | ||
| }); | ||
| } | ||
| const metadata = getClassDiMetadata(objectProvider.useClass); | ||
| return freezeNormalizedProvider({ | ||
| inject: normalizeInject(objectProvider.inject === undefined ? metadata?.inject : objectProvider.inject, objectProvider.provide), | ||
| multi: objectProvider.multi, | ||
| provide: objectProvider.provide, | ||
| scope: explicitScope ?? normalizeProviderScope(metadata?.scope, objectProvider.provide) ?? Scope.DEFAULT, | ||
| type: 'class', | ||
| useClass: objectProvider.useClass | ||
| }); | ||
| } | ||
| if ('useExisting' in objectProvider) { | ||
| if (objectProvider.useExisting == null) { | ||
| throw new InvalidProviderError('Alias provider useExisting must be a non-null token.', { | ||
| token: objectProvider.provide | ||
| }); | ||
| } | ||
| return freezeNormalizedProvider({ | ||
| inject: [], | ||
| provide: objectProvider.provide, | ||
| scope: Scope.DEFAULT, | ||
| type: 'existing', | ||
| useExisting: objectProvider.useExisting | ||
| }); | ||
| } | ||
| throw new InvalidProviderError('Provider object must declare exactly one of useValue, useFactory, useClass, or useExisting.'); | ||
| } | ||
| /** | ||
| * Validates provider declarations through the same normalization path used by `Container` registration. | ||
| * | ||
| * @param providers Provider declarations crossing an internal framework integration boundary. | ||
| * @returns The original provider list after every declaration has passed canonical normalization. | ||
| * @internal | ||
| */ | ||
| export function validateProviderInputs(providers) { | ||
| for (const provider of providers) { | ||
| normalizeProvider(provider); | ||
| } | ||
| return providers; | ||
| } |
+40
-12
| import { type Token } from '@fluojs/core'; | ||
| import type { NormalizedProvider, Provider } from './types.js'; | ||
| /** | ||
| * Public read/write seam for framework-owned testing and tooling that need to | ||
| * Factory provider resolution mode recorded after a factory returns either synchronously or through a promise. | ||
| */ | ||
| export type FactoryResolutionKind = 'async' | 'sync'; | ||
| /** | ||
| * Controlled cache adoption seam for framework-owned testing and tooling that | ||
| * need synchronous helpers to preserve container-owned singleton disposal. | ||
| */ | ||
| export interface ContainerResolutionCacheOwner { | ||
| readonly deleteMultiSingleton: (provider: NormalizedProvider) => void; | ||
| readonly deleteSingleton: (token: Token) => void; | ||
| readonly recordFactoryResolution: (provider: NormalizedProvider, kind: FactoryResolutionKind) => void; | ||
| readonly setMultiSingleton: (provider: NormalizedProvider, promise: Promise<unknown>) => void; | ||
| readonly setSingleton: (token: Token, promise: Promise<unknown>) => void; | ||
| } | ||
| /** | ||
| * Read-only factory resolution diagnostics recorded by container-owned factory | ||
| * instantiation paths. | ||
| */ | ||
| export interface ContainerFactoryResolutionState { | ||
| readonly get: (provider: NormalizedProvider) => FactoryResolutionKind | undefined; | ||
| readonly has: (provider: NormalizedProvider) => boolean; | ||
| } | ||
| /** | ||
| * Public read-only seam for framework-owned testing and tooling that need to | ||
| * inspect a container's resolved provider graph without depending on private | ||
@@ -9,8 +32,10 @@ * field names or structural casts. | ||
| export interface ContainerResolutionState { | ||
| readonly cacheOwner: ContainerResolutionCacheOwner; | ||
| readonly factoryResolutionKinds: ContainerFactoryResolutionState; | ||
| readonly parent?: ContainerResolutionState; | ||
| readonly registrations: Map<Token, NormalizedProvider>; | ||
| readonly multiRegistrations: Map<Token, NormalizedProvider[]>; | ||
| readonly multiSingletonCache: Map<NormalizedProvider, Promise<unknown>>; | ||
| readonly registrations: ReadonlyMap<Token, NormalizedProvider>; | ||
| readonly multiRegistrations: ReadonlyMap<Token, readonly NormalizedProvider[]>; | ||
| readonly multiSingletonCache: ReadonlyMap<NormalizedProvider, Promise<unknown>>; | ||
| readonly requestScopeEnabled: boolean; | ||
| readonly singletonCache: Map<Token, Promise<unknown>>; | ||
| readonly singletonCache: ReadonlyMap<Token, Promise<unknown>>; | ||
| } | ||
@@ -30,5 +55,5 @@ /** | ||
| private readonly staleDisposalTasks; | ||
| private readonly staleDisposalErrors; | ||
| private readonly singletonCache; | ||
| private readonly forwardRefTokenCache; | ||
| private readonly factoryResolutionKinds; | ||
| private readonly providerLookupPlanCache; | ||
@@ -41,3 +66,3 @@ private readonly multiProviderPlanCache; | ||
| private disposed; | ||
| private trackedByRoot; | ||
| private trackedByParent; | ||
| private graphRevision; | ||
@@ -83,8 +108,12 @@ constructor(parent?: Container | undefined, requestScopeEnabled?: boolean, singletonCache?: Map<Token, Promise<unknown>>); | ||
| * `@fluojs/testing`; callers should prefer ordinary `has(...)` and | ||
| * `resolve(...)` unless they need to preserve container cache ownership while | ||
| * implementing a framework-level helper. | ||
| * `resolve(...)` unless they need read-only graph/cache visibility while | ||
| * implementing a framework-level helper. Cache adoption for synchronous | ||
| * helpers goes through `cacheOwner`; the returned maps are not mutable | ||
| * container internals. | ||
| * | ||
| * @returns Provider registrations and resolution caches for this container scope. | ||
| * @returns Read-only provider registrations and resolution caches for this container scope. | ||
| */ | ||
| inspectResolutionState(): ContainerResolutionState; | ||
| private createCacheOwner; | ||
| private createFactoryResolutionState; | ||
| /** | ||
@@ -178,3 +207,3 @@ * Returns whether resolving a token may require a request-scope container. | ||
| private clearResolutionPlanCaches; | ||
| private waitForStaleDisposalTasks; | ||
| private assertStaleDisposalsSettled; | ||
| private scheduleStaleDisposal; | ||
@@ -193,3 +222,2 @@ private throwDisposalErrors; | ||
| private invalidateAffectedCachedEntriesInHierarchy; | ||
| private isAncestorOf; | ||
| private invalidateAffectedCachedEntries; | ||
@@ -196,0 +224,0 @@ private shouldInvalidateCachedToken; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AAW3E,OAAO,KAAK,EAOV,kBAAkB,EAElB,QAAQ,EAET,MAAM,YAAY,CAAC;AAUpB;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAC3C,QAAQ,CAAC,aAAa,EAAE,GAAG,CAAC,KAAK,EAAE,kBAAkB,CAAC,CAAC;IACvD,QAAQ,CAAC,kBAAkB,EAAE,GAAG,CAAC,KAAK,EAAE,kBAAkB,EAAE,CAAC,CAAC;IAC9D,QAAQ,CAAC,mBAAmB,EAAE,GAAG,CAAC,kBAAkB,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IACxE,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,cAAc,EAAE,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;CACvD;AAoID;;GAEG;AACH,qBAAa,SAAS;IAsBlB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,mBAAmB;IAtBtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAwC;IACtE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA0C;IAC7E,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAoB;IAC1D,OAAO,CAAC,YAAY,CAA2C;IAC/D,OAAO,CAAC,iBAAiB,CAAwD;IACjF,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAmD;IACvF,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA4B;IAC/D,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAiB;IACrD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA+B;IAC9D,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAsC;IAC3E,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAA0E;IAClH,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAyE;IAChH,OAAO,CAAC,QAAQ,CAAC,4BAA4B,CAAmD;IAChG,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAA0E;IACrH,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,aAAa,CAAK;gBAGP,MAAM,CAAC,EAAE,SAAS,YAAA,EAClB,mBAAmB,UAAQ,EAC5C,cAAc,CAAC,EAAE,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAK/C;;;;;;;;;OASG;IACH,QAAQ,CAAC,GAAG,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI;IA4CxC;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,GAAG,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI;IA0DxC;;;;;OAKG;IACH,GAAG,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO;IAI1B;;;;;;;;;OASG;IACH,sBAAsB,IAAI,wBAAwB;IAWlD;;;;;OAKG;IACH,0BAA0B,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO;IAcjD;;;;;OAKG;IACH,kBAAkB,IAAI,SAAS;IAW/B;;;;;;;;;OASG;IACG,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAW7C;;;;;OAKG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAkBhB,UAAU;IAgCxB,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,4BAA4B;IAsBpC,OAAO,CAAC,6BAA6B;IAIrC,OAAO,CAAC,qBAAqB;IAM7B,OAAO,CAAC,4BAA4B;IAIpC,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,qBAAqB;IAsB7B,OAAO,CAAC,iCAAiC;IAyBzC,OAAO,CAAC,qCAAqC;IAc7C,OAAO,CAAC,sCAAsC;IAY9C,OAAO,CAAC,mCAAmC;YAa7B,gBAAgB;YAehB,8BAA8B;IAqC5C,OAAO,CAAC,eAAe;YAgBT,kBAAkB;IAMhC,OAAO,CAAC,mCAAmC;YAoB7B,6BAA6B;YAc7B,4BAA4B;IA4B1C,OAAO,CAAC,6BAA6B;YAQvB,gCAAgC;IAuB9C,OAAO,CAAC,kCAAkC;IAY1C,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,kCAAkC;YAI5B,eAAe;YAwBf,gBAAgB;IAiB9B,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,yBAAyB;IAWjC,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,yBAAyB;IAMjC,OAAO,CAAC,cAAc;IAatB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,QAAQ;IAuBhB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,oBAAoB;YAkBd,YAAY;YAaZ,0BAA0B;YA0B1B,8BAA8B;IAc5C,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,sBAAsB;IAM9B,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,yBAAyB;YAOnB,yBAAyB;IAMvC,OAAO,CAAC,qBAAqB;IAoB7B,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,oBAAoB;IAS5B,OAAO,CAAC,YAAY;YAIN,WAAW;IA+BzB,OAAO,CAAC,+BAA+B;IAmBvC,OAAO,CAAC,2BAA2B;IAqBnC,OAAO,CAAC,gCAAgC;IAkCxC,OAAO,CAAC,wBAAwB;IA+ChC,OAAO,CAAC,8BAA8B;IAYtC,OAAO,CAAC,sBAAsB;YAUhB,mBAAmB;IAUjC,OAAO,CAAC,0CAA0C;IAgBlD,OAAO,CAAC,YAAY;IAcpB,OAAO,CAAC,+BAA+B;IAgDvC,OAAO,CAAC,2BAA2B;IAiBnC,OAAO,CAAC,8BAA8B;IAItC,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,8BAA8B;IAItC,OAAO,CAAC,8BAA8B;CAuBvC"} | ||
| {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"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"} |
+189
-156
@@ -1,8 +0,23 @@ | ||
| import { InvariantError, formatTokenName } from '@fluojs/core'; | ||
| import { formatTokenName, InvariantError } from '@fluojs/core'; | ||
| import { getClassDiMetadata } from '@fluojs/core/internal'; | ||
| import { CircularDependencyError, ContainerResolutionError, DuplicateProviderError, InvalidProviderError, RequestScopeResolutionError, ScopeMismatchError } from './errors.js'; | ||
| import { Scope, isForwardRef, isOptionalToken } from './types.js'; | ||
| import { CircularDependencyError, ContainerResolutionError, DuplicateProviderError, RequestScopeResolutionError, ScopeMismatchError } from './errors.js'; | ||
| import { normalizeProvider } from './provider-normalization.js'; | ||
| import { isForwardRef, isOptionalToken, Scope } from './types.js'; | ||
| /** | ||
| * Public read/write seam for framework-owned testing and tooling that need to | ||
| * Factory provider resolution mode recorded after a factory returns either synchronously or through a promise. | ||
| */ | ||
| /** | ||
| * Controlled cache adoption seam for framework-owned testing and tooling that | ||
| * need synchronous helpers to preserve container-owned singleton disposal. | ||
| */ | ||
| /** | ||
| * Read-only factory resolution diagnostics recorded by container-owned factory | ||
| * instantiation paths. | ||
| */ | ||
| /** | ||
| * Public read-only seam for framework-owned testing and tooling that need to | ||
| * inspect a container's resolved provider graph without depending on private | ||
@@ -12,111 +27,76 @@ * field names or structural casts. | ||
| function isClassConstructor(value) { | ||
| return typeof value === 'function'; | ||
| } | ||
| function isValueProvider(value) { | ||
| return typeof value === 'object' && value !== null && 'useValue' in value; | ||
| } | ||
| function isFactoryProvider(value) { | ||
| return typeof value === 'object' && value !== null && 'useFactory' in value; | ||
| } | ||
| function isClassProvider(value) { | ||
| return typeof value === 'object' && value !== null && 'useClass' in value; | ||
| } | ||
| function isExistingProvider(value) { | ||
| return typeof value === 'object' && value !== null && 'useExisting' in value; | ||
| } | ||
| function assertProviderToken(provider) { | ||
| if (!('provide' in provider) || provider.provide == null) { | ||
| throw new InvalidProviderError('Provider object must include a non-null provide token.'); | ||
| class ReadonlyMapView { | ||
| #source; | ||
| [Symbol.toStringTag] = 'Map'; | ||
| constructor(source) { | ||
| this.#source = source; | ||
| } | ||
| } | ||
| function assertProviderStrategy(provider) { | ||
| const strategyCount = Number('useValue' in provider) + Number('useFactory' in provider) + Number('useClass' in provider) + Number('useExisting' in provider); | ||
| if (strategyCount !== 1) { | ||
| throw new InvalidProviderError('Provider object must declare exactly one of useValue, useFactory, useClass, or useExisting.'); | ||
| get size() { | ||
| return this.#source.size; | ||
| } | ||
| } | ||
| function assertObjectProvider(provider) { | ||
| assertProviderToken(provider); | ||
| assertProviderStrategy(provider); | ||
| } | ||
| function normalizeInjectToken(token) { | ||
| if (token == null) { | ||
| throw new InvalidProviderError('Inject token must not be null or undefined. Check that all tokens in @Inject(...) are defined at the point of decoration (forward-reference cycles require forwardRef()).'); | ||
| entries() { | ||
| return this.#source.entries(); | ||
| } | ||
| return token; | ||
| forEach(callbackfn, thisArg) { | ||
| for (const [key, value] of this.#source) { | ||
| callbackfn.call(thisArg, value, key, this); | ||
| } | ||
| } | ||
| get(key) { | ||
| return this.#source.get(key); | ||
| } | ||
| has(key) { | ||
| return this.#source.has(key); | ||
| } | ||
| keys() { | ||
| return this.#source.keys(); | ||
| } | ||
| values() { | ||
| return this.#source.values(); | ||
| } | ||
| [Symbol.iterator]() { | ||
| return this.entries(); | ||
| } | ||
| } | ||
| function normalizeProvider(provider) { | ||
| if (isClassConstructor(provider)) { | ||
| const metadata = getClassDiMetadata(provider); | ||
| return { | ||
| inject: (metadata?.inject ?? []).map(normalizeInjectToken), | ||
| provide: provider, | ||
| scope: metadata?.scope ?? Scope.DEFAULT, | ||
| type: 'class', | ||
| useClass: provider | ||
| }; | ||
| class ReadonlyMultiRegistrationMapView { | ||
| #source; | ||
| [Symbol.toStringTag] = 'Map'; | ||
| constructor(source) { | ||
| this.#source = source; | ||
| } | ||
| if (isValueProvider(provider)) { | ||
| assertObjectProvider(provider); | ||
| return { | ||
| inject: [], | ||
| multi: provider.multi, | ||
| provide: provider.provide, | ||
| scope: Scope.DEFAULT, | ||
| type: 'value', | ||
| useValue: provider.useValue | ||
| }; | ||
| get size() { | ||
| return this.#source.size; | ||
| } | ||
| if (isFactoryProvider(provider)) { | ||
| assertObjectProvider(provider); | ||
| if (typeof provider.useFactory !== 'function') { | ||
| throw new InvalidProviderError('Factory provider useFactory must be a function.', { | ||
| token: provider.provide | ||
| }); | ||
| *entries() { | ||
| for (const [token, providers] of this.#source) { | ||
| yield [token, Object.freeze([...providers])]; | ||
| } | ||
| const metadata = provider.resolverClass ? getClassDiMetadata(provider.resolverClass) : undefined; | ||
| return { | ||
| inject: (provider.inject ?? []).map(normalizeInjectToken), | ||
| multi: provider.multi, | ||
| provide: provider.provide, | ||
| scope: provider.scope ?? metadata?.scope ?? Scope.DEFAULT, | ||
| type: 'factory', | ||
| useFactory: provider.useFactory | ||
| }; | ||
| } | ||
| if (isClassProvider(provider)) { | ||
| assertObjectProvider(provider); | ||
| if (typeof provider.useClass !== 'function') { | ||
| throw new InvalidProviderError('Class provider useClass must be a constructor.', { | ||
| token: provider.provide | ||
| }); | ||
| forEach(callbackfn, thisArg) { | ||
| for (const [key, value] of this.entries()) { | ||
| callbackfn.call(thisArg, value, key, this); | ||
| } | ||
| const metadata = getClassDiMetadata(provider.useClass); | ||
| return { | ||
| inject: (provider.inject ?? metadata?.inject ?? []).map(normalizeInjectToken), | ||
| multi: provider.multi, | ||
| provide: provider.provide, | ||
| scope: provider.scope ?? metadata?.scope ?? Scope.DEFAULT, | ||
| type: 'class', | ||
| useClass: provider.useClass | ||
| }; | ||
| } | ||
| if (isExistingProvider(provider)) { | ||
| assertObjectProvider(provider); | ||
| if (provider.useExisting == null) { | ||
| throw new InvalidProviderError('Alias provider useExisting must be a non-null token.', { | ||
| token: provider.provide | ||
| }); | ||
| get(key) { | ||
| const providers = this.#source.get(key); | ||
| return providers ? Object.freeze([...providers]) : undefined; | ||
| } | ||
| has(key) { | ||
| return this.#source.has(key); | ||
| } | ||
| keys() { | ||
| return this.#source.keys(); | ||
| } | ||
| *values() { | ||
| for (const providers of this.#source.values()) { | ||
| yield Object.freeze([...providers]); | ||
| } | ||
| return { | ||
| inject: [], | ||
| provide: provider.provide, | ||
| scope: Scope.DEFAULT, | ||
| type: 'existing', | ||
| useExisting: provider.useExisting | ||
| }; | ||
| } | ||
| throw new InvalidProviderError('Unsupported provider type.'); | ||
| [Symbol.iterator]() { | ||
| return this.entries(); | ||
| } | ||
| } | ||
| function isPromiseLike(value) { | ||
| return (typeof value === 'object' || typeof value === 'function') && value !== null && typeof value.then === 'function'; | ||
| } | ||
@@ -134,5 +114,5 @@ /** | ||
| staleDisposalTasks = new Set(); | ||
| staleDisposalErrors = []; | ||
| singletonCache; | ||
| forwardRefTokenCache = new WeakMap(); | ||
| factoryResolutionKinds = new WeakMap(); | ||
| providerLookupPlanCache = new Map(); | ||
@@ -145,3 +125,3 @@ multiProviderPlanCache = new Map(); | ||
| disposed = false; | ||
| trackedByRoot = false; | ||
| trackedByParent = false; | ||
| graphRevision = 0; | ||
@@ -269,17 +249,55 @@ constructor(parent, requestScopeEnabled = false, singletonCache) { | ||
| * `@fluojs/testing`; callers should prefer ordinary `has(...)` and | ||
| * `resolve(...)` unless they need to preserve container cache ownership while | ||
| * implementing a framework-level helper. | ||
| * `resolve(...)` unless they need read-only graph/cache visibility while | ||
| * implementing a framework-level helper. Cache adoption for synchronous | ||
| * helpers goes through `cacheOwner`; the returned maps are not mutable | ||
| * container internals. | ||
| * | ||
| * @returns Provider registrations and resolution caches for this container scope. | ||
| * @returns Read-only provider registrations and resolution caches for this container scope. | ||
| */ | ||
| inspectResolutionState() { | ||
| const registrations = new Map(this.registrations); | ||
| const multiRegistrations = new Map(Array.from(this.multiRegistrations, ([token, providers]) => [token, Object.freeze([...providers])])); | ||
| const multiSingletonCache = new Map(this.multiSingletonCache); | ||
| const singletonCache = new Map(this.singletonCache); | ||
| return { | ||
| cacheOwner: this.createCacheOwner(singletonCache, multiSingletonCache), | ||
| factoryResolutionKinds: this.createFactoryResolutionState(), | ||
| parent: this.parent?.inspectResolutionState(), | ||
| registrations: this.registrations, | ||
| multiRegistrations: this.multiRegistrations, | ||
| multiSingletonCache: this.multiSingletonCache, | ||
| registrations: new ReadonlyMapView(registrations), | ||
| multiRegistrations: new ReadonlyMultiRegistrationMapView(multiRegistrations), | ||
| multiSingletonCache: new ReadonlyMapView(multiSingletonCache), | ||
| requestScopeEnabled: this.requestScopeEnabled, | ||
| singletonCache: this.singletonCache | ||
| singletonCache: new ReadonlyMapView(singletonCache) | ||
| }; | ||
| } | ||
| createCacheOwner(singletonCacheSnapshot, multiSingletonCacheSnapshot) { | ||
| return Object.freeze({ | ||
| deleteMultiSingleton: provider => { | ||
| this.multiSingletonCache.delete(provider); | ||
| multiSingletonCacheSnapshot.delete(provider); | ||
| }, | ||
| deleteSingleton: token => { | ||
| this.singletonCache.delete(token); | ||
| singletonCacheSnapshot.delete(token); | ||
| }, | ||
| recordFactoryResolution: (provider, kind) => { | ||
| this.root().factoryResolutionKinds.set(provider, kind); | ||
| }, | ||
| setMultiSingleton: (provider, promise) => { | ||
| this.multiSingletonCache.set(provider, promise); | ||
| multiSingletonCacheSnapshot.set(provider, promise); | ||
| }, | ||
| setSingleton: (token, promise) => { | ||
| this.singletonCache.set(token, promise); | ||
| singletonCacheSnapshot.set(token, promise); | ||
| } | ||
| }); | ||
| } | ||
| createFactoryResolutionState() { | ||
| const root = this.root(); | ||
| return Object.freeze({ | ||
| get: provider => root.factoryResolutionKinds.get(provider), | ||
| has: provider => root.factoryResolutionKinds.has(provider) | ||
| }); | ||
| } | ||
@@ -332,2 +350,3 @@ /** | ||
| } | ||
| await this.assertStaleDisposalsSettled(); | ||
| return this.resolveWithChain(token, [], new Set()); | ||
@@ -360,4 +379,4 @@ } | ||
| try { | ||
| // Dispose all live request-scope children first (root only) | ||
| if (!this.parent && this.childScopes && this.childScopes.size > 0) { | ||
| // Dispose all live request-scope children before tearing down this scope's cache. | ||
| if (this.childScopes && this.childScopes.size > 0) { | ||
| const childResults = await Promise.allSettled(Array.from(this.childScopes).map(child => child.dispose())); | ||
@@ -378,5 +397,5 @@ for (const result of childResults) { | ||
| } finally { | ||
| if (this.parent && this.trackedByRoot) { | ||
| this.root().childScopes?.delete(this); | ||
| this.trackedByRoot = false; | ||
| if (this.parent && this.trackedByParent) { | ||
| this.parent.childScopes?.delete(this); | ||
| this.trackedByParent = false; | ||
| } | ||
@@ -627,9 +646,9 @@ } | ||
| ensureTrackedRequestScope() { | ||
| if (!this.requestScopeEnabled || !this.parent || this.trackedByRoot) { | ||
| if (!this.requestScopeEnabled || !this.parent || this.trackedByParent) { | ||
| return; | ||
| } | ||
| const root = this.root(); | ||
| root.childScopes ??= new Set(); | ||
| root.childScopes.add(this); | ||
| this.trackedByRoot = true; | ||
| this.parent.ensureTrackedRequestScope(); | ||
| this.parent.childScopes ??= new Set(); | ||
| this.parent.childScopes.add(this); | ||
| this.trackedByParent = true; | ||
| } | ||
@@ -717,8 +736,13 @@ requestCacheForWrite() { | ||
| async disposeCache(entries) { | ||
| await this.waitForStaleDisposalTasks(); | ||
| const errors = []; | ||
| try { | ||
| await this.assertStaleDisposalsSettled(); | ||
| } catch (error) { | ||
| this.collectDisposalError(error, errors); | ||
| } | ||
| const { | ||
| disposables, | ||
| errors | ||
| errors: resolutionErrors | ||
| } = await this.collectDisposableInstances(entries); | ||
| errors.push(...this.staleDisposalErrors.splice(0, this.staleDisposalErrors.length)); | ||
| errors.push(...resolutionErrors); | ||
| errors.push(...(await this.disposeInstancesInReverseOrder(disposables))); | ||
@@ -799,10 +823,26 @@ this.clearDisposalCaches(); | ||
| } | ||
| async waitForStaleDisposalTasks() { | ||
| async assertStaleDisposalsSettled() { | ||
| const errors = []; | ||
| while (this.staleDisposalTasks.size > 0) { | ||
| await Promise.all(Array.from(this.staleDisposalTasks)); | ||
| const tasks = Array.from(this.staleDisposalTasks); | ||
| await Promise.all(tasks.map(task => task.promise)); | ||
| for (const task of tasks) { | ||
| this.staleDisposalTasks.delete(task); | ||
| if (task.failed && !task.errorConsumed) { | ||
| task.errorConsumed = true; | ||
| errors.push(task.error); | ||
| } | ||
| } | ||
| } | ||
| this.throwDisposalErrors(errors); | ||
| } | ||
| scheduleStaleDisposal(instancePromise) { | ||
| let task; | ||
| task = (async () => { | ||
| scheduleStaleDisposal(instancePromise, staleDisposalOwner) { | ||
| const observers = staleDisposalOwner === this ? [this] : [this, staleDisposalOwner]; | ||
| const task = { | ||
| error: undefined, | ||
| errorConsumed: false, | ||
| failed: false, | ||
| promise: Promise.resolve() | ||
| }; | ||
| task.promise = (async () => { | ||
| try { | ||
@@ -814,8 +854,15 @@ const instance = await instancePromise; | ||
| } catch (error) { | ||
| this.staleDisposalErrors.push(error); | ||
| task.error = error; | ||
| task.failed = true; | ||
| } | ||
| })().finally(() => { | ||
| this.staleDisposalTasks.delete(task); | ||
| if (!task.failed) { | ||
| for (const observer of observers) { | ||
| observer.staleDisposalTasks.delete(task); | ||
| } | ||
| } | ||
| }); | ||
| this.staleDisposalTasks.add(task); | ||
| for (const observer of observers) { | ||
| observer.staleDisposalTasks.add(task); | ||
| } | ||
| } | ||
@@ -853,3 +900,5 @@ throwDisposalErrors(errors) { | ||
| const deps = await this.resolveProviderDeps(provider, chain, activeTokens); | ||
| return provider.useFactory(...deps); | ||
| const value = provider.useFactory(...deps); | ||
| this.root().factoryResolutionKinds.set(provider, isPromiseLike(value) ? 'async' : 'sync'); | ||
| return value; | ||
| } | ||
@@ -974,25 +1023,9 @@ case 'class': | ||
| } | ||
| invalidateAffectedCachedEntriesInHierarchy(token) { | ||
| this.invalidateAffectedCachedEntries(token); | ||
| const childScopes = this.root().childScopes; | ||
| if (!childScopes) { | ||
| return; | ||
| invalidateAffectedCachedEntriesInHierarchy(token, staleDisposalOwner = this) { | ||
| this.invalidateAffectedCachedEntries(token, staleDisposalOwner); | ||
| for (const childScope of this.childScopes ?? []) { | ||
| childScope.invalidateAffectedCachedEntriesInHierarchy(token, staleDisposalOwner); | ||
| } | ||
| for (const childScope of childScopes) { | ||
| if (this.isAncestorOf(childScope)) { | ||
| childScope.invalidateAffectedCachedEntries(token); | ||
| } | ||
| } | ||
| } | ||
| isAncestorOf(container) { | ||
| let current = container.parent; | ||
| while (current) { | ||
| if (current === this) { | ||
| return true; | ||
| } | ||
| current = current.parent; | ||
| } | ||
| return false; | ||
| } | ||
| invalidateAffectedCachedEntries(token) { | ||
| invalidateAffectedCachedEntries(token, staleDisposalOwner) { | ||
| for (const [cachedToken, cached] of this.requestCache?.entries() ?? []) { | ||
@@ -1002,3 +1035,3 @@ if (!this.shouldInvalidateCachedToken(cachedToken, token)) { | ||
| } | ||
| this.scheduleStaleDisposal(cached); | ||
| this.scheduleStaleDisposal(cached, staleDisposalOwner); | ||
| this.requestCache?.delete(cachedToken); | ||
@@ -1011,3 +1044,3 @@ } | ||
| } | ||
| this.scheduleStaleDisposal(cached); | ||
| this.scheduleStaleDisposal(cached, staleDisposalOwner); | ||
| this.singletonCache.delete(cachedToken); | ||
@@ -1021,3 +1054,3 @@ } | ||
| } | ||
| this.scheduleStaleDisposal(cached); | ||
| this.scheduleStaleDisposal(cached, staleDisposalOwner); | ||
| this.multiSingletonCache.delete(provider); | ||
@@ -1034,3 +1067,3 @@ } | ||
| } | ||
| this.scheduleStaleDisposal(cached); | ||
| this.scheduleStaleDisposal(cached, staleDisposalOwner); | ||
| multiRequestCache.delete(provider); | ||
@@ -1037,0 +1070,0 @@ } |
+10
-9
@@ -46,2 +46,3 @@ import type { Constructor, ForwardRefToken, InjectionToken, MaybePromise, OptionalInjectToken, Token } from '@fluojs/core'; | ||
| multi?: boolean; | ||
| /** Class metadata source used when the factory should inherit `@Scope(...)` metadata. */ | ||
| resolverClass?: ClassType; | ||
@@ -98,11 +99,11 @@ } | ||
| export interface NormalizedProvider<T = unknown> { | ||
| inject: InjectionToken[]; | ||
| provide: Token<T>; | ||
| scope: Scope; | ||
| type: 'class' | 'factory' | 'value' | 'existing'; | ||
| useClass?: ClassType<T>; | ||
| useFactory?: (...deps: unknown[]) => MaybePromise<T>; | ||
| useValue?: T; | ||
| useExisting?: Token; | ||
| multi?: boolean; | ||
| readonly inject: readonly InjectionToken[]; | ||
| readonly provide: Token<T>; | ||
| readonly scope: Scope; | ||
| readonly type: 'class' | 'factory' | 'value' | 'existing'; | ||
| readonly useClass?: ClassType<T>; | ||
| readonly useFactory?: (...deps: unknown[]) => MaybePromise<T>; | ||
| readonly useValue?: T; | ||
| readonly useExisting?: Token; | ||
| readonly multi?: boolean; | ||
| } | ||
@@ -109,0 +110,0 @@ /** |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"} |
+4
-4
@@ -66,6 +66,6 @@ /** | ||
| export function forwardRef(fn) { | ||
| return { | ||
| return Object.freeze({ | ||
| __forwardRef__: true, | ||
| forwardRef: fn | ||
| }; | ||
| }); | ||
| } | ||
@@ -90,6 +90,6 @@ | ||
| export function optional(token) { | ||
| return { | ||
| return Object.freeze({ | ||
| __optional__: true, | ||
| token | ||
| }; | ||
| }); | ||
| } | ||
@@ -96,0 +96,0 @@ |
+6
-2
@@ -12,3 +12,3 @@ { | ||
| ], | ||
| "version": "1.1.0", | ||
| "version": "2.0.0", | ||
| "private": false, | ||
@@ -32,2 +32,6 @@ "license": "MIT", | ||
| "import": "./dist/index.js" | ||
| }, | ||
| "./internal": { | ||
| "types": "./dist/internal.d.ts", | ||
| "import": "./dist/internal.js" | ||
| } | ||
@@ -41,3 +45,3 @@ }, | ||
| "dependencies": { | ||
| "@fluojs/core": "^1.0.3" | ||
| "@fluojs/core": "^1.1.0" | ||
| }, | ||
@@ -44,0 +48,0 @@ "devDependencies": { |
+47
-26
@@ -70,3 +70,3 @@ # @fluojs/di | ||
| - **값 provider**: `{ provide: 'API_URL', useValue: 'https://api.example.com' }` | ||
| - **팩토리 provider**: `{ provide, useFactory, inject }` | ||
| - **팩토리 provider**: `{ provide, useFactory, inject }`. 팩토리가 참조 클래스의 `@Scope(...)` 같은 DI metadata를 상속해야 하고 provider `scope`를 명시하지 않았다면 `resolverClass`를 함께 지정합니다. | ||
| - **별칭(Alias) provider**: `{ provide: ILogger, useExisting: PinoLogger }`를 사용하여 하나의 토큰을 기존에 등록된 다른 provider로 매핑할 수 있습니다. | ||
@@ -80,7 +80,7 @@ | ||
| dispose 중에는 루트 컨테이너가 먼저 살아 있는 request scope 자식을 정리한 뒤, 자식 dispose 중 하나 이상이 실패하더라도 루트가 소유한 singleton 정리를 계속 수행합니다. 자식/루트 dispose 실패가 여러 개 발생하면 `dispose()`는 모든 shutdown 실패를 확인할 수 있도록 `AggregateError`로 보고합니다. | ||
| dispose 중에는 각 컨테이너가 자신이 소유한 살아 있는 request scope 자식을 먼저 재귀적으로 정리하므로, 루트가 아닌 request scope를 dispose해도 중첩 request scope를 닫은 뒤 자신의 request cache를 정리합니다. 이후 루트 dispose는 자식 dispose 중 하나 이상이 실패하더라도 루트가 소유한 singleton 정리를 계속 수행합니다. 자식/루트 dispose 실패가 여러 개 발생하면 `dispose()`는 모든 shutdown 실패를 확인할 수 있도록 `AggregateError`로 보고합니다. | ||
| ### provider override | ||
| 테스트나 request-local 경계에서 기존 등록을 의도적으로 교체해야 할 때는 `override(...providers)`를 사용합니다. override는 각 토큰의 현재 provider set을 교체하고 현재 컨테이너와 이미 materialize된 request-scope 자식의 cached instance를 무효화하며, 오래된 instance를 즉시 dispose합니다. multi provider override는 해당 토큰의 전체 multi-provider set을 교체하므로 필요한 replacement provider를 한 번에 모두 전달하세요. 같은 토큰에 single replacement와 multi replacement를 한 override 호출에서 섞으면 모호한 교체로 보고 거부합니다. | ||
| 테스트나 request-local 경계에서 기존 등록을 의도적으로 교체해야 할 때는 `override(...providers)`를 사용합니다. override는 각 토큰의 현재 provider set을 교체하고 현재 컨테이너와 이미 materialize된 request-scope 자식의 cached instance를 무효화하며, 다음 replacement resolution이 계속되기 전에 오래된 instance의 dispose가 끝나도록 보장합니다. multi provider override는 해당 토큰의 전체 multi-provider set을 교체하므로 필요한 replacement provider를 한 번에 모두 전달하세요. 같은 토큰에 single replacement와 multi replacement를 한 override 호출에서 섞으면 모호한 교체로 보고 거부합니다. | ||
@@ -96,3 +96,3 @@ ### request scope 분리 | ||
| provider 객체는 등록 시점에 검증됩니다. 모든 객체 provider는 null이 아닌 `provide` 토큰과 정확히 하나의 전략(`useClass`, `useValue`, `useFactory`, `useExisting`)을 포함해야 합니다. 잘못된 provider 형태는 컨테이너 그래프에 영향을 주기 전에 `InvalidProviderError`를 발생시킵니다. | ||
| provider 객체는 등록 시점에 검증됩니다. 모든 객체 provider는 null이 아닌 `provide` 토큰과 정확히 하나의 전략(`useClass`, `useValue`, `useFactory`, `useExisting`)을 포함해야 합니다. class provider에서 `inject`를 생략하거나 `undefined`로 지정하면 `useClass`의 `@Inject(...)` 메타데이터로 fallback하며, 그 밖의 명시적 `inject` 값은 유효한 token 또는 올바른 `forwardRef(...)` / `optional(...)` wrapper로 구성된 배열이어야 합니다. 명시적인 `scope` 값은 `singleton`, `request`, `transient` 중 하나여야 합니다. 잘못된 provider 형태는 컨테이너 그래프에 영향을 주기 전에 `InvalidProviderError`를 발생시킵니다. | ||
@@ -135,18 +135,38 @@ ## 순환 의존성 처리 | ||
| `useValue`를 사용하면 단위 테스트 중에 컨테이너의 provider를 모의 객체(mock)나 스텁(stub)으로 쉽게 교체할 수 있습니다. | ||
| 먼저 전체 의존성 그래프를 등록한 다음, `override(...)`와 `useValue`를 사용해 기존 provider를 mock이나 stub으로 교체하세요. `register(...)`는 새 provider를 추가하며 중복 토큰을 거부하고, `override(...)`는 지원되는 교체 API입니다. | ||
| ```typescript | ||
| import { Inject } from '@fluojs/core'; | ||
| import { Container } from '@fluojs/di'; | ||
| import { expect, it, vi } from 'vitest'; | ||
| const container = new Container(); | ||
| const mockDb = { query: vi.fn() }; | ||
| class Database { | ||
| async query(): Promise<readonly string[]> { | ||
| return ['real row']; | ||
| } | ||
| } | ||
| // 실제 Database 클래스를 모의 객체 값으로 교체 | ||
| container.register({ | ||
| provide: Database, | ||
| useValue: mockDb | ||
| @Inject(Database) | ||
| class DataService { | ||
| constructor(private readonly database: Database) {} | ||
| async load(): Promise<readonly string[]> { | ||
| return this.database.query(); | ||
| } | ||
| } | ||
| it('uses a mock database', async () => { | ||
| const mockDb = { query: vi.fn().mockResolvedValue(['mock row']) }; | ||
| const container = new Container().register(Database, DataService); | ||
| container.override({ | ||
| provide: Database, | ||
| useValue: mockDb, | ||
| }); | ||
| const service = await container.resolve(DataService); | ||
| await expect(service.load()).resolves.toEqual(['mock row']); | ||
| expect(mockDb.query).toHaveBeenCalledOnce(); | ||
| }); | ||
| const service = await container.resolve(DataService); | ||
| // service는 실제 Database 인스턴스 대신 mockDb를 사용합니다. | ||
| ``` | ||
@@ -164,13 +184,13 @@ | ||
| | Export | 설명 | | ||
| |---|---| | ||
| | `Container` | 메인 DI 컨테이너 클래스입니다. | | ||
| | `register(...providers)` | 하나 이상의 프로바이더를 등록합니다. | | ||
| | `override(...providers)` | 기존 provider를 교체하고 cached instance를 무효화하며 오래된 instance를 dispose합니다. | | ||
| | `resolve<T>(token)` | 토큰을 인스턴스로 비동기 해석합니다. | | ||
| | `inspectResolutionState()` | cache ownership을 보존해야 하는 testing/tooling helper를 위한 지원 대상 framework-owned container introspection seam을 노출합니다. 애플리케이션 코드는 `has(...)`와 `resolve(...)`를 우선 사용하세요. | | ||
| | `createRequestScope()` | 요청 스코프 의존성을 위한 자식 컨테이너를 생성합니다. | | ||
| | `has(token)` | 컨테이너나 부모에 토큰이 등록되어 있는지 확인합니다. | | ||
| | `hasRequestScopedDependency(token)` | 토큰 해석 시 provider 그래프에 request-scoped 의존성이나 순환이 있어 request-scope 컨테이너가 필요할 수 있는지 확인합니다. | | ||
| | `dispose()` | request child와 루트가 소유한 singleton instance를 정리합니다. | | ||
| | Surface | 종류 | 설명 | | ||
| |---|---|---| | ||
| | `Container` | Root export | 메인 DI 컨테이너 클래스입니다. | | ||
| | `container.register(...providers)` | `Container` instance method | 하나 이상의 프로바이더를 등록합니다. | | ||
| | `container.override(...providers)` | `Container` instance method | 기존 provider를 교체하고 cached instance를 무효화하며 다음 replacement resolution이 계속되기 전에 오래된 instance dispose가 settle되도록 보장합니다. | | ||
| | `container.resolve<T>(token)` | `Container` instance method | 토큰을 인스턴스로 비동기 해석합니다. | | ||
| | `container.inspectResolutionState()` | `Container` instance method | snapshot read-only map view, frozen provider record, controlled cache adoption을 통해 cache ownership을 보존해야 하는 testing/tooling helper를 위한 지원 대상 framework-owned container introspection seam을 노출합니다. 애플리케이션 코드는 `has(...)`와 `resolve(...)`를 우선 사용하세요. | | ||
| | `container.createRequestScope()` | `Container` instance method | 요청 스코프 의존성을 위한 자식 컨테이너를 생성합니다. | | ||
| | `container.has(token)` | `Container` instance method | 컨테이너나 부모에 토큰이 등록되어 있는지 확인합니다. | | ||
| | `container.hasRequestScopedDependency(token)` | `Container` instance method | 토큰 해석 시 provider 그래프에 request-scoped 의존성이나 순환이 있어 request-scope 컨테이너가 필요할 수 있는지 확인합니다. | | ||
| | `container.dispose()` | `Container` instance method | request child와 루트가 소유한 singleton instance를 정리합니다. | | ||
| | `forwardRef(fn)` | 선언 순서 문제를 위해 조회를 지연하는 토큰 래퍼를 반환합니다. 실제 생성자 순환을 해석 가능하게 만들지는 않습니다. | | ||
@@ -184,4 +204,5 @@ | `isForwardRef(value)` | `forwardRef(...)`가 만든 값인지 확인하는 type guard입니다. 커스텀 provider tooling이 DI token wrapper와 통합될 때 사용할 수 있습니다. | | ||
| | Container helper types | `ClassType`, `Disposable`, `RequestScopeContainer`는 typed provider 선언, teardown hook, request-scope helper 경계를 지원합니다. | | ||
| | `ContainerResolutionState` | framework testing/tooling integration을 위해 `inspectResolutionState()`가 반환하는 공개 introspection record입니다. | | ||
| | Container introspection helper types | `ContainerResolutionState`, `ContainerResolutionCacheOwner`, `ContainerFactoryResolutionState`는 `inspectResolutionState()`가 반환하는 read-only graph/cache view와 controlled cache adoption helper를 설명합니다. | | ||
| | `NormalizedProvider` | 컨테이너가 검증한 provider record shape를 위한 compatibility-only 공개 타입입니다. provider를 작성할 때는 `Provider`나 구체 provider interface를 우선 사용하세요. normalized record 생성은 컨테이너가 소유합니다. | | ||
| | `@fluojs/di/internal` | sibling fluo package가 자체 순회 전에 컨테이너의 canonical provider validation을 적용할 수 있도록 `validateProviderInputs(...)`를 노출하는 package-integration seam입니다. 애플리케이션 코드는 계속 `Container`를 통해 provider를 등록해야 합니다. | | ||
| | `DiErrorContext` | DI error에 붙는 구조화된 context입니다. 로그와 테스트가 token, scope, module, dependency chain, hint를 검사할 수 있게 합니다. | | ||
@@ -188,0 +209,0 @@ | 에러 클래스 | `InvalidProviderError`, `ContainerResolutionError`, `RequestScopeResolutionError`, `ScopeMismatchError`, `CircularDependencyError`, `DuplicateProviderError`. | |
+47
-26
@@ -70,3 +70,3 @@ # @fluojs/di | ||
| - **Value Providers**: `{ provide: 'API_URL', useValue: 'https://api.example.com' }`. | ||
| - **Factory Providers**: `{ provide: 'ASYNC_CONFIG', useFactory: async (db) => await db.load(), inject: [Database] }`. | ||
| - **Factory Providers**: `{ provide: 'ASYNC_CONFIG', useFactory: async (db) => await db.load(), inject: [Database] }`. Add `resolverClass` when the factory should inherit the referenced class's DI metadata, such as `@Scope(...)`, unless an explicit provider `scope` is set. | ||
| - **Alias Providers**: `{ provide: ILogger, useExisting: PinoLogger }` allows mapping one token to another existing provider. | ||
@@ -79,7 +79,7 @@ | ||
| During disposal, the root container first tears down live request-scope children and then continues with root-owned singleton cleanup even if one or more child disposals fail. When multiple child/root disposals fail, `dispose()` reports an `AggregateError` so callers can inspect every shutdown failure without losing cleanup progress. | ||
| During disposal, each container first recursively tears down live request-scope children it owns, so disposing a non-root request scope also closes nested request scopes before its own request cache. Root disposal then continues with root-owned singleton cleanup even if one or more child disposals fail. When multiple child/root disposals fail, `dispose()` reports an `AggregateError` so callers can inspect every shutdown failure without losing cleanup progress. | ||
| ### Provider Overrides | ||
| Use `override(...providers)` when a test or request-local boundary needs to replace existing registrations deliberately. Overrides replace the current provider set for each token, invalidate cached instances in the current container and already-materialized request-scope descendants, and dispose stale instances immediately. Multi-provider overrides replace the full multi-provider set for that token, so pass every replacement provider together; mixing single and multi replacements for the same token in one override call is rejected as ambiguous. | ||
| Use `override(...providers)` when a test or request-local boundary needs to replace existing registrations deliberately. Overrides replace the current provider set for each token, invalidate cached instances in the current container and already-materialized request-scope descendants, and dispose stale instances before the next replacement resolution continues. Multi-provider overrides replace the full multi-provider set for that token, so pass every replacement provider together; mixing single and multi replacements for the same token in one override call is rejected as ambiguous. | ||
@@ -96,3 +96,3 @@ ### Request Scoping | ||
| Provider objects are validated at registration time: every object provider must include a non-null `provide` token and exactly one strategy (`useClass`, `useValue`, `useFactory`, or `useExisting`). Invalid provider shapes throw `InvalidProviderError` before they can affect the container graph. | ||
| Provider objects are validated at registration time: every object provider must include a non-null `provide` token and exactly one strategy (`useClass`, `useValue`, `useFactory`, or `useExisting`). For class providers, an omitted or `undefined` `inject` value falls back to the `useClass` `@Inject(...)` metadata; any other explicit `inject` value must be an array containing valid tokens or well-formed `forwardRef(...)` / `optional(...)` wrappers. Explicit `scope` values must be `singleton`, `request`, or `transient`. Invalid provider shapes throw `InvalidProviderError` before they can affect the container graph. | ||
@@ -135,18 +135,38 @@ ## Circular Dependency Handling | ||
| You can easily override providers in the container to use mocks or stubs during unit testing by using `useValue`. | ||
| Register the complete dependency graph first, then use `override(...)` with `useValue` to replace an existing provider with a mock or stub. `register(...)` adds new providers and rejects duplicate tokens; `override(...)` is the supported replacement API. | ||
| ```typescript | ||
| import { Inject } from '@fluojs/core'; | ||
| import { Container } from '@fluojs/di'; | ||
| import { expect, it, vi } from 'vitest'; | ||
| const container = new Container(); | ||
| const mockDb = { query: vi.fn() }; | ||
| class Database { | ||
| async query(): Promise<readonly string[]> { | ||
| return ['real row']; | ||
| } | ||
| } | ||
| // Override the real Database class with a mock value | ||
| container.register({ | ||
| provide: Database, | ||
| useValue: mockDb | ||
| @Inject(Database) | ||
| class DataService { | ||
| constructor(private readonly database: Database) {} | ||
| async load(): Promise<readonly string[]> { | ||
| return this.database.query(); | ||
| } | ||
| } | ||
| it('uses a mock database', async () => { | ||
| const mockDb = { query: vi.fn().mockResolvedValue(['mock row']) }; | ||
| const container = new Container().register(Database, DataService); | ||
| container.override({ | ||
| provide: Database, | ||
| useValue: mockDb, | ||
| }); | ||
| const service = await container.resolve(DataService); | ||
| await expect(service.load()).resolves.toEqual(['mock row']); | ||
| expect(mockDb.query).toHaveBeenCalledOnce(); | ||
| }); | ||
| const service = await container.resolve(DataService); | ||
| // service uses mockDb instead of the real Database instance | ||
| ``` | ||
@@ -164,13 +184,13 @@ | ||
| | Export | Description | | ||
| |---|---| | ||
| | `Container` | The main DI container class. | | ||
| | `register(...providers)` | Registers one or more providers. | | ||
| | `override(...providers)` | Replaces existing providers, invalidates cached instances, and disposes stale instances. | | ||
| | `resolve<T>(token)` | Asynchronously resolves a token to an instance. | | ||
| | `inspectResolutionState()` | Exposes the supported framework-owned container introspection seam for testing/tooling helpers that must preserve cache ownership. Prefer `has(...)` and `resolve(...)` for application code. | | ||
| | `createRequestScope()` | Creates a child container for request-scoped dependencies. | | ||
| | `has(token)` | Checks if a token is registered in the container or its parents. | | ||
| | `hasRequestScopedDependency(token)` | Checks whether resolving a token may require a request-scope container because its provider graph contains request-scoped dependencies or is cyclic. | | ||
| | `dispose()` | Disposes request children and root-owned singleton instances. | | ||
| | Surface | Kind | Description | | ||
| |---|---|---| | ||
| | `Container` | Root export | The main DI container class. | | ||
| | `container.register(...providers)` | `Container` instance method | Registers one or more providers. | | ||
| | `container.override(...providers)` | `Container` instance method | Replaces existing providers, invalidates cached instances, and ensures stale instance disposal settles before the next replacement resolution continues. | | ||
| | `container.resolve<T>(token)` | `Container` instance method | Asynchronously resolves a token to an instance. | | ||
| | `container.inspectResolutionState()` | `Container` instance method | Exposes the supported framework-owned container introspection seam for testing/tooling helpers that must preserve cache ownership through snapshot read-only map views, frozen provider records, and controlled cache adoption. Prefer `has(...)` and `resolve(...)` for application code. | | ||
| | `container.createRequestScope()` | `Container` instance method | Creates a child container for request-scoped dependencies. | | ||
| | `container.has(token)` | `Container` instance method | Checks if a token is registered in the container or its parents. | | ||
| | `container.hasRequestScopedDependency(token)` | `Container` instance method | Checks whether resolving a token may require a request-scope container because its provider graph contains request-scoped dependencies or is cyclic. | | ||
| | `container.dispose()` | `Container` instance method | Disposes request children and root-owned singleton instances. | | ||
| | `forwardRef(fn)` | Returns a token wrapper that defers lookup for declaration-order issues; it does not make constructor dependency cycles resolvable. | | ||
@@ -184,4 +204,5 @@ | `isForwardRef(value)` | Type guard for values produced by `forwardRef(...)`; useful when integrating custom provider tooling with DI token wrappers. | | ||
| | Container helper types | `ClassType`, `Disposable`, and `RequestScopeContainer` support typed provider declarations, teardown hooks, and request-scope helper boundaries. | | ||
| | `ContainerResolutionState` | Public introspection record returned by `inspectResolutionState()` for framework testing/tooling integrations. | | ||
| | Container introspection helper types | `ContainerResolutionState`, `ContainerResolutionCacheOwner`, and `ContainerFactoryResolutionState` describe the read-only graph/cache views and controlled cache adoption helpers returned by `inspectResolutionState()`. | | ||
| | `NormalizedProvider` | Compatibility-only public type for the container's validated provider record shape. Prefer authoring providers with `Provider` or the specific provider interfaces; the container owns normalized record construction. | | ||
| | `@fluojs/di/internal` | Package-integration seam exposing `validateProviderInputs(...)` so sibling fluo packages can apply the container's canonical provider validation before their own traversal. Application code should continue to register providers through `Container`. | | ||
| | `DiErrorContext` | Structured context attached to DI errors so logs and tests can inspect tokens, scopes, modules, dependency chains, and hints. | | ||
@@ -188,0 +209,0 @@ | Error classes | `InvalidProviderError`, `ContainerResolutionError`, `RequestScopeResolutionError`, `ScopeMismatchError`, `CircularDependencyError`, `DuplicateProviderError`. | |
112555
18.36%22
37.5%1952
17.1%218
10.66%Updated