Skip to content

运行时与边缘平台

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

大多数示例共用 examples/_shared/container.ts。Supabase Edge Functions 在保持相同请求作用域规范的同时,使用了本地的工厂替换。

示例展示内容
node-http.ts底层 Node HTTP 生命周期,配合响应清理
bun-serve.tsBun serve 请求作用域
deno-http.tsDeno HTTP 请求作用域
cloudflare-workers.tsCloudflare Workers 请求作用域与 ctx.waitUntil 时序
vercel-edge.tsVercel Edge 请求作用域与后台清理
deno-deploy.tsDeno Deploy 请求作用域与 info.waitUntil 清理
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 = 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

ts
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

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 = 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

ts
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

ts
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

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 = 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

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
}

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