Environnements d’exécution et plateformes edge
Les exemples conservent une racine au niveau du module et créent un scope par requête. Les gestionnaires couvrant toute l’opération peuvent utiliser await using ; le streaming et le travail en arrière-plan doivent se terminer avant la libération.
La plupart des exemples utilisent le graphe partagé. Cloudflare Workers et Supabase Edge Functions intègrent les ressources de leur plateforme dans des graphes locaux. examples/_shared/container.ts
| Exemple | Contenu |
|---|---|
node-http.ts | Cycle HTTP natif de Node et libération liée à la réponse |
bun-serve.ts | Scope de requête pour serve de Bun |
deno-http.ts | Scope de requête Deno HTTP |
cloudflare-workers.ts | Bindings générés par Wrangler, D1, Queues et travail encadré via ctx.waitUntil |
vercel-edge.ts | Scope de requête Vercel Edge et libération en arrière-plan |
deno-deploy.ts | Libération dans Deno Deploy via Deno.ServeHandlerInfo.completed |
supabase-edge-functions.ts | Supabase Edge Functions avec remplacement d’une fabrique |
Node HTTP
import { createServer, type ServerResponse } from 'node:http'
import {
buildRootContainer,
createRequestScope
} from '../_shared/container.js'
const root = buildRootContainer()
function attachCleanup(res: ServerResponse, cleanup: () => void) {
/*
* 'finish' fires on normal completion, 'close' on client-side abort.
* `dispose()` is idempotent — guarding once with a flag avoids issuing
* two parallel disposal walks if both fire in quick succession
*/
let done = false
const once = () => {
if (done) return
done = true
cleanup()
}
res.once('finish', once)
res.once('close', once)
}
export const server = createServer((req, res) => {
void (async () => {
const scope = createRequestScope(root, {
requestId: req.headers['x-request-id'] as string | undefined ?? crypto.randomUUID()
})
attachCleanup(res, () => {
scope.dispose().catch((err) => {
console.error('Failed to dispose request scope', err)
})
})
try {
const users = await scope.getAsync('users')
const body = await users.profile('me')
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify(body))
} catch (error) {
console.error(error)
res.writeHead(500)
res.end('Internal Server Error')
}
})().catch((error) => {
console.error(error)
if (!res.headersSent) {
res.writeHead(500)
}
res.end('Internal Server Error')
})
})Fichier du dépôt : examples/runtimes-edge/node-http.ts
Bun Serve
import {
buildRootContainer,
createRequestScope
} from '../_shared/container.js'
const root = buildRootContainer()
export default Bun.serve({
async fetch(request) {
await using scope = createRequestScope(root, {
requestId: request.headers.get('x-request-id') ?? crypto.randomUUID()
})
/*
* `Response.json(value)` serializes synchronously, so the response body is
* ready before the handler exits and `await using` is safe. For streaming
* responses, move disposal into the stream's `cancel`/`close` path
*/
const users = await scope.getAsync('users')
const profile = await users.profile('me')
return Response.json(profile)
}
})Fichier du dépôt : examples/runtimes-edge/bun-serve.ts
Deno HTTP
/*
* Deno consumers: `../_shared/container.ts` imports from `@inferdi/inferdi`.
* Map the bare specifier in your `deno.json` import map:
* { "imports": { "@inferdi/inferdi": "npm:@inferdi/inferdi" } }
*/
import {
buildRootContainer,
createRequestScope
} from '../_shared/container.ts'
const root = buildRootContainer()
Deno.serve(async (request) => {
await using scope = createRequestScope(root, {
requestId: request.headers.get('x-request-id') ?? crypto.randomUUID()
})
/*
* The handler is a bounded async unit for non-streaming responses, so
* `await using` is the compact form of try/finally + async dispose
*/
const users = await scope.getAsync('users')
const profile = await users.profile('me')
return Response.json(profile)
})Fichier du dépôt : examples/runtimes-edge/deno-http.ts
Cloudflare Workers
Configure DB et AUDIT_QUEUE dans wrangler.jsonc, puis exécute pnpm wrangler types. L’exemple utilise l’interface Env générée et garde le travail en arrière-plan indépendant du scope de requête.
import { Container } from '@inferdi/inferdi'
/*
* `Env` and the Worker runtime types come from `pnpm wrangler types`.
* The Wrangler config declares a D1 binding named DB and a Queue producer
* binding named AUDIT_QUEUE.
*/
type RequestContext = {
readonly requestId: string
}
type AuditMessage = {
readonly event: string
readonly requestId: string
readonly url: string
}
class ProfilesService {
constructor(
private readonly request: RequestContext,
private readonly db: D1Database
) {}
async get(id: string) {
const profile = await this.db
.prepare('select id, name from users where id = ?1')
.bind(id)
.first<{ id: string; name: string }>()
return profile ?? { id, name: 'Unknown' }
}
auditMessage(url: string): AuditMessage {
return {
event: 'profile.read',
requestId: this.request.requestId,
url
}
}
}
const root = new Container()
.declareScopeInputs<{
request: RequestContext
db: D1Database
}>()
.registerClass('profiles', ProfilesService, ['request', 'db'], 'scoped')
export default {
async fetch(request, env, ctx): Promise<Response> {
await using scope = root.createScope({
request: {
requestId: request.headers.get('cf-ray') ?? crypto.randomUUID()
},
db: env.DB
})
const profiles = scope.get('profiles')
const profile = await profiles.get('me')
ctx.waitUntil(
env.AUDIT_QUEUE
.send(profiles.auditMessage(request.url))
.catch((error) => {
console.error(JSON.stringify({
event: 'audit.enqueue.failed',
requestId: request.headers.get('cf-ray'),
error: String(error)
}))
})
)
return Response.json(profile)
}
} satisfies ExportedHandler<Env>Fichier du dépôt : examples/runtimes-edge/cloudflare-workers.ts
Vercel Edge
import { waitUntil } from '@vercel/functions'
import { Container } from '@inferdi/inferdi'
export const runtime = 'edge'
type RequestContext = {
readonly requestId: string
}
class ProfilesService {
constructor(private readonly request: RequestContext) {}
async get(id: string) {
return { id, requestId: this.request.requestId, name: 'Edge User' }
}
}
class AuditService {
record(event: string, meta: Record<string, unknown>) {
console.info(event, meta)
}
}
const root = new Container()
.declareScopeInputs<{ request: RequestContext }>()
.registerClass('audit', AuditService, [])
.registerClass('profiles', ProfilesService, ['request'], 'scoped')
export async function GET(request: Request) {
await using scope = root.createScope({
request: {
requestId: request.headers.get('x-vercel-id') ?? crypto.randomUUID()
}
})
const profile = await scope.get('profiles').get('me')
const audit = scope.get('audit')
/*
* The background task captures a root singleton and plain data. It does not
* retain the request scope after this bounded handler returns.
*/
waitUntil(
Promise.resolve()
.then(() => audit.record('request.completed', { url: request.url }))
.catch((error) => {
console.error('Failed to record request completion', error)
})
)
return Response.json(profile)
}Fichier du dépôt : examples/runtimes-edge/vercel-edge.ts
Deno Deploy
/*
* Deno Deploy / Deno consumers: see ../_shared/container.ts and map the bare
* specifier in your `deno.json` import map:
* { "imports": { "@inferdi/inferdi": "npm:@inferdi/inferdi" } }
*/
import {
buildRootContainer,
createRequestScope
} from '../_shared/container.ts'
const root = buildRootContainer()
Deno.serve(async (request, info) => {
const scope = createRequestScope(root, {
requestId: request.headers.get('x-request-id') ?? crypto.randomUUID()
})
try {
const users = await scope.getAsync('users')
const profile = await users.profile('me')
/*
* `completed` settles after Deno finishes sending the response. Attach
* disposal there so streaming responses retain their request scope.
*/
void info.completed
.then(() => scope.dispose())
.catch((error) => {
console.error('Failed to dispose Deno request scope', error)
})
return Response.json(profile)
} catch (error) {
await scope.dispose()
throw error
}
})Fichier du dépôt : examples/runtimes-edge/deno-deploy.ts
Supabase Edge Functions
/*
* Supabase Edge Functions / Deno consumers: see ../_shared/container.ts and
* map the bare specifier in your `deno.json` import map:
* { "imports": { "@inferdi/inferdi": "jsr:@inferdi/inferdi" } }
*
* This example uses its own root container with a Supabase-specific factory
* instead of the shared one — it shows how an InferDI root can be customized
* per deployment target while keeping the rest of the request-scope shape
*/
import { Container } from '@inferdi/inferdi'
import { createClient, type SupabaseClient } from 'jsr:@supabase/supabase-js'
/*
* EdgeRuntime is a Supabase-provided global; declare its shape locally so
* this file typechecks under a regular Deno LSP without Supabase ambient types
*/
declare const EdgeRuntime: {
waitUntil(promise: Promise<unknown>): void
}
type RequestContext = {
readonly requestId: string
}
function readSupabaseEnv() {
const url = Deno.env.get('SUPABASE_URL')
const key = Deno.env.get('SUPABASE_ANON_KEY')
if (!url || !key) {
throw new Error('SUPABASE_URL and SUPABASE_ANON_KEY are required')
}
return { url, key }
}
class ProfilesService {
constructor(
private readonly request: RequestContext,
private readonly supabase: SupabaseClient
) {}
async list() {
const { data, error } = await this.supabase.from('profiles').select('*')
if (error) throw error
return { requestId: this.request.requestId, data }
}
async audit(event: string) {
await this.supabase.from('request_log').insert({
request_id: this.request.requestId,
event
})
}
}
const root = new Container()
.declareScopeInputs<{ request: RequestContext }>()
.registerValue('supabaseEnv', readSupabaseEnv())
.registerFactory('supabase', (c) => {
const { url, key } = c.get('supabaseEnv')
return createClient(url, key)
}, ['supabaseEnv'])
.registerClass('profiles', ProfilesService, ['request', 'supabase'], 'scoped')
Deno.serve(async (request) => {
const scope = root.createScope({
request: {
requestId: request.headers.get('x-request-id') ?? crypto.randomUUID()
}
})
try {
const profiles = scope.get('profiles')
const result = await profiles.list()
/* The audit call needs the scoped RequestContext until it settles */
EdgeRuntime.waitUntil(
profiles
.audit('profiles.listed')
.finally(() => scope.dispose())
.catch((error) => {
console.error('Failed to write request audit', error)
})
)
return Response.json(result)
} catch (error) {
await scope.dispose()
throw error
}
})
/*
* Optional: flush in-flight state when Supabase signals worker shutdown.
* `beforeunload` fires when the runtime is about to terminate the instance
* (e.g. resource limit, deploy). Avoid heavy work here — the window is short
*/
addEventListener('beforeunload', () => {
// e.g. flush a small in-memory queue to a singleton-owned destination
})Fichier du dépôt : examples/runtimes-edge/supabase-edge-functions.ts
