[data-reveal]{opacity:1!important;transform:none!important}
AntelopeJS DMS
DocumentationLive pages

Live pages

Give a page a Direct and Pause state over the realtime of its blocks, with a capped refetch rate, polling while the stream is down, and a pause that freezes every block at once.

A monitoring page refetches its blocks on every realtime event. A busy topic turns that into a stream of requests, and the user has no way to stop the page to read it. The live page option adds a valve on the realtime the blocks already have. Blocks keep their realtimeTopic and tables their realtime, and the page decides when an event turns into a request. A page without live refetches on every event.

Usage

src/pages/request-logs.ts ts
import { HStack, Spacer } from "@antelopejs/interface-dms/base/stack";
import { LiveToggle } from "@antelopejs/interface-dms/base/live-toggle";

@RegisterPage()
export class RequestLogs extends PageController("logs", {
  displayName: "Request logs",
  module: "api",
  live: { maxRate: 5 },
}) {
  static toolbar = HStack().child("spacer", Spacer()).child("live", LiveToggle());
  static chart = ChartLine({ fetchUrl: "/api/logs/chart", realtimeTopic: "api:requests" });
  static table = TableView.fromSource({ /* … */ realtimeTopic: "api:requests", liveHold: true });
}

The two modes

  • Direct, the default. Each block and table refetches on its events, at most once every maxRate seconds. The first event of a quiet block reads at once. The events that follow within the interval collapse into one read when the interval ends, so the last event is never lost. Without maxRate, every event refetches.
  • Pause. Nothing is requested on an event. Each block and table counts the events it receives, and resuming refetches once each block that missed something. A rolling period (last-hour, last-24h) stops sliding, so the blocks reading it do not refetch every minute. A calendar period (today, last-7-days and the others) waits before it moves past midnight. A period the user picks still applies at once, and resuming catches up what was skipped in one read.

What the user asks for always reads, paused or not: refreshPageBlocks() (a page header button, a form saved through BlockFunctions.REFRESH_PAGE, a banner button), a table's row actions and its refresh button. The page gates only the realtime events and the refreshes nobody asked for. A module that refreshes the page on a timer or a feed calls refreshLivePageBlocks() from #dms-ui/app/utils/blockRefresh instead. That call is capped by maxRate in Direct and counted while paused.

Switching modes

The user switches with the LiveToggle() block, which the module places where it wants. In a pageToolbar(), it gives the page the same context bar as every other page. The L key also switches while a toggle is on the page, once per press however many toggles the page shows.

The browser keeps the choice per page, or per rememberKey. It applies once the page is mounted, so the server-rendered page and the hydrating page agree. ?live=0 in the URL opens the page paused without remembering it, which makes a shared link to a frozen view. Resuming drops the parameter from the URL.

A table declared with liveHold keeps its rows while paused, through the user's own search, sort and pagination, and shows a "N new · Show" strip. A custom component follows the same state with usePageLive(). It refreshes its own data through the same gate with usePageLiveRefresh().

The page also pauses by itself, and the toggle says why:

  • A hidden tab. While the browser tab is in the background, the page requests nothing and counts what arrives. It resumes when the tab is shown again, unless the user had paused it.
  • A hold. A component holds the page while the user does something the live data would disturb, with usePageLive().hold(reason). The toggle then reads "Paused · typing in the Tester". The page resumes once nothing holds it. Resuming by hand lifts the current holds until their component releases them.

What a pause counts

While paused, the page counts published rows, not messages:

  • An event naming rows in payload.ids counts each row once per pause. A table view's own row events do this.
  • An event carrying a positive integer payload.count counts that many.
  • Any other event counts one.

A module whose feed would flood the pages can therefore batch its publishes, for example one message per second with { count }, and the toggle and the table strip still read "40 new":

ts
void PublishMessage("api:requests", "requests", { payload: { count: batch.length } });

Cadence

maxRate is also the polling interval while the realtime stream is down. The page then reads its blocks every maxRate seconds, as refreshLivePageBlocks() does, until the stream is back. From the first poll on, the toggle shows a "no realtime, polling" icon beside its "Updated" time. A stream still connecting, as on every full load, does not count as down. The toggle keeps the icon's space while the icon is hidden, so the icon never widens the toggle or wraps the toolbar.

A page showing long periods declares a slower cadence for them. Once its PeriodSelector spans period or more, slowRateAfter.maxRate replaces maxRate, for the gate and the polling alike:

ts
live: { maxRate: 10, slowRateAfter: { period: "7d", maxRate: 60 }, rememberKey: "api" },

Options

live takes a PageLiveOptions. A category ignores it.

live optionTypeDescription
maxRatenumber (seconds)Shortest interval between two reads of one block in Direct, and the polling interval without a stream. Unset, every event reads and the page does not poll.
slowRateAfter{ period: "30m" | "24h" | "7d" …; maxRate }The slower cadence once the period shown spans period or more. period is a whole number of minutes (m), hours (h) or days (d).
rememberKeystringShares the remembered Direct or Pause choice between the pages that declare the same key, such as every page of a module. Unset, each page remembers its own.

Making a block live

A block is live when something tells it that its data moved. On a page declared with live, every one of those signals goes through the page's gate. Nothing else makes a block read by itself.

  • A built-in data block is live once it names a realtimeTopic. The data blocks are KpiCard, ChartCard and the charts, TopListCard, StatGroup, KeyValueList, NavCardGrid, ActivityFeed, Meter, Banner, the badges of a Tab set, and Guard. The builder registers the topic on the page (RegisterPageTopic), so the session may subscribe to it, and the backend publishes on it with PublishMessage.
  • A table view follows the row events of its data controller without any option, and the topics of its own realtimeTopic. A TableView.fromSource table has no row events, so give it a realtimeTopic.
  • A block without a realtimeTopic, such as a CodeBlock reading a fetchUrl or a list block or card left without one, has no event of its own. It reads again on refreshLivePageBlocks(), on the polling while the stream is down, and on refreshPageBlocks(). A live page does not poll its blocks while the stream is up. Data that changes with time alone needs a module clock calling refreshLivePageBlocks(), or idleRefresh in a custom component.
  • A custom component that reads through useApiData follows the events of the page's topics: every topic registered on the page, by its blocks or by RegisterPageTopic(pageId, topic) in the backend. For a topic of its own, the component registers the topic on the page in the backend and reads through usePageLiveRefresh(refresh, { realtimeTopic }), with its read opting out of the page's refresh (refreshOnPageRefresh: false). It never throttles on its own, because the gate applies maxRate.

What a pause freezes

While paused by the user, a hold or a hidden tab, the page requests nothing that nobody asked for, and every block on it freezes at once:

Frozen while pausedOn resume
The events of a block's realtimeTopic, a table's row events, the events of the page's topics behind useApiData readsCounted in the toggle's "N new". Each block that missed one reads once.
refreshLivePageBlocks(), a usePageLiveRefresh trigger() or idleRefresh, the polling while the stream is downEach block that missed one reads once.
A component's useApiData polling (refreshInterval)Reads once if a poll fell due meanwhile, then polls again. Not counted.
A rolling or calendar period moving on with the clockOne read catches up.

These still read while paused: refreshPageBlocks(), every action of the user (a table's row actions, its refresh button, a saved form), and the inputs the user changes (a period, a filter, a search, a sort, a page of results). A table with liveHold keeps its rows through those too. The shell is not part of the page, so notifications, presence and the menu keep updating.