Repository navigation
Releases
v4.0.0
Add registration policies, typed registration metadata and public operations. Factory resolvers now
keep their lifetime checks after await, which changes some runtime behavior; review the breaking
changes before upgrading.
Breaking changes:
- Pass factories a resolver bound to the requesting registration instead of the scope itself. It exposes
onlyresolve,tryResolve,resolveAllandtryResolveAll. - Keep the singleton restriction on that resolver after the factory returns. A resolver stored by a
singleton factory, or by a factory resolved while building a singleton, and alazy()view created from
such a resolver, now throw a captive-dependencyContainerErrorwhen they resolve a scoped token later. - Return Katagami's own Promise when a factory returns a native Promise, including async factories,
instead of the Promise the factory returned. Its identity and custom properties are not preserved.
Without abeforeReturnpolicy, repeated resolutions of a cached result still return the same Promise. - Detect cycles along each resolution chain, including while an async creation is pending. The factories
still building a pending creation, through their resolvers or by calling the same scope synchronously,
can no longer resolve its token again, even without waiting for it: this throws a circular-dependency
ContainerError. In 3.x it recursed without end, rejected with aTypeError, never settled or returned
the pending Promise, depending on the lifetime and on whether it was awaited. A resolver kept by a
dependency that has finished building still returns the pending Promise to later callers; a creation
that waits for its own Promise that way never settles, as in 3.x. Synchronous cycles are reported
whichever resolver or scope they pass through. - Skip a cached async creation that rejected when disposing, instead of reporting it in the disposal
AggregateError. The failure stays with the caller that resolved the token. - Throw
ContainerErrorwhen registering on, or composing into, a disposed container. - Use new shared runtime keys. Containers, scopes and disposable views from 3.x cannot be mixed with 4.x
in the same process. - Require TypeScript 5.0 or later: the published declarations use
consttype parameters.
Additions:
- Share a
policyobject across modules withcreateContainer({ policy }):requiredMetadatais checked
at compile time and at runtime, including.use(), and modules with a different policy are rejected. - Define typed metadata keys with
createMetadataKey, attach them through the{ metadata }registration
option and read them withcontainer.getMetadata(token). - Mark function-valued factories with
entrypoint()and call them through
createScope(container, { access: 'operations' }). Operations resolve on each call, always return a
Promise, and are awaited by disposal. Unknownaccessvalues are rejected. - Check each resolution synchronously with the
beforeResolvescope option, which receives the
requesting registration and the resolution path. - Check values leaving top-level resolution and operation results with
policy.beforeReturn, using the
registrations each value was observed through. The records keep neither containers nor factories alive. - Export
RegisteredTokensand the policy, metadata, resolution-event and operations-scope types, so
consumers can emit declarations for code that builds operations scopes. - Give the new type parameters defaults; the 3.0.3 documentation examples and type tests compile unchanged.
See CHANGELOG.md and the registration policies guide.
What's Changed
Full Changelog: v3.0.3...v4.0.0
v3.0.3
What's Changed
Full Changelog: v3.0.2...v3.0.3
v3.0.2
What's Changed
Full Changelog: v3.0.1...v3.0.2
v3.0.1
v3.0.0
Breaking Changes
Container is now registration-only
Container no longer has resolve(), tryResolve(), resolveAll(), or tryResolveAll() methods. All resolution must go through a Scope created via createScope().
// Before (v2.x)
const container = createContainer()
.registerSingleton(Logger, () => new Logger());
container.resolve(Logger);
// After (v3.x)
import { createContainer, createScope } from 'katagami';
const container = createContainer()
.registerSingleton(Logger, () => new Logger());
const scope = createScope(container);
scope.resolve(Logger);This separation ensures that:
Containeris responsible only for registration and compositionScopeis responsible for resolution, caching, circular dependency detection, and captive dependency enforcement- The roles are cleanly separated with no duplication of resolution logic
lazy() no longer accepts Container or DisposableContainer
Since Container no longer has resolution methods, lazy() only accepts Scope or DisposableScope:
// Before (v2.x)
const service = lazy(container, HeavyService);
// After (v3.x)
const scope = createScope(container);
const service = lazy(scope, HeavyService);DisposableContainer no longer extends Resolver
DisposableContainer no longer exposes resolve / tryResolve methods. Use createScope() on a disposable container to resolve tokens.
Migration Guide
- Replace all
container.resolve(...)calls withcreateScope(container).resolve(...) - Update
lazy(container, ...)tolazy(createScope(container), ...)
Full Changelog: v2.3.0...v3.0.0
v2.3.0
New Features
Runtime captive dependency detection
Katagami now detects captive dependencies at runtime within scopes, in addition to existing compile-time prevention. If a singleton factory attempts to resolve a scoped token — directly or through intermediaries — a ContainerError is thrown with a clear message.
const container = createContainer()
.registerScoped(RequestContext, () => new RequestContext())
.registerSingleton(DbPool, r => new DbPool(r.resolve(RequestContext)));
const scope = createScope(container);
scope.resolve(DbPool);
// ContainerError: Captive dependency detected: scoped token "RequestContext"
// cannot be resolved inside a singleton factory.This uses a depth-tracking mechanism (singletonDepth) to detect when a scoped token is being resolved inside a singleton factory chain, catching issues that type-level checks alone cannot prevent (e.g., @ts-expect-error or any casts).
Both resolve() and tryResolve() / resolveAll() / tryResolveAll() enforce this rule.
Documentation
- Added DI container comparison table to all multilingual README files
Full Changelog: v2.2.0...v2.3.0
v2.2.0
New Features
Multi-binding with resolveAll() / tryResolveAll()
Multiple factories can now be registered for the same token. resolve() returns the instance from the last registered factory, while resolveAll() returns instances from all factories in registration order.
const container = createContainer()
.registerSingleton(Logger, () => new ConsoleLogger())
.registerSingleton(Logger, () => new FileLogger());
container.resolve(Logger); // FileLogger (last registered)
container.resolveAll(Logger); // [ConsoleLogger, FileLogger]tryResolveAll() returns undefined instead of throwing when the token is not registered.
Per-registration singleton cache
Singleton caching is now per-registration rather than per-token. Each registered factory maintains its own independent cache, enabling correct behavior with multi-binding.
Lazy resolution with lazy()
The new katagami/lazy subpath export provides lazy() — a Proxy-based deferred instantiation function. The instance is not created until the first property access, then cached for subsequent accesses.
import { lazy } from 'katagami/lazy';
const service = lazy(container, HeavyService);
// HeavyService is NOT instantiated yet
service.process(); // instance created here, then cachedOnly sync class tokens are supported — async tokens and PropertyKey tokens are rejected at the type level.
Bug Fixes
- Removed unused
ServiceAclass from multi.test.ts files
Full Changelog: v2.1.0...v2.2.0
v2.1.0
New Features
Module composition with use()
Containers can now be composed via use() to group and reuse registrations. Define reusable modules as containers and apply them to other containers — only registration entries are copied, singleton instance caches are not shared.
const authModule = createContainer()
.registerSingleton(AuthService, () => new AuthService())
.registerSingleton(TokenService, () => new TokenService());
const container = createContainer()
.use(authModule)
.registerSingleton(UserService, r => new UserService(r.resolve(AuthService)));Optional resolution with tryResolve()
tryResolve() returns undefined for unregistered tokens instead of throwing ContainerError. Useful for optional dependencies in factories.
const analytics = container.tryResolve(Analytics);
// ^? Analytics | undefinedDisposableContainer / DisposableScope types
The disposable() wrapper now returns narrowed types (DisposableContainer or DisposableScope) that only expose resolve and tryResolve — registration methods are excluded at the type level, preventing accidental registration on a potentially-disposed container.
createScope() accepts disposable sources
createScope() now accepts DisposableContainer and DisposableScope in addition to Container and Scope.
Documentation
- Updated all multilingual README files with new API documentation
Full Changelog: v2.0.0...v2.1.0
v2.0.0
Breaking Changes
Tree-shakeable architecture with subpath exports
The library has been restructured into independent subpath exports for optimal tree-shaking:
katagami— Core container (createContainer,Container,ContainerError)katagami/scope— Scoped child containers (createScope,Scope)katagami/disposable— TC39 Explicit Resource Management (disposable())
Import only what you use — unused entry points are completely eliminated from the bundle. The package now declares sideEffects: false.
Disposal extracted to katagami/disposable
Container no longer implements AsyncDisposable directly. Instead, wrap a container or scope with the disposable() function from katagami/disposable to attach [Symbol.asyncDispose].
// Before (v1.x)
await using container = createContainer().registerSingleton(DB, () => new Database());
// After (v2.x)
import { disposable } from 'katagami/disposable';
await using container = disposable(
createContainer().registerSingleton(DB, () => new Database())
);createScope() moved to katagami/scope
container.createScope() method has been removed. Use the standalone createScope(container) function from katagami/scope instead.
// Before (v1.x)
const scope = container.createScope();
// After (v2.x)
import { createScope } from 'katagami/scope';
const scope = createScope(container);Internal state accessor
Container internals are now exposed via a Symbol-keyed [INTERNALS] property, enabling extension modules (scope, disposable) to access internal state without coupling to class internals.
Other
- Added multilingual README (Japanese, Korean, Chinese, Spanish, German, French)
Full Changelog: v1.1.0...v2.0.0
v1.1.0
Maintenance
- Updated version to 1.1.0
- Fixed repository URL in
package.json
Full Changelog: v1.0.0...v1.1.0