Фулстек-фреймворки
Фулстек-примеры используют scope для loaders, actions, обработчиков маршрутов и server actions. В dev-сборках корневой контейнер кешируется на globalThis, чтобы HMR не создавал дубликаты клиентов.
Оба примера используют общий examples/_shared/container.ts. Сравнивайте границу операции, которую фреймворк дожидается через await.
| Пример | Что показывает |
|---|---|
next-app-router.ts | границы scope для запроса и Server Action в Next.js App Router |
remix.ts | границы scope для loader и action в Remix |
Next.js App Router
ts
'use server'
import { revalidatePath } from 'next/cache'
import { buildRootContainer, type RootContainer } from '../_shared/container.js'
// In development, HMR can reload this module without resetting globalThis.
// Reusing the root avoids leaking DB/cache clients on every file save and
// guarantees that across reloads `buildRootContainer()` runs ONCE, so the
// async `Database` factory in `_shared/container.ts` is initialized once.
const globalForInferDI = globalThis as typeof globalThis & {
__inferdiNextRoot?: RootContainer
}
export const root: RootContainer =
process.env.NODE_ENV === 'production'
? buildRootContainer()
: (globalForInferDI.__inferdiNextRoot ??= buildRootContainer())
// Server Actions run as a single bounded async unit and Next awaits the
// function before returning to the client. `await using` is the right tool
// here: the scope is asyncDispose'd exactly when the action finishes,
// including on thrown errors.
export async function getProfileAction(formData: FormData) {
await using scope = root.createScope()
const ctx = scope.get('request')
ctx.requestId = crypto.randomUUID()
ctx.userId = String(formData.get('userId') ?? '') || undefined
const profile = await scope.get('users').profile(ctx.userId ?? 'me')
revalidatePath('/profile')
return profile
}Файл в репозитории: examples/fullstack/next-app-router.ts
Remix
ts
import type {
ActionFunctionArgs,
LoaderFunctionArgs,
} from '@remix-run/node'
import { json } from '@remix-run/node'
import {
buildRootContainer,
createRequestScope,
type RootContainer,
} from '../_shared/container.js'
// Remix dev can re-evaluate modules during HMR while globalThis survives.
// Cache the root so the async Database factory is not re-run on every save.
const globalForInferDI = globalThis as typeof globalThis & {
__inferdiRemixRoot?: RootContainer
}
export const root: RootContainer =
process.env.NODE_ENV === 'production'
? buildRootContainer()
: (globalForInferDI.__inferdiRemixRoot ??= buildRootContainer())
async function scopeFor(request: Request) {
return createRequestScope(root, {
requestId: request.headers.get('x-request-id') ?? crypto.randomUUID(),
userId: request.headers.get('x-user-id') ?? undefined,
})
}
export async function loader({ request }: LoaderFunctionArgs) {
// Loader / action functions are bounded async operations that Remix awaits
// before serializing the response. `await using` ties scope disposal to
// the same boundary — Remix never streams data out of a loader after it
// returns, so the scope is safe to tear down here.
await using scope = await scopeFor(request)
return json(await scope.get('users').profile('me'))
}
export async function action({ request }: ActionFunctionArgs) {
await using scope = await scopeFor(request)
const form = await request.formData()
return json({ ok: true, fields: [...form.keys()] })
}Файл в репозитории: examples/fullstack/remix.ts
