Remote / foldkit-remote
Advanced: the kernel
Everything above compiles to the lower-level exports below; there is not a second implementation. Reach for the kernel in tooling, SSR, tests, packages built on Remote, or applications that deliberately need one piece by hand.
Descriptors and binding
Remote.define({ entities, queries, mutations }) creates a domain without a
place in an application Model. It exposes the Remote Model, initial,
Message, reducer, RPC group, and a name-keyed registry consumed by
RemoteServer.validate.
Remote.at(definition, modelRef) binds that domain to a ModelRef;
Remote.make combines definition and binding in one step.
Entity.make(name, schema) is Remote's own entity descriptor, and what a
foldkit-entity Entity is compiled to (Entity.from). Remote accepts either
wherever it takes an entity. Declaring one directly skips foldkit-entity,
and with it relations, derived members and query bodies, which is only worth
it for a domain that Remote is all it will ever need.
Selection.make(Entity, { ... }), Selection.connection(...), and
Entity.patch(ref, values) are the kernel constructors behind the entity
helpers. Remote.select(bound, selection)(id) is the lower-level form of
Data.get.
Requirements and the planner
A Projection carries its requirements as Remote's own metadata, attached with
RemoteRequirements.of / RemoteConnections.of and read with
requirementsOf(projection) / connectionsOf(projection). Requirements are
plain data: entity, id, fields, any relation pagination windows, and the nested
target selections reached through relations. Connections describe query
connections as { identity, window, select }.
Remote.plan(bound, model, projection, options?) deterministically diffs those
requirements against the visible store and returns only missing/stale work. A
relation field being fetched rides on the same request so the server can resolve
the graph in one read; a relation already present in the store is followed into
requirements for the concrete target.
Remote.planQueries performs the same job for absent/stale connections.
RemotePolicy.toPlan(policy, now) compiles a high-level policy into planner
options.
Remote.storeOf is the visible store after pending optimistic layers. Reads are
memoized per visible store (and, for a list, its connection and overlays), so
equal reads of one Model state assemble/decode once and return one value.
Across Model states, an entity's or row's decoded value stays the same object
while its entity's fields are equal: a refetch or live patch that brought equal
data hands the view the value it already rendered, so a keyed row's lazy view
does not re-run. A write to any field of the entity, even one the row does not
select, or a pending optimistic patch on it while other writes land, gives the
row a new value. When the store changed, the RemoteData around a value and a
page's items array are new.
Remote.prefetch(bound, model, projection, options?) runs the same plans
imperatively and reduces their results into a new Model.
The reducer and its Messages
updateRemote(remote, message) is the pure reducer over Remote facts;
Data.update applies it to a bound Remote state and Data.reduce applies it to
the application Model. The common Messages that change nothing (a duplicate live
event, a repeated ReadStarted, an empty Hydrated, a retention pass that
collects nothing) return the Model they were given, so Foldkit neither renders
nor recomputes Subscription dependencies.
Its Messages cover reads and refreshes, mutations, live events/gaps, query
connections, retention, and hydration. Remote.messages is the case record for
defineMessageUnion, and Remote.reduces(message) narrows an application union
to those cases.
Remote.writeRead(store, requests, result, now) exposes the store write behind a
read result for callers deliberately managing their own Remote store.
Subscription entries
Data.subscriptions is built from three kernel entries:
Remote.observe(bound, surface, params, toMessage?, options?)
plan + entity reads + query reads
Remote.live(bound, surface, params, toMessage?, options?)
live streams from the Model-owned cursor
Remote.retain(projections, toMessage?, { connections?, grace? })
active roots -> RetentionChangedThe observe entry's dependencies are the current plan, the Model's refresh
generation, and when a held value next ages out
({ requirements, queries, refresh, expires }), so Data.refresh restarts
it. If entity requirements and queries are both empty, it performs no I/O. A
refreshing read emits RefreshStarted first, then a result Message.
A Message that changes the Model makes Foldkit recompute Subscription dependencies. For example, after a query page lands, the next computation sees the page's entity ids and plans any selected fields those entities still lack. An interrupted identical read is joined by the client coalescer instead of needlessly restarted.
The live entry's dependencies are what the Surface reads live, the refresh floor, and the stream's cursor. The stream stays open as events advance the cursor; the cursor is only where a stream restarted for another reason resumes.
Retention uses the Projection requirements/connections as roots and eventually
emits RetentionChanged; the reducer's pure gc keeps everything reachable
from those roots plus pending work and collects the rest.
The entries Data.subscriptions returns carry these dependencies in their
types: ReadDependencies for <key>.read, LiveDependencies for
<key>.live, and RetentionRoots for retain.
Connections by hand
A connection stores entity references in segments with explicit boundaries, so an unloaded middle page remains a gap rather than an implied adjacency. Query pages, live inserts/removals, and optimistic overlays are reconciled as evidence about that same structure.
A QueryRef has a canonical identity based on its descriptor and encoded input,
excluding the page window. Thus first(25) and after(cursor).first(25) merge
into the same logical connection.
// Inside Effect.gen, with RemoteClient provided.
const ref = Query.first(25)(ProjectsByOwner.ref({ ownerId }))
const page = yield* Remote.query(ref)
updateRemote(remote, Remote.queryMessage(ref, page))
Data.visibleItems(remote, ref)items, hasNext, hasPrevious, and isGapped describe the known region.
ConnectionInvalidated keeps visible rows but marks the connection stale until
a refresh/query result settles it. A ConnectionMerged page with
refreshes: true replaces a stale connection's pages rather than merging into
them.
Where a row falls: placeIn
A query whose body declares a placeable order (Query.placement in
foldkit-entity: numbers, booleans, and text under a portable collation) can
have a changed row placed in its loaded pages by the client, by the same order
the server compiles. Such a connection also reads every field its body filters
and orders by, so the rows it holds can be compared with one that changed.
// Schematic: `ProjectsByRank` is a Query.define ordered by a number, and
// `connection` one of `remote.connections`. test/placement.test.ts runs it.
placeIn(remote.entities, connection, ProjectsByRank.body!, {}, 'Project:p7')
// Before { segment, edge } | End { segment } | Outside | Unknown { reason }BeforeandEndare positions among loaded edges: before an edge, or at the end of a segment that ends the list (Terminal).Outsideis a row that sorts past aCursorboundary or into a gap between segments: it belongs to rows the client has not read, so it is not shown.Unknownis anything not judged exactly: a body that is not placeable, a row or an edge lacking a field the order reads, keys that do not compare.
It is pure and changes nothing. A mutation's answer is placed through it, as above.
Mutations by hand
Remote.mutateInto(bound, model, mutation, input, requestId, { optimistic, now }) is
the one-step imperative start/run/settle form for SSR and tests.
Remote.mutate(mutation, input, requestId) is the transport call itself. It
decodes the typed Output and returns normalized entity patches and confirmed
connection changes for a caller to reduce through updateRemote.
MutationSucceeded reconciles at most once per requestId; an unknown or
already-applied settlement is a no-op. It carries now, the clock reading of
the answer, which dates the entities it writes as a read's now does, so a
freshness policy ages them from then. Data.mutate and Remote.mutateInto
stamp it from their now option, Date.now by default.
The transport seam
RemoteClient is an Effect service with read, query, mutate, and live.
The wire schemas (ReadBatch, QueryRequest, MutationRequest,
LiveRequirement, results/errors) and the RemoteRpc group are exported.
Remote.clientLayer(rpcClient, { window? }) adapts an Effect RPC client,
including live-change mapping. It takes the client RpcClient.make(RemoteRpc)
builds, whose calls can also fail with RpcClientError; each such failure
becomes the call's Remote error rather than a defect. Compatible reads and queries coalesce:
requirements issued together become one ReadBatch, an identical in-flight
requirement/query is joined, and a failed request releases the coalesced entry.
A relation request with its own pagination window stays separate because one
response answers one window exactly.
Remote.coalesced(layer, options) adds the same behavior to a hand-written
RemoteClient layer; window widens batching beyond work issued concurrently.