Options
elements man livetable/options Read as markdownThe declaration's options, and what view() takes.
new LiveTable<T>({
table: "comments", // db table for auto-SQL; inferred from the variable name
channel: (p) => `comments:${p}`, // override the pub/sub channel name
fields: ["text", "author", { name: "createdAt", readonly: true }],
select: (partition, w) => sql(`...`), // custom SELECT
insert: (item) => { ... }, // custom insert, or omit for auto-SQL
update: (item) => { ... },
delete: (item) => { ... },
});
table.view(); // the whole table
table.view({ roomId }); // one partition
table.view({ roomId }, { orderBy: "createdAt desc", limit: 50 }); // one page of it
A LiveTable is always live. There is no snapshot mode: a page that wants rows
once, with no subscription, calls an @rpc that returns them from sql, and
e:for iterates the array it gets back.
Table
table names the database table auto-SQL reads and writes. The compiler fills
it in from the variable name (camelCase to snake_case), so let blogPosts = new LiveTable<Post>() reads blog_posts. Set it when the two differ.
Fields
fields: ["text", "author", { name: "createdAt", readonly: true }]
If omitted, fields are introspected from the database. id is always implicit
and readonly, and so are createdAt and updatedAt: the database fills them
in, so the createdAt: new Date() an optimistic insert carries as a placeholder
is never written. A readonly field is excluded from auto-insert and
auto-update. Declare fields yourself to write one of them.
camelCase in TypeScript maps to snake_case in the database (createdAt to
created_at).
Ids
Every row has a string id, and the browser mints it, as a UUIDv7, before an
optimistic insert is sent. That is what lets the broadcast echo find the row it
belongs to. A table whose ids are assigned by the database (a serial column) is
written through an @rpc, not a LiveTable mutator.
Channel Name
Every LiveTable gets a channel name automatically: <table> for the whole table
and <table>:<partition> for a partition, where the partition key is the
view's column equalities encoded as roomId=42. File-path disambiguation keeps
two declarations on the same database table from cross-broadcasting.
Set channel when something outside the LiveTable publishes on the channel: a
Postgres trigger, another service, or a second LiveTable that should share the
broadcast. The function receives the encoded partition key, or "" for the
whole table.
new LiveTable<Comment>({
channel: (partition) => partition ? `comments_shared:${partition}` : "comments_shared",
});
A trigger that notifies the channel with { op, data } works alongside the
app's own broadcasts. The browser store dedupes an insert by id, and an
update or delete is idempotent, so hearing about a write twice changes nothing.
See channel for the trigger pattern.
Sort
There is no sort option. The initial select returns rows in database order and the template orders them:
<li e:for={c of comments.sort((a, b) => +a.createdAt - +b.createdAt)}>
sort() returns a plain array, sorted, and the loop is live because it
iterated the view to build it. A windowed view is the exception: its orderBy
is the order the rows are held in, because a page has to be a page of
something. See windows.
Listen Before Select
view() opens the Postgres LISTEN and awaits its commit before running the
select. A NOTIFY fired between the two is queued on the listener and flushed
when the browser attaches, and the browser dedupes it against the snapshot by
id. You never call listen() yourself. For a one-shot query merged with a
raw channel, see channel.
Returning a view from an RPC
A route handler that opens a view and passes it to its template is the common
path. An @rpc can return a view too, for a table that depends on a runtime
decision: a permission check, a parameter, a different table per call.
/** @rpc */
export function watchRoom(roomId: string) {
if (sql(`select 1 from roomMembers where roomId = ${roomId} and userId = ${session.getOrThrow("userId")}`).empty()) {
throw new AuthError();
}
return comments.view({ roomId });
}
let comments = watchRoom(roomId); // browser: a LiveView, sync-style
Errors
AuthError,ForbiddenError: thrown by a handler that checked the session.ValidationError: thrown by a handler on bad input, or by the framework when a custom select ignores the window it was given.PartitionMismatchError: a row's partition columns did not match the view's partition. Fires before any handler runs.
Related
database:sql(),tx(), and the camelCase-to-snake_case boundary.channel: the pub/sub layer underneath LiveTable.html/events:oninit,oninsert, andonremoveon the rows a LiveTable renders. A LiveTable has no lifecycle handlers of its own; the rows do.html/integration: rendering a LiveTable from a template.