Sync / foldkit-durable

Sixty seconds, with foldkit-sync

If the journal backs a Sync contract, do not restate the shared schema or the reducer. The contract already knows the slice, the durable Messages, the initial value, and how they replay through update:

ts
import { Effect } from 'effect'
import { ActorId, DocumentId, Journal, OpId } from 'foldkit-durable'
import { TodoSync } from './sync.js'

type Principal = { readonly actorId: string }

const program = Effect.gen(function* () {
  const journal = yield* Journal.make({
    // From the Sync contract: operation and snapshot codecs, the empty
    // snapshot, the reducer, each operation's replica, and any `authorize`.
    ...TodoSync.journalContract(),

    file: 'todos.sqlite',

    // Sync and Durable brand their ids separately; re-brand at the boundary.
    opId: operation => OpId.make(operation.opId),
    actorId: (principal: Principal) => ActorId.make(principal.actorId),
  })

  return yield* journal.load(DocumentId.make('todos'))
}).pipe(Effect.scoped)

Journal.make is scoped: the SQLite connection closes with the scope, so keep it open for the server's lifetime. file is a path, :memory: for tests, or a Config<string>.

What the two packages each own:

Foldkit application        Model · Message · update
        |
   foldkit-sync            what is shared, what is durable, how it replays
        |  journalContract()
   foldkit-durable         what committed, in what order, what state follows
        |
      SQLite

The journal does not speak Sync's exchange. Your server's handler does, with append for each pending operation, read for what follows the cursor, and load for a checkpoint. The guide shows that handler whole; the rest of this README explains each call.