# Windows A page of a long table that stays live, and loads more on request. ```ts export default function route(req) { return new html({ messages: messages.view({ roomId: req.params.id }, { orderBy: "createdAt desc", limit: 50 }), }); } ``` ```html
  • {m.body}
  • ``` `orderBy` is one or more columns with a direction, `"createdAt desc"` or `["createdAt desc", "id desc"]`. `id` is always the final tie-break so a page boundary is unique; it is appended when you leave it out. `limit` is the page size. The first page is the newest fifty by that order, and the view holds its rows in that order from then on. The template can still shape them, `toReversed()` above puts oldest first for a chat, but the window's order is what defines "the next fifty". ## What the window admits A row arriving over the wire is admitted if it sorts inside the loaded range: before the last loaded row, or anywhere once every row has been loaded. So a new message lands at the top. An edit to a message far in the past, one the page never loaded, is dropped. A loaded row whose sort key changes moves, or leaves the window if it now sorts past the end. Nothing is held for rows the page has not asked for, so a chat that loaded fifty and received two hundred holds two hundred and fifty rows, never the whole history. ## more() `messages.more()` asks the server for the next page past the last loaded row and appends it. It is sync-style like the mutators, resolves to the rows that arrived, and a second call while one is in flight joins it. `hasMore` is true while the last page came back full. The request is addressed by the view's listener id, which the route minted for this view. The server already holds the partition and window the route opened, so a page request cannot widen either: if you hold the id, the route let you see this view. ## Custom select Auto-select writes the order, the limit and the keyset clause for later pages. A custom `select` receives the window as its second argument and drops the same three in as fragments: ```ts let messages = new LiveTable({ select: ({ roomId }, w) => sql(` select m.id, m.body, m.createdAt, u.name as author from messages m join users u on u.id = m.user_id where m.roomId = ${roomId} and ${w.keyset("m")} order by ${w.order("m")} ${w.page()} `), }); ``` `w.keyset(alias)` renders the cursor clause on later pages and `true` on the first. `w.order(alias)` renders the ORDER BY columns. `w.page()` renders the LIMIT. With no window on the view they render `true`, `true` and nothing, so a select written this way also serves an unwindowed view. They are `sql.raw` fragments; see `database/sql`. A select that ignores the window is refused on its first page: rows out of order, rows at or before the cursor, or more rows than the limit each throw a `ValidationError` naming the fragment that is missing. ## Optimistic inserts A row you insert is placed by the same order. If it does not carry its sort key yet, `createdAt` before the server fills it in, it sorts as newest, which for a descending window is the top, where a new row belongs. Pass a placeholder such as `createdAt: new Date()` so it renders a time rather than nothing.