Skip to Content
RealtimeMaterialized Live Rows

TypeScript Live Queries

Use client.live() to receive the current rows of a KalamDB query. The SDK loads the initial snapshot and reconciles later inserts, updates, and deletes by primary key. Replace your displayed list on each callback; do not append the whole callback array.

For React, start with the React inbox example. For typed Drizzle tables outside React, use the ORM live-table example. The Dart/Flutter and Rust examples use the same table.

Prepare the live-query example

Start a server using the server quick start. Run this schema as an account allowed to create tables, in the SQL Studio or your schema migration:

SQL
CREATE SCHEMA IF NOT EXISTS app;CREATE TABLE IF NOT EXISTS app.live_messages (  id TEXT PRIMARY KEY,  room TEXT NOT NULL,  body TEXT NOT NULL) WITH (TYPE = 'USER');

This is a private inbox: use the same authenticated user for the subscriber and the test writes. A room filter does not make a USER table shared between users. Collaborative rooms need a SHARED table and access policies.

Run a TypeScript live query

In a Node.js TypeScript project:

BASH
npm install @kalamdb/clientnpm install --save-dev tsx typescript @types/nodeexport KALAMDB_URL=http://localhost:2900export KALAMDB_USERNAME=your-userexport KALAMDB_PASSWORD=your-passwordnpx tsx inbox.ts

Save the following as inbox.ts before running the last command. Credentials here are for a local terminal; browser apps should use their signed-in user’s token through an auth provider.

TS
import { Auth, createClient } from '@kalamdb/client'; type Message = { id: string; room: string; body: string }; async function main() {  const username = process.env.KALAMDB_USERNAME;  const password = process.env.KALAMDB_PASSWORD;  if (!username || !password) throw new Error('Set KALAMDB_USERNAME and KALAMDB_PASSWORD');   const client = createClient({    url: process.env.KALAMDB_URL ?? 'http://localhost:2900',    authProvider: async () => Auth.basic(username, password),    onDisconnect: (reason) => console.warn('Disconnected:', reason.message),    onConnectionError: (error) => console.error('Connection:', error.message),  });  let stop: (() => Promise<void>) | undefined;  let finish!: () => void;  const shutdown = new Promise<void>((resolve) => { finish = resolve; });  const onSignal = () => finish();  process.once('SIGINT', onSignal);  process.once('SIGTERM', onSignal);   try {    console.log('Loading inbox…');    await client.connect();    stop = await client.live<Message>(      "SELECT id, room, body FROM app.live_messages WHERE room = 'main'",      (rows) => {        console.log(rows.length === 0 ? 'Inbox is empty' : `${rows.length} messages`);        console.table(rows);      },      {        getKey: 'id',        mapRow: (row) => ({          id: row.id.asString()!,          room: row.room.asString()!,          body: row.body.asString()!,        }),        onError: (event) => {          console.error(`Subscription [${event.code}]: ${event.message}`);          process.exitCode = 1;          finish(); // Correct the cause before running again.        },      },    );    await shutdown;  } finally {    process.off('SIGINT', onSignal);    process.off('SIGTERM', onSignal);    try { await stop?.(); }    finally { await client.disconnect(); }  }} void main().catch((error) => {  console.error('Inbox stopped:', error);  process.exitCode = 1;});

The outer catch handles connection and startup failures. onError handles server subscription errors reported after startup. Connection callbacks report transport problems. This CLI deliberately stops on a subscription error; a UI can retain its last rows with a visible stale/error state and offer a fresh subscription after the cause is resolved.

Verify inserts, updates, and deletes

While the example is running, execute these statements one at a time through another connection using the same identity:

SQL
INSERT INTO app.live_messages (id, room, body)VALUES ('demo-1', 'main', 'Hello from another connection'); UPDATE app.live_messages SET body = 'Edited in realtime' WHERE id = 'demo-1'; DELETE FROM app.live_messages WHERE id = 'demo-1';

On a fresh table, expect an empty inbox, then one message, then the edited body without a duplicate, then an empty inbox again. Press Ctrl+C to unsubscribe and disconnect. Re-running the app starts a fresh snapshot, so existing rows appear too.

Choose rows, typed tables, or events

NeedAPIWhat your code receives
A filtered listclient.live(sql, onRows, options)The current materialized rows
All rows in a tableclient.liveTable('app.live_messages', onRows)The same row-list behavior
Generated Drizzle modelsORM liveTable(client, table, onRows, options)Typed properties rather than cell wrappers
Protocol events or custom persistenceclient.liveEvents(sql, onEvent, options)Snapshot batches and individual changes

With no mapRow, fields are KalamCellValue objects: use asString(), asBool(), and other cell accessors. Include the primary key in your SELECT. Use getKey: ['first_key', 'second_key'] for declared key columns; a JavaScript key function uses the TypeScript reconciliation path.

Limits, reconnects, and recovery

  • Direct client.live() SQL supports SELECT ... FROM ... WHERE .... Keep joins, aggregates, ORDER BY, and LIMIT out of this subscription SQL.
  • batchSize controls snapshot chunks. lastRows restricts the initial history. Direct client.live()’s limit bounds the retained row set; it does not promise a SQL-sorted top-N window.
  • ORM and React orderBy / limit are local presentation controls. They retain the matching set so a removed visible row can be replaced. Narrow the filter for large datasets.
  • The SDK manages connection recovery. Do not create another subscription on every disconnect notification. If you explicitly retry, close the previous subscription first.
  • Do not restore a cursor into an empty UI cache and assume it reloads all rows. A durable resume needs matching cached data, query/identity, and checkpoint; an ordinary screen can start fresh without from.
  • Replay is bounded. A delivery gap or expired cursor needs recovery, not a claim that the displayed rows are current. See reconnection and resume and the WebSocket recovery contract.

To check failures, temporarily use a missing table or invalid credentials. Expect a visible error instead of an empty-success state. Fix the cause and restart. Disconnect the network to exercise connection callbacks; after recovery, compare the list with a normal SELECT before treating it as current.

Only the owner of a shared client should disconnect it. A component should stop its own subscription; the React hook handles that on unmount.