Fullstack-Frameworks
Fullstack-Beispiele nutzen Scopes für Loader, Actions, Route-Handler und Server Actions. Entwicklungsbuilds cachen den Root auf globalThis, damit HMR keine doppelten Clients erzeugt.
Beide Beispiele verwenden denselben Graphen. Vergleiche, bis zu welcher Operationsgrenze das jeweilige Framework wartet. examples/_shared/container.ts
| Beispiel | Inhalt |
|---|---|
next-app-router.ts | Scope-Grenzen für Requests und Server Actions im Next.js App Router |
remix.ts | Scope-Grenzen für Loader und Actions in 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
}Datei im Repository: 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()] })
}Datei im Repository: examples/fullstack/remix.ts
