Skip to content

运行时与边缘平台

运行时示例使用模块级的根容器,并为每个请求创建一个作用域。有界限的处理函数可以使用 await using;流式或后台工作应在该工作完成后再释放。

大多数示例共用 examples/_shared/container.ts。Cloudflare Workers 和 Supabase Edge Functions 围绕平台绑定构建本地容器图。

示例展示内容
node-http.ts底层 Node HTTP 生命周期,配合响应清理
bun-serve.tsBun serve 请求作用域
deno-http.tsDeno HTTP 请求作用域
cloudflare-workers.tsWrangler 生成的绑定类型、D1、Queues 和带错误处理的 ctx.waitUntil
vercel-edge.tsVercel 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 中配置 DBAUDIT_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
})

仓库文件:examples/runtimes-edge/supabase-edge-functions.ts