Site / Creating a site
Data fetching: declare, then plan
There are no loaders. A page's Surface declares what it reads; Remote plans
what the store does not satisfy and fetches it. The application never
fetches in step with navigation — navigating only changes the route, and the
reads start and stop as a consequence, through Data.subscriptions over the
site's Sources:
const OwnerPage = App.surface('OwnerPage', {
params: { ownerId: Schema.String },
model: ({ params }) => ({
projects: Data.query(
ProjectsByOwner,
{ ownerId: params.ownerId },
{ select: ProjectSummary, first: 10 },
),
}),
})While the route is elsewhere, the Surface is inactive and its requirements disappear — including the retention root. This is tested end to end: URL → route → Model → Surface → requirements → navigate away → requirements gone.
Preparing a destination: prefetch and preload are one composition
A target's route in a Model, Site.sourcesFor for its chain, Data.satisfy
to fill them. This is the whole prefetch story — hover prefetch and
programmatic preload share it, since the framework owns the topology half
and the caller owns the one-line preparation:
const target = Site.target(OwnerNode, { ownerId: 'u1' })
const there = { ...model, route: target.route }
const active = Site.sourcesFor(AppSite, App.owner, App.model.route, target.route)
const prepared = yield* Data.satisfy(there, active)Only the destination's query runs, once; the page navigation will then draw
reads synchronously, already Ready. That is the shape for a render with no
Model yet (SSR, a test). In a running application, write the same
composition as a Command, Data.preload(there, active), from the Message a
hover or a focus sends: its reads land as one Preloaded Message in the
Model as it is by then, rather than as a Model that would undo what happened
while they were out. The CMS site does this for every link on its own origin
(examples/cms/src/content/siteConfig.ts). Satisfying over every Source would
fetch no more — off-chain Surfaces resolve to nothing under the target
Model either way — so the subset is precision and inspection, not a
different fetch. What the app still owns is the trigger: a hover
subscription feeding this composition, or a programmatic call before an
expected navigation. No loader cache, no preload lane, no second protocol.
Why there are no waterfalls
Three facts compose into it:
- Requirements are declarative, not scheduled. What a destination needs is derived from its Surfaces' projections, not executed from its route handlers. Sibling sources plan together and Remote fetches them concurrently and coalesced — route nesting never serializes fetching.
- Dependence costs passes, not waterfalls. When one read reveals the
next (a page naming its cards, a Document naming its Blocks),
Data.satisfyruns its bounded fixed-point: each pass fetches a level concurrently, and instances the parent reveals are read in a later pass. Only genuine data dependence serializes, and only by level. - Navigation never awaits data. The route is set synchronously; reads
follow through subscriptions. There is no "run all loaders, then render"
gate, so there is nothing to waterfall behind — at the cost the pending-UI
convention answers:
InitialandLoadingmean unknown, a busy line carriesaria-busyand appears only once the wait is noticeable (Loading.shown), its space held from the start.
The remaining round trip is the mutation-then-refetch: a mutation patches entities, but a query whose membership changed (archiving out of an active list) refetches in a second request. Single-flight — answering the affected requirements in the mutation's response — is designed but explicitly last: it starts with a request-count benchmark, then the wire.