Remote / Server-derived state

Requirements and observation

A Surface's projection carries its remote requirements — entity, id, fields, a pagination window per relation, and through relations, the slice required of each relation's target — as plain data under Remote's key in Projection.metadata (requirementsOf(projection)). Reading is pure; it performs no I/O. A nested selection (owner: UserSummary) reads through the ref in the store and assembles the target's fields; the store itself stays normalized.

The planner diffs requirements against the store and returns only the missing or stale fields. A relation whose field is being fetched rides on the request, so the server resolves the graph in one read; a relation the store already holds is followed into concrete requirements for its targets. It is deterministic and takes now as input (PlanFreshness) rather than reading the clock, so the same store and requirements produce the same plan.

Fetching is a Foldkit Subscription derived from the active Surfaces, and activation is a fact of the Model:

ts
Data.subscriptions({
  page: Surface.at(ProjectPage, model => ({ projectId: model.route.projectId })),
})

Each active Surface gives a read entry, which plans and fetches only the missing fields through RemoteClient and emits a RemoteMessage (a fully-known Surface emits nothing). Its dependencies are plain data: the planned fields, the queries to run, and the Model's refresh generation, so Foldkit restarts it when the plan changes or a refresh is requested. Each Surface also gives a live entry for what it reads through Data.live; one retain entry keeps what the active Surfaces reach. SSR, route/hover prefetch, and tests reuse the same plan through Data.prefetch.

A query is a Projection too. Data.query(ProjectsByOwner, { ownerId }, { select: ProjectSummary, first: 25 }) reads the connection as a Page of the selected items, and carries the connection next to its requirements. The read entry plans it like a field: a connection the Model does not hold (or holds stale) is a query to run, and once its page is known, the page's items are requirements like any other, read under select. A read shows at most its window, whatever else loaded the connection. Data.more(model, projection) grows that window by a page, and the read entry fetches the rows the connection lacks.

What a field the store already holds means is a RemotePolicy on observe and prefetch: cacheFirst (default) fetches only what is missing, staleWhileRevalidate({ maxAge }) refetches an entry older than the window, and networkOnly fetches every selected field. A policy compiles to planner options; it is not a second cache. A refreshing policy emits RefreshStarted with the read's ReadStarted, which marks the refetched fields stale.

Time reaches Remote only as a Message. The read entry knows, from the Model and now, when the earliest value it holds ages out under maxAge; it sleeps until then under the Effect clock and emits RefreshStarted for what is due, and the plan that follows fetches it. A Projection never reads the clock, so a Model reads the same twice and a recorded one replays.

To revalidate on demand — a refresh button, a regained focus — update returns Data.refresh(model, projection) (or a Surface without params). It performs no I/O: it marks the selected fields the store holds stale, so they read Refreshing over their old value, invalidates the loaded connections, and bumps the refresh generation. The read entries already observing that projection plan stale data again under every policy, so the data is fetched once, by the entry, instead of by a Command racing it. The projection must therefore be observed; for one nothing observes, use Data.prefetch with networkOnly. A read in flight from before the refresh restarts rather than landing after it, and a refreshed connection's first page replaces its loaded pages, so removed or reordered items follow the server and later pages are paged again. Refreshing what is already refreshing returns the same Model. The package README's Refreshing from update has the example.

Reads through Remote.clientLayer coalesce: requirements issued together are one batch, a requirement already in flight is joined, and every waiter gets the whole result. Retention is a Message too: Remote.retain lists the observed projections as roots and emits RetentionChanged after a grace period, and the reducer's pure gc keeps what the roots reach through the store's refs plus any pending optimistic change.

In the view, a remote field is a RemoteData. Remote.select produces Ready once its selected fields are present, Refreshing while one is stale (an observer is refetching it), Failed if the server data does not decode or the server settled a selected field without a value, and NotFound for a tombstone. A value the store lacks reads as Loading while a read is fetching it and Initial when none is: the read entry emits ReadStarted before it fetches, and ReadReceived or ReadFailed clears the mark. That distinction is the one worth rendering differently, because nothing fetches a projection no active Surface observes; it reads Initial forever, and a spinner there hides the wiring mistake rather than showing progress. There is no hidden suspense; the states are explicit.