Skip to content

Фулстек-фреймворки

Фулстек-примеры используют скоупы в загрузчиках данных (loaders), действиях (actions), обработчиках маршрутов и серверных действиях (Server Actions). В режиме разработки корневой контейнер кэшируется на globalThis, чтобы HMR не создавал повторные экземпляры клиентов.

Оба примера используют общий examples/_shared/container.ts. Обратите внимание, какой операции фреймворк дожидается через await и когда после неё можно освободить скоуп.

ПримерЧто показывает
next-app-router.tsграницы скоупа для запроса и Server Action в Next.js App Router
remix.tsграницы скоупа для 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) {
  const userId = String(formData.get('userId') ?? '') || undefined
  await using scope = root.createScope({
    request: {
      requestId: crypto.randomUUID(),
      userId
    }
  })

  const users = await scope.getAsync('users')
  const profile = await users.profile(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)
  const users = await scope.getAsync('users')
  return json(await 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