Documents injecute 1.x · Migrating from 0.x
Skip to content

injecute ​

1.0.0 ​

Major Changes ​

  • #12 e4adcaf Thanks @Masyaka! - New resolution core. Registrations are plain data and the container that runs a factory owns its result.

    • null and undefined are 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.
    • keys lists overridden keys once; fork() keeps the container subclass; registration no longer mutates the dependencies array you pass; the circular-dependency check is linear instead of exponential.
    • Removed: flatten() (use fork({ isolated: true })), getFactory(), entryTypeKey, firstResult, the EntryType type (now RegistrationKind) and the beforeResolving / afterResolving / beforeReplaced options. New: getRegistration(key). The replace event reports previous: RegistrationInfo. Migration
  • #12 f0b3583 Thanks @Masyaka! - Errors with codes, paths and fixes. Every error is an InjecuteError with a stable code, the resolution path, a docs link and a hint on how to fix it; unknown keys get "did you mean" suggestions. Errors thrown by factories are wrapped as INJECUTE_RESOLUTION_FAILED with the original as cause. Classes that cannot be detected (ES5-compiled, bound) are reported as INJECUTE_CLASS_NOT_CONSTRUCTED with a construct() hint, on V8, JavaScriptCore and SpiderMonkey. Migration

  • #12 9d3875c Thanks @Masyaka! - ESM only. injecute now ships a single ES module build and requires Node.js ≥ 22 (require('injecute') keeps working there through require(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 utils object. Import helpers by name instead: utils.construct(X) → construct(X). The object broke the published type declarations for projects with skipLibCheck: false. See The utils object is removed.

  • #15 f655b3e Thanks @Masyaka! - A fork needs replace: true to override a parent's key. Registering a key that a parent container has now throws INJECUTE_ALREADY_REGISTERED unless { replace: true } is passed, in plain forks, isolated forks, modules applied to a fork and namespace() 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 needs replace: true to override a parent's key.

  • #12 5065414 Thanks @Masyaka! - Middlewares: (key, next, context). Middlewares no longer get the container as this (arrow functions work); context carries container, the resolution path and depth. next() continues, next(otherKey) redirects. Forks inherit middlewares live, fork({ skipResolvers: true }) is now fork({ middlewares: false }), and unuse(middleware) removes one. use() returns the container itself. Migration

  • #12 e398e8c Thanks @Masyaka! - Typed namespaces with small declaration files. get('Billing') returns a ServiceProvider of the namespace's services (it was an untyped container), also for nested namespaces. In the service map, the namespace name now holds a Namespace marker (AsyncNamespace for 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.ts about 4× smaller. ContainerServices<typeof app>['Billing'] → NamespaceServices<typeof app, 'Billing'>. New types: Namespace, AsyncNamespace, ServiceType<S, K>, ServicesInNamespace<S, N>. Migration

  • #12 f1c73da Thanks @Masyaka! - Node.js 22.12 or newer. The minimum Node.js version is now 22.12, the first 22.x release where require() loads ES modules without a flag, so CommonJS code can require('injecute'). ES module imports worked on older 22.x releases, but require() didn't.

    Before → after: Node.js ≥ 22 → Node.js ≥ 22.12. See Module format: ESM only.

  • #17 dbcfd55 Thanks @Masyaka! - replace: true needs something to replace. Registering a key with { replace: true } when neither the container nor a parent has that key now throws the new INJECUTE_NOTHING_TO_REPLACE error. 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 to addSingleton, addTransient, addInstance and addAlias, 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 needs replace: true to override a parent's key.

  • #12 d5391ce Thanks @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 4989f19 Thanks @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 over ServiceRegistry<{ …what they need }> and extend() reports missing services by name; namespace providers, getParent() and middleware/event containers are read-only. IDIContainer is a deprecated alias of ServiceRegistry. Migration

    Classes 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 636bb1e Thanks @Masyaka! - Docs and an Agent Skill in the package. The package now contains its documentation as Markdown (llms.txt and lib/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 d5391ce Thanks @Masyaka! - AsyncDIContainer. A container for graphs with async services: every dependency is awaited before a factory runs, so factories and classes receive resolved values without defer(), and a factory returning Promise<T> registers T. 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, and dispose() releases resolved instances. Modules for it take the new AsyncServiceRegistry, consumers the new AsyncServiceProvider; the sync ServiceRegistry / ServiceProvider types don't accept it. buildServicesGraph() and preload() accept async containers. See Async services.

  • #12 fb1c878 Thanks @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_FAILED with the resolution path (the original error is cause), like errors thrown synchronously. Check error.cause if you caught your own error types from an awaited get();
    • dispose() releases the resolved instance, not the promise, so addSingleton('db', async () => new Pool()) closes the pool. A singleton still being created is awaited first; one whose promise rejected is skipped. dispose functions in the options receive the resolved value, and their parameter is typed that way (also for addInstance promises registered with dispose).
  • #12 51bc8b2 Thanks @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 the development condition, and so do Node.js, Deno, Bun, server-side rendering and browsers without a bundler. Messages keep what happened and the docs link, and error.code and error.docs are the same everywhere.

    To have the hints in any build, import the new injecute/hints entry point, for example if (import.meta.env.DEV) await import('injecute/hints'), or import 'injecute/hints' for every build. With esbuild or Rolldown, adding the development condition works too. See Hints in browser bundles.

  • #12 2e93631 Thanks @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 a WiringError (INJECUTE_WIRING_FAILED) that lists every problem in its message and in error.problems. See Check the wiring.

  • #12 af752e8 Thanks @Masyaka! - dispose() and await using. await container.dispose() (also [Symbol.asyncDispose], so await 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 a dispose function in their registration options (dispose: false opts out). addInstance values 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 a dispose event is emitted.

  • #12 e398e8c Thanks @Masyaka! - Faster type-checking. Nothing changes in how you write registrations.

    • Code inside namespace() and inline extend() 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 addSingleton chains 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() or extend() 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; a DIContainer<S> is a ServiceRegistry<{}, 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.

  • #12 d663616 Thanks @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 removed flatten().

  • #12 95d907d Thanks @Masyaka! - Lifecycle hooks: startLifecycle() and createLifecycle(). 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 stages init → 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 for stop()) and onError. Apps can define their own stages with createLifecycle([...]); stages are tags, so collect(lifecycle.start) works too. startable(key, factory, deps, { start, stop }) registers a service and its hook in one call (in a DIContainer or an AsyncDIContainer), hooks receive the start signal by depending on startSignal, hook types are checked when they are registered, and onHook reports 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 the cause of INJECUTE_DISPOSE_FAILED) and INJECUTE_LIFECYCLE_STOPPED (starting a stopped lifecycle). A key that ends with :init, :start or :ready is a hook when you run startLifecycle(); rename such keys if they aren't hooks.

  • #12 e398e8c Thanks @Masyaka! - seal(). app.seal() ends the registrations of a container: afterwards add*, namespace() and extend() throw the new INJECUTE_SEALED error, and the returned type (SealedDIContainer / SealedAsyncDIContainer) has no registration methods. Resolving, middlewares, events, reset() and dispose() 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 95d907d Thanks @Masyaka! - Extension points with tags: createTag() and collect(). A tag names an extension point that modules contribute services to. const route = createTag('route').of<Route>(); a module registers addSingleton(route('orders'), (db) => ordersRoute(db), ['db']) (the key 'orders:route'), and the service is checked against Route at compile time; the owner depends on collect(route) and receives a Route[] 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; an AsyncDIContainer awaits them. See Extension points with tags.

    DependencyInfo (from getRegistration()) 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, and get(), call(), createResolver(), dependency lists and optional() accept them. addSingleton, addTransient, addInstance and addAlias check what is registered under them against the tag's type. Factory has an optional fourth type parameter, the type the factory must produce.

Patch Changes ​

  • #12 b421ab3 Thanks @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 before extend(). The Agent Skill follows the same advice.

  • #12 e398e8c Thanks @Masyaka! - replace: true changes a service's type. Registering a key again with replace: true now gives the service the new type; before, the old and the new type were intersected, so replacing a string with a number made get() return never. In a fork, pass replace: true when 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 007441e Thanks @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 f655b3e Thanks @Masyaka! - A fork needs replace: true to override a parent's key. Registering a key that a parent container has now throws INJECUTE_ALREADY_REGISTERED unless { replace: true } is passed, in plain forks, isolated forks, modules applied to a fork and namespace() 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 needs replace: true to override a parent's key.

  • #12 f1c73da Thanks @Masyaka! - Node.js 22.12 or newer. The minimum Node.js version is now 22.12, the first 22.x release where require() loads ES modules without a flag, so CommonJS code can require('injecute'). ES module imports worked on older 22.x releases, but require() didn't.

    Before → after: Node.js ≥ 22 → Node.js ≥ 22.12. See Module format: ESM only.

  • #17 dbcfd55 Thanks @Masyaka! - replace: true needs something to replace. Registering a key with { replace: true } when neither the container nor a parent has that key now throws the new INJECUTE_NOTHING_TO_REPLACE error. 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 to addSingleton, addTransient, addInstance and addAlias, 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 needs replace: true to override a parent's key.

Minor Changes ​

  • #12 51bc8b2 Thanks @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 the development condition, and so do Node.js, Deno, Bun, server-side rendering and browsers without a bundler. Messages keep what happened and the docs link, and error.code and error.docs are the same everywhere.

    To have the hints in any build, import the new injecute/hints entry point, for example if (import.meta.env.DEV) await import('injecute/hints'), or import 'injecute/hints' for every build. With esbuild or Rolldown, adding the development condition works too. See Hints in browser bundles.

  • #12 2e93631 Thanks @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 a WiringError (INJECUTE_WIRING_FAILED) that lists every problem in its message and in error.problems. See Check the wiring.

1.0.0-rc.1 ​

Minor Changes ​

  • #12 95d907d Thanks @Masyaka! - Lifecycle hooks: startLifecycle() and createLifecycle(). 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 stages init → 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 for stop()) and onError. Apps can define their own stages with createLifecycle([...]); stages are tags, so collect(lifecycle.start) works too. startable(key, factory, deps, { start, stop }) registers a service and its hook in one call (in a DIContainer or an AsyncDIContainer), hooks receive the start signal by depending on startSignal, hook types are checked when they are registered, and onHook reports 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 the cause of INJECUTE_DISPOSE_FAILED) and INJECUTE_LIFECYCLE_STOPPED (starting a stopped lifecycle). A key that ends with :init, :start or :ready is a hook when you run startLifecycle(); rename such keys if they aren't hooks.

  • #12 95d907d Thanks @Masyaka! - Extension points with tags: createTag() and collect(). A tag names an extension point that modules contribute services to. const route = createTag('route').of<Route>(); a module registers addSingleton(route('orders'), (db) => ordersRoute(db), ['db']) (the key 'orders:route'), and the service is checked against Route at compile time; the owner depends on collect(route) and receives a Route[] 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; an AsyncDIContainer awaits them. See Extension points with tags.

    DependencyInfo (from getRegistration()) 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, and get(), call(), createResolver(), dependency lists and optional() accept them. addSingleton, addTransient, addInstance and addAlias check what is registered under them against the tag's type. Factory has an optional fourth type parameter, the type the factory must produce.

Patch Changes ​

  • #12 b421ab3 Thanks @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 before extend(). The Agent Skill follows the same advice.

1.0.0-rc.0 ​

Major Changes ​

  • #11 e4adcaf Thanks @github-actions! - New resolution core. Registrations are plain data and the container that runs a factory owns its result.

    • null and undefined are 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.
    • keys lists overridden keys once; fork() keeps the container subclass; registration no longer mutates the dependencies array you pass; the circular-dependency check is linear instead of exponential.
    • Removed: flatten() (use fork({ isolated: true })), getFactory(), entryTypeKey, firstResult, the EntryType type (now RegistrationKind) and the beforeResolving / afterResolving / beforeReplaced options. New: getRegistration(key). The replace event reports previous: RegistrationInfo. Migration
  • #11 f0b3583 Thanks @github-actions! - Errors with codes, paths and fixes. Every error is an InjecuteError with a stable code, the resolution path, a docs link and a hint on how to fix it; unknown keys get "did you mean" suggestions. Errors thrown by factories are wrapped as INJECUTE_RESOLUTION_FAILED with the original as cause. Classes that cannot be detected (ES5-compiled, bound) are reported as INJECUTE_CLASS_NOT_CONSTRUCTED with a construct() hint, on V8, JavaScriptCore and SpiderMonkey. Migration

  • #11 9d3875c Thanks @github-actions! - ESM only. injecute now ships a single ES module build and requires Node.js ≥ 22 (require('injecute') keeps working there through require(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 utils object. Import helpers by name instead: utils.construct(X) → construct(X). The object broke the published type declarations for projects with skipLibCheck: false. See The utils object is removed.

  • #11 5065414 Thanks @github-actions! - Middlewares: (key, next, context). Middlewares no longer get the container as this (arrow functions work); context carries container, the resolution path and depth. next() continues, next(otherKey) redirects. Forks inherit middlewares live, fork({ skipResolvers: true }) is now fork({ middlewares: false }), and unuse(middleware) removes one. use() returns the container itself. Migration

  • #11 e398e8c Thanks @github-actions! - Typed namespaces with small declaration files. get('Billing') returns a ServiceProvider of the namespace's services (it was an untyped container), also for nested namespaces. In the service map, the namespace name now holds a Namespace marker (AsyncNamespace for 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.ts about 4× smaller. ContainerServices<typeof app>['Billing'] → NamespaceServices<typeof app, 'Billing'>. New types: Namespace, AsyncNamespace, ServiceType<S, K>, ServicesInNamespace<S, N>. Migration

  • #11 d5391ce Thanks @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 4989f19 Thanks @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 over ServiceRegistry<{ …what they need }> and extend() reports missing services by name; namespace providers, getParent() and middleware/event containers are read-only. IDIContainer is a deprecated alias of ServiceRegistry. Migration

    Classes 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 636bb1e Thanks @github-actions! - Docs and an Agent Skill in the package. The package now contains its documentation as Markdown (llms.txt and lib/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 d5391ce Thanks @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 without defer(), and a factory returning Promise<T> registers T. 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, and dispose() releases resolved instances. Modules for it take the new AsyncServiceRegistry, consumers the new AsyncServiceProvider; the sync ServiceRegistry / ServiceProvider types don't accept it. buildServicesGraph() and preload() accept async containers. See Async services.

  • #11 fb1c878 Thanks @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_FAILED with the resolution path (the original error is cause), like errors thrown synchronously. Check error.cause if you caught your own error types from an awaited get();
    • dispose() releases the resolved instance, not the promise, so addSingleton('db', async () => new Pool()) closes the pool. A singleton still being created is awaited first; one whose promise rejected is skipped. dispose functions in the options receive the resolved value, and their parameter is typed that way (also for addInstance promises registered with dispose).
  • #11 af752e8 Thanks @github-actions! - dispose() and await using. await container.dispose() (also [Symbol.asyncDispose], so await 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 a dispose function in their registration options (dispose: false opts out). addInstance values 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 a dispose event is emitted.

  • #11 e398e8c Thanks @github-actions! - Faster type-checking. Nothing changes in how you write registrations.

    • Code inside namespace() and inline extend() 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 addSingleton chains 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() or extend() 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; a DIContainer<S> is a ServiceRegistry<{}, 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.

  • #11 d663616 Thanks @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 removed flatten().

  • #11 e398e8c Thanks @github-actions! - seal(). app.seal() ends the registrations of a container: afterwards add*, namespace() and extend() throw the new INJECUTE_SEALED error, and the returned type (SealedDIContainer / SealedAsyncDIContainer) has no registration methods. Resolving, middlewares, events, reset() and dispose() 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 e398e8c Thanks @github-actions! - replace: true changes a service's type. Registering a key again with replace: true now gives the service the new type; before, the old and the new type were intersected, so replacing a string with a number made get() return never. In a fork, pass replace: true when 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 007441e Thanks @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.

Released under the MIT License.