@asterql/sync-server
v0.1.0
Published
Server side of the AsterQL sync protocol: scope ledgers, sequenced publishing, replay, and a transport-agnostic connection handler.
Maintainers
Readme
@asterql/sync-server
The server side of the AsterQL sync protocol: per-scope envelope ledgers,
sequenced publishing, replay from client cursors, and a transport-agnostic
connection handler. Pairs with @asterql/sync
on the client.
Install
npm install @asterql/sync-server @asterql/view-protocolUsage
import { SyncHub, InMemorySyncLedger } from "@asterql/sync-server";
import { WebSocketServer } from "ws";
const hub = new SyncHub({
ledger: new InMemorySyncLedger({ retain: 10_000 }),
authorize: (scope, user) => canRead(user, scope),
});
const wss = new WebSocketServer({ noServer: true });
server.on("upgrade", (request, socket, head) => {
const user = authenticate(request); // your session check
if (!user) return socket.destroy();
wss.handleUpgrade(request, socket, head, (ws) => {
const connection = hub.connect(ws, user);
ws.on("message", (data) => connection.handleMessage(String(data)));
ws.on("close", () => connection.close());
});
});
// The one write path — mutations publish envelopes; the ledger assigns seq:
await hub.publish("run:r1", (seq) => ({
eventId: crypto.randomUUID(),
kind: "patch",
scope: "run:r1",
seq,
entities: [
{
key: "message:m1",
patches: [{ op: "appendText", path: "/text", delta: "…" }],
},
],
}));Semantics
- Ledgers assign sequence numbers.
publish(scope, makeEnvelope)appends atomically; the ledger passes the next contiguous seq to your factory. Clients replay any range gap-free because the ledger — not delivery order — is the source of truth. - Replay from cursors. A
subwithsincereplays everything after the cursor before going live;sinceomitted means live-from-now. A cursor below the retention floor getsscopeError snapshot_required— the client re-bootstraps from a registered view and resubscribes. - Seed snapshots. Publish a
kind: "snapshot"envelope as each scope's first entry so replay-from-zero is deterministic for any consumer. - Transport-agnostic.
connect()takes anything withsend(string); feed inbound frames tohandleMessage. Works withws, Durable Object sockets, or test fakes. - Multi-instance. Pass a shared
SyncBus(Postgres LISTEN/NOTIFY, Redis pub/sub) and a shared persistentSyncScopeLedger; the in-memory implementations are correct for one process and for tests.
