Scope-Eingaben
Scope-Eingaben sind Werte, die der Code beim Öffnen eines Scopes bereitstellt: HTTP-Anfrage, angemeldeter Benutzer, Mandant, Jobdaten oder Trace-Kontext. InferDI trägt diese Anforderungen durch den Graphen und verhindert die Auflösung abhängiger Services, solange ihre Eingaben fehlen.
Eingaben deklarieren
declareScopeInputs() ergänzt Schlüssel ausschließlich im Typgraphen. Es erzeugt weder Laufzeitregistrierungen noch Werte.
interface RequestContext {
requestId: string
}
interface AuthContext {
userId: string
}
class PublicService {
constructor(readonly request: RequestContext) {}
}
class AccountService {
constructor(
readonly request: RequestContext,
readonly auth: AuthContext
) {}
}
const root = new Container()
.declareScopeInputs<{
request: RequestContext
auth: AuthContext
}>()
.registerClass('publicService', PublicService, ['request'], 'scoped')
.registerClass(
'accountService',
AccountService,
['request', 'auth'],
'scoped'
)Scope-Eingaben haben die Lebensdauer scoped. Ein Singleton darf nicht davon abhängen; der Compiler lehnt daher für beide gezeigten Services eine weggelassene oder explizite 'singleton'-Lebensdauer ab.
Typisierte Profile öffnen
createScope(inputs) akzeptiert beliebige Teilmengen fehlender Eingaben. Der zurückgegebene Containertyp hält fest, welche Anforderungen nun erfüllt sind.
const publicScope = root.createScope({request})
publicScope.get('publicService')
// @ts-expect-error: auth has not been provided
publicScope.get('accountService')
const authenticatedScope = publicScope.createScope({auth})
authenticatedScope.get('accountService')Gib den Profilen deiner Anwendung über Funktionen einen Namen. Deren abgeleitete Rückgabetypen erhalten die exakte Menge bereiter Schlüssel ohne manuelle Containerannotationen.
const openPublicScope = (request: RequestContext) =>
root.createScope({request})
const openAuthenticatedScope = (
request: RequestContext,
auth: AuthContext
) => root.createScope({request, auth})
type PublicScope = ReturnType<typeof openPublicScope>
type AuthenticatedScope = ReturnType<typeof openAuthenticatedScope>Öffne das Profil an der Grenze einer Anfrage, Nachricht oder eines Jobs. Halte den Root-Graphen unabhängig von Framework-Objekten.
Anforderungen folgen dem Graphen
InferDI propagiert Eingabeanforderungen durch Klassen, Lazy-Begleiter, synchrone Factories mit deps und deklarative asynchrone Factories.
const app = root
.registerFactory(
'requestId',
(c) => c.get('request').requestId,
['request'],
'scoped'
)
.registerAsyncFactory(
'session',
async (auth: AuthContext) => loadSession(auth.userId),
['auth'],
'scoped'
)Das Abhängigkeitstupel der registerFactory-Überladung mit deps deklariert Kanten im Typsystem und beschränkt den Callback auf einen Resolver für diese Schlüssel. Der Callback erhält weiterhin diesen Resolver. registerAsyncFactory funktioniert anders: Es löst das Tupel auf und übergibt dem Callback die Werte positionsweise.
Auch die Argumentreihenfolge unterscheidet sich:
registerFactory(key, factory, deps, lifetime)
registerAsyncFactory(key, factory, deps, lifetime)Verantwortung bei verschachtelten Scopes
Ein Kind erbt Eingabewerte und erstellt eigene scoped Instanzen. Die Eingabewerte bleiben anwendungseigen und werden vom Container nicht freigegeben.
await using publicScope = openPublicScope(request)
await using authenticatedScope = publicScope.createScope({auth})
await authenticatedScope.getAsync('session')JavaScript gibt diese Deklarationen in umgekehrter Reihenfolge frei; das verfeinerte Kind wird vor seinem Eltern-Scope geschlossen. root.dispose() schließt keinen der beiden Scopes.
Wiederverwendbare Typverträge
ScopeInputMap beschreibt Eingaben eines benannten Module. WithRequirements fügt einem Modulergebnis Eingabeanforderungen hinzu.
import {
type ScopeInputMap,
type Spec,
type WithRequirements
} from '@inferdi/inferdi'
type RequestInputs = ScopeInputMap<{
request: RequestContext
auth: AuthContext
}>
type RequestServices = {
accountService: WithRequirements<
Spec<AccountService, 'scoped'>,
'request' | 'auth'
>
}Auch generische Helfer müssen die Bereitschaft erhalten:
function resolveSync<
T extends DependenciesMap,
K extends Container.SyncReadyKeys<Container<T>>
>(container: Container<T>, key: K) {
return container.get(key)
}
function resolveAny<
T extends DependenciesMap,
K extends Container.ReadyKeys<Container<T>>
>(container: Container<T>, key: K) {
return container.getAsync(key)
}Eingabevertrag
- Deklarationen erfordern endliche, verpflichtende String- oder Symbolschlüssel. Optionale und numerische Schlüssel,
__proto__, breite Indexsignaturen und Unions mit unterschiedlichen Schlüsselmengen werden abgelehnt. - Ein vorhandenes Pflichtfeld mit Wert
undefinedgilt als bereitgestellt. Entscheidend ist die Existenz der Eigenschaft, nicht ihr Wahrheitswert. createScope(inputs)erstellt eine flache Momentaufnahme aufzählbarer eigener String- und Symboleigenschaften. Getter und Proxy-Traps werden dabei ausgeführt; übergib daher ein passives Datenobjekt.- Das Eingabeschema existiert nur in TypeScript. JavaScript,
anyoder Casts können unbekannte Schlüssel hinzufügen oder eine Registrierung im Kind-Cache überschatten. {fast: true}unterstützt Eingabeverfeinerung, behält aber seinen festen Graphen: Beende Registrierungen vor dem ersten.get()oder.createScope().
Scopes und Freigabe erläutert die Ressourcenverantwortung, Asynchrone Abhängigkeiten behandelt asynchrone Services mit Scope-Eingaben.
Compilerprüfung
Derselbe Graph erhält weitere bereite Schlüssel, sobald die Eingabe bereitgestellt wird:
import { Container } from '@inferdi/inferdi'
type RequestContext = { requestId: string }
class Handler {
constructor(readonly request: RequestContext) {}
}
const root = new Container()
.declareScopeInputs<{ request: RequestContext }>()
.registerClass('handler', Handler, ['request'], 'scoped')
root.get('handler')
const scope = root.createScope({ request: { requestId: 'req-1' } })
const handler = scope.get('handler')