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 a TransportClient, for Sync.transport.fromPromise in process; journalChanges the 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 authorize or validate refuses, 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 connection opens refuses, or admit or 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): true admits, a refusal is rejected by id with its reason, and it may answer with an Effect. A rule about the document belongs in authorize.
  • opens, an Effect of whether the connection may open the document at all: refused, every exchange fails and nothing is read or appended, and serveJournal sends it no notice of a commit. It is asked each exchange, so access taken away holds from the next one. defineDocumentHost takes both as functions of the principal.
  • settle runs after each exchange's appends: apply what committed to another store, through journal.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.