Container
A small DI container with string tokens, singleton and transient bindings, overrides for tests, and child scopes per request.
Tokens are namespaced strings, conventionally <feature>.<role>: notes.repo, notes.service, db. Bindings register a factory that receives the container it is resolved through.
import type { Container } from '@human-synthesis/norns/server'
export default (app: Container) =>
app.single 'notes.repo', (c: Container) => new NotesRepo c.resolve('db')
app.single 'notes.service', (c: Container) => new NotesService c.resolve('notes.repo')#Methods
| Method | Behaviour |
|---|---|
bind(token, factory) |
Transient: the factory runs on every resolve(token). |
single(token, factory) |
Singleton: the factory runs once per container scope where the binding is declared, and the result is memoised there. |
override(token, factory) |
Wins over any binding in this scope or any parent. Made for tests: swap a real service for a fake without touching the production module. |
resolve(token) |
Returns the value. Walks the scope chain: nearest override first, then nearest binding; throws Container: no binding for token "..." when nothing matches. |
has(token) |
Whether a binding or override is reachable. |
scope() |
Creates a child container that inherits bindings through the parent walk but keeps its own overrides and singletons. |
migrations(dir) |
Registers a migrations directory on the root container. |
getMigrationDirs() |
All registered migration directories, from the root. |
Factories are called with the leaf container, the one resolve was called on. A factory can therefore resolve other tokens at the same scope, which is how notes.service gets a request-scoped db even though notes.service itself was declared on the root.
#Singleton versus transient
singleon the root container is a process-wide instance: a repo over a local SQLite file, a service without per-request state.bindre-runs the factory each resolve. Use it when the value depends on the request, such as a D1 binding that only exists onevent.platform.env(see Cloudflare D1). Resolving a transient binding several times inside one request yields several instances; resolve once and pass it along if that matters.- A
singledeclared on a child scope is per request, because the child scope is created per request.
#Tests
import { createContainer } from '@human-synthesis/norns/server'
c := createContainer()
c.single 'notes.repo', => new FakeRepo!
c.single 'notes.service', (c) => new NotesService c.resolve('notes.repo')
svc := c.resolve 'notes.service'Or boot the real modules and override one token on a child scope.