WebSocket API
State Broker WebSocket
The sub, unsub, set, merge, del, echo and tell operations, change notifications, and every reply and error, with frames.
Endpoint: wss://api.thuum.dev/api/{domain}/state/connect?token=…. The connection needs a key linked to
a State Broker claim. Framing, refs and general errors are on WebSocket protocol.
Operations#
| Op | Fields | Flag | Reply |
|---|---|---|---|
sub | topic, ref? | S | sub:ok |
unsub | topic, ref? | — | unsub:ok |
set | topic, key, value, ref? | P | set:ok with the state |
merge | topic, key, value, ref? | P | merge:ok with the state |
del | topic, key, ref? | P | del:ok |
echo | topic, key, ref? | P | echo:ok with the state |
tell | topic, key, ref? | R | tell:ok with the state |
"With the state" means the reply carries value, version and ts (the state's last write, epoch
ms), next to the echoed topic, key and ref. The server pushes one more op, change, for every
change on subscribed topics.
sub and unsub#
Subscribe to changes on a topic and its whole subtree, or stop. Both are idempotent. A subscription
sends no current values: follow it with tell (one key) or a REST listing (many keys).
→ [{ "op": "sub", "topic": "board:42", "ref": "1" }]
← [{ "op": "sub:ok", "topic": "board:42", "ref": "1" }]set#
Create or replace a value (any JSON except null).
→ [{ "op": "set", "topic": "board:42", "key": "item:7", "value": { "title": "Fix login", "column": "todo" }, "ref": "2" }]
← [{ "op": "set:ok", "topic": "board:42", "key": "item:7", "ref": "2", "value": { "title": "Fix login", "column": "todo" }, "version": 1, "ts": 1792591000000 }]merge#
Apply a JSON Merge Patch (an object): members merge recursively, null removes a member. Creates the
key if it is missing. Applied atomically.
→ [{ "op": "merge", "topic": "board:42", "key": "item:7", "value": { "column": "doing", "assignee": "ana" }, "ref": "3" }]
← [{ "op": "merge:ok", "topic": "board:42", "key": "item:7", "ref": "3", "value": { "title": "Fix login", "column": "doing", "assignee": "ana" }, "version": 2, "ts": 1792591003000 }]del#
Delete a key. Idempotent: deleting a missing key succeeds, and notifies no one.
→ [{ "op": "del", "topic": "board:42", "key": "item:7", "ref": "4" }]
← [{ "op": "del:ok", "topic": "board:42", "key": "item:7", "ref": "4" }]tell#
The current value, to this connection only. Nobody else is notified.
→ [{ "op": "tell", "topic": "board:42", "key": "item:7", "ref": "5" }]
← [{ "op": "tell:ok", "topic": "board:42", "key": "item:7", "ref": "5", "value": { "title": "Fix login", "column": "doing", "assignee": "ana" }, "version": 2, "ts": 1792591003000 }]On a missing key: tell:error with code not_found.
echo#
Send the current value to every subscriber again, unchanged: same version, same ts. Useful to
re-announce a state after a client reconnects, without writing it.
→ [{ "op": "echo", "topic": "board:42", "key": "item:7", "ref": "6" }]
← [{ "op": "echo:ok", "topic": "board:42", "key": "item:7", "ref": "6", "value": { … }, "version": 2, "ts": 1792591003000 }]On a missing key: echo:error with code not_found.
change#
One frame per change on a subscribed topic or its subtree, whichever route made it (WebSocket or REST):
← [{ "op": "change", "topic": "board:42", "key": "item:7", "value": { "title": "Fix login", "column": "doing", "assignee": "ana" }, "version": 2, "ts": 1792591003000 }]
← [{ "op": "change", "topic": "board:42", "key": "item:7", "value": null, "version": null, "ts": 1792591010000 }]The second one is a delete: value and version are null. Drop any change whose version is lower
than the last one you saw for that key; it arrived out of order. Expiry by TTL or size never sends a
change.
Errors#
| Op | Possible codes |
|---|---|
sub, unsub | bad_request (invalid topic), forbidden (sub only) |
set, merge | bad_request (topic, key, value, size), forbidden, internal |
del | bad_request, forbidden, internal |
tell, echo | bad_request, forbidden, not_found, internal |
→ [{ "op": "merge", "topic": "board:42", "key": "item:7", "value": [1, 2], "ref": "7" }]
← [{ "op": "merge:error", "topic": "board:42", "key": "item:7", "ref": "7", "code": "bad_request", "message": "…" }]With the client#
const socket = acme.stateBroker.connect();
socket.onChange(({ topic, key, value, version }) => render(topic, key, value, version));
await socket.ready;
await socket.sub('board:42');
const { version } = await socket.set('board:42', 'item:7', { title: 'Fix login', column: 'todo' });
await socket.merge('board:42', 'item:7', { column: 'doing' });
const current = await socket.tell('board:42', 'item:7');
await socket.del('board:42', 'item:7');Failed ops reject with StateBrokerSocketError (code: one of the codes above, or closed). See
Sockets.