Live data: WebSockets, events, webhooks, SignalR

Data that keeps arriving: a WebSocket, a stream of server events, a webhook, a SignalR hub. Each one is a LiveFeed: the connection lives on its own thread, messages wait in a queue, and Draw() takes them when it's ready -- it never waits for the network. Feeds reconnect by themselves when the connection drops and close when the sketch stops.

websocketsseserver-sent eventswebhooksignalrstreamliverealtimefeed

On this page · 6 entries

class LiveFeed<T> · feed.Next(out msg) · feed.ReadAll(max)

What every feed has. Next(out msg) takes the oldest waiting message (false when there's none); ReadAll() takes them all, oldest first -- or at most max, leaving the rest for the next frame. State (Connecting, Open, Reconnecting, Closed, Failed), Connected, Error, a ready-made Status line, Received, PerSecond and Pending. A firehose can't eat the memory: past Capacity (10,000) waiting messages, the oldest are dropped and counted in Dropped. Close() ends it.

ConnectWebSocket

  • ConnectWebSocket(url, reconnect) → WebSocketFeed

A WebSocket (ws:// or wss://): each text message arrives as a string -- usually JSON, for JSONObject.Parse(). feed.Send(text) sends one back (or a JSONObject); sent at once when connected, otherwise as soon as it is. Exchanges' trade streams, Bluesky's Jetstream, live chats, your own servers.

ListenEvents

  • ListenEvents(url, reconnect) → LiveFeed<ServerEvent>

A stream of server-sent events (SSE, text/event-stream): Wikipedia's live edits, Firebase, .NET 10's TypedResults.ServerSentEvents. Each ServerEvent has its Type ("message" unless the server names it), Data (multi-line data joined), Id, and JSON(). When it reconnects it tells the server the last id it saw, so nothing is missed, and it honors the server's retry:.

ListenWebhook

  • ListenWebhook(port, path) → LiveFeed<WebhookCall>

A small web server on your computer, at http://localhost:port/path/: every request it gets becomes a WebhookCall -- Method, Path, Query, Headers, Body and JSON() -- and is answered 200 OK. Point GitHub, Stripe or ntfy at it through a tunnel (smee.io, ngrok) and every star, payment or message lands in the sketch.

ConnectSignalR

  • ConnectSignalR(hubUrl, reconnect) → SignalRFeed

An ASP.NET Core SignalR hub (http(s)://host/hub), spoken directly -- SignalR's JSON protocol over a WebSocket, no packages. Whatever the hub sends to its clients (Clients.All.SendAsync("Target", args...)) arrives as a HubMessage: its Target and Arguments, with Object(i), Text(i) and Number(i). hub.Invoke("Method", args...) calls a method on the hub. It handshakes, keeps itself alive with pings and reconnects by itself.

Runnable example

A complete sketch using this section. Test in Dana opens it in the IDE, or paste it into a new tab and press Run.

C#
// Wikipedia, live: every edit made anywhere, as it happens (Wikimedia's public
// event stream, no key). Each edit falls as its page title; the bigger the change,
// the bigger the word. New pages are green, edits by bots are dim.
public class WikiLive : Sketch
{
    LiveFeed<ServerEvent> edits;
    readonly List<(string Title, float X, float Y, float Size, Color C)> words = new();

    public override void Setup()
    {
        Size(900, 560);
        edits = ListenEvents("https://stream.wikimedia.org/v2/stream/recentchange");
    }

    public override void Draw()
    {
        Background(12, 14, 22);
        // Everything that arrived since the last frame (at most 40, the rest wait).
        foreach (var e in edits.ReadAll(40))
        {
            var edit = e.JSON();
            if (edit.GetString("type") is not ("edit" or "new")) continue;
            var length = edit.GetJSONObject("length");
            float change = length == null ? 0 : Abs(length.GetFloat("new") - length.GetFloat("old"));
            var c = edit.GetString("type") == "new" ? Color(120, 230, 150)
                  : edit.GetBoolean("bot") ? Color(110, 120, 150) : Color(230, 235, 255);
            words.Add((edit.GetString("title"), Random(40, Width - 40), -10, Constrain(9 + Sqrt(change), 9, 48), c));
        }
        TextAlign(TextAlignH.Center, TextAlignV.Center);
        for (int i = words.Count - 1; i >= 0; i--)
        {
            var w = words[i];
            w.Y += 1.2f + w.Size * 0.03f;
            if (w.Y > Height + 20) { words.RemoveAt(i); continue; }
            words[i] = w;
            Fill(w.C);
            TextSize(w.Size);
            Text(w.Title, w.X, w.Y);
        }
        Fill(255, 255, 255, 140);
        TextAlign(TextAlignH.Left, TextAlignV.Top);
        TextSize(13);
        Text($"Wikipedia, live · {edits.Status} · {edits.Received:N0} events", 16, 14);
    }
}