Aller au contenu

InferDI ​

InferDI est un conteneur d’injection de dépendances pour TypeScript, sans décorateurs, réflexion ni dépendances à l’exécution. Le middleware @inferdi/hono crée une portée de requête à chaque invocation du middleware, l’expose via c.var.di, puis la libère une fois le traitement de la route terminé.

Le conteneur porte son graphe de dépendances dans son type. TypeScript signale les dépendances manquantes ou mal ordonnées et les relations de durée de vie invalides.

Installation ​

bash
npm install @inferdi/inferdi @inferdi/hono

NOTE

InferDI est également disponible sur JSR. Avec Deno, exécutez deno add jsr:@inferdi/inferdi jsr:@inferdi/hono npm:hono.

Premiers pas ​

1. Construire le conteneur ​

Les données de requête doivent rester à la frontière HTTP. Définissez un petit type applicatif au lieu de placer le Context de Hono dans le graphe de dépendances.

ts
// 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() ajoute un emplacement uniquement typé au graphe. Il n’enregistre aucune valeur. Comme users dépend de request, InferDI interdit la résolution de ce service tant qu’une portée n’a pas fourni l’entrée. users est lié à la portée, car un singleton ne peut pas dépendre de données liées à une requête.

2. Créer la portée de requête ​

Utilisez une fonction pour créer la portée concrète nécessaire à l’application :

ts
const openRequestScope = (request: RequestContext) =>
  root.createScope({ request })

type RequestScope = ReturnType<typeof openRequestScope>

3. Ajouter le middleware ​

InferdiHonoScopeEnv expose la portée concrète et prête à l’emploi RequestScope via les variables de contexte de Hono.

ts
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> reste utile lorsque le middleware utilise le type renvoyé par createScope() sans argument sur la racine. Une fabrique de portée personnalisée peut renvoyer un type plus précis, comme ici ; cet exemple utilise donc InferdiHonoScopeEnv<RequestScope>.

4. Résoudre les services ​

Utilisez get() pour les clés synchrones prêtes et getAsync() pour les clés asynchrones déclaratives. L’entrée request est synchrone, tandis que users est asynchrone car il dépend de db.

ts
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 app

c.get('di') équivaut à c.var.di et possède le même type.

Dépendances asynchrones ​

registerAsyncFactory() stocke le type final Database dans le graphe, et non Promise<Database>. Son caractère asynchrone se propage à UserService ; les deux services se résolvent donc avec getAsync(). Bien que getAsync() accepte aussi les clés synchrones prêtes, utilisez get() pour les services synchrones ordinaires.

Un callback de registerFactory() qui renvoie une Promise a une sémantique différente. La Promise elle-même est la valeur du service, reste une entrée synchrone du graphe et se résout avec get().

Clé de contexte personnalisée ​

Transmettez key pour exposer la même portée sous une autre variable de contexte Hono. La fabrique de portée personnalisée doit toujours fournir l’entrée de requête déclarée.

ts
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')))
})

Options ​

inferdiHono accepte les options suivantes :

OptionValeur par défautDescription
containerObligatoireConteneur racine. Le middleware ne le libère jamais.
key'di'Variable de contexte utilisée par c.var[key] et c.get(key).
createScoperoot.createScope()Crée la portée de requête. Permet de transmettre des entrées typées depuis Hono. Peut être asynchrone.
setupScopeAucuneS’exécute après la création de la portée et avant les gestionnaires de route. Peut être asynchrone.
disposeScopescope.dispose()Remplace la libération de la portée de requête. Peut être asynchrone.
autoDisposetrueDéfinissez false, ou renvoyez false, lorsque le code applicatif gère la libération.
onDisposeErrorconsole.errorGère les échecs de nettoyage de la portée de requête.

Diffusion en continu ​

Les fonctions stream(), streamText() et streamSSE() de Hono peuvent renvoyer une Response avant la fin de leurs callbacks. Appelez skipInferdiDispose() avant de renvoyer la réponse, puis libérez la portée lorsque le flux se termine.

ts
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() désactive le nettoyage automatique uniquement pour une réponse réussie. Si la route échoue avant de renvoyer la réponse, le middleware libère quand même la portée.

Voir aussi ​

Publié sous licence MIT.