Skip to content
Connect > Data APIs > Use Cases

Live quote ticker in the browser

Goal: a lightweight top-of-book ticker (bid / ask / last per symbol) that lives outside the Web UI in your own web page, updating in place as the market moves.

This is the canonical WebSocket example: subscribe to a table, build the initial state, then apply live updates.

Prerequisites

  • The WebSocket server is enabled (ami.websocket.port=3410): see Configuration.
  • A QUOTES table (SYM, bid, ask, last) with rows updating; the Quickstart creates it.

The client

The pattern is: keep a map keyed by objectId, apply each update, and re-render. On subscribe you receive a status frame and a subscribed ack, in either order, then the current rows as add updates, then a snapshot marker; after that you receive only changes. (objectId is a row id, not the SYM: use it as your map key, but don't try to look rows up by symbol.) See WebSocket API → First messages on connect.

<table id="tape"><thead>
  <tr><th>Symbol</th><th>Bid</th><th>Ask</th><th>Last</th></tr>
</thead><tbody></tbody></table>

<script>
  const rows = new Map();               // objectId -> row object
  const tbody = document.querySelector("#tape tbody");

  function render() {
    tbody.innerHTML = "";
    // Sort by a real field. The snapshot arrives in arbitrary row order, and objectId
    // is not the symbol, so sort by SYM for a stable display.
    const sorted = [...rows.values()].sort((a, b) => (a.SYM ?? "").localeCompare(b.SYM ?? ""));
    for (const r of sorted) {
      const tr = tbody.insertRow();
      tr.innerHTML = `<td>${r.SYM ?? ""}</td><td>${r.bid ?? ""}</td>` +
                     `<td>${r.ask ?? ""}</td><td>${r.last ?? ""}</td>`;
    }
  }

  const ws = new WebSocket("ws://localhost:3410/center");

  ws.onopen = () => ws.send(JSON.stringify({
    action: "subscribe", id: "quotes", sql: "SELECT SYM,bid,ask,last FROM QUOTES"
  }));

  ws.onmessage = (e) => {
    const msg = JSON.parse(e.data);
    if (msg.type === "updates") {
      for (const u of msg.updates) {
        if (u.action === "delete") {
          rows.delete(u.objectId);
        } else {
          // add or update: merge the changed fields onto the existing row
          const cur = rows.get(u.objectId) ?? {};
          rows.set(u.objectId, { ...cur, ...u.row });
        }
      }
      render();
    } else if (msg.type === "error") {
      console.error("subscription error:", msg.message);
    }
  };
</script>

Why merge instead of replace

An update frame carries only the changed columns, not the whole row. Merging the update's row onto the row you already hold ({ ...cur, ...u.row }) is what keeps SYM and the unchanged prices intact when only bid changes. Replacing the row wholesale would blank the untouched fields.

What you will see

  • On connect: a status frame and a subscribed ack, in either order, then an updates frame carrying one add per existing symbol, then a snapshot marker: the ticker is populated.
  • Continuously: updates frames as prices change, coalesced to roughly one flush per 100 ms (see conflation). Fast markets are smoothed, not dropped mid-render.

Notes

  • Authentication. If the server requires it, connect as ws://user:pass@localhost:3410/center (browsers can't set an auth header on the handshake). The browser sends these credentials after the server challenges the first attempt with 401.
  • Filter server-side. To watch a subset, add a WHERE in AMI syntax: SELECT SYM,bid,ask,last FROM QUOTES WHERE SYM == "IBM". To keep a filtered set accurate as membership changes, see focus filtering.
  • Reconnects. If the socket drops, reopen it and re-subscribe; you'll get a fresh snapshot.
  • First-connect warm-up. The very first connection after a server restart can take ~2 seconds before the status frame; warm connections are near-instant. Give the initial connect a generous timeout instead of reconnect-looping on a short one (see WebSocket streaming API → First-connection warm-up).