运行时与边缘平台
运行时示例使用模块级的根容器,并为每个请求创建一个作用域。有界限的处理函数可以使用 await using;流式或后台工作应在该工作完成后再释放。
大多数示例共用 examples/_shared/container.ts。Cloudflare Workers 和 Supabase Edge Functions 围绕平台绑定构建本地容器图。
| 示例 | 展示内容 |
|---|---|
node-http.ts | 底层 Node HTTP 生命周期,配合响应清理 |
bun-serve.ts | Bun serve 请求作用域 |
deno-http.ts | Deno HTTP 请求作用域 |
cloudflare-workers.ts | Wrangler 生成的绑定类型、D1、Queues 和带错误处理的 ctx.waitUntil |
vercel-edge.ts | Vercel Edge 请求作用域与后台清理 |
deno-deploy.ts | 通过 Deno.ServeHandlerInfo.completed 清理 Deno Deploy 作用域 |
supabase-edge-functions.ts | 使用自定义工厂替换的 Supabase Edge Functions |
Node HTTP
ts
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')
})
})仓库文件:examples/runtimes-edge/node-http.ts
Bun Serve
ts
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)
}
})仓库文件:examples/runtimes-edge/bun-serve.ts
Deno HTTP
ts
/*
* 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)
})仓库文件:examples/runtimes-edge/deno-http.ts
Cloudflare Workers
在 wrangler.jsonc 中配置 DB 和 AUDIT_QUEUE 绑定,然后运行 pnpm wrangler types。示例使用生成的 Env 接口,后台任务不会保留请求作用域。
ts
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>仓库文件:examples/runtimes-edge/cloudflare-workers.ts
Vercel Edge
ts
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)
}仓库文件:examples/runtimes-edge/vercel-edge.ts
Deno Deploy
ts
/*
* 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
}
})仓库文件:examples/runtimes-edge/deno-deploy.ts
Supabase Edge Functions
ts
/*
* 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
})