Repository navigation
v0.9.0
foldkit-entity 0.3.0; foldkit-surface 0.4.0; foldkit-remote,
foldkit-remote-server and foldkit-remote-drizzle 0.6.0; foldkit-crud and
foldkit-mixins-crud 0.2.0. Republished only so their pinned dependencies are
the current ones: foldkit-agent 0.3.2, foldkit-bundle-surface 0.1.2,
foldkit-cms and foldkit-cms-drizzle 0.1.1, foldkit-form and
foldkit-mixins-form 0.1.2, foldkit-mirror 0.2.2 and foldkit-sync 0.5.3.
foldkit-mixins-surface 0.3.1 moves its peer range to foldkit-surface@^0.4.0.
Remote reads now tell the truth about failure and loading. A request that
failed used to leave a read at Initial, which looks exactly like "nobody
asked", and nothing asked again, so a list could sit empty for good with no sign
of an error. A failed refresh read Ready, as if it had worked. Now a failed
read is Failed, keeping what was on screen as previous. It is retried when
something asks for it, never in a loop. A list whose first page is on its way
reads Loading. The CRUD views keep the rows through a failed refresh and can
offer a Try again button.
The client can run a query over the rows it already holds. The reference
interpreter moved to foldkit-entity, so Data.filtered can filter a loaded
list without asking the server, and says whether its answer covers the whole
list. Live inserts are judged against the query before the declared policy is
used.
Also: a list can survive a reload (its rows, never its cursors),
Data.explain describes a query read in one serializable value, and
Surface.when makes why a Surface is active something a tool can read. Two
declared behaviours that had never been wired now work: a connection's
LivePolicy, and live invalidation.
Upgrading from 0.8
Upgrade every foldkit-* package you use together. They pin each other's
versions exactly, and foldkit-mixins-surface 0.3.0 does not accept
foldkit-surface 0.4. Most applications then need only the first two steps.
-
Persistence takes a
Snapshot. Wrap the Model insnapshotOf, and pass
both halves of what comes back toHydrated:// 0.8 RemotePersistence.dehydrate(model.remote.entities, { scope }) // 0.9 RemotePersistence.dehydrate(RemotePersistence.snapshotOf(model.remote), { scope }) const restored = RemotePersistence.hydrate(text, { scope }) ?? RemotePersistence.emptySnapshot Data.reduce(model, { _tag: 'Hydrated', entities: restored.entities, connections: restored.connections, merge: 'preserve-existing', })
REMOTE_CACHE_VERSIONis 4, so a cache written by 0.8 is discarded once
and refetched, never misread. -
Failednow also means the request failed, not only that stored data
did not decode.RemoteData.renderalready drawsFailed { previous }as
the old value with aStalefreshness, so a view built on it needs nothing.
A view that matchesFaileditself should drawpreviouswhen it is there.
Nothing retries a failed read on its own, so give the user a way to ask:
Data.refresh(model, projection), a placed list's or detail's
refresh(model), oronRetryonListView/DetailView. -
One new Remote Message,
QueryStarted. Code that spreads
Remote.messagesinto its union and routes throughData.reduceor
Data.wiringneeds nothing. A hand-written exhaustive switch over Remote's
Messages needs one more case. -
A
RemoteModelbuilt by hand (rather than fromRemote.initial) needs
failures: noFailures. -
Removed:
invalidateConnection,isStale,refreshConnectionand
LiveState.stale. They recorded staleness that nothing read. To invalidate
a connection, reduceConnectionInvalidated. -
Expr.containsaccepts only text (a text field, nullable included, or
anExpr<string>). A number field used to typecheck and then fail in the
database. -
A hand-written
DomainLikepassed tofoldkit-crudneedsrefresh.
Remote's own domain already has it.
evaluate, supported, assertSupported and Row can now be imported from
foldkit-entity. Their old home, foldkit-remote-server, still re-exports
them, so nothing has to move.
Breaking
foldkit-remote: a snapshot is aSnapshot, not a store.
RemotePersistence.dehydrate,hydrate,saveandrestoretake and return
{ entities, connections }rather than anEntityStore. Build one with
RemotePersistence.snapshotOf(model.remote); to get the old behaviour, name no
connections. That is the default.restore's fallback is
RemotePersistence.emptySnapshot, which replaces returningemptyStore.
REMOTE_CACHE_VERSIONis 4, so a cache written by an earlier version is
discarded, never misread. Migrating:dehydrate(model.remote.entities)
becomesdehydrate(RemotePersistence.snapshotOf(model.remote)), and code
that used the valuerestorereturns as a store reads its.entities.foldkit-remote:invalidateConnection,isStaleandrefreshConnection
are gone, and so isLiveState.stale. They kept a second record of stale
connections that no read or plan consulted, so an invalidation written
through them had no effect (see Fixed below). A connection's staleness is
its ownstaleflag. To invalidate one, reduceConnectionInvalidated.foldkit-remote: a read whose request failed readsFailed. Before, a
query or entity read that failed before anything loaded readInitialand
was never asked for again, because nothing it planned had changed. One that
failed to refresh readReady, as if nothing had happened. Now failures are
kept inRemoteModel.failures, as{ connections, fields }. A read with
nothing to show readsFailed { error }, and one with a value reads
Failed { error, previous }, whichRemoteData.renderdraws as data with
Stalefreshness. A list fails when a row's field failed, and a read through
a relation fails when the target's did. What failed is not asked for again on
its own.Data.refreshretries it, including something that never loaded.
The value arriving, a live invalidation or delete, or retention dropping it
also clears it.Remote.inspectreportsfailures. A view that matched on
Failedfor decode errors only will now also see request failures, and code
that builds aRemoteModelby hand needsfailures: noFailures.foldkit-remote: a broken live stream records a gap instead of ending a
read. ItsReadFailednow carries thestreamand adds it to
RemoteModel.gaps. It no longer clears the loading and stale marks of the
fields it watched: nothing was reading them, and a read in flight for the
same fields used to lose itsLoading.
Added
foldkit-remote:Data.filtered(model, over, by, input)filters a loaded
list on the client. It runs the body ofby, a registeredQuery.define
query, over the rows of the connectionoverreads, and returns
{ items, complete }. The items are decoded throughover's own Selection,
so one view function renders a filtered item or a listed one. It filters a
list and does not run a query:completeis true only when every row was
judged, every match could be shown, and the list is terminal at both ends
with no gap. An empty, incomplete answer means "none that I can see yet",
which is a different answer from "none". It creates no connection and fetches
nothing. It throws whenbyreads a different Entity fromover.foldkit-remote:Remote.matching(store, descriptor, input), the
primitive underData.filtered. It returns the keys a body matches among the
rows the store holds, plus the keys it skipped because a row lacked a field
the body reads. The input is given decoded and encoded through the
descriptor'sInput. The store holds wire values, and a decoded input
compared against them matches nothing, with no error.foldkit-remote: a declared connection survives a reload.
snapshotOf(model, { connections: [ref] })keeps that connection's edges,
segment by segment, and never its cursors, because a cursor may name server
state that no longer exists. A restored connection hasUnknownboundaries
and is stale, so its rows show at once, it reads asRefreshing, and the
planner refetches it. It reportshasNextandhasPreviousas true in both
directions, becauseUnknownis notTerminal.foldkit-remote: a live insert is judged before the declared policy is
used. For a connection whose query has a body, an inserted row that the
client holds and can tell does not match is ignored. A row that matches, or
that the client cannot judge, gets the connection'sLivePolicy. A row held
with a stale value in a field the body reads counts as one it cannot judge,
so an outdated value never suppresses an insert.foldkit-remote:Data.explain(model, projection, { surfaces? })
returns one serializable value describing a query read: the definition and
its input, the connection identity, the window, the Selection, the body as
text with its dependencies, and what the read answers from this Model. Given
the application's Surfaces, it also lists every active Surface that reads the
connection and why each is active.foldkit-surface:Surface.when(surface, place, Case, value => params)
activates a Surface while a place in the Model holds one case of a tagged
union, with the case's fields inferred forparams. It does whatSurface.at
does, but the place and the tag are values, soData.explaincan report why a
Surface is on. It takes aModelPlace(dependencyandget), which a
ModelRefsatisfies.Surface.atis unchanged.foldkit-entity:Expr.showandQuery.showrender a body as text for
reading. This is deliberately not SQL: an input shows as$name, and
containsis named rather than rendered as any dialect'slike.foldkit-entity:evaluate,supported,assertSupportedandRow
are exported fromfoldkit-entity. The reference interpreter depends only on
the IR, and a client needs it without a server package.
foldkit-remote-serverstill re-exports all four, so existing imports are
unchanged.foldkit-entity/conformance, a new subpath, holds the conformance suite
(Subject,rows,cases) that every interpreter runs. The fixture has an
atcolumn whose encoded form differs from its domain form, so an
interpreter that compares decoded values fails.
Fixed
foldkit-mixins-crud: a failed refresh keeps the rows on screen.
ListViewandDetailViewdrew anyFailedas a lone error, which was
right while only decode errors produced it. Now that a failed refresh reads
Failed { previous }, they draw the previous rows or value with the error
above them.onRetryon either view adds a Try again button (theretry
slot, worded bywords.retry), andfoldkit-crud's placed lists and details
gainrefresh(model)for its Message to return.foldkit-remote: a list waiting on its first page readsLoading. It
readInitial, which is documented as "nothing is fetching this", so a view
could not tell a slow network from a Surface that was never activated. The
read entry now sends a newQueryStartedMessage before it runs queries,
and the page or the failure ends it. Retention dropping the list forgets it,
so a query the entry stopped waiting for cannot leave a listLoadingfor
good. An application whose own code switches exhaustively over Remote's
Messages has one more case.foldkit-remote: a declaredLivePolicyis honoured.
Query.connection(E, { live })was typed, carried on the descriptor and
documented, but nothing set the policy on aLiveReceivedMessage, so every
live insert used the default. The boundreducenow resolves the policy.foldkit-remote: a live invalidation invalidates the connection. A
server'sConnectionInvalidateevent and aLiveInsertionof'invalidate'
used to advance the cursor and do nothing else. Both now mark the connection
stale, so it reads asRefreshingand the planner refetches it.foldkit-remote: a page bigger than the window that asked for it is
refused. More edges thanfirstorlastrequested is nowQueryFailed
with a protocol error that carries both numbers. The edges never reach the
store, and a connection already loaded keeps the rows it had.foldkit-remote-drizzle: acontainswhose search is null compiles.
It threw at compile time. It is now SQL's unknown, so a negated or compared
containsover a missing input means what the reference interpreter says it
means, rather than stopping the request.foldkit-entity:Expr.containsaccepts only text. A number field
typechecked and reached the database aslower(rank) like …, which SQLite
coerces and Postgres rejects at runtime. Its operand must be a text field
(nullable included) or anExpr<string>. Code in this repository needed no
change.
Interpreters
-
How text compares when ordering is the backend's, and outside the
conformant subset. A third interpreter (TanStack DB, inexamples/tanstack)
sorts strings by locale where SQLite sorts by code point, and the semantics
had said nothing at all about collation — two interpreters written here had
agreed, and the agreement had been mistaken for a rule. Picking code point and
enforcing it was the first answer and the wrong one: SQLite without ICU cannot
sort by locale, Postgres would needCOLLATE "C"on both the ordering and the
keyset comparison that pages it (losing the index built in its own collation),
and code point puts every capital before every lowercase. No interpreter
changed behaviour; the conformance suite (nowfoldkit-entity/conformance)
drops its text-ordering case, because it pinned whichever engine was written
first rather than anything promised. -
foldkit-entity:Query.unsupported(query, supported)and theOperation
vocabulary. The operations a query needs that an interpreter does not run,
so it can refuse rather than skip one — skipping answers a different question
and still passes every case it does support. The refusal belongs to the
interpreter, since one that compiles at registration and one that runs a body
directly fail at different moments. -
Both interpreters declare what they run.
foldkit-remote-drizzlechecks
at registration, beside its column check, so a server that starts is one whose
queries it can answer; the reference interpreter exportssupportedand
assertSupported(fromfoldkit-entity, re-exported by
foldkit-remote-server), andevaluatecalls it. Both currently run the whole
kernel, so there is nothing to refuse yet; it exists for the interpreter that
does not.