@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/koine
Use
import { KoineClient } from "@elementaio/koine";
const koine = new KoineClient({
url: "wss://chat.example.com/ws",
token: jwtFromYourBackend,
device: "web-1",
});
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();
const ack = await koine.send("general", "hello");
console.log("stored as seq", ack.seq);
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
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
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 socket
License
MIT © Emad Jumaah