Рантаймы и edge-платформы
Примеры для рантаймов держат корневой контейнер на уровне модуля и создают один scope на запрос. Ограниченные по времени обработчики могут использовать await using; стриминг и фоновая работа должны очищать scope после своего завершения.
Большинство примеров используют общий examples/_shared/container.ts. Supabase Edge Functions делает локальную замену фабрики, но сохраняет ту же дисциплину scope на запрос.
| Пример | Что показывает |
|---|---|
node-http.ts | низкоуровневый жизненный цикл Node HTTP с очисткой после ответа |
bun-serve.ts | scope на запрос в Bun serve |
deno-http.ts | scope на запрос в Deno HTTP |
cloudflare-workers.ts | scope на запрос в Cloudflare Workers и порядок ctx.waitUntil |
vercel-edge.ts | scope на запрос в Vercel Edge и фоновая очистка |
deno-deploy.ts | scope на запрос в Deno Deploy и очистка через info.waitUntil |
supabase-edge-functions.ts | Supabase Edge Functions с локальной заменой фабрики |
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 = await 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 body = await scope.get('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')
})
})Файл в репозитории: 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 = await 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 profile = await scope.get('users').profile('me')
return Response.json(profile)
},
})Файл в репозитории: 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 = await 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 profile = await scope.get('users').profile('me')
return Response.json(profile)
})Файл в репозитории: examples/runtimes-edge/deno-http.ts
Cloudflare Workers
import {
buildRootContainer,
createRequestScope,
type RootContainer,
} from '../_shared/container.js'
type Env = Record<string, string | undefined>
// Workers re-uses the module-scope between requests for as long as the
// isolate is warm. Lazily build the root once per isolate from the bindings
// passed into `fetch`. `env` typically contains DATABASE_URL/LOG_LEVEL so
// `readConfig(env)` validates inside buildRootContainer.
let root: RootContainer | undefined
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
root ??= buildRootContainer(env)
const scope = await createRequestScope(root, {
requestId: request.headers.get('cf-ray') ?? crypto.randomUUID(),
})
try {
const profile = await scope.get('users').profile('me')
// Background work that touches scoped services must complete BEFORE the
// scope is disposed. `.finally` sequences disposal after the background
// work, so the scoped `RequestContext` / async `Database` are still alive
// while the audit record is being written.
const background = (async () => {
scope.get('audit').record('request.completed', { url: request.url })
})()
ctx.waitUntil(background.finally(() => scope.dispose()))
return Response.json(profile)
} catch (error) {
await scope.dispose()
throw error
}
},
}Файл в репозитории: examples/runtimes-edge/cloudflare-workers.ts
Vercel Edge
import { waitUntil } from '@vercel/functions'
import {
buildRootContainer,
createRequestScope,
} from '../_shared/container.js'
export const runtime = 'edge'
const root = buildRootContainer()
export async function GET(request: Request) {
const scope = await createRequestScope(root, {
requestId: request.headers.get('x-vercel-id') ?? crypto.randomUUID(),
})
try {
const profile = await scope.get('users').profile('me')
// Background work that touches scoped services must complete BEFORE the
// scope is disposed. Chaining via `.finally` ensures the background
// promise (which still reads RequestContext / Logger from the scope)
// is sequenced before the dispose — never in parallel with it.
const background = (async () => {
scope.get('audit').record('request.completed', { url: request.url })
})()
waitUntil(background.finally(() => scope.dispose()))
return Response.json(profile)
} catch (error) {
await scope.dispose()
throw error
}
}Файл в репозитории: 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 = await createRequestScope(root, {
requestId: request.headers.get('x-request-id') ?? crypto.randomUUID(),
})
try {
const profile = await scope.get('users').profile('me')
// Async background work that uses scoped services. MUST finish before
// dispose — chaining via `.finally` guarantees that. Running it in
// `Promise.all([background, scope.dispose()])` would tear down the
// scope while the background promise is still reading from it.
const background = (async () => {
scope.get('audit').record('request.completed', { path: new URL(request.url).pathname })
})()
info.waitUntil(background.finally(() => scope.dispose()))
return Response.json(profile)
} catch (error) {
await scope.dispose()
throw error
}
})Файл в репозитории: 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
}
class RequestContext {
requestId = ''
}
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()
.registerValue('supabaseEnv', readSupabaseEnv())
.registerFactory('supabase', (c) => {
const { url, key } = c.get('supabaseEnv')
return createClient(url, key)
})
.registerClass('request', RequestContext, [], 'scoped')
.registerClass('profiles', ProfilesService, ['request', 'supabase'], 'scoped')
Deno.serve(async (request) => {
const scope = root.createScope()
try {
scope.get('request').requestId = request.headers.get('x-request-id') ?? crypto.randomUUID()
const profiles = scope.get('profiles')
const result = await profiles.list()
// Background work that uses scoped services. `EdgeRuntime.waitUntil` keeps
// the function instance alive until the promise settles, so we sequence
// dispose AFTER the audit write — never in parallel with it. Without the
// `.finally(scope.dispose)` the scoped supabase client and RequestContext
// would be torn down while the audit write is still in flight.
EdgeRuntime.waitUntil(
profiles.audit('profiles.listed').finally(() => scope.dispose()),
)
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.
})Файл в репозитории: examples/runtimes-edge/supabase-edge-functions.ts
