Manual LiveTable Partitions

Partitions

elements man livetable/partitions Read as markdown

One slice of a table per page: the comments for a post, the messages in a room, the tasks for a project.

let comments = new LiveTable<Comment>();

export default function route(req) {
  return new html({ comments: comments.view({ postId: req.params.id }) });
}

The object passed to view() is a set of column equalities. One object does four jobs:

  • It names the channel. Each partition gets its own pub/sub channel, so a mutation in one post's comments only reaches browsers watching that post.
  • It narrows the select. Auto-select adds where post_id = $1; a custom select receives the object and writes the same clause.
  • It fills the columns on insert. comments.insert({ text }) gets postId from the view. Passing a different value throws.
  • It is checked on every mutation. A row naming another partition is refused with PartitionMismatchError, before any handler runs and independent of anything the handler checks.

Several columns compose the same way, and the order you write them does not matter:

tasks.view({ projectId, status: "open" })

Only a column holding a string, number, or boolean can partition. The value has to become part of a channel name, a SQL parameter, and a string comparison on the browser.

Authorization lives at the call

Nothing about a partition is a permission. comments.view({ postId }) opens that post's comments for whoever asked, so the route or RPC that calls it is where the check goes, because that is the only place that knows who is asking:

export default function route(req) {
  session.isLoggedInOrThrow();

  return new html({ transfers: transfers.view({ userId: session.getOrThrow("userId") }) });
}

view() is server-only, and so is the LiveTable it is called on. The browser receives the view already partitioned. There is no way for browser code to widen it, because it never holds the declaration that could.

A row that changes partition

When an update moves a row from one partition to another, status going from "open" to "closed" say, the server broadcasts a delete to the partition it left and an insert to the one it joined. Any column may be a partition key, and a view stays correct when rows move.

Whole table and partition together

A page on view() and a page on view({ roomId }) can both be open. A write through either view broadcasts on the whole-table channel and on the channel of every partition column set the table has been opened with, named by the row's own values, so both pages hear it. That holds for a write through view() on the server too, as long as this process has opened a partitioned view of the table before. A freshly started process that has only ever served view() reaches the whole-table channel alone, so when a server-side write belongs to a room, make it through view({ roomId }).

Filtering beyond the partition

Everything narrower than a column equality is filter() in the template:

<li e:for={c of comments.filter((c) => !c.archived)}>

A custom select may also filter in SQL to keep the first render small, but the stream does not know about that clause: a row broadcast on the partition's channel reaches the browser whether or not the select would have returned it. Keep the template's filter as the rule and treat the select's as an optimization of the snapshot. For a table where the unwanted rows outnumber the wanted ones, use a window (see windows) or a narrower partition.