Skip to content

Container

@remodulo/container is the kernel: a hierarchical DI container. Zero dependencies, no React, no decorators, no reflect-metadata.

tsx
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.

tsx
const child = container.fork()

child.parent === container // true

register

container.register(provider | provider[]): void - every form is on Providers.

tsx
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

MethodReturnsOn miss
resolve(token, mode?)Tthrows
resolveOptional(token, mode?)T | undefinedundefined
resolveOr(token, fallback, mode?)T | Fthe 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; undefined when this container declares none. Throws when this container declared the token multi - use entries.
  • entries(token) - the snapshots of a multi token's contributions.

All three read this container only and build nothing.

tsx
const snapshot = container.entry(ApiClient)
// { kind: "class", token: ApiClient, scope: "singleton", multi: false }

snapshot?.metadata // the frozen bag the registration carried, if any

The 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.

EventFires
beforeResolutionbefore a read lands on an entry
afterResolutionafter a read returns an instance
beforeMaterializebefore an instance is constructed
afterMaterializeafter 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.

MIT licensed.