rusty_pubsub

durable · at-least-once · websocket
idle
A · B · C · System — multi-topic routing + progressive unregister

    

How rusty_pubsub works

rusty_pubsub is a durable publish/subscribe broker. Clients connect over a single WebSocket, then subscribe to topics and publish messages. Every message is appended to a per-topic durable log and delivered at‑least‑once: each subscription has its own cursor, the server redelivers anything un‑acked, and cursors survive a restart.

0 · Namespaces — the isolation boundary

Every token carries an ns claim (the namespace = tenant). All topics, messages, and subscriptions are scoped to it: a client can only see and touch topics in its own namespace. The same topic name in two namespaces is two different topics (different ids) — and a client in one namespace cannot subscribe to another namespace's topic even if it knows the exact id (the server rejects it). You pick the namespace when you mint the token; that's the whole isolation story.

// two tenants, fully isolated — same topic name, different worlds
const demo  = jwt({ ns: "demo",  sub: "svc" });
const other = jwt({ ns: "other", sub: "svc" });
// "topic-system" in `demo` and in `other` are distinct topics;
// a publish in `demo` is never delivered to a subscriber in `other`.

1 · Topics are addressed by id (no central registry)

A topic has a stable id (UUID) and a human name, scoped to your namespace (the ns claim in your JWT — the isolation boundary). There is no discovery service: producers and consumers simply agree on a topic out of band. A client either already knows the id, or resolves it once from the name and remembers it.

# register / resolve a topic (REST) — returns its id
curl -XPOST $URL/v1/topics -H "authorization: Bearer $JWT" \
     -d '{"name":"topic-1"}'
# → { "id": "8f3a…", "name": "topic-1", "ns": "demo", ... }

2 · Connect + subscribe to a known topic

Open a WebSocket with your token, then send a subscribe frame naming the topic id and a stable sub_name (your durable cursor key). The server replays anything you haven't seen, then streams live.

const ws = new WebSocket(`${WS_URL}?token=${JWT}`);
ws.onopen = () => ws.send(JSON.stringify({
  type: "subscribe", topic: topicId, sub_name: "A", from_seq: 0
}));

3 · Publish

Publish on the same socket (or over REST). The broker assigns a monotonic seq and acknowledges with pub_ack.

ws.send(JSON.stringify({ type: "publish", topic: topicId, payload: "hello" }));
// ← { "type": "pub_ack", "topic": topicId, "seq": 42 }

4 · Receive payloads + ack

Subscribed messages arrive as message frames. Process the payload, then ack through that seq to advance your durable cursor (so it isn't redelivered).

ws.onmessage = (e) => {
  const f = JSON.parse(e.data);
  if (f.type === "message") {
    handle(f.payload);                         // ← your payload
    ws.send(JSON.stringify({ type: "ack",
      topic: f.topic, sub_name: "A", seq: f.seq }));
  }
};

5 · Unregister

A client leaves by disconnecting and deleting its subscription (DELETE /v1/subscriptions/{id}). After that, publishes to the topic are no longer routed to it — exactly what the scenario on the first tab demonstrates as clients drop out one by one.

Delivery guarantees

  • Ordered per topic (gap-free seq).
  • At-least-once: un-acked messages are redelivered on reconnect.
  • Durable: the log + every cursor survive a broker restart.
  • Backpressure: a slow subscriber lags (bounded in-flight window) — it never silently drops.
  • Isolated: a namespace can only see its own topics.