@nolag/dash
Live dashboards with streaming metrics, rolling aggregation and widget snapshots.
Overview
@nolag/dash powers live operational dashboards. Servers publish metric data points and widget snapshots to panels; viewers on the same panel receive them as they happen and can query rolling aggregations (average, min, max, sum, count, last) over a time window, computed in the browser from the points they have received. Your app owns one core NoLag client and injects it into NoLagDash; the wrapper attaches its behaviour to that connection and never opens or closes the socket.
Key features
- Streaming metric ingestion with a per-stream buffer
- Client-side rolling aggregation over a time window
- Widget snapshots for gauges, charts, counters, tables, text and custom types
- Tags on metrics, and optional subscription filters per panel
- Viewer presence per panel
How it works
NoLagDash attaches to the injected client and subscribes to the online lobby. joinPanel(name) returns a DashboardPanel: each panel is a room, and the panel subscribes to two topics in it.
| Topic | Purpose |
|---|---|
metrics | Time-series data points: value, timestamp, unit, tags |
widgets | Widget snapshots: type, data, label, timestamp |
A MetricStore in the panel keeps the points per stream ID and computes aggregations, and a WidgetManager keeps the latest snapshot per widget ID. Metrics and widgets are published with echo: false; the publisher adds its own point or snapshot to its local store instead of receiving it back.
A viewer's buffer starts empty and fills from the points published after it joined. kraken offers clients no message history and the wrapper does not retain widget snapshots, so if new viewers need a starting state, have your publisher re-send its current widget snapshots on a timer, and keep long-term metrics in a time-series store of your own.
Topics to grant
The wrapper expects these names in its app (the nolag-dash-sdk blueprint):
| Kind | Names |
|---|---|
| Topics | metrics, widgets (both subscribed in every joined panel) |
| Rooms | overview, plus every panel name you pass to joinPanel() |
| Lobby | online (subscribed on every connect) |
A typical split is publish (or pubSub) for the server that produces data and subscribe for viewers. If the online lobby is not granted, ready() resolves only after the 10 second lobby subscribe timeout.
With kraken's static auth file (format). Viewer presence only works in rooms that have a rule carrying room_slug and room_id, and publishers and viewers must use the same appId:
{
"tokens": {
"viewer-token": {
"actorTokenId": "viewer-1",
"projectId": "demo-project",
"actorType": "user",
"appId": "dash-app",
"appName": "dash",
"allowedTopics": [
{ "pattern": "dash/overview/#", "permission": "subscribe", "room_id": "dash-overview", "room_slug": "overview" }
],
"allowedLobbies": [{ "lobby_slug": "online", "lobby_id": "dash-online" }]
},
"metrics-publisher-token": {
"actorTokenId": "metrics-publisher",
"projectId": "demo-project",
"actorType": "service",
"appId": "dash-app",
"appName": "dash",
"allowedTopics": [
{ "pattern": "dash/overview/#", "permission": "pubSub", "room_id": "dash-overview", "room_slug": "overview" }
],
"allowedLobbies": [{ "lobby_slug": "online", "lobby_id": "dash-online" }]
}
}
}
With a @nolag/core project document (format), as one entry in apps. With accessMode: "open" every actor in the project gets pubSub; use "restricted" plus per-actor appAccess grants for a subscribe-only viewer (Access Control):
{
"slug": "dash",
"name": "Dash",
"accessMode": "open",
"topics": ["metrics", "widgets"],
"rooms": [{ "slug": "overview", "name": "Overview" }],
"lobbies": [{ "slug": "online", "name": "Online", "rooms": ["overview"] }]
}
Installation
npm install @nolag/dash @nolag/js-sdk
@nolag/dash peer-depends on @nolag/js-sdk ^1.12.0. Both packages are MIT licensed.
Quick start
import { NoLag } from '@nolag/js-sdk'
import { NoLagDash } from '@nolag/dash'
// The app owns one core client. `token` is an actor token or a TokenProvider.
const client = NoLag(token, { url: 'ws://localhost:8080/ws' })
const dash = new NoLagDash({
client,
appName: 'dash', // the App Slug the topics are granted under
username: 'Alice',
})
await client.connect() // the app owns the connection
await dash.ready() // wrapper setup complete
const panel = dash.joinPanel('overview')
// Publish a metric (usually from a server). The point is stamped with
// Date.now() and added to the publisher's own buffer.
panel.publishMetric('cpu.usage', 74.2, {
unit: 'percent',
tags: { host: 'web-01', region: 'us-east-1' },
})
// Metrics from other publishers
panel.on('metric', ({ streamId, value, timestamp }) => {
console.log(`[${streamId}] ${value} @ ${new Date(timestamp).toISOString()}`)
})
// Buffered points and a rolling aggregation over the last 5 minutes
const points = panel.getMetrics('cpu.usage')
const agg = panel.getAggregation('cpu.usage', 5 * 60_000)
console.log(points.length, 'points; avg', agg.avg.toFixed(1), 'max', agg.max.toFixed(1))
// Publish and read widget snapshots
panel.publishWidget('active-sessions', 'counter', { value: 1_248, trend: 'up' }, 'Active Sessions')
panel.on('widgetUpdate', ({ widgetId, type, data }) => {
console.log(`Widget ${widgetId} (${type}):`, data)
})
const widget = panel.getWidget('active-sessions')
// Teardown: the wrapper releases its handlers; the app closes the socket.
dash.detach()
client.disconnect()
Reconnects
After the core client reconnects, the wrapper subscribes to the online lobby again, re-sends presence for every joined panel and emits reconnected. It does not subscribe to its panel topics again: it assumes the broker restored them. kraken v0.9.0 restores subscriptions on reconnect only from what its auth backend returns, and the static auth file returns none, so on a static-auth kraken metrics and widgets stop after a reconnect unless you subscribe again (see Production).
In 1.2.0 the wrapper's handlers are registered on the core client by topic and survive the reconnect; only the broker-side subscriptions are gone. Re-issue them from a connect handler on the core client (not reconnect, which fires before the socket is back). kraken treats a repeated subscribe as a replacement, so this is also safe where the backend did restore them.
client.on('connect', () => {
for (const panel of dash.getPanels()) {
const ctx = client.setApp('dash').setRoom(panel.name)
const { metrics, widgets } = panel.filters
ctx.subscribe('metrics', metrics.length > 0 ? { filters: metrics } : {})
ctx.subscribe('widgets', widgets.length > 0 ? { filters: widgets } : {})
}
})
Points published while a viewer was disconnected leave a gap in its buffer.
Filters
joinPanel(name, { filters }) and panel.setFilters(values, { topic? }) narrow the metrics and widgets subscriptions (or one of them) to data published with one of the given filter values (publishMetric(id, value, { filter })). An empty set restores the unfiltered subscription. Subscribers without filters still receive filtered data, so filters reduce traffic but do not hide data: put data that some viewers must not see in a separate panel with its own grants. The legacy metricFilters join option and setMetricFilters() family still work and act on metrics only; metricFilters: [] subscribes to nothing on metrics. See Filters.
Limitations on kraken v0.9.0
- One presence room per connection. Each joined panel sets presence in its room, and kraken keeps a connection's presence in one room at a time, so with several panels joined only the last one shows you as a viewer.
- Lobby. At kraken v0.9.0 a lobby's room list comes from the rooms the subscribing actor can reach and is cached per node for 5 minutes after the last lobby subscribe; after that,
viewerOnlineandviewerOfflinestop until a client subscribes to the lobby again. See Lobbies.
API reference
NoLagDash
Constructor options
| Option | Type | Description |
|---|---|---|
client | NoLagSocket | Required. The injected core client the app owns and connects. |
username | string | Optional display name for this viewer. |
metadata | Record<string, unknown> | Optional custom viewer data carried in presence. |
appName | string | The App Slug used as the topic prefix (default 'dash'). |
panels | string[] | Panels to subscribe to once the wrapper is ready. |
maxMetricPoints | number | Max points kept in memory per stream (default 1000). |
aggregationWindow | number | Accepted and stored (default 60000) but not read: getAggregation() uses 60000 ms whenever windowMs is omitted. |
debug | boolean | Enable wrapper debug logging (default false). |
| Method / property | Returns | Description |
|---|---|---|
ready() | Promise<void> | Resolves once the first setup completed. joinPanel() throws before this. |
detach() | void | Release this wrapper's handlers and topics. Terminal; never closes the socket. |
joinPanel(name, opts?) | DashboardPanel | Join a panel and set presence in it. opts.filters filters metrics and widgets; opts.metricFilters filters metrics only. |
leavePanel(name) | void | Leave a panel and unsubscribe its topics. |
getPanels() | DashboardPanel[] | All joined panels. |
getOnlineViewers() | DashboardViewer[] | Viewers currently online. |
localViewer | DashboardViewer | null | The local viewer, available after ready(). |
Events
| Event | Payload | Description |
|---|---|---|
connected | none | First setup completed. |
disconnected | reason: string | The connection closed. |
reconnecting | none | The core client scheduled a reconnect attempt. |
reconnected | none | Setup ran again after a reconnect (see Reconnects). |
error | error: Error | An error from the core client, including broker refusals. |
viewerOnline | viewer: DashboardViewer | A viewer came online. |
viewerOffline | viewer: DashboardViewer | A viewer went offline. |
DashboardPanel
| Method / property | Returns | Description |
|---|---|---|
publishMetric(streamId, value, opts?) | MetricPoint | Publish a data point; opts is { unit?, tags?, filter?, filters? }. The timestamp is always Date.now(). |
publishWidget(widgetId, type, data, label?, opts?) | WidgetUpdate | Publish a widget snapshot, replacing the previous one for that widget. type is 'gauge' | 'chart' | 'counter' | 'table' | 'text' | 'custom'. |
getMetrics(streamId?) | MetricPoint[] | Buffered points for one stream, or for every stream when omitted. |
getAggregation(streamId, windowMs?) | Aggregation | { streamId, min, max, avg, sum, count, last, windowMs } over the last windowMs ms (60000 when omitted). |
getWidget(widgetId) / getWidgets() | WidgetUpdate | undefined / WidgetUpdate[] | The latest snapshot for one widget, or all widgets. |
getViewers() | DashboardViewer[] | Remote viewers present in this panel. |
filters | { metrics, widgets } | The current filters per topic. |
setFilters(values, opts?) / addFilters(values, opts?) / removeFilters(values, opts?) | void | Change the filters; opts.topic ('metrics' or 'widgets') limits the change to one topic. |
Events
| Event | Payload | Description |
|---|---|---|
metric | MetricPoint | A point arrived from another publisher: { id, streamId, value, unit?, tags?, timestamp }. |
widgetUpdate | WidgetUpdate | A snapshot arrived: { id, widgetId, type, data, label?, timestamp }. |
viewerJoined | DashboardViewer | A viewer joined this panel. |
viewerLeft | DashboardViewer | A viewer left this panel. |
Types
| Type | Shape |
|---|---|
DashboardViewer | { viewerId, actorTokenId, username?, metadata?, joinedAt, isLocal } |