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:

ts
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:

ts
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:

  1. 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.
  2. Dependence costs passes, not waterfalls. When one read reveals the next (a page naming its cards, a Document naming its Blocks), Data.satisfy runs 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.
  3. 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: Initial and Loading mean unknown, a busy line carries aria-busy and 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.