norns

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

  • single on the root container is a process-wide instance: a repo over a local SQLite file, a service without per-request state.
  • bind re-runs the factory each resolve. Use it when the value depends on the request, such as a D1 binding that only exists on event.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 single declared 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.