Repository navigation
v0.17.0
foldkit-agent 0.7.0; foldkit-bundle 0.6.0; foldkit-cms 0.6.0;
foldkit-cms-drizzle 0.6.0; foldkit-composition 0.2.0; foldkit-crud 0.7.0;
foldkit-durable 0.8.0; foldkit-entity 0.8.0; foldkit-mixins 0.9.0;
foldkit-mixins-form 0.5.0; foldkit-mixins-ui 0.8.0; foldkit-primitives
0.8.0; foldkit-remote 0.14.0; foldkit-remote-server 0.13.0;
foldkit-remote-drizzle 0.11.0; foldkit-ssr 0.5.0; foldkit-surface 0.8.0;
foldkit-sync 0.10.0; foldkit-data-grid and foldkit-mixins-data-grid 0.1.2.
New: foldkit-guard and foldkit-site 0.1.0. Republished to re-pin:
foldkit-agent-a2a, foldkit-agent-mcp and foldkit-agent-native 0.5.2,
foldkit-agent-webmcp 0.6.1, foldkit-builder and foldkit-mixins-builder
0.1.1, foldkit-bundle-surface 0.3.2, foldkit-form 0.4.2, foldkit-mirror
0.5.2, foldkit-mixins-crud 0.6.4, foldkit-mixins-richtext 0.2.3,
foldkit-mixins-surface 0.5.3 and foldkit-richtext-dom 0.3.1, each of which
pins a bumped package. Every other package is unchanged.
Who may do what is declared once, and Remote places rows instead of
refetching them. foldkit-guard declares guards on the shared domain and
enforces them on every seam: reads compiled into SQL, declared writes and
handlers checked before they land and answered as their writer may see the
row, Sync's cell edits, live updates and agents, with a startup check that
refuses a Source left unguarded. A write is declared as data (Write.update,
insert, delete) and served with no handler. A list whose query body
declares its order has a change placed in it with no request, checked against
the server under verifyPlacement. foldkit-site states a routed site's
topology once. And a review of this release's work fixed the ways a write or a
read could get past its rules, lose another author's edit, or send the server's
SQL to a client.
Upgrading from 0.16
journalExchange'srefuseisadmit(foldkit-sync), which answers as
authorizedoes and may be anEffect;opensrefuses a connection whole.
defineDocumentHosttakes both as functions of the principal, and its
settleis(env, journal) => Effect.foldkit-sync/entity's overlay helpers are gone (overlay,held,
newlyHeld,settled,shows):RemoteEditsfromfoldkit-sync/remote
shows the edits through Remote.Remote.visibleItemsisData.visibleItems(remote, connection)
(foldkit-remote), which draws a list as its reads do.- An
OrderTermis taggedTerm, and a body'sorderByholdsOrderings
(foldkit-entity): an interpreter resolvesQuery.orderFor(body, input)
first.foldkit-remote-drizzlerefuses anorderBybeside a body that
declares its order. Cms.slugTakenand theCmsConflict:/CmsSlugTaken:message prefixes
are gone (foldkit-cms): read a refusal withData.refusal
(Cms.refusal.field,Cms.refusal.conflict).CmsServer.maketakes a guard set (foldkit-cms-drizzle):policyand
authorsforisAuthor,transitionsbesideallow, and eachcontent
entry its guardedentity;published(column, isAuthor)is gone, bind with
GuardDrizzle.visible.Transactionmay fail with aRemoteServerError.LiveHub.deletedmay fail and needs the Sources' services
(foldkit-remote-server), aschangeddoes; it re-reads each subscriber's
selection rather than deleting for all.returning.rowfails with aRemoteServerError(foldkit-remote-drizzle)
when the database refuses it; run a handler's own statements through
runStatement.
Added
-
foldkit-remote-server: fields every principal reads alike skip the live
hub's re-read. A Source declaresshared(RemoteServer.entity,
RemoteServer.memoryper entity,foldkit-remote-drizzle'ssource);
hub.changed(ref, fields, values)sends a shared field's given value to
each subscriber as it is and re-reads the rest, and the handlers give it a
declared write's answer, the row as stored (a handler's answer is still
re-read). A shared field the Source does not serve is refused when it is
made.foldkit-guard:GuardServer.validaterefuses a shared field a Read
rule could tell readers apart by. -
One editor for create and edit.
foldkit-entity: anOption-typed
input key maps itself to the field its value fits, as a nullable one does,
so an input's id may beSchema.OptionFromNullOr(PostId).foldkit-crud's
editor then saves a blank form withOption.none()and an opened one with
its id.foldkit-guard:GuardServer.mutation'sidmay answer an
Option, andnoneis a create, checked against the row'sCreaterule. -
foldkit-agent:authorizemay refuse with a reason. It answers
{ allowed: false, reason }(anAuthorizationDecision) beside a boolean,
and the reason is theAgentAuthorizationError's message.
foldkit-guard:maycarries explained refusals. Its answer gains
reasons, the sentence anexplaingave each row interaction it refused,
read withMay.reason;GuardAgent.authorizerefuses with it, so an
agent learns why as a client's write would. -
foldkit-entity,foldkit-remote: a list is fetched, not placed, when
a row changes what the server's read rule filters on.ReadFilter.of(members)
is Entity metadata naming the members a server filters rows by under a rule
the client does not run;foldkit-guard'sattachdeclares it from the
row's Read rules. Remote gathers it per Entity name when the domain is
defined (registry.filtered), so a query declared over the Entity before
the rule was attached still sees it, and a row a list does not hold that
changes one invalidates the list. A row it holds is judged by the body as
before. -
foldkit-remote-drizzle: avisiblerule may answer with anEffect,
and is given its own binding.(principal, self) => SQL | undefined | Effect<SQL | undefined, RemoteServerError>, so a rule that must ask
something first (a guard's body) can, and one written beforebindnames
columns throughself.columns. Every path a table is read by applies it as
before. -
foldkit-remote-drizzle:source(binding, { filter }), the
per-record read planRemoteServer.entitytakes, passed through. A source
and aquerycarry their binding'svisible, for a startup check. -
foldkit-agent:withPrincipal(key), the principal's type taken from
the Context key a server reads it from (G.Principal), so the agent's and
the server's are one type.withPrincipal<P>()stays. -
foldkit-crud: an editor locks the keys its row's reader may not set.
Crud.editor(name, { form, mutation, locks })loads the memberslocks
selects beside the form's, andeditor.at(...).locked(model)names the
form's keys whose member (or relation)locks.lockedrefuses, for
foldkit-mixins-form'slocked. Nothing is locked for a new one, or
before the row is loaded.foldkit-guard'sMay.locksis one. -
foldkit-mixins-form: keys drawn but not editable. The view input
locked: ['ownerId']draws those keysdisabled, every control kind (a
many picker's choices included), for a key the person may not set. A key in
a nested row is not locked by it, and a Bundle-backed key draws as its
Bundle does. -
foldkit-remote,foldkit-remote-server: a refusal of who is asking,
end to end. A mutation Source fails withRemoteServer.forbid(reason?);
the wire carries it asRemoteForbidden(_tag: 'Forbidden',reason
only when given),RemoteServer.answersends it as a 403 with
{ error, forbidden: { reason? } }, andRemote.jsonreads it back. A 403
whose body is not Remote's answer, from middleware in front of the server,
fails a mutation asForbiddenand any other call with the status.
Data.mutationisFailedwith theForbiddenerror, andRemoteError
carriesreason. -
foldkit-remote-server: a principal read from Context on each call, and
a live hub keyed by person.RemoteServer.handlers(server, principal)
takes a Context key forprincipalas well as a value, read by every read,
query, mutation and live subscription, so one handler set (one RPC layer)
answers every request and authentication provides the key per request; the
key is added to what the handlers require.RemoteServer.liveHub(sources, { key })shares a re-read between subscribers whose principals have one
key, read as the first of them; without one, only the same value shares. -
foldkit-remote-server,foldkit-remote-drizzle: a write answered as its
writer may now see the row.EntityWriter.read, when a writer has one,
reads a row as a principal may see it;RemoteServer.writeanswers through
it, and a row the write hid from its writer answers indeleted. A Drizzle
writerover a binding with avisiblerule reads through it. A writer may
fail withRemoteForbidden. A declared write's Source carrieswrites,
the Entity and the writer, for a startup check. -
foldkit-remote-server:EntitySource.visibleand
QuerySource.visible, the rule that decides which rows a source returns,
as it was given one. The server never runs it; a startup check such as
GuardServer.validatereads it.RemoteServer.memory's sources carry its
readable. -
foldkit-remote-server: what a principal sees, per record and per row.
EntitySource.filter(principal, fields)returns a read plan: the fields to
read (the allowed ones, and any the decision needs) andapply, one record
as the principal sees it; a member it drops is settled like one the Source
did not return. It runs for reads by id and live patches.RemoteServer.memory
takesfilterper entity, as it takesauthorize, andreadable(given a
lookup of another row by id), which
hides a row the principal may not read from reads by id, from every query,
and from relations (aoneref readsnull), as a binding'svisibledoes. -
foldkit-guard: a guard set declared once, decided on the server, and
enforced on every seam.Guards.declare(name, PrincipalSchema, guard => ({ … }), { key })
names principal guards (bodies only on the server, throughG.implement,
which must cover each one) and row guards (anExprover one Entity's
fields, the principal's fields asprincipal.*inputs).Policy.decideis
anEffectoftrue,falseor{ allowed: false, reason }; a visitor is
Option.none().G.Principalis the server's Context key for who is asking.
Guards.attach(G, { row, interactions, members, relations, always })
attaches guards to an Entity per surface and interaction (Writefor all
three writes; one the application names,Interaction.make('Archive'),
underinteractions: { [Archive.key]: … }), as metadata:
a list is a disjunction,Guards.alla conjunction,alwaysis ANDed
after every surface, andGuards.onwithredactorexplainchooses a
refusal other than the surface's default. A member the Entity lacks, a row
guard over another Entity, a guard of another set, or a redacted value of
the wrong type is a type error.Guards.matrixprints every surface and
Guards.guardFieldsnames what a row interaction's guards read.
A visitor passes no row guard that reads a principal's fields, and does
pass one over the row alone (isPublished).
Policy.readable(entity, principal)decides a principal's read rules into a
Conditionover the row, every principal guard asked once;Condition.holds
runs it over stored values, andPolicy.readMembersdecides each member's
own Read rule with its refusal.foldkit-guard/serverenforces them:
GuardServer.memory(Policy, entities)givesRemoteServer.memoryits
readableandfilter, so a refused row is absent from reads, queries and
relations (an inherited Read decided by the related row's own rules) and a
refused member is withheld or redacted;GuardServer.filteris a Source's
filter.foldkit-guard/drizzle'sGuardDrizzle.visible(Policy, entities)
compiles the row's Read rules into each binding'svisible, an inherited
Read as a subquery over the related table's own rule, checked when built
(the inherited Entity listed and ruled, no loop). A relation's Read rules
are decided over its parent row, as a member's are, and withhold the
relation where they refuse; over Drizzle, member and relation Reads are the
Source'sfilter(source(binding, { filter })).
GuardServer.validate(Policy, { entities, domain, server })checks at
startup that every guarded Entity's Source, and each query listing it,
applies the rule and filter those seams built for it from that Policy, and
throws naming each that does not.RemoteServer.handlers(server, G.Principal)reads who is asking per call, andPolicy.keyOf(principal)
names a principal by every field its rules may read, a visitor included,
for a live hub'skey.Policy.writable(entity, principal, interaction),
Policy.writeMembersandPolicy.linkabledecide write rules the same way,
a body told which interaction it is asked about.
GuardServer.writer(entity, Policy, writer)checks them on a declared
write before it lands: an update or a delete reads the row first as its
writer may see it (a hidden row's update is refused, its delete changes
nothing), the row's rule must hold over the row and, for an update, over it
as it would be after; each member set needs its own rule, each relation
pointed elsewhere itsUnlinkandLink; a create is checked over what it
proposes, an unset field matching no guard. A refusal isForbidden, with
anexplain's sentence.presets({ ownerId: user => user.id }) sets a
field from who is asking on every create, before the rules are checked;
an input that sets one is refused, and a visitor has none to give.
GuardServer.validatealso refuses a declared write of a guarded Entity
that does not land through theGuardServer.writerbuilt for it.
GuardServer.mutation(M, { entity, policy, interaction, id, source }, run)
is a handler's Source with the row's rule forinteraction(a named one
such asArchive.keyincluded) checked over the rowidnames, read
through the guardedsource, beforerunis given it; with noid, of
who is asking alone. Each row of the Entity its answer carries is read
back throughsource, one the handler hid from its writer answered as
deleted.Guards.mayadds a derived member,may, which
GuardServer.filteranswers per row as its reader sees it: the row's
Update,Deleteand named interactions, each member's ownUpdate, each
relation'sLinkandUnlink.May.can,May.canSetandMay.canLink
read it on the client, a member or relation following the row'sUpdate.
It is a hint read with the row; the write is checked when it is made.
May.locks(entity)isfoldkit-crud'slocksfor it.
foldkit-guard/agent'sGuardAgent.authorizeandGuardAgent.available
gate afoldkit-agentcapability on themayof a row the Model holds,
refusing until it is answered.
foldkit-guard/syncrulesfoldkit-sync/entity's cell edits:
GuardSync.document(edits, { readers })is a document's audience, refused
where it is declared when an edit could reach a reader its rows hide it
from;GuardSync.authorizeis a journalauthorizeof the principal's own
guards, under its lock;GuardSync.admitandGuardSync.opensare an
exchange'sadmitandopens, reading each edited row through the
Entity's guarded Source. -
foldkit-entity:Expr.in(value, among), whether a value is one of a
list, written out or given as an input. SQL'sinin every interpreter:
a null value is unknown, so is no match against a list holding a null, and
an empty list matches no row. The reference evaluator runs it (and is
checked against SQLite's ownin),foldkit-remote-drizzlecompiles it for
SQLite and Postgres (an empty list asfalse, which Postgres needs), and
the TanStack example runs it. The conformance suite gains eight cases. The
first slice ofguard-DESIGN.md, whose membership rules need it. -
foldkit-remote:verifyPlacement, a placement checked against the
server.Remote.make({ verifyPlacement: true })places as usual, fetches
each list it placed an answer, a read or a live patch in too, and keeps any
disagreement between the order placed and the server's first page in
remote.placementMismatchesandData.inspect. Pending edits are not
checked. -
foldkit-remote:placeIn, where a row falls in a loaded list. By the
body's declared order: before an edge, at a segment's end that closes the
list,Outsidewhat is loaded, orUnknownwith why. A list whose body is
placeable reads the fields its body filters and orders by, for it.
foldkit-entityexportscompareRows, the evaluator's comparison of two
rows under an order. -
foldkit-entity:Order.chosen, a sort the input picks, declared in the
body.Order.chosen(input.sort, { title: Post.fields.title })maps each
name a{ by, direction } | nullsort can hold to a field, and a name
without one does not compile.Query.orderFor(query, input)resolves it to
fixed terms; the evaluator,foldkit-remote-drizzle's compiler and the
in-memory server sort by it, and impact judges a list by the field its own
input chose. The conformance suite has chosen-order cases. -
foldkit-entity: a text field declares how it orders, and a query says
whether a client can order as the server does.Collation.binary,
.asciiFoldand.locale(name), attached withCollation.ofor on a term
(Order.asc(field, { collation })); an id isbinaryby default. The
reference evaluator compares text by code point (it compared UTF-16 code
units), folds ASCII underasciiFoldand refuses a locale;
foldkit-remote-drizzlecompiles each, in the order and the keyset; TanStack
and LiveStore honourbinaryand refuse what they cannot say.
Query.placement(query, input)isPlaceableorNotPlaceablewith why.
The conformance suite orders text underbinaryandasciiFold. -
Where rows without a value go is said, and agreed. An
OrderTermhas
nulls: 'first' | 'last', last ascending and first descending by default
(Order.asc(field, { nulls })to change it). The reference evaluator places
nulls by it instead of refusing them;foldkit-remote-drizzlecompiles it as
NULLS FIRST/NULLS LASTand pages by it. That fixes paging over a nullable
column on SQLite, whoseORDER BYput nulls first while the keyset predicate
assumed last. -
foldkit-entity: every resolved order is total.Query.orderForends
it on the Entity's id unless the terms already read it, so ties break the
same way on every interpreter; the evaluator used to leave them in row
order while SQL broke them by id. -
foldkit-sync/remote: a replica's cell edits as Remote overlays.
RemoteEdits.make(Data, Edits).reconcile(model, edits, replica)shows each
edit its row has not reached, holds one the journal absorbed while the
cached row is below it, and lifts it once a read reaches it, returning the
newlyheldand thereplaced.shownlists them for a cell's marks;
clearlifts them for a reset.foldkit-remoteis an optional peer. -
foldkit-sync:Sync.mount'safterUpdateruns after every live
update, durable and reinstall included, and never in replay, for local state
derived from the shared slice. -
foldkit-remote:Data.overlays(model), the idsoverlayshows now, in
drawing order. -
foldkit-remote: a broken live stream restarts itself. A transport
failure or a gap restarts the live entry after Sync's backoff (50 ms,
doubling to 5 s, ±20% jitter;retryBaseandmaxRetryDelayin the
subscriptions options). It resubscribes from its cursor and refetches what
the stream covers, since no server replays what it missed.
Data.liveStatus(model, active)isIdle,Liveor
Reconnecting { attempt, error }, for a view. -
foldkit-remote:Data.persistencekeeps the cache across reloads as one
wiring. It restores the snapshot stored underkey(model)from a
KeyValueStore, keeping what the Model already holds, and saves the cache
under that key after each change (debounced, bounded bymaxBytes). It saves
only once that key's own snapshot is in, so startup never writes the empty
store over it.Hydratedtakes an optionalfrom, the key it came from, and
RemoteModel.restoredFromrecords it. -
foldkit-remote-drizzle:applyEdits(binding, { live })publishes what
the journal applies. Each edit it wrote is told to the hub (the member and
the revision); one the row had passed is not. -
foldkit-remote-drizzle: a retried guarded write does not conflict with
itself. When a write's guard misses, the writer reads the row: one
revision past the expected one with exactly these values means it landed,
and the row answers. A guarded delete of a row already gone succeeds.
EntityWriter.deleteanswers'deleted' | 'absent' | 'moved'. -
foldkit-crud: an editor over a write thatexpects a revision follows
it. After its own save is applied, the form takes the revision the save
moved the row to, so its next save is not a conflict with itself; a revision
moved by another client reads as the new statusMoved, the draft kept. -
Declared inserts and deletes.
Write.insert(input, { id })writes a row
under an id the client chose andWrite.delete(input, { id, expect? })
deletes one;Mutation.writetakes any kind (it wasMutation.update, and
RemoteServer.updateis nowRemoteServer.write). An insert retried finds
its row and answers it; a delete of a row already gone succeeds; a delete
from a moved revision is a conflict.EntityWritergainsinsertand
delete, and remote-drizzle'swriterimplements both. -
A declared write points a
onerelation.Write.updatetakes a key
mapped withRelation.inputto aonerelation;Write.bindreturns it in
linksas a ref (or none). Remote patches it as the ref key
(patchOfWrite), and remote-drizzle'swritersets the foreign key. -
foldkit-remote-drizzle:applyEdits(binding), a journal'sapply. One
committed cell edit written to the member's column with its sequence as the
revision, never moving a row back. The registry example's hand-written SQL
and column list go; itsProductTable.applyreturns an Effect. -
foldkit-crud: an editor over a declared write sends only what changed.
With aMutation.writemutation, a save names the input keys whose value
differs, by each key's schema, from what the form was filled with; Remote
shows them until the server answers.EditorModelgainsfilledWith, and
DomainLike.mutatean options argument. -
foldkit-remote-server,foldkit-remote-drizzle: a declared write needs
no handler.RemoteServer.write(mutation, writer)serves a
Mutation.write: the input bound to its write, only the keys the client
named, written through anEntityWriterand answered as the row's patch.
writer(binding)is oneupdate ... returningon the binding's table; a
write thatexpects a revision is guarded by it and moves it on in the same
statement, so the second of two writes from one revision is refused as a
conflict.MutationSource.runreceives the request'skeys.
foldkit-entity'sWrite.expected(write, value)is that revision, encoded;
Write.expectnow holds its field as well as its key. -
foldkit-remote:Mutation.write(name, write), a mutation that is a
declaredWrite.Data.mutateshows the write bound to the input until
the server answers, unless given its ownoptimisticlist. The new
keysoption narrows that to the input keys an author changed, and travels
onMutationRequest.keys.MutationDescriptorgains an optionalwrite. -
foldkit-entity:Write.updatedeclares what an input writes. Over an
Entity.input, it names the key holding the row's id (and optionally the
key holding the revision it was read at) and sets every other key mapped to
a field.Write.writesis what it may change;Write.bind(write, value, keys?)the row and its new values, encoded as the store holds them. -
foldkit-crud: the editor shows a server's refusal. A save refused with
Refusal.field(key, reason)for one of the form's keys marks that key
invalid with the reason, once per save; one refused withRefusal.conflict
reads as the new statusConflict.EditableFormgainscontrolsand
Message.Refused,EditorModelrefusedFor, andDomainLikerefusal;
foldkit-remoteadds the guardsRefusal.isFieldandRefusal.isConflict. -
foldkit-remote,foldkit-remote-server: a mutation refuses with data.
Mutation.maketakesRefusal, a codec of what the server may refuse with
(Refusal.field(key, reason),Refusal.conflict). A Source fails with
RemoteServer.refuse(mutation, value); the server checks the refusal is
that mutation's, encodes it, and sends it onRemoteMutationError.refusal
(a 422 over JSON).Data.refusal(model, requestId, mutation)reads it
decoded.MutationDescriptorgains a fourth parameter,Refused, and a
Refusalfield;MutationSourceaRefusalcodec. -
foldkit-entity:Query.whererefuses predicates that expand past
maxQueryNodes(1,000) nodes. A shared node (Expr.eq(n, n)) is counted
on each path that reaches it, becauseevaluateand the SQL compiler read a
predicate as a tree: thirty nested levels were thirty-one objects and a
billion nodes of work at every row. -
foldkit-entity:Query.dependenciesnames each field's role. Beside
fields, it returnspredicate(fields that can change which rows match)
andorder(fields that can change where a row sits), typed
QueryDependencies. A field that filters and orders is in both. -
foldkit-primitives: place a popup on a viewport point.
Placing.placeAtPointis that point in the offset parent's coordinates.
placeAtTriggertakesat: a point wins, and none places under the
trigger.OnContextMenucarries no coordinates, so a right-click records
its own. -
foldkit-data-grid: a date column edits in a date field. A schema
that decodes to a date (Schema.DateFromString) isCellEditor.Date.
The draft the field opens on is the local calendar day,YYYY-MM-DD.
toISOStringis the UTC day, so a local midnight used to open the field
on the wrong one. -
foldkit-mixins-ui: AspectRatio and Icon. Both are a slot and a
recipe, like Separator.AspectRatioholds content in a square, a 16:9
frame, or a portrait.Iconhides a decorative glyph and names one that
means something;Icons.glyphstays the mask for an element that already
exists. -
foldkit-primitives: place a popup once under its trigger.
Placing.placeAtis the offset under the trigger's left edge;
Placing.placeAtTriggerwritesleftand--fk-placed-topon insert
and clears them on release. The panel reads the variable astop, so
keepWithincan still flip.ListNavigationtakestypeahead: false
so a search field keeps its letters while arrows still move the pointer,
and an optionalcommitso Enter activates the current item from that
same handler. -
examples/cloudflare: the whole stack in one deployable worker. Reads
are Remote over D1 throughfoldkit-remote-drizzle; writes are Sync through a
Durable Object's D1 journal, whosesettleapplies each committed operation
(create, rename, toggle, delete) to the table Remote reads back; live is a
pollingLiveSourcethat re-reads D1 from inside the subscriber's own
request.pages/is a Pages site that serves the list. It forwards
/remoteto that Worker and calls the Worker's Durable Object for/sync
(a service binding drops the socket's replies).pnpm demoprints the loop
against real workerd and
pnpm testasserts it, both on miniflare over real HTTP. -
foldkit-sync/do: serve one document's exchange from a Durable Object.
defineDocumentHost(DurableObject, { sync, openJournal, resolvePrincipal })
defines the single-writer class: each accepted socket exchanges against the
same journal, opened once on the first upgrade over D1 or object storage.
Sync.transport.workerSocketwraps an acceptedWebSocketPairend anywhere
else. Proven in-process and, bundled, against workerd with D1. -
foldkit-remote-drizzle: proven over Cloudflare D1.
The same reads run throughdrizzle-orm/d1with no adapter changes
(databaseLayer(drizzle(env.DB)));test/d1.test.tscovers refs,
collections, windowed pages, keyset queries, compiledcontains, and
typed writes against a miniflare binding. -
foldkit-durable: the journal over Cloudflare D1 (d1: true).
D1 refuses transactions,PRAGMA user_version, andVACUUM, so the
journal adapts when the options say so: the schema version lives in a
durable_metatable (migrations are the same steps, each safe to rerun),
commits retry on uniqueness conflicts instead of holding a write lock
(concurrent appends still land gap-free),appendAllcommits each
operation on its own, snapshots replay only the prefix their cursor
names, a tail a compaction cut short mid-read is refused rather than
returned short, andvacuumsucceeds without doing anything (D1
auto-vacuums). Route appends through one writer:Journal.make(the
node:sqlitefile entry) refusesd1: true. Proven against a miniflare
binding, which rejects what production D1 rejects. -
foldkit-mixinstheme: deeper shadows and a strength knob. The shadow
scale gains a2xlmodal step (the Dialog recipe moved onto it),lg/xl
diffuse wider instead of ringing, and every layer mixes toward transparent
by the newshadow-strengthknob (100%keeps today's look), so one
override dims the whole elevation system. The design-system demo exposes
the knob as Faint/Soft/Full presets in its header. -
foldkit-remote-server/fetch: Remote over the Fetch API.
serveFetch({ server, resolvePrincipal, layer })is a Worker'sfetch
(or anyRequest/Responseruntime): reads, queries, and mutations ride
onePOSTeach, asRemote.httpsends them, and a live open rides one
POSTanswered with atext/event-streamofLiveChangeframes (one
data:frame per change, a terminalevent: errorframe on failure).
The principal is resolved per request and the Sources' requirements are
provided per request from the environment
(layer: env => databaseLayer(drizzle(env.DB))).
Non-POSTs and other paths are 404; a body that is not JSON, or not a
request, is a 400 that reaches no handler; a throwing principal
resolution is a 401; anything else this side breaks is a 500 that says
nothing of it. Disconnecting interrupts the subscription. -
foldkit-remote:Remote.httpWithLive.
Reads, queries, and mutations asRemote.httpsends them, live as the
event stream above, forRemote.clientLayer; abandoning the stream
aborts the request.Remote.httpalso takes an optionalfetch
implementation for tests. -
foldkit-mixins-ui: recipes for every remaining slot contract.
Recipes.Menu,Listbox,Combobox(one shared popup panel, rows, and
density),Select(the Input control's size and variant axes),
RadioGroup(checked circle in a tone),Slider(toned fill and thumb),
Toast(bottom-right stack, accented entries),FileDrop(dashed zone
that tints on drag-over),DatePicker(trigger and panel),Popover
(trigger and sized panel), andTooltip(dark pill). The popup trio share
an internal base; the file input hides off the root because a mixin class
beside the component's own class does not reach the element. -
foldkit-primitives: viewport placing for floating panels.
Placing.placeFor(rect, viewport)(pure shift/flip geometry) plus the
KeepWithinmount andkeepWithinbehavior, proven by five showcase
islands. No timers, observers, or node relocation — inert without layout.
Named apart fromdom'sKeepInView(scrolls content into view). -
foldkit-mixins: motion knob on the shared durations.
knob.motion(default'1') multipliesmotion.fast/normal, so a
still theme is one scoped override and every recipe transition follows;
prefers-reduced-motionstill wins through the reset. The control-size
knob stays unbuilt: no height scale exists for it to multiply. -
examples/widgets: Wave A showcase, eight thin compositions, live page.
Toolbar (RovingTabindex + Collection), Toggle (ToggleState, no Bundle),
ToggleGroup (single Selection), Accordion (Collection + per-item
disclosure), NumberField (SpinValue + FieldAssociation), CheckboxGroup
(multiple Selection over native boxes), Meter (stateless value/max), and
Command (Collection + inline filter + ListNavigation + single Selection).
Each with tests beside a mutation spec; onemakeElementisland per
widget insrc/entry.ts(pnpm dev,vite buildclean); a jsdom test
boots that same entry and answers one interaction per island. 44 tests.
Notable finds: noFilter.texthelper exists in source (the filter stays
a three-line function of the Model);Disclosurenames one pair, so N
sections derive its attributes per item;embedreplaces its container,
so the test queries the drawn page. -
Wave B (
examples/widgets): alert-dialog, autocomplete, otp-field,
hover-card, context-menu, menubar, navigation-menu. Seven more thin
compositions: explicit-response andnonModalOverlay policies get their
first consumers; right-click (OnContextMenu) and hover (OnMouseEnter/
OnPointerLeave) arrive as triggers; roving cells carry an OTP with a
Backspace handoff. Pointer-exact positioning stays open. 96 tests. -
foldkit-primitives: Overlay policy over the existing pieces.
Overlay.modal/Overlay.nonModal(or an explicitPolicy) into
Overlay.behaviors(...), spread into the view's pipe:DismissLayer
marking,FocusScope, and the scroll/inert mounts the policy keeps —
nothing reimplemented. Positioning stays per-widget, presence stays CSS.
Proved byexamples/drawer(modal settings panel: Escape/outside-press
dismiss through the stack, focus contained and restored). -
foldkit-mixins-ui: DatePicker adapter over a view fork with atoView
seam.DatePickerView.viewkeeps upstream's Popover+Calendar composition
(trigger face viatriggerContent, grid viatoCalendarView) and seams
only the popover assembly (pinned@foldkit/ui@0.165.0, attributed):
state, Messages, update, Commands stay upstream, and a parity battery draws
both views over the same models (incl. ISO hidden inputs and a full
day-select through the fork).DatePickeradapts the fork (trigger/
panel/backdropslots; the adapter is concrete over the date picker's
own Message since the bundles mix pre-bound popover groups with plain
attributes) with aDatePickerA11y pattern and catalog entry. -
foldkit-mixins-ui: Combobox adapter over a view fork with atoView
seam.ComboboxView.create/Multi.createbind the upstream bundles and
swap in transcribed markup assembly (pinned@foldkit/ui@0.165.0,
attributed): single and multi share one transcription, proved by a
three-view parity battery (incl. typing through the input, disabled
handlers dropped, and multi staying open on select).Comboboxadapts the
fork (11 slots: wrapper/inputWrapper/input/toggleButton/backdrop/items/
scroll/item/group/heading/separator) with aComboboxA11y pattern and
catalog entry. -
foldkit-mixins-ui: Listbox adapter over a view fork with atoView
seam.ListboxView.create/Multi.createbind the upstream bundles and
swap in transcribed markup assembly (pinned@foldkit/ui@0.165.0,
attributed): single and multi share one transcription (their Models are
structurally identical), proved by a three-view parity battery (upstream
single/multi plus the forks, incl. selection display, readonly, hidden
form inputs, and multi staying open on select).Listboxadapts the fork
with aListboxA11y pattern and catalog entry. -
foldkit-mixins-ui: Toast adapter over a view fork with atoViewseam.
ToastView.makebinds upstream'sToast.makefor the payload and swaps in
transcribed markup assembly (pinned@foldkit/ui@0.165.0, attributed):
state, Messages, update, Commands, and Mounts stay upstream, and a parity
battery draws both views over the same models (empty/entries/classes plus a
full dismiss lifecycle through the fork).Toastadapts the fork
(ToastSlots: container/entry; the dismiss control stays inside the
consumer'sentryToView), with aToastA11y pattern and catalog entry. -
foldkit-mixins-ui: FileDrop adapter, no fork needed.FileDropSlots
(root,input) withFileDrop.resolve/FileDrop.toViewover
upstream's ownFileDropAttributesseam, plus aFileDropA11y pattern
and catalog entry. The root's drag handlers have no Event tokens, but the
resolver still refuses a second owner by event name (proved by a test). -
foldkit-mixins-ui: Menu adapter over a view fork with atoViewseam.
@foldkit/ui/menubuilds its markup internally, soMenuViewtranscribes
its markup assembly from the pinned@foldkit/ui@0.165.0source
(attributed in the module) with the missingtoViewadded: state,
Messages, update, Commands, and Mounts stay upstream, and a parity battery
draws both views over the same models (closed/open/disabled/grouped/scroll
plus a pointer-open click-select through the fork).Menuadapts the fork
(MenuSlots: wrapper/button/backdrop/items/scroll/item/group/heading/
separator;Menu.resolve/Menu.toViewin the established nested-adapter
shape), with aMenuA11y pattern and catalog entry. If upstream gains a
seam, the fork goes away. -
foldkit-site: incremental node building.Site.node(router, case)
returns a builder (.title(...),.section(...), ...,.node) for nodes
annotated in steps or annotation bundles shared across nodes. Methods, not
pipeable fragments: each stage of a pipe is a separate generic call and
cannot infer the route, while a method already knows its node's. Named
bundles compose through the builder's.pipe(or freepipe) with no
annotation. Every
step freezes, so intermediates are usable nodes too.Site.route's options
are sugar over the builder — one implementation, proved by behavioral
parity tests. -
foldkit-site:Site.sourcesFor, the route-level Sources for one
destination.sourcesFor(site, owner, place, target.route)is the same
entries asSite.sourcesbut only the target's chain — what a prefetch or
SSR preparation reads, prepared caller-side withData.satisfy({ ...model, route: target.route }, active). Unknown tags resolve to no Sources.
Deliberately noSitePlanobject yet: the chain is alreadySite.chainOf,
and head/access metadata arrive with their real consumers. Hover or
programmatic prefetch adoption in an app is the next slice. -
foldkit-site: the SSR connection, proved by composition, no new API.
URL → route → target Model →Site.sourcesFor→Data.satisfy→ an SSR
plan over those Sources with Remote's resume part renders the destination
carrying its data (packages/site/test/ssr.test.ts): the route determines
the route-level SurfaceSources, preparation fills them, and the envelope
captures what they resolved. The browser takeover stays proved in
foldkit-ssr. -
foldkit-bundle:Bundle.follow, an ask that waits for its owner.
follow(placed, { pending, release, ready, toMessages })wraps an update
result: while a pending ask waits and its owner is ready, the ask goes
through the owner's own Messages (Commands merged after the result's own)
and is let go. Otherwise the result passes untouched, pending kept — an
address can ask something of a child with no Model yet, once it has one. -
foldkit-site(new, 0.1.0): the deployed route topology as data.
Site.route(router, case, options?)declares one location from its Foldkit
Router;Site.mount/Site.makefreeze the hierarchy;Site.targetbuilds
the route value and URL from one declaration;Site.chainOf/nodeOfand
parentOf/ancestorsOf/depthOfinspect it;title/sectionannotate
once withtitleOf/sectionOfreaders,landinggives each section its
nav address (Site.landing), andshortcutnames the keys that go there;
Site.historyOfdeclares push-vs-replace once; andSite.sourceslowers
surfaced nodes toSurface.whenvalues for Remote, SSR, and retention.Site.routingis the
link-click and URL-change lifecycle as one wiring: internal clicks navigate
perSite.historyOf, external links load, URL changes set the route field
(or touch nothing on an echo), and the application's own update guards with
Routing.reduces.Site.placementstates one routed page once — its link
drivesplaced.updateandplaced.view, andSite.routinginforms it
throughchangedwhen its route arrives. Per-route Bundle placement stays
per-application until the next cut. -
foldkit-composition:SurfaceBlock.families, one family per
Surface Block.SurfaceBlock.families(catalog, { from, document })is
every Surface Block on the page as oneSurface.eachfamily per Block,
keyed by Block name with each instance keyed by its node id — for
Data.wiring,Data.subscriptions,Data.satisfy, and SSR plans, wherever
per-node requirements, retention, and coverage matter rather than one
combined read. Fetching follows the Document, draft edits included. -
foldkit-ssr: plans takeSurfaceSources, and coverage inspects each
instance. ASurface.eachfamily is one entry of a plan'ssurfaces;
the envelope carries its parent's reads with its instances', and
SSR.inspectcovers the parent once and each instance by key (Member[u1]
with itsviaparent), so a gap names the instance. Preparation stays
caller-composed:Data.satisfythe Model, then render it. -
foldkit-remote: subscriptions, satisfy, and diagnostics follow
SurfaceSources. The active record taken byData.subscriptions,
Data.wiring, andData.satisfyacceptsSurface.eachfamilies beside
Surface.at/Surface.whenvalues. A family is one record entry whose read
unions its parent's requirements with its instances', so the parent is
fetched before what it reveals andData.satisfyreaches the instances in
a later pass.RemoteUnsatisfied,Data.explain, andData.whyname a
still-reading instance asChild[key]. -
foldkit-surface:Surface.each, one Surface as many keyed instances.
Surface.each(child, { from, instances })instantiates a child Surface per
item of a parent Surface's Model — a stored page's Blocks, say — where
Surface.at/Surface.whengive zero or one. The family keeps itsfrom
parent, so consumers can tell the requirements needed to discover
instances from the requirements of them; it refuses empty and
__proto__keys and duplicate identities.SurfaceSourceis the shared
shape (at/whenvalues now resolve their lone instance through it, keyed
by Surface name), read withSurface.instances(source, model).
Changed
-
Breaking:
foldkit-sync: an exchange'srefuseisadmit, and an
exchange may be refused whole (opens).admit(operation)answers as the journal's
authorizedoes (trueadmits; a refusal, with its reason, is rejected by
id) and may answer with anEffect, for a rule that reads the rows an edit
names before the journal's lock.opens, anEffectof a boolean, fails
every exchange of a connection that may not open the document.
defineDocumentHosttakes both as functions of the principal, which its
refusewas never given.
Sync.presence.servetakesopenstoo: a connection joins the presence
hub only once it may open the document, and one refused is relayed
nothing, either way. -
foldkit-cms-drizzle: the audience boundary is afoldkit-guardguard set.
CmsServer.maketakespolicyandauthors(principal guards) in place of
isAuthor, andtransitions(principal guards per transition) beside
allow, which stays for a rule about the entry; the principal is the guard
set'sOption. Eachcontententry namesentity, its Entity with guards
attached; its source carriesGuardServer.filter, andmakerefuses a
publishable type whose Read rule does not read its published field, or
whose binding'svisibleis not the guard set's (GuardServer.validate).
published(column, isAuthor)is gone: bind withGuardDrizzle.visible. A
refusal of who is asking isForbidden, with its sentence. The casts of an
untyped principal are gone: one that is not the set's is a defect. -
foldkit-cms: an entry'screatedAtdeclaresbinary. It is an
ISO-8601 instant, so code-point order is time order, andCmsEntries
orders by it: a saved or created entry is now placed in the worklist
instead of refetching it. On Postgres the order compiles with
collate "C". -
foldkit-remote: placement reads a row's id from its key. Every order
ends on the id tie-break, and a row whose Selection never storedidwas
unplaceable, so its list was fetched again: the entity example's post list
was, after every create. -
examples/entity:WritePostandDeletePostwrite in one
transaction. The author was inserted before the post, and the comments
deleted before the post, each on its own, so a second statement that failed
left the first. They use Drizzle's SQLite transaction, which is synchronous.
foldkit-remote-drizzle's README says why the adapter wraps no
transaction: the drivers disagree on what one is. -
foldkit-remote: an inserted row is drawn where its list's order puts
it. Aprepend/appendconnection change and a liveConnectionInsert
say a row is in a list, not where; in a list the client can place, a read
draws it in order, or leaves it out past aCursor. One it cannot place
stays at the end its change names. Breaking:Remote.visibleItemsis
replaced byData.visibleItems(remote, connection), which draws a list as
its reads do.examples/cloudflaredropsorderedPage, its own sort of the
page. -
examples/entity,examples/registry:verifyPlacementis on in
development and tests (import.meta.env.DEV). The entity HTTP test checks
a rename against SQLite's order, and every registry page test asserts no
placement disagreed with the server's. -
foldkit-remote: a mutation's answer places a row new to the client, and
one that moves in from past aCursor. A row joined a list only through
a filtered field, so a created row never joined a list that filters on
nothing, and a row past a list's loaded rows whose order now put it among
them stayed out.EntityChange.Changedgainscreated. -
examples/cloudflare:AllTodosdeclares its order in its body, and
titledeclaresbinary. A create's answer names no list: the client
places the row by title instead of the server'sprepend. -
examples/entity:AuthorsQuerydeclares its body too, a name search
by id, so an author a nested write creates joins the loaded list instead
of refetching it. With the id fix above, the example's tests invalidate no
list. -
examples/entity:PostsQuerydeclares its body (Query.define, a
title search andOrder.chosen), andtitledeclaresbinary, in place of
the server'swhereandorderBy. A rename moves a post in a list sorted by
title with no list request; the list was refetched before. -
foldkit-remote: a pending edit, a read and a live patch place the rows
a list holds. A pendingoptimisticpatch orData.overlayto an
ordering or filtered field shows its row where the edit puts it, or out of
the list, until it settles; the server's pages are untouched. A read or an
EntityPatchedwith a new value moves a held row in the pages. Before, the
row kept its old place until the list was fetched again, so a registry
price edit stayed out of order even after the server had it. Neither brings
a row into a list. -
foldkit-remote: a mutation's answer is placed in the lists it changed,
not refetched. A held row that stops matching leaves a list; a row that
joins, or a held row whose ordering field changed, goes where the body's
order puts it among the loaded rows (placeIn), or out of them past a
Cursor. A list is still invalidated for what cannot be placed: no body or
no declared order, text with no collation, a row it cannot judge, or a row
an overlay shows.ImpactgainsPlaced, and is aData.taggedEnum. -
examples/registry: its text columns declarebinary, as its SQLite
orders them, so its sorted page is placeable. -
foldkit-remote-drizzle: a page's cursor holds the order's key values
where it ended. It was the last row's id, which the next page looked up:
"load more" failed once that row was deleted or stopped matching, and
continued from the wrong place once it moved. A cursor (k:[...]) is
validated as client input; one from before keys is still read as an id.
Relation pages keep their id cursors. -
foldkit-entity: anOrderTermis taggedTerm, and a body's
orderByholdsOrderings (Term | Chosen). An interpreter walking it
resolvesQuery.orderFor(body, input)first. -
foldkit-remote-drizzle: a body that declares its order refuses an
orderBybeside it. Overriding the body's order was allowed; it let the
server sort one way while the client read another. A body's order is
compiled per request now, since a chosen order depends on the input. -
examples/registry: its sort is inProductsQuery's body
(Order.chosen), not a serverorderByfunction. -
foldkit-remote: a list whose body declares no order is invalidated by
any change to a row it holds. Its order is the server's (anorderBy
on the server, say), by fields the client cannot see, so a change it judged
harmless could have moved the row. It was kept before, and the row showed
its new value in its old place. -
examples/cloudflare: a tick of the live poll reads only the rows
written since the last. Triggers log each row a write touches
(migrations/0003_row_changes.sql, the last 1000 entries); the poll asks
the log first, re-reads only those rows, and reads the list's window only
when the stream opens.pollLivetakes an options object (interval,
list,changes, aRowLog). The local stack runs the migration files
rather than repeating them. Apply the migrations before deploying the
worker; until then it reads every tick. -
examples/registry: the rows the grid draws follow the table live. The
journal's writes are told to a hub (applyEdits(Db.Product, { live })); the
dev server answers throughserveFetch, live streams included, and the
client usesRemote.httpWithLive. A replacement said both by a read and by
the slice is listed once, the slice's, which names the author. -
foldkit-sync/remote:replacedcovers an edit still in the slice. A
read reaching this replica's edit with another value says it, held or not,
and this replica's landed edit is kept (showing the row's own value) until
the slice drops it, so a later edit arriving live is seen replacing it. -
examples/registry: the rows read survive a reload.Data.persistence
keeps Remote's cache inlocalStorage, so a reload with the server out of
reach paints the last rows read, the pending edits over them.
mountRegistry'sresourcesalso provides aKeyValueStore. -
foldkit-sync/entity: the overlay helpers are gone;foldkit-sync/remote
shows the edits.overlay,held,newlyHeld,settledandshowsare
removed:RemoteEditsdoes their work over Remote, where every read draws
it.EditableEntity.make's value also carries theentityit was made over. -
examples/registry: the edits are Remote overlays. Theretiredfield,
retiredOf,retiresAny,settledOfand the edits laid overrowsOfare
gone;RemoteEditsreconciles inafterUpdate, so any read of a product,
not only the grid's row, shows its edit. -
foldkit-remote: a gap stays until its stream restarts. An in-order
event after a gap used to clear it, though the event that arrived ahead was
dropped and what it said was lost. Now only the restart'sGapCleared
clears it.LiveDependenciesgainsrestartsandfailures, and
RemoteModelgainsstreams. -
examples/cloudflare: the stored list isData.persistence. The
hand-written save Subscription and the boot-timeapplyCacheare gone; the
wiring restores each actor's snapshot (snapshot: snapshotForkeeps the
visible edges) fromKeyValueStore.layerStorage(() => localStorage). The
rows now arrive just after the first frame instead of in it. -
examples/entity: the post edit is a declared write.EditPostis
Mutation.writeoverWrite.update, its editor relation included, and the
server serves it withRemoteServer.write(..., writer(Db.Post)); the Drizzle
handler goes. The delete stays a handler: it removes the post's comments too. -
examples/cloudflare: rename and toggle are declared writes. Their
Drizzle handlers and hand-built optimistic patches are gone:
Mutation.writeandRemoteServer.write(..., writer(Db.Todo))say the
same, and the rename editor sends only the title it changed. -
foldkit-cms,foldkit-cms-drizzle: refusals are data, not prefixes.
SaveDraftandPublishdeclareRefusal:Cms.refusal.field(key, reason)
for a taken address on its key,Cms.refusal.conflictfor work saved or
published since. The editor reads both withData.refusal.Cms.slugTaken
is removed, and theCmsConflict:andCmsSlugTaken:message prefixes are
gone: a message is now only for a person to read.EditorDomaingains
refusal.RemoteServer.refusetakes an optional message for that person. -
foldkit-remote: a mutation's answer invalidates the loaded lists it may
have changed. Each patch is judged against every loaded list over its
Entity by the list's query body: a list stands when its rows and order are
provably unchanged (a rename, a row that still matches), and is otherwise
marked stale for its read entry to fetch again, restarting a read of it in
flight. A list over aQuery.makequery, with no body to judge by, is
invalidated by any change to its Entity, so such lists now refetch after a
mutation of their rows. A liveConnectionInvalidatealso restarts a read in
flight now. -
foldkit-remote-server: a mutation Source is given the client's
requestId, which the client already sent and which is the same on every
retry of a request. Record a write under it to recognise a retry; nothing
deduplicates by it yet.foldkit-cms-drizzleruns the application's own
mutation, when it publishes, aspublish:<entry>:<revision>. -
foldkit-remote-server: handlers given alivehub publish every
mutation's answer to it. Each answered patch is a change of the fields it
carries and each deletion a deletion, re-read per subscriber as before. Delete
ahub.changedcall that follows a mutation's write, or subscribers hear the
change twice; keep one for writes that are not mutations. A failed publish is
logged and the mutation still answers, since its write has happened. -
examples/cloudflarepage: the Pages site is a Foldkit app. The list is
Crud.listover theAllTodosquery. The first visit says "Loading…". A
later visit paints the rows stored in the browser at once and asks again.
Add, rename, toggle, and delete are Remote mutations;Data.mutateapplies
the optimistic patch before the POST returns. Another tab hears the change
on the live stream. Sync through the Durable Object stays for the demo and
the tests; both writers use the sametodostable. -
foldkit-sync/do:settlenow takes the environment and the journal.
settle: (env, journal) => Effectis called per exchange with the object's
own environment and the journal it serves, so applying what committed to a D1
table needs no closure over a binding captured elsewhere. -
foldkit-remote-server/fetch: live streams heartbeats. A: ping
comment opens every live stream and follows everyliveHeartbeat
(default 15 seconds), so an idle stream is neither dropped by a proxy nor
cancelled by a worker runtime. The heartbeat stops with the events it
serves, so a stream with nothing to serve still ends. -
foldkit-mixins:Theme.oklch's families are one shape, and hold up in a
dark scheme. Every family hasdefault,hover,active,subtle,
outline,on-fillandink;outlinesteps away from the page in each
scheme, and the feedback fills are lighter in a dark one (the error fill
keeps light text).surface.bedrock, a dialog's backdrop, is the deepest
surface in a dark scheme too, where it was near-white, andoutline.focus
is the accent itself rather than a darker, more saturated shift of it. -
foldkit-mixins/defaults: native checkboxes, radios and sliders take
the accent, selected text is the accent's tint,kbdis a raised key,hr
one quiet line, and inline code sits onsurface.muted. -
foldkit-mixins-ui: recipes over the palette. A neutral Checkbox or
Switch that is on is in the page's ink (tones.ink), where the neutral
surface drew it paler on than off; the neutral tone's fill is a quiet grey
and an outlined control draws its tone'sline, so a neutral outline can be
seen; a chosen pill tab is the accent's tint and ink; the danger tone hovers
toerror.hover.pnpm --filter foldkit-mixins-ui specimenwrites a page
of every recipe in every variant and state.
Fixed
-
foldkit-durable: concurrent appends over D1 commit. A commit that lost
a race for its sequence retried at once and at most ten times, so with a
dozen writers on one document one could lose every round and fail with a
JournalError; it retries up to 64 times, spaced and jittered. -
foldkit-guard:GuardServer.writerrefuses a writer for another
Entity where they meet, rather than leaving it tovalidate; the README's
limits say a write's check and the write are two statements. -
foldkit-guard: a handler's answer naming a row with no values keeps
it. The read-back asked for no field, which Drizzle's Source answers with
no row, so the row was answered as deleted; it reads the id too. -
foldkit-remote: a list an answer names still has its other rows
judged. A list the answer put a row into was skipped whole, so a second
row of it the same answer moved was neither placed nor fetched; only the
rows the answer itself placed are taken as the server says now. -
foldkit-entity: an order over a number a row may lack is placeable.
Query.placementreadNullOr(Number)(andOptionFromNullOr(Number)) as
text with no collation, so such a list was fetched on every change; where
nulls go is said on every term, so it is placed now. -
foldkit-remote-drizzle:asciiFoldfolds only ASCII on Postgres. It
ordered bylower(x) COLLATE "C", wherelowerstill follows the column's
collation and, under a UTF-8 one, foldsÉtoé; the client's evaluator
and SQLite'sNOCASEdo not, so a client placed rows where Postgres would
not. It islower(x COLLATE "C")now, the cursor's values folded alike, as
containsalready did. -
foldkit-remote:Data.persistencesays what keeps one reader's cache
from the next. A restore keeps what the Model holds, so a change of
principal withoutData.forgetin the sameupdatesaved one person's rows
under the next one's key; the README now saysforgetis required there,
and that a snapshot is serialized on each change though its write waits. -
foldkit-cms-drizzle:authorsis asked as what the author is doing.
Every operation asked its guards with interactionRead, so a body that let
a reviewer read drafts let them save and publish too. An operation now asks
Update, and reading unpublished workRead. -
foldkit-sync: a connection whose access went hears and speaks no more.
Presence askedopensonce, at join, so a peer kept relaying presence after
its access was taken; it is asked again with each frame, and a refused peer
leaves.serveJournalsent a refused connection a notice of every commit,
which woke it into a failing exchange and told it when the document
changed; notices now passopenstoo. -
foldkit-remote:Remote.httpreads a middleware's{ error }403 as
Forbidden. A 403 whose body happened to be shaped as an error answer was
taken for Remote's own, an ordinary mutation error; a 403 is now forbidden
unless its body says so itself, as Remote's server's does. -
foldkit-remote: placing a change no longer redraws or refetches what it
left in place. A row whose order field changed but whose place did not
still gave its list a new identity, so every read of it was drawn again and,
underverifyPlacement, the list was fetched; it is now kept as it was. Each
list's held rows were rebuilt for every change of a read or an answer; they
are read once per list. -
foldkit-guard:Policy.keyOftells every claim apart. It keyed a
principal byJSON.stringifyof its rule inputs, so a set claim read as
{}(an admin and a revoked admin shared a live hub's re-read) and a
bigint threw. It keys by the principal encoded throughSchema.toCodecJson.
And besideWrite, a slot of its own's refusal replaces Write's whichever
key is written first; it hung on the order. -
foldkit-cms-drizzle: a statement the database refuses no longer sends
its SQL. Its statements ran as defects, asfoldkit-remote-drizzle's
writes did, so a refusal reached the client with the driver's error; they
now fail as aRemoteServerError.Transaction's type says it may fail so.
foldkit-remote-drizzleexports the helper,runStatement(statement, failed), for a handler's own statements, and its README's handler uses it. -
foldkit-cms-drizzle: archiving an entry on show asksunpublish.
Archiving takes a published row off show, as unpublishing does, but asked
onlyarchive's guards: an authortransitionskept from unpublishing
could archive a published post, and unarchive it as unpublished work. -
foldkit-remote: placement no longer drops a row it cannot judge, and
compares a number id as a number. A row that was the last edge before a
cursor (or the first after one) was left out when it sorted past its
page's other rows, though its own old keys were the boundary and might
still hold it: it now invalidates the list. Placement read every id from its
key, so a number id ordered as text ('10' before '9'): the stored id is used,
and the key's only for an id stored as text.verifyPlacementalso records
a row placing took out that the server's page still lists, which comparing
the shared prefix never could;placednow holds{ order, dropped }. -
foldkit-entity: a declared write binds the keysEntity.inputlets
map themselves. An input key that is anOption, or optional, type-checked
and then threw inWrite.bind. Asomenow writes what it holds, and a
noneor a key left out writes nothing; a write whose id may be absent is
refused where it is declared. -
foldkit-crud: an editor's later saves no longer lose another author's
change. After its own save, the editor took any revision that came after
as its own, so another client's later save was overwritten without a
conflict; it compared each save with what the form was first filled with,
so a key it had saved was sent again over another author's change; and
typing while a save was in flight lost that save's answer, so the next save
conflicted with itself. The editor now takes each save's answer once, as it
lands (edited,sentandsettledForin its Model), andMoved
outranksSaved. A create's form starts blank once it lands, so saving it
again is not the same create twice. -
foldkit-remote-drizzle: a keyed cursor over an id column of another
name. A page read each order key back by its column's SQL name, beside an
id selected asid, so an id column named otherwise (task_id) read as
nothing and every page fell back to an id cursor.foldkit-remote-server:
RemoteServer.memoryrefuses asharedentity the domain does not declare,
or a field it does not, asRemoteServer.entityandsourcealready did. -
foldkit-guard:GuardServer.validaterefuses what it could not
check. A seam built from an Entity before its guards were attached (a
GuardDrizzle.visibleorGuardServer.filtergiven the base Entity)
decided nothing yet passed as the guarded one's, since seams were recorded
by name: they are now recorded by the Entity's attachment. A live Source of
its own over a guarded Entity, a served query the domain does not declare,
and a shared relation whose target is not validated are now refused. -
foldkit-remote-server: the live hub sends a deletion only to whom a
row is gone. A write that hid a row from its writer answered it as
deleted, and the hub sent every subscriber that deletion, readers who still
saw it included; one who lost sight of a row was sent nothing.deleted
now re-reads each subscriber's selection andchangedsends a deletion
where the re-read finds no row. One subscriber's failed read no longer
stops the rest. Breaking:LiveHub.deletedmay fail and needs the
Sources' services, aschangeddoes. -
foldkit-remote-drizzle: a write the database refuses fails as
Database write failed. Writes,applyEditsandreturning.rowran
their statements as defects, which Effect RPC sends whole: the driver's
error, with the SQL and its parameters, reached the client. They now fail
as aRemoteServerError, the driver's error logged, as reads already did;
returning.row's error type says so. -
foldkit-guard: three write checks that let a write through. An
update that unlinked a relation whose row is hidden from its writer read the
link as none and skippedUnlink: the link is now read as stored, through
a newEntityWriter.linked(foldkit-remote-drizzle'swriterhas it),
and a writer without it treats a link it sees as none as one that may be
there. A create's guard over a field it leaves unset held forisNotNull
(and died oncontains): such a guard now never holds. A write's answer was
read back without the Entity's member Read rules, so it could carry a field
its writer may not read:GuardServer.writerandGuardServer.mutation
now answer through them, with no more fields than the write named. -
foldkit-remote-drizzle: an insert under an id taken by another row
fails. It answered with whatever row held the id, values its writer may
not read included, and let the client think its own were stored. A retry,
whose row holds what it writes, is still answered with that row. -
foldkit-guard/sync:admitjudges an operation's edits to a row
together. Each edit was checked over the row with that edit alone, so
two that each kept a row in its editor's reach, and together moved it out,
were admitted. An edit to a member outside the document, or a value its
member cannot encode, is now refused by the operation's id rather than
failing the exchange;GuardSync.documentrefuses a member that is not a
field. -
foldkit-remote: a filter or live insert the reference interpreter
refuses no longer throws inupdate.Data.filteredwith such an input
(a search holding NUL) answers no items andcomplete: false; a live
insert into such a connection takes its declared policy. -
foldkit-remote-drizzle:containsrefuses searched text holding NUL
when that text is an input or a literal, asevaluatedoes. It refused a
NUL only in the search, and answered[]on SQLite and Postgres for the
rest.foldkit-entity/conformancenow exportsrefusals, bodies every
interpreter must refuse, and the reference, Drizzle (SQLite and Postgres)
and Remote matching suites run them. -
foldkit-crud: any authored edit after a save returns the editor to
Editing. It used to take only a typed field (Changed), so a nested row
added or removed, or a Bundle-backed control's edit, left it atSavedor
SaveFailed. The editor now asks the form (authoredChanged), and a
Message that changes nothing returns its Model as it was. -
foldkit-primitives: a popup placed in the same turn stays where it was placed.
keepWithinand the placement write start together, so the fit runs again
when the write lands and judges that box. A point that would hang off the
bottom shifts up (Placing.shiftIntoViewport). A trigger still flips, and
the panel carriesdata-fk-placed="above"when it does. Enter in
ListNavigationcommits an enabled current item. -
examples/widgets: the hover bridge follows a flip. Navigation and the
hover card overlap the trigger on the side the panel opened. Opening the
context menu for the row it already shows returns the same model. -
examples/cloudflarepage: a new or renamed row is drawn where the
title order will keep it. The list sorts by title, then id, which is what
AllTodosasks D1 for, so a live insert does not move it. -
examples/cloudflarepage: the list is kept in the browser, one snapshot
per actor. A later visit paints those rows immediately. A failed refetch,
including offline, leaves them. The snapshot is the server's rows. An unsent
edit is not kept. -
examples/cloudflarepage: an update no longer refetches the list. The
one-second refresh, and the refresh after a successful write, flashed the
rows. The page subscribes to the live stream. A field change is a patch. A
row joining or leaving is a connection event. -
examples/cloudflarepage: saving, loading, and a field error keep the
space they will occupy. "Saving…" and "Loading…" useLoading.shown, so a
fast answer does not flash, and none of them push the form or the list. -
examples/widgets: a context menu opens at the pointer. A right-click
records the point, and the menu opens there. The keyboard, which has no
point, still opens under the row. Navigation and the hover card keep a
hit target across the gap, so the pointer can move onto the panel. -
examples/widgets: hover, placement, and keyboard on the showcase.
A click after a hover leaves that section open. A popup sits under the
trigger that opened it, and reopening a menu keeps the chosen item
selected. Enter in the command field runs the highlighted command. Shut
accordion panels stay mounted. Absent values in these models are
Option, and the page theme reads tokens. -
Examples: the todo app's chosen filter is one color. Its pill drew a grey
line around the accent's tint, and its text went to the default color on
hover; it is the tint and the accent's ink, hovered or not. -
foldkit-cms: an entry opened from the server isLoadinguntil its state
is read too. The editor counted as open once its entry, draft and row were
read; the lifecycle state, a read of its own, could answer a moment later,
and what it draws (the state badge, the history, Publish) dropped in after
the form and pushed the screen down. Something new waits for no state. -
examples/cms: the studio opens an entry whole. Opening a page or a post
from its list kept the list's place empty but for the editor's bar while the
entry loaded, then drew the rest at once; the list now stays until the
editor can draw. An image on a page keeps its room (4:3 until it loads) so
the text around it does not move when it arrives. The studio's editor bar
and the site's header are solid in the page's color: translucent over a
blurred backdrop, they drew darker and more saturated than the page.