Initial Commit
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# `loaders` — per-request DataLoader scope
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`loaders` defines the per-request DataLoader scope the engine creates
|
||||
around every incoming GraphQL operation. Two concurrent requests
|
||||
receive distinct loader instances; the cache never leaks across
|
||||
request boundaries even though both requests draw from the same set of
|
||||
loader factories registered on the abstract `GraphqlRegistry`.
|
||||
|
||||
The module is deliberately the thinnest possible wrapper around the
|
||||
`LoaderFactory.create()` surface owned by `@suaveplan/api-types/graphql`.
|
||||
The engine calls `createRequestLoaderScope()` once per request; resolvers
|
||||
that need a loader call `scope.get(name, factory)`; the engine disposes
|
||||
the scope at request end.
|
||||
|
||||
## Features
|
||||
|
||||
- One scope per request — distinct cache per request guaranteed.
|
||||
- Loader lazily instantiated on first `get(name, factory)` call;
|
||||
subsequent calls reuse the instance.
|
||||
- `dispose()` blocks subsequent `get()` calls so a buggy late resolver
|
||||
cannot resurrect a freed cache.
|
||||
- Zero runtime dependencies — pure JS, fits in ~40 lines.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createRequestLoaderScope } from "@suaveplan/api-graphql-apollo/loaders";
|
||||
|
||||
const scope = createRequestLoaderScope();
|
||||
const userLoader = scope.get("users", userLoaderFactory);
|
||||
const user = await userLoader.load("u_42");
|
||||
scope.dispose();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Wire the scope into a resolver context object so resolvers consume
|
||||
loaders by name through the request context:
|
||||
|
||||
```ts
|
||||
import { createRequestLoaderScope } from "@suaveplan/api-graphql-apollo/loaders";
|
||||
|
||||
function makeContext(): RequestContext {
|
||||
const scope = createRequestLoaderScope();
|
||||
return {
|
||||
loaders: {
|
||||
users: () => scope.get("users", userLoaderFactory),
|
||||
orders: () => scope.get("orders", orderLoaderFactory),
|
||||
},
|
||||
finalize: () => scope.dispose(),
|
||||
} as unknown as RequestContext;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `RequestLoaderScope`:
|
||||
- `get<TKey, TValue>(name, factory): { load }`
|
||||
- `dispose(): void`
|
||||
- `size(): number`
|
||||
- `createRequestLoaderScope(): RequestLoaderScope`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The scope is a closure over a `Map<string, LoaderInstance>` plus a
|
||||
`disposed` boolean. Throwing on `get` after `dispose` surfaces the
|
||||
late-resolver bug at the call site instead of silently producing
|
||||
inconsistent reads. The size accessor is provided so OTel histograms
|
||||
can record loader-fanout per request.
|
||||
Reference in New Issue
Block a user