Skip to Content
RealtimeRaw Events & Checkpoints

Advanced Realtime Mode

Use advanced mode when you want protocol events instead of a pre-materialized row set.

If your UI only needs current rows, use live() or liveTable() first. They expose onCheckpoint too, so durable resume does not require raw event handling.

liveEvents(sql, onEvent, options?) is the low-level API.

Low-level SQL events

TS
import { ChangeType, MessageType } from '@kalamdb/client'; const stop = await client.liveEvents(  'SELECT * FROM app.messages WHERE conversation_id = 42',  (event) => {    if (event.type !== MessageType.Change) {      return;    }     if (event.change_type === ChangeType.Insert) {      console.log(event.rows);    }  },  {    batchSize: 100,    lastRows: 50,  },); await stop();

Use this path for protocol tooling, debugging, custom reconciliation, or event processors that need direct access to subscription_ack, initial_data_batch, change, and error frames.

Options

TS
{  batchSize?: number;  lastRows?: number;  from?: SeqId | number | string;  autoFetchBatches?: boolean;  onCheckpoint?: ({ lastSeqId, subscriptionId }) => void;  onError?: (event) => void;}
  • batchSize chunks the initial snapshot from the server.
  • lastRows includes the last N rows before live changes begin.
  • from resumes from a known SeqId checkpoint.
  • autoFetchBatches controls whether the client asks for remaining startup batches automatically.
  • onCheckpoint fires whenever the SDK observes a newer applied sequence ID.
  • onError receives protocol error frames without forcing your event callback to branch on every error.

Checkpoints

TS
let latestSeq; const stop = await client.liveEvents(  'SELECT id, body FROM app.messages WHERE conversation_id = 42',  (event) => {    if (event.type === MessageType.Change) {      applyChange(event.change_type, event.rows, event.old_values);    }  },  {    lastRows: 20,    from: latestSeq,    onCheckpoint: ({ lastSeqId }) => {      latestSeq = lastSeqId;      localStorage.setItem('messages.seq', lastSeqId.toString());    },  },);

The checkpoint is a single SeqId. Snapshot and commit boundaries stay server-owned.

Event model

Use runtime enums from @kalamdb/client:

TS
import { MessageType, ChangeType } from '@kalamdb/client';

Common sequence:

  1. subscription_ack
  2. one or more initial_data_batch
  3. change events with insert, update, or delete
  4. optional error

SQL safety for dynamic filters

liveEvents() accepts a SQL string. For dynamic values:

  • Prefer validated numeric IDs before interpolation.
  • Do not interpolate raw user text directly into SQL.
TS
function buildConversationSql(conversationId: number): string {  if (!Number.isInteger(conversationId) || conversationId <= 0) {    throw new Error('conversationId must be a positive integer');  }   return `SELECT * FROM app.messages WHERE conversation_id = ${conversationId}`;}

SHARED table ownership and resume domains

Clustered servers default to shared_shards = 4. Each SHARED table owns exactly one Raft group. CREATE persists table_options.shared_shard_id and a fresh history_incarnation in replicated table metadata. Increasing the configured count places new tables across the expanded set; existing owners do not change. Do not remove a shard that owns tables. Legacy metadata without an owner stays on shard 0. Moving existing data requires explicit export/import into a fresh history.

_version = (raft_log_index << 16) | ordinal is ordered only within its table’s domain. Different tables on the same shard can interleave, so per-table versions can have gaps. Transactions spanning different owner groups are rejected.

A SHARED subscription acknowledgement includes batch_control.version_domain with history_incarnation, table_id, scope_id (the shared owner), and partition_id (currently null). Persist this complete envelope together with the last safely consumed version. Resume by supplying both options.from and options.version_domain. Missing envelopes, another table/group/history, and unsupported partitions are rejected as stale cursors. Take a fresh snapshot after CURSOR_EXPIRED; do not reuse the old domain.

The Rust transport preserves the acknowledgement domain for automatic reconnect. In TypeScript, use versionDomain with from when restoring a persisted checkpoint; it serializes as version_domain. Treat the domain as opaque and never infer it from the numeric version.