Container
@remodulo/container is the kernel: a hierarchical DI container. Zero dependencies, no React, no decorators, no reflect-metadata.
import { Container } from "@remodulo/container"
const container = new Container()There is no global container. You construct one, and everything else follows from it.
fork
container.fork(): Container - a child container. It sees everything its ancestors declare; what it declares itself is invisible to them. container.parent reads the link back - null at the root.
const child = container.fork()
child.parent === container // trueregister
container.register(provider | provider[]): void - every form is on Providers.
container.register([{ provide: API_URL, useValue: "https://api.acme.dev" }, ApiClient])One token - one registration per container; a second claim throws. multi: true providers form a collection instead.
Reads
| Method | Returns | On miss |
|---|---|---|
resolve(token, mode?) | T | throws |
resolveOptional(token, mode?) | T | undefined | undefined |
resolveOr(token, fallback, mode?) | T | F | the fallback; a function fallback is called |
resolveAll(token, mode?) | T[] | [] |
isRegistered(token, mode?) | boolean | - |
Modes: "nearest" (default) - the first declaration at or above this container; "self" - own declarations only. resolveAll adds "chained" (its default) - every level accumulated, nearest first.
A registered undefined is a value, not a miss. resolveOptional and resolveOr hand it back; the fallback answers only an actual miss.
A singleton is one instance per container that declares it - not one per process. A fork that shadows a token gets its own instance.
construct
container.construct(Clazz): T - builds a class in this container's context without registering it. Everything the class injects resolves through this container; the instance is yours - not cached, no lifecycle, no events.
Snapshots
registrations()- every own registration, in order.entry(token): EntrySnapshot | undefined- the snapshot of a single token's registration;undefinedwhen this container declares none. Throws when this container declared the tokenmulti- useentries.entries(token)- the snapshots of a multi token's contributions.
All three read this container only and build nothing.
const snapshot = container.entry(ApiClient)
// { kind: "class", token: ApiClient, scope: "singleton", multi: false }
snapshot?.metadata // the frozen bag the registration carried, if anyThe snapshot is frozen. kind is "class" | "value" | "factory" | "alias"; the alias arm carries target and no scope - an alias has none.
Resolver
Resolver.for(container) - the container's read half: the same reads, the same snapshots, on - and no register, no fork, no construct. Canonical: the same container gives the same instance every time, held weakly. This is the object modules hand out.
Observation
container.on(event, listener): () => void - returns the unsubscribe.
| Event | Fires |
|---|---|
beforeResolution | before a read lands on an entry |
afterResolution | after a read returns an instance |
beforeMaterialize | before an instance is constructed |
afterMaterialize | after an instance is constructed |
Hooks are container-global. No token filter - the listener filters for itself.
A throwing before* hook refuses the operation - for every caller on this container, not only the code that attached it.
Hooks are per-container. A fork inherits none. They live until disposed or until the container dies.