Scopes
A scope determines how many instances of a binding exist and for how long. Every binding has exactly one scope.
Built-in scopes
import { Scopes } from '@caffeine-projects/dicaf'
| Identifier | Decorator | Description |
|---|---|---|
Scopes.SINGLETON | @Lifetime(Scopes.SINGLETON) | One instance per container. Default. |
Scopes.TRANSIENT | @Lifetime(Scopes.TRANSIENT) | New instance on every resolution. |
Scopes.REQUEST | @Lifetime(Scopes.REQUEST) | One instance per AsyncLocalStorage context. Node.js only. |
Scopes.REFRESH | @Lifetime(Scopes.REFRESH) | Singleton that can be refreshed via container.refresher. |
Set a scope with the @Lifetime decorator or the fluent binder:
// Decorator
@Injectable()
@Lifetime(Scopes.REQUEST)
class RequestContext { ... }
// Fluent API
di.bind(RequestContext).toSelf().lifetime(Scopes.REQUEST)
Singleton
The default scope. The container creates one instance on first resolution and
returns the same instance on every subsequent get().
@Injectable()
class DatabasePool {
constructor() {
this.pool = createPool()
}
}
Scopes.SINGLETON is the default — no decorator needed. Use
@Lifetime(Scopes.SINGLETON) only when overriding a parent binding or making
the intent explicit. Singletons are created eagerly during init() unless
lazy: true is set.
Transient
A new instance is created every time the binding is resolved. Transient
instances are not tracked by the container; dispose() does not call
@PreDestroy on them.
@Injectable()
@Lifetime(Scopes.TRANSIENT)
class EmailMessage {
readonly id = generateId()
}
Request scope
One instance per asynchronous execution context. This scope relies on Node.js
AsyncLocalStorage and is only available in Node.js environments.
@Injectable()
@Lifetime(Scopes.REQUEST)
class RequestContext {
readonly requestId = generateId()
}
To start a new request context, run code through
container.requestScopeManager.run():
app.use((req, res, next) => {
container.requestScopeManager.run(() => next())
})
Any code running within that callback — regardless of how deeply nested — will
resolve the same RequestContext instance.
Refresh scope
A refresh binding behaves like a singleton but can be refreshed on demand. After refresh, the next resolution creates a fresh instance.
@Injectable()
@Lifetime(Scopes.REFRESH)
class RemoteConfig {
constructor() {
this.data = loadConfigFromRemote()
}
}
Refresh all refresh-scoped instances:
await container.refresher.refresh()
Scope compatibility
By default, DiCaf enforces compatible-scope rules: a singleton may not depend
on a transient directly, because the transient would be created once and
effectively become a singleton. The container throws during init() if this
rule is violated.
Behaviour is controlled by the checks.scopes option:
new DiCaf({ checks: { scopes: 'no-mix' } }) // strict: exact scope match
new DiCaf({ checks: { scopes: 'compatible-scopes-only' } }) // default
new DiCaf({ checks: { scopes: 'off' } }) // no validation
To inject a shorter-lived dependency into a longer-lived one, use
provide() to wrap it in a Provider:
@Injectable([provide(TransientService)])
@Lifetime(Scopes.SINGLETON)
class SomeSingleton {
constructor(private readonly transient: Provider<TransientService>) {}
doWork() {
const svc = this.transient.get() // new instance on every call
}
}
Custom scopes
Implement the Scope interface and register it with bindScope(). The scope
factory receives the container so the scope can resolve dependencies if needed.
import { bindScope, type Binding, type Factory, type ResolutionContext, type Scope } from '@caffeine-projects/dicaf'
const CUSTOM_SCOPE = Symbol.for('my-app.scope.session')
class CustomScope implements Scope {
private readonly store = new Map<number, unknown>()
get lazy() {
return true
}
get durable() {
return false
}
provide<T>(ctx: ResolutionContext, factory: Factory<T>): T {
const cached = this.store.get(ctx.binding.id)
if (cached !== undefined) {
return cached as T
}
const instance = factory(ctx)
this.store.set(ctx.binding.id, instance)
return instance
}
cachedInstance<T>(binding: Binding<T>): T | undefined {
return this.store.get(binding.id) as T | undefined
}
reset(binding: Binding) {
this.store.delete(binding.id)
}
configure(binding: Binding) {
// Track managed bindings here if the scope needs that metadata.
}
}
bindScope(CUSTOM_SCOPE, () => new CustomScope())
Use the scope in a binding:
@Injectable()
@Lifetime(CUSTOM_SCOPE)
class UserSession { ... }
Scope factories are global — call bindScope() once at application startup,
before any new DiCaf() call.
Scope interface
interface Scope {
get lazy(): boolean
get durable(): boolean
provide<T>(ctx: ResolutionContext, factory: Factory<T>): T
cachedInstance<T>(binding: Binding<T>): T | undefined
reset(binding: Binding): void | Promise<void>
configure(binding: Binding): void
}
| Member | Description |
|---|---|
lazy | When true, the container does not eagerly instantiate this scope's bindings during init(). |
durable | When true, the scope is compatible with other durable scopes during scope validation. Singleton-like scopes are durable; request-like scopes are not. |
provide(ctx, factory) | Returns an instance for the resolution context. Call factory(ctx) to create an uncached instance, or cache the result by ctx.binding.id. |
cachedInstance(binding) | Returns the cached instance for binding, or undefined if none exists or the scope does not cache instances. |
reset(binding) | Clears the cached instance for binding when the scope supports reset. |
configure(binding) | Runs once for each binding during container initialization so the scope can track managed bindings. |
Scope registry
Scopes are registered globally before container creation. A scope factory is a
function that receives a container and returns a Scope instance.
type ScopeFactory = (container: Container) => Scope
bindScope
bindScope(id, factory)
Registers a scope under id. Throws ErrScopeAlreadyRegistered if id is
already bound.
import { bindScope } from '@caffeine-projects/dicaf'
bindScope(MY_SCOPE, (container) => new MyCustomScope())
hasScope
hasScope(id)
Returns true if a scope is registered for id.
unbindScope
unbindScope(id)
Removes the scope registered under id.