Skip to content

Surqlize for TypeScript

Surqlize live queries

db.live() opens a LIVE SELECT subscription and resolves to a typed LiveSubscription. Each notification carries the action. A change also carries the record id and the record, parsed against your schema.

Note

Live queries need a WebSocket connection. Over HTTP, db.live() rejects with the SDK's UnsupportedFeatureError. The table must also exist before you subscribe, so define it first, for example with DEFINE TABLE. Subscribing to a table that is not defined rejects with the SDK's NotFoundError.

The examples on this page use the tables below, with db created as described in connections.

Example schema
import { orm, t, table } from "surqlize";

const user = table("user", {
    name: t.object({ first: t.string(), last: t.string() }),
    email: t.string(),
    age: t.number(),
});

const post = table("post", {
    title: t.string(),
    content: t.string(),
    author: t.record("user"),
});

const db = orm(surreal, user, post);
Define the tables
DEFINE TABLE user SCHEMALESS;
DEFINE TABLE post SCHEMALESS;

db.live() takes a table name. Awaiting it starts the subscription.

Subscribe to a table
const sub = await db.live("user");

// Handle notifications. The return value stops this handler.
const off = sub.subscribe((msg) => {
    // msg.action:   "CREATE" | "UPDATE" | "DELETE" | "KILLED"
    // msg.recordId: the affected RecordId
    // msg.value:    the affected record, parsed against the schema
    if (msg.action === "KILLED") return;
    console.log(msg.action, msg.value);
});

// Stop receiving notifications on this handler
off();

// Or end the subscription
await sub.kill();

The value of a notification is typed from the table, once you have ruled out KILLED:

Typed notifications
const typed = await db.live("user");

typed.subscribe((msg) => {
    if (msg.action === "KILLED") return;
    msg.value.name.first; // string
    msg.value.age;        // number
});

Pass a RecordId to db.live() to follow one record. The subscription selects the table and filters on the record's id, so it only delivers changes to that record. The two-argument form, db.live(table, id), does the same.

Follow one record
import { RecordId } from "surrealdb";

const byRecordId = await db.live(new RecordId("user", "ada"));
const byTableAndId = await db.live("user", "ada");

A .where() on a record subscription applies alongside the id, so a change must match both.

KILLED means the server ended the subscription, for example because its table was removed. It is the last message a subscription emits, and it has no record, so recordId and value exist only after you rule it out. LiveMessage is a union, so the compiler enforces this.

sub.isAlive becomes false when a subscription ends. That happens after kill(), after the server ends it, or after a signal given to db.withSignal() aborts.

Calling subscribe() on a subscription that is no longer alive does nothing. It returns a function, and calling that function does nothing as well. The handler is never called, and no error is raised. A for await loop over such a subscription ends at once.

Errors are reported as follows. While a subscription is alive, an error from its stream, or an error thrown by a handler, is rethrown from a microtask. It is then an uncaught exception, not a rejected promise that you can catch. Errors from a stream that has already ended, or that you have unsubscribed from, are ignored.

A subscription is async-iterable, so you can read it with for await. The loop ends after the KILLED message, and when you call kill().

Read notifications in a loop
const sub = await db.live("user");

// Call kill() from elsewhere, such as a shutdown handler. This example stops after 200 ms.
setTimeout(() => void sub.kill(), 200);

for await (const msg of sub) {
    if (msg.action === "KILLED") break;
    console.log(msg.action, msg.value);
}

Calling .subscribe(handler) on the builder starts the query and returns a function that both removes the handler and kills the subscription.

Subscribe and stop
const stop = await db
    .live("user")
    .where((user) => user.age.gte(18))
    .subscribe((msg) => {
        if (msg.action !== "KILLED") console.log(msg.action, msg.value);
    });

// later
stop();

A live query takes .where(), .return() and .fetch(), as select does. .where() accepts both fluent and object filters, and chained calls combine with AND. clearWhere() removes every .where() condition set so far. A live query cannot use LIMIT, ORDER BY, START, GROUP or SPLIT, so it has no methods for them.

Filter, project and fetch
// Only notify for adult users
const adults = await db.live("user").where((user) => user.age.gte(18));

// Project a subset of fields
const names = await db.live("user").return((user) => ({
    name: user.name,
    email: user.email,
}));

// Resolve record links in notifications
const posts = await db.live("post").fetch("author");
Note

Filtering or projecting a live query relies on query parameters, which need SurrealDB 3.0 or later.

.diff() delivers JSON Patch operations instead of full records, which renders LIVE SELECT DIFF. The diff replaces the projection, so do not combine .diff() with .return().

Subscribe to diffs
const sub = await db.live("user").diff();

sub.subscribe((msg) => {
    // msg.value is an array of JSON Patch operations
});

A subscription to a table, with any of .where(), .fetch() and .diff(), is managed by the SDK. When the connection drops and returns, the SDK registers the live query again and notifications keep arriving. The id of the subscription changes when that happens.

A subscription with a .return() projection, and a subscription to a record id, cannot be expressed that way, so they are unmanaged. They stop delivering after a reconnect, and isAlive stays true. Check sub.isManaged where this matters, or subscribe to the whole record and project it on the client.

Check whether a subscription is managed
import { RecordId } from "surrealdb";

const adults = await db.live("user").where((user) => user.age.gte(18));
adults.isManaged; // true

const names = await db.live("user").return((user) => ({ name: user.name }));
names.isManaged; // false

const one = await db.live(new RecordId("user", "ada"));
one.isManaged; // false

For the SDK's own live query API, see live queries in the SDK documentation.

A subscription made through db.withSignal(signal) is killed when the signal aborts. See cancellation, timeouts, and retries.

Was this page helpful?