@confect/foldkit bridges Confect into Foldkit, The Elm
Architecture in TypeScript powered by Effect. It maps Confect’s client surface
onto Foldkit’s three integration seams: an application-scoped Client becomes
a resource, reactive queries become
Subscription entries, and queries,
mutations, and actions become
Commands. Args and return values are
encoded and decoded through the Effect
Schemas in your function
specs, and every failure is folded into a Message,
so Command and Subscription error channels stay never as Foldkit requires.
Functions are referenced via refs (from confect/_generated/refs) instead
of Convex’s api object, the same as with the other Confect clients. The
bindings work with Confect functions only—refs for plain Convex
functions carry no schemas, so they aren’t
accepted.
Installation
Foldkit pins an exact With npm, use an
effect version in its peerDependencies. When
Confect tracks a newer Effect release than Foldkit does, tell your package
manager to accept the mismatch—with pnpm, in pnpm-workspace.yaml:overrides
entry that pins effect to the version your app uses.Setup
Pass theClient layer to your application’s resources. The client combines
the WebSocket API with the pagination-session allocator, lives for the lifetime
of the app, and closes the WebSocket at teardown.
src/entry.ts
@confect/foldkit require the
Confect.Client.Client service, which this layer satisfies.
Reactive queries as Subscriptions
Subscription.reactiveQuery builds a complete Foldkit subscription entry for
a query ref. Its dependencies are the query args wrapped in an Option
extracted from your Model: None closes the subscription, a change from one
Some to another resubscribes with the new args, and structurally equal args
leave the subscription running (dependency equivalence is derived from the
ref’s args schema).
src/subscription.ts
onSuccess receives the decoded return value on every server update;
onError receives the ref’s decoded typed error (see Error
Handling), a transport-level WebSocketClientError,
or a SchemaError. Errors are emitted as Messages without ending the reactive
query: Convex handles retryable infrastructure failures itself, and the same
subscription can produce a later value if its query result recovers.
Queries without args may omit the args extractor, which leaves the
subscription always open:
Custom entries
When an entry needs extra dependencies or custom gating, write it by hand and useSubscription.reactiveQueryStream as its dependenciesToStream body:
Commands
Command.query, Command.mutation, and Command.action build Foldkit
Command definitions whose Command args are the ref’s args. messages
declares the Messages the Command can produce—the same declaration
Foldkit’s own Command.define takes—and the handlers must produce
instances of them. Call the definition from update to construct a Command
instance—nothing runs until the Foldkit runtime executes it. Add the handler
to your exhaustive update Match with Match.tag:
src/command.ts
src/update.ts
SucceededSaveNote/FailedSaveNote). onError receives the same error
union as subscriptions—Confect.Command.Error<typeof ref> names it when you
want to factor a handler out.
Interruption
Passinterrupt to make a factory-built Command
interruptible. The returned definition
gains an Interrupt constructor that builds an ordinary Command: it stops
every in-flight invocation and results in toMessage(outcome), where the
outcome is Interrupted (at least one invocation was stopped—its result
Messages are guaranteed never to dispatch) or NotFound (nothing was in
flight).
interrupt: true keys every invocation by the Command name—right when at
most one invocation is meaningfully in flight:
Match.tag update handler:
interrupt: { keyFields, toKey } derives the key from the ref’s args, so
concurrent invocations can be interrupted independently. Interrupt then
requires the key args:
Interruption stops the client-side Effect and guarantees the invocation’s
result Messages never dispatch—it does not cancel the Convex function on the
server. Once a mutation or action is on the wire, it runs to completion;
interruption means its result is ignored. Foldkit also runs a batch of
Commands concurrently with no ordering guarantee, so to cancel and replace,
dispatch the replacement from the Interrupt’s result Message rather than
returning both Commands in one batch.
Effect helpers
Command.queryEffect, Command.mutationEffect, and Command.actionEffect
return an execute body—an Effect whose failures are already folded into
Messages—for hand-written Command.define calls. Reach for them when the
Command needs a custom args schema or several calls in one Command:
Query state in the Model
Foldkit’sAsyncData is the idiomatic
Model representation for query state. Map errors to a view-ready schema type
in your onError handler, then settle the AsyncData field with Match.tags
in update:
src/model.ts
src/update.ts
Paginated queries
PaginatedQuery navigates a paginated
query one page at a time—next and
previous—over Convex’s cursor-based pagination. The machine lives in your
Model and is the single source of truth for one live, reactive page
subscription. Navigation and page-split handling are pure Model transitions,
and Subscription.paginatedQuery keeps the subscription in sync with them.
PaginatedQuery.make takes the ref and returns the machine’s Model and
correlated-settlement schemas together with its operations. The machine uses
the same vocabulary as Foldkit’s AsyncData: Idle, Loading, Refreshing,
Success, Failure, and Stale. Refreshing and Stale retain the complete
last good page.
src/model.ts
src/subscription.ts
Client, opens the Convex query, and includes the
id in its first Result-based settlement. settle installs the id and result
atomically. Allocation is not a separate application event, and installing the
id does not restart the live subscription.
Like AsyncData.settle, a success becomes Success; a failure becomes Stale
when a page is held and Failure otherwise. The request carries a logical
generation as well as its cursor and page identity, so outcomes superseded by
navigation, new args, close/reopen, or reset are ignored. All public machine
operations remain pure:
src/update.ts
getPage and getItems return Options because the initial load can have no
data. Once a page succeeds, they keep returning that page while the next one
loads and after a refresh fails. Page.number is the page being displayed;
targetPageNumber is the page currently requested, which can differ during a
navigation:
isIdle, isLoading, isRefreshing, isSuccess, isFailure, and isStale
are refinements; isPending, hasPage, hasError, and the exhaustive match
cover the common view branches. A Failure or Stale does not close the
subscription: Convex query errors are deterministic rather than manually
retryable, and the live query may recover after its data, arguments,
authentication, or deployment changes.
Failed settlements carry an exhaustive error union: FunctionError wraps an
error declared by the query or its middleware, WebSocketClientError
represents an unexpected client failure, and SchemaError is carried directly
from argument encoding or result decoding. Convex’s InvalidCursor
pseudo-error is also represented explicitly in settlements, but settle
handles it internally by starting a fresh session at page one while retaining
the displayed page.
first is ordinary navigation back to page one within the current session.
reset purely requests a fresh session at page one while retaining the
displayed page, and reinitialize changes query args or page options and starts
again from page one. close returns to Idle while retaining a generation
tombstone, so a later init cannot accept a late first settlement from an
earlier identical session. Options use Convex’s names: initialNumItems is
required, while maximumRowsRead and maximumBytesRead are optional positive
integers.
When you navigate forward, the page you leave is pinned to the range it
displayed—from its cursor to its continuation cursor—so going back reloads
exactly that range, however the data has moved since. This is what keeps
consecutive pages gap-free and duplicate-free for
stream-paginated queries, which have no
query journal to remember page ranges. The page currently on screen is a live
window of initialNumItems documents from its cursor, so it stays full as
documents are inserted and deleted.
Convex may signal that a page has grown too large by recommending or requiring
a page split. The machine handles the full protocol transparently: the current
page is pinned to end at the split point and reloads as a range query, and the
next page picks up from there. Users see at most a brief reload of the current
page—never a torn or incomplete one. If a reactive terminal page becomes
empty, the machine automatically retreats to the previous page.
Authentication
setAuth is available on the Client service. Wire it up as a
Command:
Not yet covered
An infinite-scroll, load-more counterpart to@confect/react’s
usePaginatedQuery (a growing list of concurrently-live pages) is not yet
built in. PaginatedQuery covers page-at-a-time navigation; for a growing
list, paginated refs can be called through the underlying
WebSocketClient service directly.