Basic Usage
Create tokens once, build a container from an immutable provider array, then resolve a typed service object at your composition root.
import { createContainer, factoryProvider, token, valueProvider } from '@vielzeug/conduit';
const Config = token<{ baseUrl: string }>('Config');
const Client = token<{ url: string }>('Client');
const container = createContainer([
valueProvider(Config, { baseUrl: '/api' }),
factoryProvider(Client, [Config], (config) => ({ url: `${config.baseUrl}/users` })),
]);
const services = await container.resolve({ config: Config, client: Client });
console.log(services.client);
await container.dispose();Define Dependencies
Factory dependencies tuples are authoritative. Conduit resolves tuple values in order, validates every edge at construction, and disposes created services in reverse dependency order.
const Logger = token<{ info(message: string): void }>('Logger');
const Api = token<{ get(path: string): Promise<unknown> }>('Api');
const Service = token<{ load(): Promise<unknown> }>('Service');
const container = createContainer([
valueProvider(Logger, { info: (message) => console.log(message) }),
factoryProvider(Api, [], () => ({ get: async (path: string) => fetch(path) })),
factoryProvider(Service, [Api, Logger], (api, logger) => ({
async load() {
logger.info('Loading data');
return api.get('/data');
},
})),
]);Choose Lifetimes
Factories are singletons by default and cached on the registering container. Concurrent singleton resolutions share one in-flight attempt; a rejected attempt is evicted for retry. Use 'transient' for one value per resolution or a ScopeToken for request, job, or test ownership. Singletons cannot depend on transient or scoped factories, and one named scope cannot depend on another.
import { createContainer, factoryProvider, scope, token } from '@vielzeug/conduit';
const Request = scope('request');
const Session = token<{ id: string }>('Session');
const root = createContainer([
factoryProvider(Session, [], () => ({ id: crypto.randomUUID() }), { lifetime: Request }),
]);Factory dependency tuples are copied at construction, so later caller mutation cannot change Conduit's graph. Add disposalSignalToken to a factory tuple when work must observe its owning container or scope cancellation.
Create Named Scopes
Use a scope token when a resource belongs to a request, job, or test lifecycle. Pass immutable local providers through createScope() options to override parent registrations.
const TraceId = token<string>('TraceId');
const request = root.createScope(Request, {
providers: [valueProvider(TraceId, 'request-trace')],
});
const services = await request.resolve({ session: Session });
await request.dispose();
await root.dispose();Resolving Session from root throws ConduitScopedResolutionError because no matching scope owns it.
Resolve at Composition Roots
Resolve a map at composition roots to receive an explicitly typed service object. Direct resolve(token) remains available for one root service and focused tests; avoid scattering service-locator calls through application code.
const client = await container.resolve(Client);
const services = await container.resolve({ client: Client, config: Config });
// Both results are fully typed.Dispose Resources
dispose() rejects new work, aborts disposalSignal, disposes child scopes, waits for in-flight creation, then releases services in reverse creation order. A factory that finishes after disposal starts is immediately cleaned up and its resolver receives ConduitDisposedError.
await container.dispose();ConduitDisposeError.errors contains every cleanup failure after Conduit attempts all hooks, including cleanup from in-flight factories and child scopes.
Testing
Build a container per test with explicit values for external dependencies. Because the provider array is immutable and validated at construction, a misconfigured test container fails immediately.
const Clock = token<{ now(): number }>('Clock');
const Service = token<{ timestamp: number }>('Service');
const container = createContainer([
valueProvider(Clock, { now: () => 123 }),
factoryProvider(Service, [Clock], (clock) => ({ timestamp: clock.now() })),
]);
const services = await container.resolve({ service: Service });
expect(services.service).toEqual({ timestamp: 123 });
await container.dispose();Best Practices
- Create tokens at module scope.
- Use
valueProvider()andfactoryProvider()so tokens, values, dependencies, and disposers remain type-safe. - Declare every factory dependency in its tuple.
- Keep factories focused on one service.
- Use scopes for request/job-owned resources.
- Resolve once at a composition root; pass the typed service object downstream.
- Dispose every scope and root container.
- Keep optional application fallback policy outside Conduit.
- Use
await using container = createContainer([...])when lexical async disposal fits application lifetime.