injecute
1.0.0
Major Changes
#12
e4adcafThanks @Masyaka! - New resolution core. Registrations are plain data and the container that runs a factory owns its result.nullandundefinedare valid service values (0.x threw "No service registered"). Migration- A dependency on the key being registered resolves the previous definition, so a child can decorate a parent service:
fork().addSingleton('logger', (l) => wrap(l), ['logger']). Migration - Middlewares run once per
get()instead of once per container level. extend()accepts extensions that return a child container, as documented.keyslists overridden keys once;fork()keeps the container subclass; registration no longer mutates thedependenciesarray you pass; the circular-dependency check is linear instead of exponential.- Removed:
flatten()(usefork({ isolated: true })),getFactory(),entryTypeKey,firstResult, theEntryTypetype (nowRegistrationKind) and thebeforeResolving/afterResolving/beforeReplacedoptions. New:getRegistration(key). Thereplaceevent reportsprevious: RegistrationInfo. Migration
#12
f0b3583Thanks @Masyaka! - Errors with codes, paths and fixes. Every error is anInjecuteErrorwith a stablecode, the resolutionpath, adocslink and a hint on how to fix it; unknown keys get "did you mean" suggestions. Errors thrown by factories are wrapped asINJECUTE_RESOLUTION_FAILEDwith the original ascause. Classes that cannot be detected (ES5-compiled, bound) are reported asINJECUTE_CLASS_NOT_CONSTRUCTEDwith aconstruct()hint, on V8, JavaScriptCore and SpiderMonkey. Migration#12
9d3875cThanks @Masyaka! - ESM only. injecute now ships a single ES module build and requires Node.js ≥ 22 (require('injecute')keeps working there throughrequire(esm)). The package works unchanged in Deno, Bun, browsers and CDNs (esm.sh, jsDelivr), and ships its TypeScript sources so go-to-definition lands on real code. See Module format: ESM only.Removed the
utilsobject. Import helpers by name instead:utils.construct(X)→construct(X). The object broke the published type declarations for projects withskipLibCheck: false. See The utils object is removed.#15
f655b3eThanks @Masyaka! - A fork needsreplace: trueto override a parent's key. Registering a key that a parent container has now throwsINJECUTE_ALREADY_REGISTEREDunless{ replace: true }is passed, in plain forks, isolated forks, modules applied to a fork andnamespace()callbacks. Before, the fork registered it silently, and services registered in the parent kept the parent's instance without anything saying so. The error now says what the override reaches: in a plain fork it names the parent services that depend on the key (directly or through other services) and keep the parent's instance, and how to give them the fork's one.addAlias()takes{ replace: true }too.Before → after:
app.fork().addInstance('logger', requestLogger)→app.fork().addInstance('logger', requestLogger, { replace: true }). Decorating a parent's service in a fork needs it as well. See A fork needsreplace: trueto override a parent's key.#12
5065414Thanks @Masyaka! - Middlewares:(key, next, context). Middlewares no longer get the container asthis(arrow functions work);contextcarriescontainer, the resolutionpathanddepth.next()continues,next(otherKey)redirects. Forks inherit middlewares live,fork({ skipResolvers: true })is nowfork({ middlewares: false }), andunuse(middleware)removes one.use()returns the container itself. Migration#12
e398e8cThanks @Masyaka! - Typed namespaces with small declaration files.get('Billing')returns aServiceProviderof the namespace's services (it was an untyped container), also for nested namespaces. In the service map, the namespace name now holds aNamespacemarker (AsyncNamespacefor async containers), and each namespaced service is stored once, under its full key. Nested namespaces no longer multiply the container type, so an exported container with three levels of namespaces emits a.d.tsabout 4× smaller.ContainerServices<typeof app>['Billing']→NamespaceServices<typeof app, 'Billing'>. New types:Namespace,AsyncNamespace,ServiceType<S, K>,ServicesInNamespace<S, N>. Migration#12
f1c73daThanks @Masyaka! - Node.js 22.12 or newer. The minimum Node.js version is now 22.12, the first 22.x release whererequire()loads ES modules without a flag, so CommonJS code canrequire('injecute'). ES module imports worked on older 22.x releases, butrequire()didn't.Before → after: Node.js ≥ 22 → Node.js ≥ 22.12. See Module format: ESM only.
#17
dbcfd55Thanks @Masyaka! -replace: trueneeds something to replace. Registering a key with{ replace: true }when neither the container nor a parent has that key now throws the newINJECUTE_NOTHING_TO_REPLACEerror. Before, it quietly registered a new service, so a typo in a test override, or a module that wasn't applied, left the original in place without anything saying so. This applies toaddSingleton,addTransient,addInstanceandaddAlias, in every kind of container, module and namespace.Before → after:
new DIContainer().addInstance('config', cfg, { replace: true })(first registration) →new DIContainer().addInstance('config', cfg). See A fork needsreplace: trueto override a parent's key.#12
d5391ceThanks @Masyaka! -preload()returns a promise that settles once async services are created and rejects with the first failure (before, a failing async service became an unhandled rejection). Synchronous failures still throw immediately.preload(app)→await preload(app). Migration#12
4989f19Thanks @Masyaka! - Container roles and a new type layer.ServiceProvider<S>(resolve + inspect) ⊂ServiceRegistry<S>(+ register) ⊂DIContainer<S>(+ middlewares, events, fork, reset, dispose). The chain keeps its type, so.use()works after.add*(); modules can be plain functions overServiceRegistry<{ …what they need }>andextend()reports missing services by name; namespace providers,getParent()and middleware/event containers are read-only.IDIContaineris a deprecated alias ofServiceRegistry. MigrationClasses are registered directly (
addSingleton('users', UserRepository, ['db']));construct()stays for ES5-compiled and bound classes.optional(key)replaces the'undefined'dependency key.Better type errors: a typo in a dependency key is reported on the key with "Did you mean", and errors show flat service maps instead of nested helper types.
Renamed:
get(key, { allowUnresolved })→{ optional },has(key, false)→has(key, { local: true }),new DIContainer({ parentContainer })→parent.fork(),ArgumentsKey→ServiceKey,DependenciesToTypes→ResolveDependencies. The default export and unused helper types are removed. Migration
Minor Changes
#12
636bb1eThanks @Masyaka! - Docs and an Agent Skill in the package. The package now contains its documentation as Markdown (llms.txtandlib/docs/: guides, concepts, error codes, the migration guide) and an Agent Skill (skills/injecute), so coding agents read the docs of the installed version. See Using injecute with AI agents.#12
d5391ceThanks @Masyaka! -AsyncDIContainer. A container for graphs with async services: every dependency is awaited before a factory runs, so factories and classes receive resolved values withoutdefer(), and a factory returningPromise<T>registersT.get(),call(),injecute()and resolvers return promises (a missing key rejects instead of throwing). Forks stay async, singletons are created once under concurrent resolutions and retried after a failure, anddispose()releases resolved instances. Modules for it take the newAsyncServiceRegistry, consumers the newAsyncServiceProvider; the syncServiceRegistry/ServiceProvidertypes don't accept it.buildServicesGraph()andpreload()accept async containers. See Async services.#12
fb1c878Thanks @Masyaka! - Async singletons are retried, wrapped and disposed. When a singleton factory returns a promise, the container caches the promise as before, but:- a rejected promise is no longer cached forever: the next
get()calls the factory again; - the rejection is wrapped in
INJECUTE_RESOLUTION_FAILEDwith the resolutionpath(the original error iscause), like errors thrown synchronously. Checkerror.causeif you caught your own error types from an awaitedget(); dispose()releases the resolved instance, not the promise, soaddSingleton('db', async () => new Pool())closes the pool. A singleton still being created is awaited first; one whose promise rejected is skipped.disposefunctions in the options receive the resolved value, and their parameter is typed that way (also foraddInstancepromises registered withdispose).
- a rejected promise is no longer cached forever: the next
#12
51bc8b2Thanks @Masyaka! - Smaller browser bundles: error hints are left out of production builds. Bundlers that build for browsers now leave the "how to fix it" hints out of injecute's error messages, which saves about 1.5 kB gzipped. Development builds with Vite and webpack keep them, through thedevelopmentcondition, and so do Node.js, Deno, Bun, server-side rendering and browsers without a bundler. Messages keep what happened and the docs link, anderror.codeanderror.docsare the same everywhere.To have the hints in any build, import the new
injecute/hintsentry point, for exampleif (import.meta.env.DEV) await import('injecute/hints'), orimport 'injecute/hints'for every build. With esbuild or Rolldown, adding thedevelopmentcondition works too. See Hints in browser bundles.#12
2e93631Thanks @Masyaka! -checkWiring(): check every service's wiring in one test.await checkWiring(container)reports every dependency key that nothing registers, without running any factory. With{ resolve: true }it also creates every service and reports the ones whose factory throws. Each failure is reported once, with the services that need it, and its factory runs once.{ keys }adds keys that must be registered, for services that the compiler can't see (found in files or registered by decorators). It rejects with aWiringError(INJECUTE_WIRING_FAILED) that lists every problem in its message and inerror.problems. See Check the wiring.#12
af752e8Thanks @Masyaka! -dispose()andawait using.await container.dispose()(also[Symbol.asyncDispose], soawait using scope = app.fork()works) releases every instance the container owns, dependents first. Singletons are disposed with[Symbol.asyncDispose]/[Symbol.dispose]when they have one, or with adisposefunction in their registration options (dispose: falseopts out).addInstancevalues are disposed only with{ dispose: true | fn }. Namespace containers are disposed with their parent; forks dispose what they own. Afterwards the container throws on use, and adisposeevent is emitted.#12
e398e8cThanks @Masyaka! - Faster type-checking. Nothing changes in how you write registrations.- Code inside
namespace()and inlineextend()callbacks typechecks up to about 3× faster: a callback's registry keeps the container's services as one fixed type and grows only its own additions, so lookups into the container are computed once per callback. - Each
namespace()call no longer gets slower with the number of namespaces before it. - Long
addSingletonchains need about a third fewer type instantiations. - What a module (
extend()) or namespace adds becomes one object in the service map instead of one entry per registration: registrations and forks after the modules typecheck about 25–45% faster, and hovers and declaration files show one object per module.
A
namespace()orextend()callback must return the registry it received, as the types always intended; returning another container is now a type error.ServiceRegistry<S, A>now keeps the given services (S) apart from its own additions (A), which shows up in inferred module types; aDIContainer<S>is aServiceRegistry<{}, S>. See Container roles.Exported containers. The TypeScript guide now shows naming the service map with
interface AppServices extends ContainerServices<typeof built> {}for packages that export a container: declaration files get about half the size. See TypeScript.- Code inside
#12
d663616Thanks @Masyaka! -fork({ isolated: true }). An isolated fork runs and caches every service it resolves, including services registered in its parents. Overriding a dependency in the fork (for example a fake database in a test) therefore reaches the whole graph, while the parent stays untouched. Namespaces are re-created inside the fork, and forks of an isolated fork share its instances. Replaces the removedflatten().#12
95d907dThanks @Masyaka! - Lifecycle hooks:startLifecycle()andcreateLifecycle(). Modules can now run code when the app starts and undo it when it stops, without the composition root knowing each of them. A hook is a singleton registered under a stage key,addSingleton(lifecycle.start('consumer'), (consumer) => { consumer.start(); return () => consumer.stop(); }, ['consumer']).const running = await startLifecycle(app)runs the stagesinit→start→ready, including hooks in namespaces.running.stop()undoes them in reverse, then disposes the container. A failed start undoes what ran. Options:concurrent,dispose: false,signal(timeouts, also forstop()) andonError. Apps can define their own stages withcreateLifecycle([...]); stages are tags, socollect(lifecycle.start)works too.startable(key, factory, deps, { start, stop })registers a service and its hook in one call (in aDIContaineror anAsyncDIContainer), hooks receive the start signal by depending onstartSignal, hook types are checked when they are registered, andonHookreports each hook and undo with its duration. See Startup and shutdown.New error codes:
INJECUTE_INVALID_HOOK(a hook isn't a singleton, or depends on another hook),INJECUTE_UNDO_FAILED(an undo function failed or didn't finish, in thecauseofINJECUTE_DISPOSE_FAILED) andINJECUTE_LIFECYCLE_STOPPED(starting a stopped lifecycle). A key that ends with:init,:startor:readyis a hook when you runstartLifecycle(); rename such keys if they aren't hooks.#12
e398e8cThanks @Masyaka! -seal().app.seal()ends the registrations of a container: afterwardsadd*,namespace()andextend()throw the newINJECUTE_SEALEDerror, and the returned type (SealedDIContainer/SealedAsyncDIContainer) has no registration methods. Resolving, middlewares, events,reset()anddispose()keep working, and forks stay open, so request scopes and tests fork the sealed root. The sealed type holds the services as one object instead of an intersection of every registration, so forks built on it typecheck about 25% faster and hovers stay readable. See Sealing the composition root.#12
95d907dThanks @Masyaka! - Extension points with tags:createTag()andcollect(). A tag names an extension point that modules contribute services to.const route = createTag('route').of<Route>(); a module registersaddSingleton(route('orders'), (db) => ordersRoute(db), ['db'])(the key'orders:route'), and the service is checked againstRouteat compile time; the owner depends oncollect(route)and receives aRoute[]of every service under the tag, including namespaces (Orders.orders:route), in registration order. Services are collected in the container that creates the owner, so isolated forks see their replacements and additions; anAsyncDIContainerawaits them. See Extension points with tags.DependencyInfo(fromgetRegistration()) has a new variant,{ type: 'collect', tag }: code that switches over every dependency type should handle it.Typing: keys built by a tag are
TaggedKeys. In the service map they are plain strings, andget(),call(),createResolver(), dependency lists andoptional()accept them.addSingleton,addTransient,addInstanceandaddAliascheck what is registered under them against the tag's type.Factoryhas an optional fourth type parameter, the type the factory must produce.
Patch Changes
#12
b421ab3Thanks @Masyaka! - Docs: adapting a module's dependencies. A new section in Containers, forks and modules shows how to apply a module whose dependencies the host has under other keys or in another shape: register adapters (addAlias(), or a factory that derives the value) in a namespace beforeextend(). The Agent Skill follows the same advice.#12
e398e8cThanks @Masyaka! -replace: truechanges a service's type. Registering a key again withreplace: truenow gives the service the new type; before, the old and the new type were intersected, so replacing astringwith anumbermadeget()returnnever. In a fork, passreplace: truewhen an override changes the type. A module that replaces a service of the container it is applied to still intersects the types. See Decorating a service.#12
007441eThanks @Masyaka! - New guides: request context and per-request state.Request context: pass request data that services need (trace id, tenant, user) through a context accessor backed by
AsyncLocalStorage. The host registers it first in the root container, and singletons read it when they use it. Covers Fastify, queue consumers, logging, pitfalls and testing.Per-request state: patterns to try before a fork per request, each with a tested example:
- an argument
- a factory with
using(a unit of work) - one instance per context (caches, DataLoaders)
- a strategy chosen by the tenant
It also says when a fork is still the right tool, with a table for choosing.
The docs shipped in the package, the Agent Skill and the
fork()TSDoc follow the same advice.
1.0.0-rc.2
Major Changes
#15
f655b3eThanks @Masyaka! - A fork needsreplace: trueto override a parent's key. Registering a key that a parent container has now throwsINJECUTE_ALREADY_REGISTEREDunless{ replace: true }is passed, in plain forks, isolated forks, modules applied to a fork andnamespace()callbacks. Before, the fork registered it silently, and services registered in the parent kept the parent's instance without anything saying so. The error now says what the override reaches: in a plain fork it names the parent services that depend on the key (directly or through other services) and keep the parent's instance, and how to give them the fork's one.addAlias()takes{ replace: true }too.Before → after:
app.fork().addInstance('logger', requestLogger)→app.fork().addInstance('logger', requestLogger, { replace: true }). Decorating a parent's service in a fork needs it as well. See A fork needsreplace: trueto override a parent's key.#12
f1c73daThanks @Masyaka! - Node.js 22.12 or newer. The minimum Node.js version is now 22.12, the first 22.x release whererequire()loads ES modules without a flag, so CommonJS code canrequire('injecute'). ES module imports worked on older 22.x releases, butrequire()didn't.Before → after: Node.js ≥ 22 → Node.js ≥ 22.12. See Module format: ESM only.
#17
dbcfd55Thanks @Masyaka! -replace: trueneeds something to replace. Registering a key with{ replace: true }when neither the container nor a parent has that key now throws the newINJECUTE_NOTHING_TO_REPLACEerror. Before, it quietly registered a new service, so a typo in a test override, or a module that wasn't applied, left the original in place without anything saying so. This applies toaddSingleton,addTransient,addInstanceandaddAlias, in every kind of container, module and namespace.Before → after:
new DIContainer().addInstance('config', cfg, { replace: true })(first registration) →new DIContainer().addInstance('config', cfg). See A fork needsreplace: trueto override a parent's key.
Minor Changes
#12
51bc8b2Thanks @Masyaka! - Smaller browser bundles: error hints are left out of production builds. Bundlers that build for browsers now leave the "how to fix it" hints out of injecute's error messages, which saves about 1.5 kB gzipped. Development builds with Vite and webpack keep them, through thedevelopmentcondition, and so do Node.js, Deno, Bun, server-side rendering and browsers without a bundler. Messages keep what happened and the docs link, anderror.codeanderror.docsare the same everywhere.To have the hints in any build, import the new
injecute/hintsentry point, for exampleif (import.meta.env.DEV) await import('injecute/hints'), orimport 'injecute/hints'for every build. With esbuild or Rolldown, adding thedevelopmentcondition works too. See Hints in browser bundles.#12
2e93631Thanks @Masyaka! -checkWiring(): check every service's wiring in one test.await checkWiring(container)reports every dependency key that nothing registers, without running any factory. With{ resolve: true }it also creates every service and reports the ones whose factory throws. Each failure is reported once, with the services that need it, and its factory runs once.{ keys }adds keys that must be registered, for services that the compiler can't see (found in files or registered by decorators). It rejects with aWiringError(INJECUTE_WIRING_FAILED) that lists every problem in its message and inerror.problems. See Check the wiring.
1.0.0-rc.1
Minor Changes
#12
95d907dThanks @Masyaka! - Lifecycle hooks:startLifecycle()andcreateLifecycle(). Modules can now run code when the app starts and undo it when it stops, without the composition root knowing each of them. A hook is a singleton registered under a stage key,addSingleton(lifecycle.start('consumer'), (consumer) => { consumer.start(); return () => consumer.stop(); }, ['consumer']).const running = await startLifecycle(app)runs the stagesinit→start→ready, including hooks in namespaces.running.stop()undoes them in reverse, then disposes the container. A failed start undoes what ran. Options:concurrent,dispose: false,signal(timeouts, also forstop()) andonError. Apps can define their own stages withcreateLifecycle([...]); stages are tags, socollect(lifecycle.start)works too.startable(key, factory, deps, { start, stop })registers a service and its hook in one call (in aDIContaineror anAsyncDIContainer), hooks receive the start signal by depending onstartSignal, hook types are checked when they are registered, andonHookreports each hook and undo with its duration. See Startup and shutdown.New error codes:
INJECUTE_INVALID_HOOK(a hook isn't a singleton, or depends on another hook),INJECUTE_UNDO_FAILED(an undo function failed or didn't finish, in thecauseofINJECUTE_DISPOSE_FAILED) andINJECUTE_LIFECYCLE_STOPPED(starting a stopped lifecycle). A key that ends with:init,:startor:readyis a hook when you runstartLifecycle(); rename such keys if they aren't hooks.#12
95d907dThanks @Masyaka! - Extension points with tags:createTag()andcollect(). A tag names an extension point that modules contribute services to.const route = createTag('route').of<Route>(); a module registersaddSingleton(route('orders'), (db) => ordersRoute(db), ['db'])(the key'orders:route'), and the service is checked againstRouteat compile time; the owner depends oncollect(route)and receives aRoute[]of every service under the tag, including namespaces (Orders.orders:route), in registration order. Services are collected in the container that creates the owner, so isolated forks see their replacements and additions; anAsyncDIContainerawaits them. See Extension points with tags.DependencyInfo(fromgetRegistration()) has a new variant,{ type: 'collect', tag }: code that switches over every dependency type should handle it.Typing: keys built by a tag are
TaggedKeys. In the service map they are plain strings, andget(),call(),createResolver(), dependency lists andoptional()accept them.addSingleton,addTransient,addInstanceandaddAliascheck what is registered under them against the tag's type.Factoryhas an optional fourth type parameter, the type the factory must produce.
Patch Changes
- #12
b421ab3Thanks @Masyaka! - Docs: adapting a module's dependencies. A new section in Containers, forks and modules shows how to apply a module whose dependencies the host has under other keys or in another shape: register adapters (addAlias(), or a factory that derives the value) in a namespace beforeextend(). The Agent Skill follows the same advice.
1.0.0-rc.0
Major Changes
#11
e4adcafThanks @github-actions! - New resolution core. Registrations are plain data and the container that runs a factory owns its result.nullandundefinedare valid service values (0.x threw "No service registered"). Migration- A dependency on the key being registered resolves the previous definition, so a child can decorate a parent service:
fork().addSingleton('logger', (l) => wrap(l), ['logger']). Migration - Middlewares run once per
get()instead of once per container level. extend()accepts extensions that return a child container, as documented.keyslists overridden keys once;fork()keeps the container subclass; registration no longer mutates thedependenciesarray you pass; the circular-dependency check is linear instead of exponential.- Removed:
flatten()(usefork({ isolated: true })),getFactory(),entryTypeKey,firstResult, theEntryTypetype (nowRegistrationKind) and thebeforeResolving/afterResolving/beforeReplacedoptions. New:getRegistration(key). Thereplaceevent reportsprevious: RegistrationInfo. Migration
#11
f0b3583Thanks @github-actions! - Errors with codes, paths and fixes. Every error is anInjecuteErrorwith a stablecode, the resolutionpath, adocslink and a hint on how to fix it; unknown keys get "did you mean" suggestions. Errors thrown by factories are wrapped asINJECUTE_RESOLUTION_FAILEDwith the original ascause. Classes that cannot be detected (ES5-compiled, bound) are reported asINJECUTE_CLASS_NOT_CONSTRUCTEDwith aconstruct()hint, on V8, JavaScriptCore and SpiderMonkey. Migration#11
9d3875cThanks @github-actions! - ESM only. injecute now ships a single ES module build and requires Node.js ≥ 22 (require('injecute')keeps working there throughrequire(esm)). The package works unchanged in Deno, Bun, browsers and CDNs (esm.sh, jsDelivr), and ships its TypeScript sources so go-to-definition lands on real code. See Module format: ESM only.Removed the
utilsobject. Import helpers by name instead:utils.construct(X)→construct(X). The object broke the published type declarations for projects withskipLibCheck: false. See The utils object is removed.#11
5065414Thanks @github-actions! - Middlewares:(key, next, context). Middlewares no longer get the container asthis(arrow functions work);contextcarriescontainer, the resolutionpathanddepth.next()continues,next(otherKey)redirects. Forks inherit middlewares live,fork({ skipResolvers: true })is nowfork({ middlewares: false }), andunuse(middleware)removes one.use()returns the container itself. Migration#11
e398e8cThanks @github-actions! - Typed namespaces with small declaration files.get('Billing')returns aServiceProviderof the namespace's services (it was an untyped container), also for nested namespaces. In the service map, the namespace name now holds aNamespacemarker (AsyncNamespacefor async containers), and each namespaced service is stored once, under its full key. Nested namespaces no longer multiply the container type, so an exported container with three levels of namespaces emits a.d.tsabout 4× smaller.ContainerServices<typeof app>['Billing']→NamespaceServices<typeof app, 'Billing'>. New types:Namespace,AsyncNamespace,ServiceType<S, K>,ServicesInNamespace<S, N>. Migration#11
d5391ceThanks @github-actions! -preload()returns a promise that settles once async services are created and rejects with the first failure (before, a failing async service became an unhandled rejection). Synchronous failures still throw immediately.preload(app)→await preload(app). Migration#11
4989f19Thanks @github-actions! - Container roles and a new type layer.ServiceProvider<S>(resolve + inspect) ⊂ServiceRegistry<S>(+ register) ⊂DIContainer<S>(+ middlewares, events, fork, reset, dispose). The chain keeps its type, so.use()works after.add*(); modules can be plain functions overServiceRegistry<{ …what they need }>andextend()reports missing services by name; namespace providers,getParent()and middleware/event containers are read-only.IDIContaineris a deprecated alias ofServiceRegistry. MigrationClasses are registered directly (
addSingleton('users', UserRepository, ['db']));construct()stays for ES5-compiled and bound classes.optional(key)replaces the'undefined'dependency key.Better type errors: a typo in a dependency key is reported on the key with "Did you mean", and errors show flat service maps instead of nested helper types.
Renamed:
get(key, { allowUnresolved })→{ optional },has(key, false)→has(key, { local: true }),new DIContainer({ parentContainer })→parent.fork(),ArgumentsKey→ServiceKey,DependenciesToTypes→ResolveDependencies. The default export and unused helper types are removed. Migration
Minor Changes
#11
636bb1eThanks @github-actions! - Docs and an Agent Skill in the package. The package now contains its documentation as Markdown (llms.txtandlib/docs/: guides, concepts, error codes, the migration guide) and an Agent Skill (skills/injecute), so coding agents read the docs of the installed version. See Using injecute with AI agents.#11
d5391ceThanks @github-actions! -AsyncDIContainer. A container for graphs with async services: every dependency is awaited before a factory runs, so factories and classes receive resolved values withoutdefer(), and a factory returningPromise<T>registersT.get(),call(),injecute()and resolvers return promises (a missing key rejects instead of throwing). Forks stay async, singletons are created once under concurrent resolutions and retried after a failure, anddispose()releases resolved instances. Modules for it take the newAsyncServiceRegistry, consumers the newAsyncServiceProvider; the syncServiceRegistry/ServiceProvidertypes don't accept it.buildServicesGraph()andpreload()accept async containers. See Async services.#11
fb1c878Thanks @github-actions! - Async singletons are retried, wrapped and disposed. When a singleton factory returns a promise, the container caches the promise as before, but:- a rejected promise is no longer cached forever: the next
get()calls the factory again; - the rejection is wrapped in
INJECUTE_RESOLUTION_FAILEDwith the resolutionpath(the original error iscause), like errors thrown synchronously. Checkerror.causeif you caught your own error types from an awaitedget(); dispose()releases the resolved instance, not the promise, soaddSingleton('db', async () => new Pool())closes the pool. A singleton still being created is awaited first; one whose promise rejected is skipped.disposefunctions in the options receive the resolved value, and their parameter is typed that way (also foraddInstancepromises registered withdispose).
- a rejected promise is no longer cached forever: the next
#11
af752e8Thanks @github-actions! -dispose()andawait using.await container.dispose()(also[Symbol.asyncDispose], soawait using scope = app.fork()works) releases every instance the container owns, dependents first. Singletons are disposed with[Symbol.asyncDispose]/[Symbol.dispose]when they have one, or with adisposefunction in their registration options (dispose: falseopts out).addInstancevalues are disposed only with{ dispose: true | fn }. Namespace containers are disposed with their parent; forks dispose what they own. Afterwards the container throws on use, and adisposeevent is emitted.#11
e398e8cThanks @github-actions! - Faster type-checking. Nothing changes in how you write registrations.- Code inside
namespace()and inlineextend()callbacks typechecks up to about 3× faster: a callback's registry keeps the container's services as one fixed type and grows only its own additions, so lookups into the container are computed once per callback. - Each
namespace()call no longer gets slower with the number of namespaces before it. - Long
addSingletonchains need about a third fewer type instantiations. - What a module (
extend()) or namespace adds becomes one object in the service map instead of one entry per registration: registrations and forks after the modules typecheck about 25–45% faster, and hovers and declaration files show one object per module.
A
namespace()orextend()callback must return the registry it received, as the types always intended; returning another container is now a type error.ServiceRegistry<S, A>now keeps the given services (S) apart from its own additions (A), which shows up in inferred module types; aDIContainer<S>is aServiceRegistry<{}, S>. See Container roles.Exported containers. The TypeScript guide now shows naming the service map with
interface AppServices extends ContainerServices<typeof built> {}for packages that export a container: declaration files get about half the size. See TypeScript.- Code inside
#11
d663616Thanks @github-actions! -fork({ isolated: true }). An isolated fork runs and caches every service it resolves, including services registered in its parents. Overriding a dependency in the fork (for example a fake database in a test) therefore reaches the whole graph, while the parent stays untouched. Namespaces are re-created inside the fork, and forks of an isolated fork share its instances. Replaces the removedflatten().#11
e398e8cThanks @github-actions! -seal().app.seal()ends the registrations of a container: afterwardsadd*,namespace()andextend()throw the newINJECUTE_SEALEDerror, and the returned type (SealedDIContainer/SealedAsyncDIContainer) has no registration methods. Resolving, middlewares, events,reset()anddispose()keep working, and forks stay open, so request scopes and tests fork the sealed root. The sealed type holds the services as one object instead of an intersection of every registration, so forks built on it typecheck about 25% faster and hovers stay readable. See Sealing the composition root.
Patch Changes
#11
e398e8cThanks @github-actions! -replace: truechanges a service's type. Registering a key again withreplace: truenow gives the service the new type; before, the old and the new type were intersected, so replacing astringwith anumbermadeget()returnnever. In a fork, passreplace: truewhen an override changes the type. A module that replaces a service of the container it is applied to still intersects the types. See Decorating a service.#11
007441eThanks @github-actions! - New guides: request context and per-request state.Request context: pass request data that services need (trace id, tenant, user) through a context accessor backed by
AsyncLocalStorage. The host registers it first in the root container, and singletons read it when they use it. Covers Fastify, queue consumers, logging, pitfalls and testing.Per-request state: patterns to try before a fork per request, each with a tested example:
- an argument
- a factory with
using(a unit of work) - one instance per context (caches, DataLoaders)
- a strategy chosen by the tenant
It also says when a fork is still the right tool, with a table for choosing.
The docs shipped in the package, the Agent Skill and the
fork()TSDoc follow the same advice.