Skip to content

v0.15.0

Choose a tag to compare

@github-actions github-actions released this 06 Oct 03:32
· 2213 commits to main since this release

foldkit-sync 0.9.0; foldkit-durable 0.7.0; foldkit-remote 0.12.0;
foldkit-remote-server 0.12.0; foldkit-remote-drizzle 0.10.0;
foldkit-cms-drizzle 0.5.0; foldkit-crud 0.6.0; foldkit-mixins 0.7.0;
foldkit-primitives 0.6.0; foldkit-surface 0.7.0; foldkit-mixins-ui
0.6.1. New: foldkit-data-grid 0.1.0 and foldkit-mixins-data-grid 0.1.0,
experimental. Republished to re-pin: foldkit-agent 0.6.1,
foldkit-agent-a2a, foldkit-agent-mcp, foldkit-agent-native and
foldkit-agent-webmcp 0.5.1, foldkit-bundle-surface 0.3.1, foldkit-cms
0.4.2, foldkit-mirror 0.5.1, foldkit-mixins-crud 0.6.2,
foldkit-mixins-form 0.4.2, foldkit-mixins-richtext 0.2.1,
foldkit-mixins-surface 0.5.1 and foldkit-ssr 0.3.1, each of which pins a
bumped package exactly. Every other package is unchanged.

Editing server data, and a data grid to do it in. A table a server owns
can now be edited offline as durable operations: foldkit-sync/entity keeps
the edits per cell and lays them over Remote's rows, and
foldkit-sync/journal's editsJournal applies each committed edit to the
table as the journal's read model, recovering from crashes and refusing a
table ahead of its journal. The data grid that edits it is new: an
accessible, virtualized grid over 100,000 rows with focus, selection, column
state, typed editors, the clipboard, fill, and undo as the application's.
Servers also run in the browser now: SharedHost in foldkit-primitives/net
hosts one for every tab, reached over MessagePorts by Remote and Sync, which
is how the demos at foldkit-plus.pages.dev
run.

Upgrading from 0.14

  • ReplicaStatus.rejected is a list of rejections (foldkit-sync):
    ReadonlyArray<Rejection>, each { opId, reason: Option<string> }, where it
    was a list of operation ids.
  • Sync.transport.socket takes a url or a makeSocket, not both
    (foldkit-sync), and makeSocket takes no argument.
  • editsJournal requires tableRevision (foldkit-sync/journal), and
    fails with TableAheadOfJournalError while the table holds a revision past
    the journal's.
  • Install drizzle-orm yourself beside foldkit-remote-drizzle and
    foldkit-cms-drizzle: it is a peer dependency now, so the application's
    copy is the one they use.
  • contains on Postgres folds ASCII only (foldkit-remote-drizzle), as it
    already did in SQLite and in memory: é no longer finds É there.

Added

  • foldkit-mixins-data-grid: Delete and Backspace clear the range's
    editable cells, or the focused cell, as a cut does without copying, so they
    report Out.Pasted with empty text and a column that refuses it says so
    (#144).

  • examples/registry searches on the server. A search box sets the
    Products query's input; the query is now a Query.define whose body,
    Expr.contains on the description, the Drizzle binding compiles (#144).

  • examples/data-grid keeps its column layout in the browser: saved with
    a Command after any transition that changes the column state, and read
    back with columnState.restore as the page opens (#144).

  • foldkit-data-grid and foldkit-mixins-data-grid are published, at
    an experimental 0.1.0: the grid's state, geometry and keyboard, and the
    grid drawn as an accessible, virtualized WAI-ARIA grid through slots. Their
    entries below are the first release's (#158).

  • Fill in the data grid. foldkit-data-grid's Fill is a spreadsheet's
    fill as pure functions: plan (the source's box and the box beyond it
    toward a cell, along one axis), cells (what each lane writes there) and
    series (numbers a constant step apart continue it; anything else
    repeats). In the grid, Ctrl or Meta with D or R, or a drag of the range's
    fill handle (the fillHandle Slot, its target drawn data-fill; held at
    the grid's edge, the drag scrolls it), reports
    Out.Filled({ source, to }), so Grid.Out.match needs that arm; the
    application writes Grid.fill(rows, model.grid, request), judged as a
    paste is. The registry undoes a fill as one step. Columns.textOf is a
    value as a cell shows it (#144).

  • foldkit-data-grid: Grid.window(input), the rows and columns a
    placed grid's view draws, worked out as the view does (the view now calls
    it), so an application can count what is drawn without estimating. The
    registry shows it in a strip under its title, beside a How this page
    works
    panel that lists the owners Module.manifest reads from its
    contracts and what each field holds now (#163).

  • Undo and redo in the data grid, and in the registry as new edits. Ctrl
    or Meta with Z on a foldkit-data-grid grid sends UndoRequested, with
    Shift (or Ctrl+Y) RedoRequested, each reported as the OutMessage of the
    same name. They carry nothing, since the history is the application's; an
    open editor keeps the keys. Grid.Out.match now needs the two arms. The
    registry keeps a per-tab stack: an undo is an EditedProducts of what each
    cell held, so it goes through the outbox like any edit, and a cell changed
    since is left alone and said. foldkit-sync/entity's EditableEntity
    gains changeAt(row, member), a cell's value as a Change (#164).

  • foldkit-remote-server: RemoteServer.memory authorizes per entity.
    It takes authorize, per entity name, the function RemoteServer.entity
    takes, and the principal its layer answers as; server answers as any
    principal through handlers. The principal's type is inferred from them, and
    principal is required unless that type admits undefined. A name the
    domain does not declare is refused when the backend is made. A memory-backed
    test or demo no longer copies the read path to try authorization (#147).

  • foldkit-sync/journal: TableAheadOfJournalError. editsJournal's
    settle refuses to start while the table holds a revision past the
    journal's cursor, as a table that outlived a reset of its journal does:
    every new edit would otherwise be skipped by apply and counted absorbed by
    the replicas, and lost with nothing said. It refuses on every exchange until
    the table is rebuilt with the journal (#176).

  • foldkit-primitives/net: SharedHost, one in-browser server for every
    tab and frame.
    SharedHost.define({ name, opening }) gives serve, a
    SharedWorker's entry, and connect({ worker, inPage }), whose open(opening)
    returns a MessagePort for one conversation. The host decodes each opening
    first and ignores one that does not decode or has no port, and its
    signal aborts when the page's Web Lock is released. Without SharedWorker
    the top document hosts, and same-origin frames send their openings there
    (#167).

  • foldkit-sync: Sync.transport.portSocket(port, { signal? }), a socket
    over a MessagePort.
    Its frames are the port's strings, anything else is
    ignored, and onClose fires once, on close() or when signal aborts, so
    a host that serves a pane stops serving it when the pane goes. The registry
    sandbox's own copy, whose onClose never fired, is gone (#167).

  • foldkit-remote/port and foldkit-remote-server/port: Remote over a
    MessagePort.
    port(open) is Remote's client and servePort(handlers, port) its server, over Effect's own RPC worker protocol, driven through
    effect/workers with no browser package: reads, queries, mutations and
    live streams cross it with their typed errors, an interrupted call
    interrupts its handler, and the serving ends when the client closes its
    end. portProtocol(open) is the protocol alone, for a hand-made RPC
    client (#167).

  • Docs: Editing server data through a
    journal
    .
    When rows are too many to
    replicate but edits to them must survive offline: who owns what, the one
    rule (a cell shows its edit while pending or committed after the row's
    revision), a one-column path through foldkit-sync/entity and
    editsJournal, conflicts, refusals, absorbing, and each failure case,
    with the registry as the worked example. Its snippets compile in
    examples/registry/test/guide.test-d.ts; the registry README keeps what is
    its own (#171).

  • foldkit-sync/journal: editsJournal, the table as the journal's read
    model.
    Its settle applies each committed change through the
    application's apply as a recovery intent and its absorb records what
    the table holds and compacts behind it. Recovery starts at the journal's
    floor, so a server restarted over a compacted journal no longer fails
    every exchange asking for history that is gone; intents are keyed by the
    journal's epoch, so an operation sent again after a reset is applied
    again. The registry's journal is built on it (#166).

  • foldkit-sync/entity: edits to rows a server owns. EditableEntity.make( entity, { members }) keeps edits one per cell, each value typed by its
    member, committed at a sequence by an author ({ actor, replica }),
    and lays them over Remote's rows by each row's revision. Pure functions
    merge and absorb them, hold an absorbed edit while its cached row is
    behind it and settle it when a read reaches it, report a cell another
    tab's later commit replaced, and name a refusal's cells. examples/registry
    is built on it: a change is one cell and the server writes the one column
    it names. Another editable column is an entry in members, its grid
    column, its arm in changeOf and its table column; none of the edit rules
    is written per field any more (#166).

  • foldkit-sync: a reinstall says when the server was reset.
    ReplicaStatus.epoch is the server history the cursor points into, and
    Sync.mount's onReinstall(next, previous, { reset }) is told when it
    changed. examples/registry reads its rows again on a reset: their
    revisions counted the old history, so an edit committed since at a low
    sequence looked as if the rows already held it (#166).

  • foldkit-mixins-data-grid: a choice is a list the grid draws. A
    column of literals edits as a role="combobox" beside its role="listbox"
    (new choiceList Slot), styled with the page, opening below the cell or
    above it near the bottom of the view; arrows, Home, End and the page keys
    walk it (EditStepped in foldkit-data-grid), a letter finds an option
    (EditTyped on a choice), and a press chooses one (EditChosen) without
    the blur committing the old draft first. choiceEditor: 'native' keeps
    the platform's select (#168).

  • foldkit-mixins-data-grid: shared cell marks and their legend.
    GridMarks names five marks (pending, saved, refused, replaced,
    peer); GridMarkStyle, attached after GridStyle, draws each on the
    theme's tokens with a shape of its own and an outline under forced
    colours; GridLegend draws what they mean beside swatches
    GridLegendStyle paints with the cells' rules. examples/registry uses
    them in place of its hex colours and hand-drawn legend (#168).

  • foldkit-mixins/theme: shadow tokens. Theme.tokens.shadow is a
    scale (xs, sm, md, lg, xl, inset) drawn in one color,
    --fk-shadow-color, falling back to a faint black; Theme.oklch sets it,
    a near-black of the neutral hue that is denser in a dark scheme, where a
    shadow mixed from the text glowed. GridStyle's editing lift and error
    bubble and the dialog and segmented recipes use the scale (#168).

  • foldkit-data-grid: an unchanged edit reports nothing. The edit keeps
    the text it began from (editing.from); a commit of that text, or of text
    that decodes to the same value (Equal.equals), closes the edit and moves
    focus with no Out.Edited. Leaving an editor as it opened, after another
    device changed the cell beneath it, no longer sends the old value over the
    new one (#174). EditTyped carries from, the text the cell showed.
    A paste does the same per cell: Clipboard.pasteAt takes { editable, from } (was a positional editable), each Pasted cell carries from,
    unchanged cells are dropped, and a paste or cut that changes nothing
    reports nothing. examples/registry drops its own holds check.

  • foldkit-mixins-data-grid: clicking away saves, and a refused draft
    says why.
    Focus leaving the editor commits it, as Enter does without
    moving; a draft the column refuses stays open, its field in the error
    colour, with the column's message shown below it in a new editorError
    Slot (role="alert", named by the field's aria-describedby, which
    replaces its aria-description). GridStyle draws the sort direction as
    a small chevron whose place is kept while unsorted, so sorting no longer
    wraps a narrow header or moves its label, keeps header labels on one
    line, and parts columns with a faint line that is, on a header, the
    resize handle. The registry's page is in neutral greys.

  • A mark says what it means, and an unchanged edit is none. A cell's
    mark is also its title, shown on hover (the description a screen
    reader hears was there already). The registry draws a legend of its marks
    above the grid, presence in violet so it is not taken for a replacement,
    and a cell committed with the value it already shows (the same text, or
    01.73 for 1.73) makes no edit: nothing is sent and nothing marked.

  • foldkit-mixins-data-grid: a column that does not edit says so. In a
    grid where some columns edit, the others' headers and cells carry
    aria-readonly, and GridStyle draws a padlock before the header's
    label (with a Read-only title) and the cells' text muted, with the
    ordinary pointer. A grid where nothing edits marks nothing.

  • foldkit-mixins-data-grid: a choice opens its list. The select a
    column of literals edits in opens its list as it opens (showPicker,
    where the browser allows), so a double-click shows the choices without
    another click. The registry's status column is wide enough for its
    longest.

  • foldkit-mixins-data-grid: an edit looks like one, and a pin shows.
    A double-click opens an editable cell's editor, as Enter does. The cell
    being edited carries data-editing, and GridStyle lifts it with a
    shadow and gives the field the whole cell, outlined and with a caret in
    the focus colour, where before it was the browser's bare input inside the
    cell's padding; an editable cell carries data-editable and shows a text
    cursor. The pinned column next to the scrolling ones carries
    data-pinned-edge, drawn as a divider, so a pinned column is seen to be.
    The registry sorts every column the server can order, UPC, line and
    status too, and edits the line and the status as well (a choice of the
    Product's statuses), so a double-click on any column but the UPC edits.

  • foldkit-durable/core: the journal without Node. Journal.layer(options)
    and Journal.define(key).layer(options) give the journal as a layer that
    needs a SqlClient, any effect/sql SQLite client, such as
    @effect/sql-sqlite-wasm in a browser, where a browser test runs append,
    retry, refusal, recovery, compaction and reset over it. Provide the driver
    to the layer, and the database opens and closes with the journal; a
    journal handed out as a value could keep a connection already closed
    (#169). Payloads are hashed with @noble/hashes instead of
    node:crypto, to the same digest, and epochs come from
    globalThis.crypto.randomUUID. The main entry's Journal.make over a
    node:sqlite file is unchanged (#162).

  • examples/registry says where each edit is. A cell's mark shows an
    edit not yet sent, one the journal has and the table does not yet, and one
    the server refused, edged until its line (naming the cells and the
    server's reason) is dismissed. A Work offline switch pauses the
    transport, and switching back exchanges at once (#161).

  • examples/registry says when another device's edit won. The journal
    stamps who committed each edit (by); a field this device last wrote that
    another device's later commit replaced is edged amber, with a line naming
    the device and the value it had, until dismissed. A connection names its
    device (?device= on the socket). The registry's server and journal no
    longer import Node (an SQLite seam, and foldkit-durable/core), so a
    sandbox can run them in the page (#162).

  • examples/registry runs in the browser: two devices, one page.
    build:sandbox builds Device A and Device B side by side, each a page in a
    frame with its own replica, against one server in a SharedWorker: the
    products table on sql.js and the journal on SQLite compiled to WebAssembly,
    over MessagePorts. An e2e test builds it, serves the files, and drives a
    shared edit, a conflict the losing device is told of, and an offline edit
    that survives a reload (#162).

  • examples/registry: presence, and each device's status on its card.
    A device announces the cell it has focused over Sync's presence, on the
    same socket the journal is served on, and the other draws it outlined in
    violet; a device working offline leaves. In the sandbox, each card's dot
    and badge say where that device's edits stand, from a status its frame
    posts to the page.

  • foldkit-sync: why an edit was refused. An exchange may carry
    reasons: [{ opId, reason }] beside rejected (each at most 500
    characters; a reason for an operation not rejected fails the exchange),
    and journalExchange gives each rejection one: an authorize rule's own,
    now that a rule may return { allowed: false, reason } as Durable's does,
    or a fixed sentence ("Not a valid operation", "This connection may not
    make changes"), never an error's message. Breaking:
    ReplicaStatus.rejected is ReadonlyArray<Rejection>, { opId, reason: Option<string>, operation }, in place of the ids: the refused operation
    comes with its refusal, so what it changed is read from it rather than
    looked up while it was pending, which a page that heard late could miss. The todo-app's rules give their
    reasons (#161).

  • foldkit-mixins-data-grid: cell marks. marks: address => Option.some({ name, description }) gives a cell a state of the
    application's: data-mark on its Slot, which the default style shows as a
    dot in the corner and an application styles by name, and the description
    as its aria-description. Asked only for the cells drawn (#161).

  • foldkit-durable: a commit stamp. JournalOptions.stamp(operation, { sequence, actorId }) writes what the commit decided into the operation,
    after validate and authorize and before reduce. The stamped operation
    is what is stored, read, loaded and recovered; a retry is still recognized
    by what was sent, since a retransmission is now compared by the hash of
    the operation as sent rather than by the stored text. A stamp that changes
    the opId is refused. For a read model that must know which commits it
    holds (#159).

  • foldkit-sync: commit stamps on the contract. make({ stamp: { Variant: (message, { sequence, actorId, replicaId }) => … } }) writes what the
    server's commit decided into a durable Message, per variant, and returns
    that variant (another is rejected by id; a non-durable key is a type
    error). It rides in journalContract() as Durable's stamp, so replicas
    replay the stamped Message in place of what they sent. defineSync takes
    the same as stamp(message, commit). New types CommitStamp and
    StampPolicy. A replica exposes its replicaId, the one its commits
    are stamped with.

  • foldkit-data-grid, the grid's pure model. Phase 0
    of the DataGrid design: typed Columns
    by stable id, a RowModel with a Known or Unknown RowCount, a
    saveable ColumnLayout of start, center and end regions with hidden
    columns, and a GridProjection that answers moves, edges and rectangular
    ranges in display order, each as an Option. A cell is named by row key
    and column id, so it survives a re-sort or a reorder.

  • foldkit-data-grid, focus (Phase 1). GridFocus.make(columns) is a
    Bundle holding the focused cell as an Option, stored as null, and
    decoded only against the grid's own column ids. GridFocus.target is the
    cell a key moves to (arrows, Home and End, Ctrl for the corners, PageUp and
    PageDown, RTL), tabStop the cell holding the one tab stop, and cellId a
    DOM id for aria-activedescendant that no two cells share.

  • foldkit-data-grid, virtualization (Phase 2). VirtualGrid.window is
    the rows and center columns a viewport shows, with overscan and spacers;
    rows are one height, so it never walks them, and pinned columns are always
    drawn. VirtualGrid.reveal is the least scroll that shows a cell.
    GridViewport keeps the container's offsets and size in the Model, its
    Measure Mount reports them on mount, scroll and resize, and scrollTo is
    the Command that applies a reveal. A benchmark at 100,000 rows is in
    docs/benchmarks.md.

  • foldkit-data-grid, DataGrid.make({ id, columns }). Focus and the
    viewport as the one Bundle a grid places. A key's Moved carries the
    reveal the view worked out, and the update issues the scroll for the
    container named id.

  • foldkit-mixins-data-grid, the grid drawn (Phase 3).
    DataGridView<Message>().define(Grid) draws a DataGrid as a WAI-ARIA
    grid of the viewport's window of cells: aria-rowcount (or -1 when the
    count is unknown), each cell's logical aria-rowindex and aria-colindex,
    one tab stop with aria-activedescendant while the focused cell is drawn,
    sticky pinned columns, a placeholder for rows not loaded, and the keyboard
    through GridFocus.target. Every element is a Slot; a Style that sets the
    geometry the window depends on is refused. GridStyle is a default look in
    the components layer. It renders on a server as markup a parser reads
    back the same.

  • foldkit-data-grid, column state (Phase 4). A column spec takes
    width, minWidth, maxWidth, resizable and hideable. The
    DataGrid Model holds the column state, a ColumnLayout plus the widths
    resized, with ColumnResized, ColumnHidden, ColumnShown and
    ColumnMoved (a reorder, a pin or an unpin) to change it; each returns the
    Model it was given when it changes nothing, and the last column shown
    stays. columnState.restore reads a saved state back leniently and lists
    the ids it dropped. Grid.project(rows, state) is the projection for a
    column state, built once per rows model and state. The view takes rows
    in place of projection and width, and draws the column state.

  • foldkit-mixins-data-grid, resize handles. Each resizable column has a
    role="separator" handle with its width in aria-valuenow: a pointer drag
    resizes from where it began (ResizeStarted, ResizeMoved,
    ResizeEnded; a cancelled drag puts the width back), and a focused handle
    steps with the arrow keys, mirrored in right-to-left text. The grid now
    takes only keys aimed at itself, so a handle's keys do not move focus.

  • foldkit-data-grid, selection (Phase 5, the state). Opt-in with
    rowSelection: 'single' | 'multiple' and cellSelection: true. Rows are a
    RowSelection of Keys or AllExcept, so select-all holds rows not
    loaded; RowSelected, RowsExtended, AllRowsSelected and
    RowsCleared change it, with an anchor for Shift ranges. One rectangle of
    cells is held by its corners (CellsSelected, CellsCleared); a plain
    click or key lets it go. GridSelection.extend is the range a Shift key
    makes and rowsBetween a Shift range of rows.

  • foldkit-mixins-data-grid, selection drawn and driven. Rows say
    aria-selected and cells in the range say it; the grid is
    aria-multiselectable. One click listener on the body (CellPressed,
    parsed and checked in the grid's update) focuses a cell, spans a range
    with Shift and toggles a row with Ctrl or Meta. Shift with a move extends
    a range and scrolls its far corner in, Space and Shift+Space select rows,
    Ctrl or Meta with A selects all, and Escape lets a range go. The per-cell
    mousedown handlers are gone.

  • Over Remote and CRUD (Phase 7). foldkit-data-grid/crud (optional
    peers foldkit-crud and foldkit-remote): GridCrud.columns(list, { columns, words }) from a Crud.list's members and Displays, with the
    grid's own per-member options (pinning, widths, edit, a header), rows(page, key) a row model of a
    Remote page (unknown while there are more; the rows a failed read had),
    and status(page) a RowStatus. The view takes status (busy while
    loading or refreshing, a failure with onRetry), onMore while the count
    is unknown, and sort for aria-sort and a sort button, Enter on a
    header sending it too.

  • The clipboard (Phase 8). Clipboard.toTsv and parseTsv write and
    read spreadsheet text (quoted cells, any line end); copy is a box's cells
    and pasteAt where pasted text lands. The grid's OutMessage is now a union,
    Out.Edited | Out.Pasted: a paste is checked cell by cell against each
    column and reported once, with what was refused. The view copies the range
    (or the focused cell), pastes from the range's corner, and cuts by copying
    and clearing, leaving the clipboard to a cell being edited.

  • pnpm e2e, the examples with a server end to end. A Vitest project
    of its own (vitest.e2e.config.ts), kept out of pnpm test, and a CI job:
    each test starts the example's server and Vite and drives Chromium with
    Playwright. The registry's covers reading on scroll, a server sort, an
    edit read back from the table and kept across a reload, and the column
    menu. The todo app's covers a todo reaching a second person's page without
    a reload, written to the journal, and kept across one. The CMS's covers a
    writer's draft that the site does not show, and an editor's publish that
    it does. The entity example's covers a title saved from the form, read
    back from the table and kept across a reload; the pages example's, two
    tabs typing into one page, read back by a browser with nothing stored.
    examples/pages gains startPagesServer (run by serverMain.ts) and a
    Vite config: the page connects to /sync on its own origin, proxied to
    the server, and the workspace packages resolve to their source, so it no
    longer needs pnpm build.

  • Messages carry intent: the guide, and Frames.track to test it.
    docs/state-model.md says why a Message should carry what the user did
    and update work out what it means, with the cases that were bugs.
    Frames.track() (foldkit-mixins/testing) tracks the page's frames:
    hold() keeps them back so a test's events meet the view as last drawn,
    and settle() waits until everything asked for is drawn, instead of a
    sleep. Sync.mount's handle gains settled(), which waits until the Model
    has caught up with the replica (#170). Sort.inputs's callback also
    gets the column clicked (foldkit-crud), so a sort header sends it and
    update toggles; the entity and registry examples do, and two clicks
    inside one frame now sort descending.

  • App.runnable({ initial, update }) (foldkit-surface). An
    application made from its Model and Messages is made runnable afterwards,
    with the same references and owner, for an update built over those
    references (a Remote domain, a placed Bundle) that Sync replays. The
    registry had two applications with two owners for this; it has one.

  • foldkit-sync/journal, the server's exchange over a journal.
    serveJournal(socket, options) answers one socket's exchanges over a
    foldkit-durable journal (an optional peer) and notifies it of each
    commit; journalExchange(options) is the same as a TransportClient, and
    journalChanges the notice alone. An operation that does not decode,
    names another document, is refused by authorize or validate, or reuses
    an id is rejected by its id, so one bad operation no longer blocks the
    outbox; a cursor ahead of the journal's fails before anything is appended,
    and a replica of another epoch is answered from the start. refuse
    rejects by what the transport established, and settle applies commits
    elsewhere after each exchange. The todo-app, registry and kitchen-sink
    servers use it; todo-app's replicas now hear of each other's commits.

  • Remote over plain JSON (foldkit-remote, foldkit-remote-server).
    Remote.http(url, { headers }) is Remote's client over POSTed JSON,
    Remote.json(send) the same over any way of sending it (a worker, a
    server in the page), and RemoteServer.answer(handlers, body) the server's
    side: it decodes the request and payload by the protocol's own schemas
    before a handler sees them, and answers 200, 400 for a body that is not a
    request (an operation named constructor included), or 500, a defect
    saying only Internal error. RemoteJsonRequest and RemoteJsonAnswer
    are the wire. The entity, CMS and registry examples use them in place of
    their own copies.

  • Typed editing from Schema (foldkit-data-grid). A column's edit
    takes a schema from the text typed to its value, in place of
    validate: a draft is committed only when it decodes, and the failure's
    message is the cell's error. Grid.matchEdit(cell, handlers) reads a
    reported cell's value back, one handler per editable column, each typed
    by its column's schema. GridCrud.columns keeps each member's options in
    its type, and Columns carries its specs type-only, so a grid infers a
    column's schema exactly. The editor is read from the schema
    (CellEditor, Grid.editorFor): a union of string literals is a select
    of them (new choice and choiceOption slots), a number a text field
    with inputmode="decimal", anything else text.

  • RowModel.map(rows, input, make) (foldkit-data-grid). Transforms
    each row as it is read, with the per-row function made from input once
    per rows and input, and the same model while both are. The registry's
    edits over Remote's rows are one call.

  • examples/registry, the registry over Remote and Sync. 100,000
    products in SQLite behind a Drizzle RemoteServer, read a page at a time
    through a Crud.list, sorted on the server, loaded more on scroll. Edits
    are a Sync document kept on the device until the server's journal commits
    them; the journal applies each to the table through recover, and the
    grid draws a row with the replica's edits over it, so an edit survives a
    reload while offline and another device's shows on the next exchange.

  • A sorted header shows its direction (foldkit-mixins-data-grid). The
    sort button carries data-sort (asc or desc), and GridStyle now
    styles the sort slot as the header's text with an arrow after it; it
    had been left a native button.

  • Fast typing on a grid keeps every key (foldkit-data-grid). A key
    typed on the grid itself is now EditTyped({ address, text }): it starts
    the edit, or adds to the one open on that cell. Typing "Dowel" faster than
    a frame used to restart the edit on each key that reached the grid before
    its editor took focus, leaving "l"; Escape in that gap now cancels.

  • A column menu (foldkit-mixins-data-grid). columnMenu: true puts a
    menu button on each header (menuButton) opening a role="menu"
    (menu, menuItem) of Grid.menuItems; Alt+ArrowDown, Shift+F10 or the
    menu key opens it from a focused header. It takes focus, walks with the
    arrows, Home and End, runs an item on Enter, Space or a click, and closes
    on Escape, a choice or focus leaving it. Words: menu, pinStart,
    pinEnd, unpin, hide, show.

  • A column menu's state (foldkit-data-grid). menu in the Model
    holds the column whose menu is open and its active item; MenuOpened,
    MenuMoved, MenuChosen and MenuClosed drive it. Grid.menuItems
    lists pin to another region, hide, and show each hidden column, each only
    when it changes something, as Grid.MenuItems; a chosen item runs the
    column Message it stands for. Hiding a column now also takes its header's
    focus and closes its menu, as it already ended its edit.

  • More rows as the end comes into view (foldkit-mixins-data-grid).
    moreOnScroll: true beside onMore sends it when the More button comes
    within 200px of the grid's visible box (an IntersectionObserver rooted at
    the grid), and again after each load while the button stays in view; it
    asks nothing while status is busy.

  • A column header dragged to reorder it (foldkit-data-grid). The grid
    holds a drag as its column and the pointer's delta (dragging), from
    ColumnDragStarted with the header's DOM id (GridFocus.headerOf reads
    it back), ColumnDragged and ColumnDragEnded. Let go, the column moves
    within its region past the middle of each shown neighbour it crossed,
    worked out then by ColumnState.dropAt from the columns as they stand; a
    cancelled drag moves nothing. ColumnState.regionOf says which region a
    column is in. foldkit-mixins-data-grid draws it: a press on a header
    moved past 4px drags it (a click, on a sort button too, stays a click),
    the header follows the pointer with data-dragging, the neighbour it
    would land beside has data-drop, and Escape lets it go back.

  • examples/data-grid, a 100,000-row product registry. The UPC
    pinned, descriptions and prices edited with validation, rows and ranges
    selected, and copy and paste; the products are the application's, written
    by onOut from the grid's Edited and Pasted. Tested in jsdom and, at
    full size, in Chromium.

  • Editing (Phase 6). A column with edit: { draft?, schema? } is
    editable as text. The DataGrid Model holds the session (editing), with
    EditStarted, EditChanged, EditCommitted and EditCancelled; a commit
    the column accepts moves focus and returns the OutMessage Edited({ row, column, text }), so every grid is now placed with onOut
    (Bundle.ignore for one that edits nothing). A refused draft stays with
    its error; a click elsewhere commits first; hiding the column ends it. The
    view opens a focused field on Enter, F2 or a typed key, commits with Enter
    and Tab, cancels with Escape, marks a refused draft aria-invalid, and
    hands focus back to the grid.

  • The header row by keyboard. Focus holds a header beside the current
    cell (HeaderFocused, revealing a header scrolled away): ArrowUp from the
    first row reaches it, the arrows and Home and End walk it, and ArrowDown or
    Escape return. On a header, Shift with an arrow resizes the column and Ctrl
    or Meta with Shift and an arrow reorders it. GridFocus.headerId names a
    header cell.

  • foldkit-data-grid, GridFocus.cellOf reads a cell id back into an
    address; an id from another grid, or with a malformed escape, is none.

  • foldkit-data-grid, Columns.define written inline keeps its row type.
    Inside DataGrid.make({ columns: Columns.define<Row>()({...}) }) every
    row was unknown; the return type is NoInfer now.

  • foldkit-data-grid, GridViewport.scrollTo on a container that cannot
    scroll
    still reports Revealed: a DOM with no layout has no scrollTo,
    and the Command threw there.

Changed

  • foldkit-mixins-data-grid: more on scroll is the MoreOnScroll
    Behavior, not a GridInput flag.
    moreOnScroll: true is gone: attach it
    to the view, DataGridView<Message>().define(Grid).pipe(MoreOnScroll). The
    More button is now keyed by the rows loaded and whether a load is in
    flight, whatever is attached (#157).
  • foldkit-sync/journal: editsJournal takes tableRevision, the
    highest revision the table holds, required, for the check under Added. An edit to
    a row the table lacks is documented as the journal's validate to refuse,
    since recovery cannot refuse what has committed; the registry does so (#176).
  • foldkit-sync: Sync.transport.socket takes a url or a makeSocket,
    not both.
    makeSocket takes no argument now, and a socket opened by one
    needs no placeholder url; passing both is a type error (#167).

Fixed

  • foldkit-remote-drizzle: contains folds ASCII alone on Postgres too.
    Postgres's lower follows the database collation, so under a UTF-8 one a
    search for é found Élan, which SQLite and the reference interpreter do
    not. The fold now runs under the C collation on a Postgres table, and the
    conformance suite runs against Postgres (PGlite) as well as SQLite (#145).
  • foldkit-remote-drizzle: a search the SQL cannot hold is the query's own
    error.
    A query input holding NUL threw QueryCompileError from inside the
    request, which the RPC server answered as a protocol-level defect, so any
    client could turn a query into a server defect and the two backends told the
    same input apart. It now fails with RemoteServerError, which the client
    receives as RemoteQueryError, as the memory backend's does (#146).
  • foldkit-sync: Sync.presence.serve stops when its socket closes, as
    Sync.transport.serve does. A peer whose socket had closed stayed in the
    hub and was sent every other peer's presence for the life of the server.
  • foldkit-sync: statusChanges and changes miss no change. Each
    emitted the current value and subscribed to later changes only once that
    value was taken, so a subscriber slow to take it, and any change between
    the read and the subscription, went untold. They subscribe first now.
  • examples/registry: an edit lost while offline is said. When the
    journal absorbed another device's later edit of a cell before this device
    heard of it, the page kept showing its own value as saved until some read,
    then changed it silently. A newly retired edit now asks for its rows again,
    and a read at or past an edit's revision holding another value says "a
    later edit replaced yours" (Replacement.by is an Option). The page names
    its device in the Model (DeviceNamed), so replacedOf reads it there (#173).
  • foldkit-sync/journal no longer reaches Node. It imported
    foldkit-durable's main entry, which brings node:sqlite, so a browser
    bundle of it failed; it imports foldkit-durable/core.
  • foldkit-remote-drizzle and foldkit-cms-drizzle take drizzle-orm as
    a peer.
    As a dependency, an application whose Drizzle resolved with other
    optional peers got a second copy, and its tables did not type-check against
    the adapter's. Install drizzle-orm beside them.
  • examples/registry: a committed edit no longer hides the table for good.
    Every edit a device knew of was laid over every row Remote read, with
    nothing to say when the table had it, so a row changed afterwards by any
    other commit kept showing the old edit. The table is the journal's read
    model: each row carries the revision it has read the journal to, written
    with the change in one statement that never moves a row back; the journal
    stamps each edit with the sequence it committed at; and a row shows an edit
    only while it is pending or committed after the row's revision. A table
    write that throws now fails its recovery intent and is retried on the next
    exchange, where it died before; an operation that says when it committed
    is refused. Every few seconds the server records, as its own operation
    (AbsorbedEdits, refused from a client), that the table holds every edit
    through its recovery cursor: replicas drop those edits and the log is
    compacted, so the replicated slice is what the table lacks. A page keeps an
    absorbed edit in retired, set by onReinstall, until its cached row is
    read at the edit's revision. The document is registry-edits-2, since
    edits changed shape, so a replica stored under the old one is not opened
    (#159).
  • examples/cms starts its studio again. Since the studio's sections
    share one runtime, the entry drew it into a new element in place of #app
    that had no id, which the runtime refuses before drawing, so pnpm dev and
    the sandbox build showed a blank studio. The element takes #app's id.
  • examples/todo-app adds what was typed, and keeps edits from a second
    browser.
    The composer sent RequestedTodo({ title: model.draft }), the
    draft of the frame last drawn, so Enter pressed before the next frame
    added nothing; it sends DraftSubmitted and update reads the draft, and
    an agent's RequestedTodo no longer clears it. The replica was named by
    the token, so a second browser signed in as the same person restarted its
    operation ids and the journal refused each new one as reused; each tab is
    its own replica now. The end-to-end test found both.
  • foldkit-sync, Mounted.dispatch takes a Message as its type is. It
    handed the value to a Foldkit inbound Port, which decodes what it is sent,
    so a Message with a transforming field (Schema.OptionFromNullOr, a
    NumberFromString) failed with a SchemaError and never reached
    update. It encodes the Message first now.