Octopus
Octopus Developersgateway.octopusoperations.co.za/v1

Tutorial · about 25 minutes

Build a live board

A display that shows today's tasks and keeps itself current, without polling, and without going stale after the Wi-Fi drops.

The shape of it

Snapshot, then deltas, and repeat after a reconnect
Drawing…

The dotted line is the part people leave out. While you were disconnected, things changed and nobody told you, so a reconnect must re-load the snapshot, not just resume listening. A board that silently drifts out of date is worse than one that is obviously broken.

Step 1: connect and take a snapshot

board.js
import { io } from "socket.io-client";

const socket = io("https://gateway.octopusoperations.co.za/v1", {
  transports: ["websocket"],
  auth: { apiKey: process.env.OCTOPUS_API_KEY },
});

const tasks = new Map();

function call(event, payload = {}) {
  return new Promise((resolve, reject) => {
    socket.once(`${event}Success`, resolve);
    socket.once(`${event}Error`, (e) => reject(new Error(e.message)));
    socket.emit(event, payload);
  });
}

async function loadSnapshot() {
  const result = await call("listTasks");
  tasks.clear();
  for (const task of result.populatedItem) tasks.set(task.id, task);
  render();
}

Step 2: load on connect, and on every reconnect

Socket.IO reconnects for you. Hanging the snapshot off the connect event means it runs the first time and every time after, which is exactly what you want, and one line rather than two code paths.

javascript
socket.on("connect", () => {
  loadSnapshot().catch((err) => console.error("snapshot failed:", err.message));
});

socket.on("connect_error", (err) => {
  // A bad or revoked key lands here, not on "connect".
  console.error("cannot connect:", err.message);
});

Do not hang your snapshot off ready

ready is pushed the instant the connection completes. If you attach that handler after awaiting connection, the packet has already been delivered to a socket with no listener and nothing replays it. Use connect, or ask for describe explicitly.

Step 3: apply changes as they arrive

javascript
socket.on("change", async (change) => {
  if (change.resource !== "tasks") return;

  if (change.action === "deleted") {
    tasks.delete(change.id);
    return render();
  }

  // The change says WHAT moved, not the whole record — fetch the fresh one.
  try {
    const { populatedItem } = await call("getTask", { taskId: change.recordId ?? change.id });
    tasks.set(populatedItem.id, populatedItem);
    render();
  } catch (err) {
    console.error("could not refresh", change.id, err.message);
  }
});

You only hear what you may see

Change events are delivered per .read scope. A key with only tasks.read hears task changes and nothing else, so you do not have to filter for safety, only for relevance.

Step 4: make the failure visible

A wall display nobody is watching will happily show yesterday. Put the connection state on screen, so a stale board is obviously stale:

javascript
let connected = false;
socket.on("connect", () => { connected = true; render(); });
socket.on("disconnect", () => { connected = false; render(); });

function render() {
  statusEl.textContent = connected ? "Live" : "Reconnecting…";
  statusEl.dataset.state = connected ? "live" : "stale";
  // ...draw the tasks
}

What you have built

  • A snapshot on connect, and again on every reconnect.
  • Row-level updates, with no polling at all.
  • A visible state, so nobody trusts a frozen screen.