@insler/di
di is a single-entrypoint core: the root @insler/di import is the whole
public surface. It is standalone — no dependency on the RPC stack or any
other subsystem — and its runtime dependencies are exactly debug and
object-hash.
bun add @insler/diThe token API
Section titled “The token API”Tokens are typed keys. The container resolves each one to exactly its declared type — no casts, no string lookups.
token<T>(name)/token<T, C>(name, config)— a basic token, optionally carrying typed configuration readable attoken.config.factoryToken<T, Config>(name)— declares a family: one creation recipe serving many instances.parameterizedToken<T, C>(name, parameter, config)— an eager instance of a family. The parameter is hashed into the instance name; all instances share the family’sbaseNameand route to its factory.lazyToken<T, C>(name, parameter, config)— a lazy instance: materialized on demand viaapp.resolve(...)after start, not eagerly during it.- Type helpers:
Token,AnyToken,LazyToken,InferToken,InferTokens,AnyDeps.
import { factoryToken, lazyToken, parameterizedToken, token } from '@insler/di';
const Port = token<number>('port');const Conn = factoryToken<Connection, { host: string }>('conn');const primary = parameterizedToken<Connection, { host: string }>('conn', 'primary', { host: 'primary.db',});const reporting = lazyToken<Connection, { host: string }>('conn', 'reporting', { host: 'reporting.db',});The container builder
Section titled “The container builder”container() returns a ContainerBuilder; .start() resolves the graph
(independent bindings in parallel) and returns a ResolvedContainer.
.provide(token, factory)/.provide(token, deps, factory)— bind a token.depsis an array (positional arguments) or a record (one typed object); factories may be async..factory(familyToken, metaFactory)— register the creation recipe for a family. The meta-factory receives each instance’s config and returns the instance factory — or{ deps, factory }/ awithDeps(...)wrapper to declare per-instance dependencies..lazy(token, factory)— register an on-demand binding directly..use(pack)— apply a pack (a(builder) => builderfunction): the composition seam everything else sugars over..defer(async (builder) => ...)— asynchronous registration before start (dynamic imports, discovered plugins, conditional bindings)..init(async (resolved) => ...)— run after all bindings resolve, before.start()returns. If an initializer throws, the container stops itself..link(rule)— add ordering-only dependencies (afterDeps): the rule inspects each binding’s{ name, deps, hasBinding }and may return names that must resolve first. No value is passed..manifest()— introspect without starting:bindings,levels,factories,unresolved,initializerCount,deferredCount, plustoString()andtree(name)renderings.
Registration is first-registration-wins: later bindings for an already-bound token back off. The dev/prod swap is built on exactly that — apply the selected infra pack first, then the defaults.
The resolved container
Section titled “The resolved container”app.get(token)— synchronous, eager tokens only (a lazy token is a compile error here).app.resolve(lazyToken)— materialize a lazy instance on demand.app.resolveAll(deps)— resolve a single token, a tuple, or a record of tokens in one call.app.stop()— run managed cleanups in reverse dependency order.
Lifecycle primitives
Section titled “Lifecycle primitives”managed(value, cleanup)— pair a value with its cleanup; dependents see the plain value, andstop()runs cleanups dependents-first.ManagedandisManagedare exported for advanced composition.singleton(factory)— wrap a factory so every caller shares one reference-counted instance across containers; cleanup fires when the last reference releases, and the next use after that creates a fresh instance. The production pattern combines all three: afactorywhose instances aresingleton-backedmanagedvalues.
Composition sugar
Section titled “Composition sugar”Both helpers are pure sugar over provide/use/tokens — they add no
resolution semantics and never bypass first-registration-wins.
module(build)— packages the pack + configure pattern into a callable, configurable definition unit. A module is always called —.use(mod())or.use(mod({ ...config }))— never passed bare. Config can carryPacks, so plugin lists fall out for free. Types:Module<Config>,Pack.inject(deps, fn)— deps-bound partial application: returns aBoundTokenwhose value isfnwith its first parameter bound to the resolved deps (shape mirrorsprovide: single token, tuple, or record). Register it with a bareprovide(boundToken)— it resolves eagerly, sogetreturns the callable synchronously. A plain token without a factory does not type-check in that overload; treat aBoundTokenas opaque.
import { container, inject, module, token } from '@insler/di';
const Url = token<string>('url');const database = module((b, cfg: { url: string }) => b.provide(Url, () => cfg.url));
const sendEmail = inject({ smtp: Smtp }, ({ smtp }, to: string, body: string) => smtp.send(to, body));
const app = await container() .use(database({ url: 'postgres://localhost' })) .provide(Smtp, () => createSmtp()) .provide(sendEmail) .start();
app.get(sendEmail)('a@b.com', 'hi');Use it well
Section titled “Use it well”- Prefer
managed/singletonover hand-rolled lifecycle wiring; never mutate a started container. - Inspect
.manifest()(especiallyunresolved) in a test before shipping a composition. - Keep di out of library internals: composing other subsystems (transports, hosts, clients as tokens) belongs in the consuming application — coupling flows consumer → libraries, never into di.