InferDI
InferDI ist ein Dependency-Injection-Container für TypeScript. Er benötigt weder Decorators noch Reflection oder Laufzeitabhängigkeiten. Die Middleware @inferdi/hono erstellt bei jedem Middleware-Aufruf einen Anfrage-Scope, stellt ihn als c.var.di bereit und gibt ihn nach Abschluss der Routenverarbeitung wieder frei.
Der Container bildet seinen Abhängigkeitsgraphen in seinem Typ ab. TypeScript meldet fehlende oder falsch angeordnete Abhängigkeiten sowie ungültige Beziehungen zwischen Lebensdauern.
Installation
npm install @inferdi/inferdi @inferdi/honoNOTE
InferDI ist auch auf JSR verfügbar. Führe mit Deno deno add jsr:@inferdi/inferdi jsr:@inferdi/hono npm:hono aus.
Erste Schritte
1. Container erstellen
Anfragedaten gehören an die HTTP-Grenze. Definiere einen kleinen Anwendungstyp, statt Honos Context in den Abhängigkeitsgraphen aufzunehmen.
// container.ts
import { Container } from '@inferdi/inferdi'
import { connectDatabase, UserService } from './services'
export interface RequestContext {
readonly requestId: string
readonly userId: string | undefined
}
const config = {
dsn: 'postgres://localhost/app',
} satisfies { readonly dsn: string }
export const root = new Container()
.registerValue('config', config)
.declareScopeInputs<{ request: RequestContext }>()
.registerAsyncFactory(
'db',
(config: typeof config) => connectDatabase(config.dsn),
['config']
)
.registerClass('users', UserService, ['db', 'request'], 'scoped')declareScopeInputs() fügt dem Graphen einen rein typbezogenen Platzhalter hinzu. Es wird kein Wert registriert. Da users von request abhängt, erlaubt InferDI die Auflösung dieses Dienstes erst, wenn ein Scope die Eingabe bereitstellt. users hat eine Scope-gebundene Lebensdauer, weil ein Singleton nicht von anfragegebundenen Daten abhängen darf.
2. Anfrage-Scope erstellen
Erstelle mit einer Funktion den konkreten Scope, den die Anwendung benötigt:
const openRequestScope = (request: RequestContext) =>
root.createScope({ request })
type RequestScope = ReturnType<typeof openRequestScope>3. Middleware hinzufügen
InferdiHonoScopeEnv stellt den konkreten, einsatzbereiten RequestScope über Honos Kontextvariablen bereit.
import { Hono } from 'hono'
import { inferdiHono, type InferdiHonoScopeEnv } from '@inferdi/hono'
type AppEnv = InferdiHonoScopeEnv<RequestScope>
const app = new Hono<AppEnv>()
const createRequestScope = (userId: string | undefined) =>
openRequestScope({
requestId: crypto.randomUUID(),
userId,
})
app.use(
'*',
inferdiHono({
container: root,
createScope: (_root, c) =>
createRequestScope(c.req.header('x-user-id')),
})
)InferdiHonoEnv<typeof root> bleibt nützlich, wenn die Middleware den Typ verwendet, den der parameterlose Aufruf von createScope() am Root-Container zurückgibt. Eine eigene Scope-Factory kann wie in diesem Beispiel einen spezifischeren Typ zurückgeben. Deshalb wird hier InferdiHonoScopeEnv<RequestScope> verwendet.
4. Dienste auflösen
Verwende get() für verfügbare synchrone Schlüssel und getAsync() für deklarative asynchrone Schlüssel. Die Eingabe request ist synchron, während users asynchron ist, weil es von db abhängt.
app.get('/users/:id', async (c) => {
const request = c.var.di.get('request') // RequestContext
const users = await c.var.di.getAsync('users') // UserService
const user = await users.profile(c.req.param('id'))
return c.json({ requestId: request.requestId, user })
})
export default appc.get('di') ist gleichbedeutend mit c.var.di und hat denselben Typ.
Asynchrone Abhängigkeiten
registerAsyncFactory() speichert den endgültigen Typ Database im Graphen, nicht Promise<Database>. Der asynchrone Status wird an UserService weitergegeben, sodass beide Dienste mit getAsync() aufgelöst werden. Obwohl getAsync() auch verfügbare synchrone Schlüssel akzeptiert, solltest du für gewöhnliche synchrone Dienste get() verwenden.
Ein Callback für registerFactory(), der ein Promise zurückgibt, hat eine andere Bedeutung. Das Promise selbst ist der Dienstwert, bleibt ein synchroner Eintrag im Graphen und wird mit get() aufgelöst.
Eigener Kontextschlüssel
Übergib key, um denselben Scope unter einer anderen Hono-Kontextvariablen bereitzustellen. Die eigene Scope-Factory muss weiterhin die deklarierte Anfrageeingabe liefern.
type CustomEnv = InferdiHonoScopeEnv<RequestScope, 'container'>
const customKeyApp = new Hono<CustomEnv>()
customKeyApp.use(
'*',
inferdiHono({
container: root,
key: 'container',
createScope: (_root, c) =>
createRequestScope(c.req.header('x-user-id')),
})
)
customKeyApp.get('/users/:id', async (c) => {
const users = await c.var.container.getAsync('users')
return c.json(await users.profile(c.req.param('id')))
})Optionen
inferdiHono akzeptiert folgende Optionen:
| Option | Standard | Beschreibung |
|---|---|---|
container | Erforderlich | Root-Container. Die Middleware gibt ihn niemals frei. |
key | 'di' | Kontextvariable, die von c.var[key] und c.get(key) verwendet wird. |
createScope | root.createScope() | Erstellt den Anfrage-Scope. Damit kannst du typisierte Eingaben aus Hono übergeben. Darf asynchron sein. |
setupScope | Keine | Wird nach der Scope-Erstellung und vor den Routen-Handlern ausgeführt. Darf asynchron sein. |
disposeScope | scope.dispose() | Überschreibt die Freigabe des Anfrage-Scopes. Darf asynchron sein. |
autoDispose | true | Auf false setzen oder false zurückgeben, wenn der Anwendungscode die Freigabe übernimmt. |
onDisposeError | console.error | Behandelt Fehler bei der Bereinigung des Anfrage-Scopes. |
Streaming
Honos stream(), streamText() und streamSSE() können eine Response zurückgeben, bevor ihre Callbacks abgeschlossen sind. Rufe vor der Rückgabe der Antwort skipInferdiDispose() auf und gib den Scope frei, wenn der Stream endet.
import { streamText } from 'hono/streaming'
import { skipInferdiDispose } from '@inferdi/hono'
app.get('/users/:id/export', (c) => {
skipInferdiDispose(c)
const scope = c.var.di
const id = c.req.param('id')
return streamText(c, async (stream) => {
try {
const users = await scope.getAsync('users')
const user = await users.profile(id)
await stream.write(JSON.stringify(user) ?? 'null')
} finally {
await scope.dispose()
}
})
})skipInferdiDispose() unterdrückt die automatische Bereinigung nur bei einer erfolgreichen Antwort. Wenn die Route vor der Rückgabe der Antwort fehlschlägt, gibt die Middleware den Scope weiterhin frei.