Simple Realtime Mode
Use simple mode when your application wants the current row set, not raw subscription protocol events.
Start with live().
It materializes the latest rows in the client and applies insert, update, and delete for you.
Recommended: materialized live rows
const inboxSql = ` SELECT id, room, role, body, created_at FROM support.inbox WHERE room = 'main'`; const unsub = await client.live( inboxSql, (rows) => { // USER tables are tenant-private. // The same SQL can run for every signed-in account, but the callback only // receives rows from that caller's own table partition. renderInbox(rows); }, { limit: 100, lastRows: 100, onError: (event) => { console.error(event.code, event.message); }, },);Use liveTable() for the same behavior with table-name sugar:
const unsub = await client.liveTable('support.inbox', (rows) => { renderInbox(rows);});This path is implemented in the shared Rust core, so TypeScript and Dart use the same row materialization behavior by default.
Resume from a specific SeqId
Persist the latest applied SeqId and feed it back into from on the next session.
import { SeqId } from '@kalamdb/client'; const inboxSql = ` SELECT id, room, role, body, created_at FROM support.inbox WHERE room = 'main'`; const savedSeqText = localStorage.getItem('support.inbox.seq');const startFrom = savedSeqText ? SeqId.from(savedSeqText) : undefined; const unsub = await client.live( inboxSql, (rows) => { renderInbox(rows); }, { limit: 100, lastRows: 100, ...(startFrom ? { from: startFrom } : {}), onCheckpoint: ({ lastSeqId }) => { localStorage.setItem('support.inbox.seq', lastSeqId.toString()); }, },);This is the recommended pattern for chat timelines, activity feeds, audit streams, and reconnect-heavy mobile sessions.
Table subscription sugar
If you only need SELECT * FROM table, use the shorthand APIs:
const lowLevel = await client.liveEvents('SELECT * FROM app.messages', (event) => { console.log(event.type);}); const highLevel = await client.liveTable('support.inbox', (rows) => { renderInbox(rows);});Custom row identity in TypeScript
By default, the high-level API reconciles rows using the row id field in the Rust core.
If your query does not expose a stable id, prefer declarative getKey column names so reconciliation still stays inside the shared Rust core:
const unsub = await client.live( "SELECT room, message_id, body, created_at FROM chat.messages WHERE room = 'main'", (rows) => { console.log(rows); }, { getKey: ['room', 'message_id'], },);Use getKey only when the identity must be derived by arbitrary JavaScript code:
const unsub = await client.live( "SELECT room, created_at, body FROM chat.messages WHERE room = 'main'", (rows) => { console.log(rows); }, { getKey: (row) => `${row.room.asString()}:${row.created_at.asString()}`, },);When getKey is provided, reconciliation falls back to the TypeScript SDK layer because arbitrary JavaScript callbacks cannot be shared through the Rust core.
What simple mode gives you
live()orliveTable()for ready-to-render row arrays- SDK-managed row reconciliation for
insert,update, anddelete limitto cap the materialized row set after startup- the simplest path for UI state in dashboards, feeds, and chat windows