Skip to Content
Simple Mode

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.

ts snippetTS
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:

ts snippetTS
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.

ts snippetTS
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:

ts snippetTS
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:

ts snippetTS
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:

ts snippetTS
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() or liveTable() for ready-to-render row arrays
  • SDK-managed row reconciliation for insert, update, and delete
  • limit to cap the materialized row set after startup
  • the simplest path for UI state in dashboards, feeds, and chat windows
Last updated on