Sync / foldkit-sync
The server
The server answers the exchange: it takes a cursor and pending operations,
commits what it accepts, and returns what the replica is missing. With
foldkit-durable, the same contract supplies the journal's codecs, empty
snapshot, reducer, and any authorization:
ts
import { Effect } from 'effect'
import { ActorId, Journal, OpId } from 'foldkit-durable'
import type { SocketLike } from 'foldkit-sync'
import { serveJournal } from 'foldkit-sync/journal'
type Principal = { readonly actorId: string }
const server = Effect.gen(function* () {
const journal = yield* Journal.make({
...TodoSync.journalContract(),
file: 'todos.sqlite',
opId: operation => OpId.make(operation.opId),
actorId: (principal: Principal) => ActorId.make(principal.actorId),
})
// One accepted socket, as the principal its authentication established.
return (socket: SocketLike, principal: Principal) =>
serveJournal(socket, { sync: TodoSync, journal, principal })
})foldkit-sync/journal is the exchange over the journal (foldkit-durable,
an optional peer of this package):
serveJournal(socket, options)answers one socket's exchanges and sends a notice after each commit, so its replica hears of others' edits.journalExchange(options)is the same exchange as aTransportClient, forSync.transport.fromPromisein process;journalChangesthe notice alone.- What cannot be taken is rejected by its id, not left to fail the
exchange. An operation whose message does not decode, that names another
document, that
authorizeorvalidaterefuses, or that reuses an id for other content fails the same way on every retry, so the replica drops it and the edits behind it still go. Only an entry with no id, a cursor ahead of the journal's, a connectionopensrefuses, oradmitor the journal failing fails the exchange. admit(operation)decides before the journal reads it, by what the transport established (a principal that may only read) or by what cannot run under the journal's lock (a rule that reads the rows an edit names,foldkit-guard/sync):trueadmits, a refusal is rejected by id with its reason, and it may answer with anEffect. A rule about the document belongs inauthorize.opens, anEffectof whether the connection may open the document at all: refused, every exchange fails and nothing is read or appended, andserveJournalsends it no notice of a commit. It is asked each exchange, so access taken away holds from the next one.defineDocumentHosttakes both as functions of the principal.settleruns after each exchange's appends: apply what committed to another store, throughjournal.recover. It runs every exchange, so a write that failed is tried again.- The reply carries what committed since the cursor, a page at a time
(
limit,more), a checkpoint when that history was compacted, and the journal's epoch: a replica of a reset journal is answered from the start.
The replicated state guide
says why each rule is there, and examples/sync writes
the exchange by hand, with an effect policy per committed operation.
A server that is not Durable implements the same exchange.