@softwareseva/sync
v1.6.0
Published
Offline-first sync for Hono on Cloudflare Workers + D1: idempotent batched push of client operations with replay, and a change log with a monotonic cursor for pulling upserts and deletes per scope. Pairs with the kashi_sync Flutter package.
Readme
@softwareseva/sync
Offline-first sync for the kashi stack: idempotent batched push, a cursor-based pull, and a React client built on RxDB + TanStack DB. kashi_sync is the Flutter half.
Install
pnpm add @softwareseva/sync hono zod
# React client also needs:
pnpm add rxdb @tanstack/dbWhat's inside
@softwareseva/sync(root import, Hono on Cloudflare Workers + D1):syncRouter()mountsPOST /pushandGET /pull;changeStatement()/recordChanges()log changes from any ordinary route so web edits reach mobile devices too.POST /push { ops: [{ opId, type, payload }] }: each op runs its handler once. The result is stored by(user, opId), so a retried batch replays results instead of applying twice. Failures reportretryableso clients know whether to back off or give up.GET /pull?since=<seq>: returns{ changes: { <entity>: { upserts, deletes } }, next, hasMore, reset }from thesync_changeslog for the user's scopes. The cursor is a monotonically increasing sequence, so resuming is exact.
@softwareseva/sync/client:SyncEngine— an RxDB-backed outbox of ops plus a pull cursor, mirroringkashi_sync's Dart engine.@softwareseva/sync/react:createSyncedCollection()— a TanStack DB collection backed by an RxDB collection, with live reads and writes that enqueue an op on theSyncEnginein the same call;useSyncStatus()for a status badge (idle/syncing/offline/failed, pending count, needs-attention count). See thereact-offline-syncskill.kashi_sync(pub.dev, Flutter): the same outbox-and-cursor design on Drift. See theflutter-drift-syncskill.
Example
app.route("/v1/sync", syncRouter({
auth: requireAuth(authConfig),
user: (c) => c.get("user"),
scopes: (user) => [`user:${user.id}`],
handlers: { "note.upsert": { schema: noteSchema, apply: (ctx, p) => { ctx.batch.push(upsertNote(ctx.db, p)); ctx.changed({ scope: `user:${ctx.user.id}`, entity: "notes", id: p.id }); return { id: p.id }; } } },
entities: { notes: { load: (db, ids, user) => loadNotes(db, ids, user.id) } },
}));Migrations
Ships migrations/sync_0001_ops_changes.sql; copy it into your migrations directory with npx @softwareseva/cli migrate.
See also
sync-endpoints (server), react-offline-sync (React), and flutter-drift-sync (kashi_sync) skills.
