Architecture & pipeline
This page walks from .tsx source to a validated GraphQL operation, step by step. It also explains why the responsibilities are split the way they are.
Responsibility split
The system owns everything about the data graph. An existing client owns the wire.
| This system owns | A client/transport adapter owns |
|---|---|
| field-read extraction · selection/path model · operation generation · de-duping · batching · read maps · graph value runtime · Suspense/cache behavior · normalization/invalidation | HTTP transport · auth headers · request cancellation · retries · the subscription stream (SSE in-box, or graphql-ws) · network-level persisted queries |
The packages
| Package | Responsibility |
|---|---|
@gleanql/core |
Query IR, q.* builder, selection merger, GraphQL printer, schema model, operation artifact, devtools, fluent escape hatch. |
@gleanql/compiler |
GraphCompilerBackend seam, a typescript backend, and the analyzer. |
@gleanql/client |
Client adapter, normalized/path cache, normalizer, Suspense runtime, route seam, and the React glue factories (createGraphClient/createGraphServer) the generated entrypoints shim over (react peer, >=18). |
@gleanql/vite |
The build plugin: generates the schema (glean accessor, types, operations) into @gleanql/client. Framework-specific decisions sit behind a FrameworkPreset seam. The core pipeline stays neutral. |
The compile pipeline
Worked example
Two components read different parts of the same product. Each contributes a partial selection. The merger combines them.
ProductHero reads
Product.title
Product.featuredImage.urlBuyBox reads
Product.priceRange
.minVariantPrice.amount
Product.priceRange
.minVariantPrice.currencyCodeThe analyzer connects the root call to both components and emits one merged operation:
Query.product(handle: params.handle)
├─ ProductHero reads
└─ BuyBox reads
▼ (one operation)
query ProductRoute($handle: String!) {
product(handle: $handle) {
__typename
id
title
featuredImage { __typename url }
priceRange {
__typename
minVariantPrice { __typename amount currencyCode }
}
}
}Compiler vs. runtime authority (hybrid)
The compiler is authoritative for the initial operation. The runtime may fetch fields that were not statically reachable (lazy/dynamic paths). The mode is configurable:
| Mode | Behavior |
|---|---|
hybrid v1 default |
compiled query first; runtime misses allowed, warned in dev. |
strict |
compiled query only; an unexpected runtime miss throws. |
runtime-first |
runtime tracking is the source of truth; the compiler is an optimization. |
v1 implements hybrid and exposes unexpectedMissingField: "allow" | "warn" | "error" on the runtime to select the others.
The backend seam
The analyzer walks the TypeScript AST for structure. It routes every type/symbol question through GraphCompilerBackend. The default backend ships a real ts.Program + TypeChecker. The seam is the only contact point for type info, so a Go-based engine (tsgo / @typescript/native-preview) plugs in without touching analysis logic. It already does, as an experimental backend option.
interface GraphCompilerBackend {
getSourceFile(fileName): ts.SourceFile | undefined;
getGraphTypeNames(node): readonly string[] | undefined; // union → many
getGraphTypeName(node): string | undefined;
isGraphBackedType(node): boolean;
resolveDeclaration(node): ts.Declaration | undefined;
}The build creates one ts.Program over all files and analyzes each route against it (analyzeFile + a shared backend). It does not recreate a full program per route, so O(routes × files) program builds collapse to one. All type/symbol queries still go through the seam, which keeps the engine swappable. The in-process typescript backend is the default. An experimental Go-native tsgo backend (@typescript/native-preview) is selectable via the Vite plugin's backend option. It implements the same interface and type-checks much faster on large route sets. When the optional dep can't be resolved, the build falls back gracefully to typescript.
Next: @gleanql/core — the IR, merger, and printer that turn extracted reads into a document.