Skip to content

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 22 Sep 20:43
· 3246 commits to main since this release

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.

  1. Persistence takes a Snapshot. Wrap the Model in snapshotOf, and pass
    both halves of what comes back to Hydrated:

    // 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_VERSION is 4, so a cache written by 0.8 is discarded once
    and refetched, never misread.

  2. Failed now also means the request failed, not only that stored data
    did not decode. RemoteData.render already draws Failed { previous } as
    the old value with a Stale freshness, so a view built on it needs nothing.
    A view that matches Failed itself should draw previous when 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), or onRetry on ListView/DetailView.

  3. One new Remote Message, QueryStarted. Code that spreads
    Remote.messages into its union and routes through Data.reduce or
    Data.wiring needs nothing. A hand-written exhaustive switch over Remote's
    Messages needs one more case.

  4. A RemoteModel built by hand (rather than from Remote.initial) needs
    failures: noFailures.

  5. Removed: invalidateConnection, isStale, refreshConnection and
    LiveState.stale. They recorded staleness that nothing read. To invalidate
    a connection, reduce ConnectionInvalidated.

  6. Expr.contains accepts only text (a text field, nullable included, or
    an Expr<string>). A number field used to typecheck and then fail in the
    database.

  7. A hand-written DomainLike passed to foldkit-crud needs refresh.
    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 a Snapshot, not a store.
    RemotePersistence.dehydrate, hydrate, save and restore take and return
    { entities, connections } rather than an EntityStore. 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 returning emptyStore.
    REMOTE_CACHE_VERSION is 4, so a cache written by an earlier version is
    discarded, never misread. Migrating: dehydrate(model.remote.entities)
    becomes dehydrate(RemotePersistence.snapshotOf(model.remote)), and code
    that used the value restore returns as a store reads its .entities.
  • foldkit-remote: invalidateConnection, isStale and refreshConnection
    are gone, and so is LiveState.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 own stale flag. To invalidate one, reduce ConnectionInvalidated.
  • foldkit-remote: a read whose request failed reads Failed. Before, a
    query or entity read that failed before anything loaded read Initial and
    was never asked for again, because nothing it planned had changed. One that
    failed to refresh read Ready, as if nothing had happened. Now failures are
    kept in RemoteModel.failures, as { connections, fields }. A read with
    nothing to show reads Failed { error }, and one with a value reads
    Failed { error, previous }, which RemoteData.render draws as data with
    Stale freshness. 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.refresh retries it, including something that never loaded.
    The value arriving, a live invalidation or delete, or retention dropping it
    also clears it. Remote.inspect reports failures. A view that matched on
    Failed for decode errors only will now also see request failures, and code
    that builds a RemoteModel by hand needs failures: noFailures.
  • foldkit-remote: a broken live stream records a gap instead of ending a
    read.
    Its ReadFailed now carries the stream and 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 its Loading.

Added

  • foldkit-remote: Data.filtered(model, over, by, input) filters a loaded
    list on the client.
    It runs the body of by, a registered Query.define
    query, over the rows of the connection over reads, and returns
    { items, complete }. The items are decoded through over'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: complete is 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 when by reads a different Entity from over.
  • foldkit-remote: Remote.matching(store, descriptor, input), the
    primitive under Data.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's Input. 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 has Unknown boundaries
    and is stale, so its rows show at once, it reads as Refreshing, and the
    planner refetches it. It reports hasNext and hasPrevious as true in both
    directions, because Unknown is not Terminal.
  • 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's LivePolicy. 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 for params. It does what Surface.at
    does, but the place and the tag are values, so Data.explain can report why a
    Surface is on. It takes a ModelPlace (dependency and get), which a
    ModelRef satisfies. Surface.at is unchanged.
  • foldkit-entity: Expr.show and Query.show render a body as text for
    reading. This is deliberately not SQL: an input shows as $name, and
    contains is named rather than rendered as any dialect's like.
  • foldkit-entity: evaluate, supported, assertSupported and Row
    are exported from foldkit-entity. The reference interpreter depends only on
    the IR, and a client needs it without a server package.
    foldkit-remote-server still 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
    at column 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.
    ListView and DetailView drew any Failed as 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. onRetry on either view adds a Try again button (the retry
    slot, worded by words.retry), and foldkit-crud's placed lists and details
    gain refresh(model) for its Message to return.
  • foldkit-remote: a list waiting on its first page reads Loading. It
    read Initial, 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 new QueryStarted Message 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 list Loading for
    good. An application whose own code switches exhaustively over Remote's
    Messages has one more case.
  • foldkit-remote: a declared LivePolicy is honoured.
    Query.connection(E, { live }) was typed, carried on the descriptor and
    documented, but nothing set the policy on a LiveReceived Message, so every
    live insert used the default. The bound reduce now resolves the policy.
  • foldkit-remote: a live invalidation invalidates the connection. A
    server's ConnectionInvalidate event and a LiveInsertion of 'invalidate'
    used to advance the cursor and do nothing else. Both now mark the connection
    stale, so it reads as Refreshing and the planner refetches it.
  • foldkit-remote: a page bigger than the window that asked for it is
    refused.
    More edges than first or last requested is now QueryFailed
    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: a contains whose search is null compiles.
    It threw at compile time. It is now SQL's unknown, so a negated or compared
    contains over a missing input means what the reference interpreter says it
    means, rather than stopping the request.
  • foldkit-entity: Expr.contains accepts only text. A number field
    typechecked and reached the database as lower(rank) like …, which SQLite
    coerces and Postgres rejects at runtime. Its operand must be a text field
    (nullable included) or an Expr<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, in examples/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 need COLLATE "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 (now foldkit-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 the Operation
    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-drizzle checks
    at registration, beside its column check, so a server that starts is one whose
    queries it can answer; the reference interpreter exports supported and
    assertSupported (from foldkit-entity, re-exported by
    foldkit-remote-server), and evaluate calls it. Both currently run the whole
    kernel, so there is nothing to refuse yet; it exists for the interpreter that
    does not.