Concepts
Event Broker
Publish events, receive them live on a topic's whole subtree, and catch up from a bounded history. Delivery semantics, ordering and retention.
The Event Broker moves events: things that happened. Publishing one stores it in the topic's recent history and delivers it, live, to everyone subscribed to the topic or to one of its super topics.
Events#
An event has a type you choose and JSON data. Thuum adds an id and a timestamp:
{
"id": "5bac385f-e73a-48c2-a505-8f1f8caa3c85",
"topic": "orders:1042",
"type": "order.shipped",
"data": { "carrier": "DHL", "tracking": "JD014600003SE" },
"ts": 1792590697447
}| Field | Rules |
|---|---|
id | Generated by the server (UUID). Use it to de-duplicate. |
topic | The exact topic it was published on. |
type | 1–64 characters of a-z A-Z 0-9 _ . : -. Dotted names (order.shipped) read well. |
data | Any JSON value except null: object, array, string, number, boolean. At most 64 KiB as JSON. |
ts | When the server received it, in epoch milliseconds. |
Events are immutable: there is no update or per-event delete. History can be purged in bulk.
Publish#
Publish over the WebSocket (pub, needs P) from a connected client, or over REST
(POST /api/{domain}/event/data/{topic}) from anywhere:
// WebSocket
const { id, ts } = await socket.pub('orders:1042', 'order.shipped', { carrier: 'DHL' });
// REST
const event = await acme.eventBroker.data.publish('orders:1042', {
type: 'order.shipped',
data: { carrier: 'DHL' }
});curl -X POST https://api.thuum.dev/api/acme/event/data/orders:1042 \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"type":"order.shipped","data":{"carrier":"DHL"}}'A successful reply means the event was stored and broadcast. It doesn't mean anyone received it: there may be no subscribers.
Subscribe#
sub (needs S) delivers every event published on the topic and on all its sub-topics, from now
on. A socket subscribed to orders receives orders:1042's events, each with its exact topic.
socket.onMessage((event) => {
console.log(event.topic, event.type, event.data);
});
await socket.sub('orders');Delivery guarantees#
Know these before you design on top of the Event Broker:
- Live delivery is best-effort. A subscriber that is disconnected, or not yet subscribed, misses events published meanwhile. Nothing is queued per subscriber. History is the recovery path: read it after (re)subscribing.
- Everyone subscribed receives it, publisher included. If the publishing connection is subscribed to the topic, it receives its own event. Many apps show a message only when it comes back, so every screen shows the same order.
- Once per connection. Overlapping subscriptions (
ordersandorders:1042) still deliver an event once. - Ordering. Events from one publisher on one topic arrive in the order they were published.
There is no global order across publishers; sort by
ts(andidfor ties) when it matters. - Permissions are fixed at connect. A claim change applies to new connections.
History#
Every Main Topic keeps a window of recent events: a maximum count and/or a maximum age. Read it
with GET /api/{domain}/event/data/{topic} (needs R): newest first, covering the topic's subtree.
const recent = await acme.eventBroker.data.list('chat:lobby', { limit: 50 });
const sinceYesterday = await acme.eventBroker.data.list('orders', {
type: 'order.shipped',
since: Date.now() - 86_400_000
});| Filter | Meaning |
|---|---|
type | Only events of this type. |
since | Epoch ms, inclusive. |
until | Epoch ms, exclusive. Page backwards with the oldest ts. |
limit | Default 100, at most 1000. |
The standard catch-up pattern: subscribe first, then read the history, and merge by id.
Anything published in between arrives through one path or both, never through neither. See
Build reliable clients.
Retention#
Without a config, a Main Topic's family keeps its newest 1000 events, for at most one hour.
Set your own window per Main Topic (needs the claim's topic flag W):
await acme.eventBroker.topics.put('chat', { max_events: 50_000, ttl_seconds: 7 * 86_400 });
await acme.eventBroker.topics.put('audit', { max_events: null, ttl_seconds: 30 * 86_400 });- An event is kept while it satisfies both bounds;
nullmeans unbounded. - The count is shared by the whole family:
max_events: 50000onchatis 50 000 across every room. - Expired events are removed by a background sweep, about once a minute, without notifying anyone.
- A config never blocks publishing.
To delete history on purpose, purge it (DELETE …/event/data/{topic}, needs W), optionally
filtered by type, since and until. Purging covers the subtree and notifies no one.
Try it#
When to use the State Broker instead#
If subscribers only care about the latest value (a position, a score, a status), use a state: late joiners read the current value directly instead of replaying events, and old values don't fill your history. Many apps use both: the State Broker for "how things are", the Event Broker for "what just happened". See State Broker.