Skip to content

Laufzeit- und Edge-Plattformen

Die Beispiele halten den Root auf Modulebene und erstellen einen Scope je Anfrage. Handler, die die gesamte Operation abdecken, können await using nutzen; bei Streaming oder Hintergrundarbeit muss die Freigabe bis zu deren Abschluss warten.

Die meisten Beispiele nutzen den gemeinsamen Graphen. Cloudflare Workers und Supabase Edge Functions binden Plattformressourcen in lokale Containergraphen ein. examples/_shared/container.ts

BeispielInhalt
node-http.tsNativer Node-HTTP-Lebenszyklus mit Freigabe nach der Antwort
bun-serve.tsRequest-Scope für Bun serve
deno-http.tsRequest-Scope für Deno HTTP
cloudflare-workers.tsVon Wrangler erzeugte Bindings, D1, Queues und verwaltete ctx.waitUntil-Arbeit
vercel-edge.tsRequest-Scope und Freigabe im Hintergrund bei Vercel Edge
deno-deploy.tsFreigabe in Deno Deploy über Deno.ServeHandlerInfo.completed
supabase-edge-functions.tsSupabase Edge Functions mit ausgetauschter Factory

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')
  })
})

Datei im Repository: 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)
  }
})

Datei im Repository: 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)
})

Datei im Repository: examples/runtimes-edge/deno-http.ts

Cloudflare Workers

Konfiguriere DB und AUDIT_QUEUE in wrangler.jsonc und führe pnpm wrangler types aus. Das Beispiel nutzt das erzeugte Env-Interface und hält Hintergrundarbeit vom Request-Scope unabhängig.

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>

Datei im Repository: 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)
}

Datei im Repository: 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
  }
})

Datei im Repository: 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
})

Datei im Repository: examples/runtimes-edge/supabase-edge-functions.ts