Skip to content
Connect > Data APIs > Use Cases

Realtime limit-breach alerts

Goal: a live risk monitor that shows a row the instant a limit is breached and removes it the instant the breach clears: no polling, no scanning. The filter is the alert: if a row is in your subscription, it is currently breached.

This is the sharpest use of focus filtering (oof): you subscribe to the breach condition and let the Center push membership changes to you.

Prerequisites

1
2
3
4
5
6
curl -sk -u username:password -G \
  --data-urlencode 'cmd=CREATE PUBLIC TABLE LIMITS (LIMIT_ID String, desk String, metric String, value Double, threshold Double, breached Boolean) USE Broadcast="true" RefreshPeriodMs="100"' \
  "https://localhost:33333/3forge_rest/query?display=jsonMaps"
curl -sk -u username:password -G \
  --data-urlencode 'cmd=CREATE INDEX LIMITS_PK ON LIMITS(LIMIT_ID HASH) USE Constraint="PRIMARY"' \
  "https://localhost:33333/3forge_rest/query?display=jsonMaps"

How breached is maintained (a computed column, a trigger, or an upstream feed) is a Center-side concern; the WebSocket just watches the result. For a manual test, insert a row with breached false and flip it to true over REST.

Subscribe to the breach condition

{"action":"subscribe","id":"alerts","sql":"SELECT LIMIT_ID,desk,metric,value,threshold FROM LIMITS WHERE breached == true","oof":true}

With oof:true, the Center re-evaluates WHERE breached == true whenever breached changes:

  • A limit crosses its threshold (breached flips to true) → you receive an add: raise the alert.
  • The breach clears (breached flips back to false) → you receive a delete: retire the alert.
  • A breached limit's value keeps moving while still breached → normal updates: refresh the displayed number.

The initial rows, sent as add entries before the snapshot frame, are exactly the limits breached right now, so a monitor that starts mid-session is immediately correct.

Client handling

let alerts = new Map();                // objectId -> row: the limits breached now
let loading = new Map();               // rows of a load in progress (first connect or reconnect)
let live = false;                      // true between a snapshot frame and the next disconnect
let loadedOnce = false;                // false until the first load completes
const ws = new WebSocket("ws://localhost:3410/center");

ws.onopen = () => ws.send(JSON.stringify({
  action: "subscribe", id: "alerts",
  sql: 'SELECT LIMIT_ID,desk,metric,value,threshold FROM LIMITS WHERE breached == true',
  oof: true
}));

ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === "status" && !msg.connected) { live = false; loading = new Map(); return; }
  if (msg.type === "snapshot") { finishLoad(); return; }
  if (msg.type !== "updates") return;
  for (const u of msg.updates) {
    if (!live) {                                                              // part of a load
      if (u.action === "delete") loading.delete(u.objectId);
      else loading.set(u.objectId, { ...(loading.get(u.objectId) ?? {}), ...u.row });
    }
    else if (u.action === "add")    raiseAlert(u.objectId, u.row);            // new breach
    else if (u.action === "delete") clearAlert(u.objectId);                   // breach resolved
    else updateAlert(u.objectId, u.row);                                      // still breached
  }
};

// A load ends with the snapshot frame. Compare it with what the monitor already holds.
function finishLoad() {
  for (const id of [...alerts.keys()])
    if (!loading.has(id)) clearAlert(id);                                     // cleared while disconnected
  for (const [id, row] of loading) {
    if (loadedOnce && !alerts.has(id)) notify(id, row);                       // breached while disconnected
    alerts.set(id, row);
  }
  loading = new Map(); live = true; loadedOnce = true;
}

function raiseAlert(id, row) { alerts.set(id, row); notify(id, row); }
function updateAlert(id, row) { alerts.set(id, { ...(alerts.get(id) ?? {}), ...row }); }
function clearAlert(id) { alerts.delete(id); /* dismiss */ }
function notify(id, row) { /* sound, banner, badge, or a POST to a paging service */ }

An add before the snapshot frame is a limit that was already breached; an add after it is a new breach. The sample notifies only on the second kind.

The same load happens again if the server's connection to the Center drops. The server sends a status frame with connected:false, then one with connected:true. It then sends every limit that is breached now as an add, followed by a new snapshot frame. A breach that cleared during the outage is not sent as a delete. So the sample collects each load until its snapshot frame and compares it with the alerts it holds:

  • A held alert missing from the reload cleared during the outage, so it is dismissed.
  • A row in the reload that is not held breached during the outage, so it raises a notification.
  • A row that is in both is still breached, so it is kept without a second notification.

Why not oof:false

Without focus filtering the alert list would only ever grow: rows enter at snapshot/add time, but a cleared breach is never removed (no delete), so the monitor would keep showing stale, already-resolved alerts. Membership is the entire point here, so oof:true is required.

Why not just poll REST

You could poll SELECT … WHERE breached == true over the REST API on a timer, but you would trade latency for load: a breach isn't visible until the next poll, and every poll re-runs the query whether anything changed or not. The WebSocket subscription pushes the transition the moment it happens and sends nothing in between.

Notes

  • The condition can be anything the Center can express over the row's columns. value > threshold or a status flag works in the WHERE. The Center re-checks the WHERE only when a column it reads changes, so a time-based condition, such as a staleness check, does not retire an alert by itself: compute it into a column that is updated, and filter on that.
  • One table per subscription. To monitor several conditions, open a subscription per table (or fold them into one LIMITS table with a metric column, as here).