API-слои
RPC- и GraphQL-интеграции должны создавать один InferDI scope на HTTP-запрос, а не отдельный scope на procedure или resolver.
Эти примеры используют общий examples/_shared/container.ts. Сравнивайте, где интеграция создаёт scope и какая граница отвечает за dispose.
| Пример | Что показывает |
|---|---|
trpc.ts | tRPC fetchRequestHandler со scope вокруг всего HTTP-запроса |
apollo-server.ts | scope в контексте Apollo Server для выполнения без стриминга |
graphql-yoga.ts | scope в контексте GraphQL Yoga для выполнения без стриминга |
tRPC
ts
import { initTRPC } from '@trpc/server'
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import {
buildRootContainer,
createRequestScope,
type RequestContainer
} from '../_shared/container.js'
const root = buildRootContainer()
type Ctx = { container: RequestContainer }
const t = initTRPC.context<Ctx>().create()
export const router = t.router({
me: t.procedure.query(({ ctx }) => ctx.container.get('users').profile('me'))
})
/*
* HTTP-level scope. One scope per HTTP request — NOT per procedure.
* tRPC's batched-link sends multiple procedure calls in a single HTTP request;
* `fetchRequestHandler` resolves them all under one `createContext` call. We
* dispose ONCE after the response is built, which is the only correct moment.
*
* (A procedure-level middleware that disposes the container would dispose the
* scope between batched procedures on the same request, breaking later calls.)
*/
export async function handleTrpcRequest(req: Request): Promise<Response> {
await using scope = await createRequestScope(root, {
requestId: req.headers.get('x-request-id') ?? crypto.randomUUID(),
userId: req.headers.get('authorization') ?? undefined
})
return fetchRequestHandler({
endpoint: '/trpc',
req,
router,
createContext: () => ({ container: scope })
})
}Файл в репозитории: examples/api-layers/trpc.ts
Apollo Server
ts
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import {
buildRootContainer,
createRequestScope,
type RequestContainer
} from '../_shared/container.js'
const root = buildRootContainer()
function normalizeHeader(value: string | string[] | undefined): string | undefined {
return Array.isArray(value) ? value[0] : value
}
type GraphQLContext = { readonly container: RequestContainer }
const typeDefs = `#graphql
type User { id: ID!, name: String! }
type Query { user(id: ID!): User! }
`
const resolvers = {
Query: {
user: (_parent: unknown, args: { id: string }, ctx: GraphQLContext) =>
ctx.container.get('users').profile(args.id)
}
}
export const server = new ApolloServer<GraphQLContext>({
typeDefs,
resolvers,
plugins: [
{
async requestDidStart() {
return {
/*
* NOTE: `willSendResponse` fires once Apollo has built the response
* payload. For `@defer`/`@stream` operations parts of the response
* are still streaming AFTER this point — if your resolvers consume
* scoped DB connections that must survive streaming, dispose from a
* transport-level hook (e.g. res.once('finish') in your HTTP layer)
* instead and pass the scope through the standalone server's
* `context` callback as below
*/
async willSendResponse({ contextValue }) {
await contextValue.container.dispose()
}
}
}
}
]
})
export async function start() {
return startStandaloneServer(server, {
context: async ({ req }) => ({
container: await createRequestScope(root, {
requestId: crypto.randomUUID(),
userId: normalizeHeader(req.headers.authorization)
})
})
})
}Файл в репозитории: examples/api-layers/apollo-server.ts
GraphQL Yoga
ts
import { createYoga, createSchema } from 'graphql-yoga'
import {
buildRootContainer,
createRequestScope,
type RequestContainer
} from '../_shared/container.js'
const root = buildRootContainer()
type GraphQLContext = { readonly container: RequestContainer }
export const yoga = createYoga<GraphQLContext>({
schema: createSchema<GraphQLContext>({
typeDefs: /* GraphQL */ `
type User { id: ID!, name: String! }
type Query { user(id: ID!): User! }
`,
resolvers: {
Query: {
user: (_parent, args: { id: string }, ctx) =>
ctx.container.get('users').profile(args.id)
}
}
}),
context: async ({ request }) => ({
container: await createRequestScope(root, {
requestId: crypto.randomUUID(),
userId: request.headers.get('authorization') ?? undefined
})
}),
plugins: [
{
/*
* NOTE: `onExecuteDone` fires once the GraphQL operation finishes. For
* `@defer`/`@stream` the response continues streaming afterwards, so
* resolvers running in those incremental payloads would race a disposal
* started here. For schemas that use incremental delivery, dispose from
* the HTTP transport-level (`onResponse` in your server framework) and
* remove this plugin
*/
onExecuteDone({ args }) {
const ctx = args.contextValue as GraphQLContext
return ctx.container.dispose().catch((err) => {
console.error('Failed to dispose Yoga request scope', err)
})
}
}
]
})Файл в репозитории: examples/api-layers/graphql-yoga.ts
