@elementaio/koine
v0.1.1
Published
Zero-dependency TypeScript client for Koine embedded chat: one WebSocket, typed events, idempotent sends, automatic reconnect, exact cursor catch-up.
Maintainers
Readme
@elementaio/koine
Zero-dependency TypeScript client for Koine embedded chat.
One WebSocket per device. Your backend mints a short-TTL JWT; you hand it to the
client and it speaks the Koine wire protocol for you — idempotent sends with ack
tracking, typed events, automatic reconnect with backoff, and exact
cursor-based catch-up (it remembers the highest seq it has seen per
conversation and re-syncs the gap on reconnect, once).
Runs in the browser and in Node 22+ — both expose a global WebSocket.
Install
npm install @elementaio/koineUse
import { KoineClient } from "@elementaio/koine";
const koine = new KoineClient({
url: "wss://chat.example.com/ws",
token: jwtFromYourBackend, // HS256, signed with the secret you share with Koine
device: "web-1", // stable per device → resumes cursors on reconnect
});
koine.on("message", (m) => render(m.conv, m.from, m.payload, m.seq));
koine.on("receipt", (r) => markRead(r.conv, r.from, r.seq));
koine.on("typing", (t) => showTyping(t.conv, t.from));
koine.on("error", (e) => console.warn("koine:", e.reason));
koine.connect();
// Idempotent send — the returned promise resolves with the durable seq.
const ack = await koine.send("general", "hello");
console.log("stored as seq", ack.seq);
// Live signals and read state:
koine.typing("general");
koine.read("general", ack.seq);
koine.presence("alice");Reconnect & catch-up
By default the client reconnects on an unexpected drop (exponential backoff with jitter) and, once reconnected, re-syncs every conversation it has seen from its cursor — so you get exactly the messages you missed, once. After a full page reload, seed the cursors from your own persisted state first:
koine.setCursor("general", lastSeqYouRendered);
koine.connect();API
| Method | Purpose |
|---|---|
| connect() / close() | open the socket (reconnects are automatic) / close for good |
| send(conv, payload, { id?, kind? }) | send; resolves with the ack (kind: "ephemeral" = live-only) |
| read(conv, seq) | advance your read watermark |
| typing(conv) | ephemeral typing signal |
| readState(conv, seq) | ask group read state (seen-by count) |
| presence(user) | ask if a user is online / last-seen |
| sync(conv, after?) | explicit catch-up (defaults to the tracked cursor) |
| cursor(conv) / setCursor(conv, seq) | read / seed catch-up state |
| on(event, handler) / off(...) | subscribe / unsubscribe |
| connected | is the socket open right now |
Events: open, close, reconnecting, message, ack, receipt,
sync_page, read_state, presence, system, typing, error — all typed.
Options
| Option | Default | Meaning |
|---|---|---|
| url / token | (required) | gateway URL and the client JWT |
| device | random | stable device id; resumes this device's cursors |
| WebSocket | globalThis.WebSocket | inject an implementation if none is global |
| autoReconnect | true | reconnect on unexpected close |
| autoResync | true | re-sync tracked conversations on reconnect |
| reconnectBackoffMs | exp + jitter | (attempt) => ms schedule |
| ackTimeoutMs | 10000 | how long send() waits for its ack before rejecting |
The full wire protocol is documented in docs/PROTOCOL.md.
Develop
npm install
npm test # builds, then runs the node:test suite against a mock socketLicense
MIT © Emad Jumaah
