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#

OpFieldsFlagReply
subtopic, ref?Ssub:ok
unsubtopic, ref?—unsub:ok
settopic, key, value, ref?Pset:ok with the state
mergetopic, key, value, ref?Pmerge:ok with the state
deltopic, key, ref?Pdel:ok
echotopic, key, ref?Pecho:ok with the state
telltopic, key, ref?Rtell: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).

JSON
→ [{ "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).

JSON
→ [{ "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.

JSON
→ [{ "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.

JSON
→ [{ "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.

JSON
→ [{ "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.

JSON
→ [{ "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):

JSON
← [{ "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#

OpPossible codes
sub, unsubbad_request (invalid topic), forbidden (sub only)
set, mergebad_request (topic, key, value, size), forbidden, internal
delbad_request, forbidden, internal
tell, echobad_request, forbidden, not_found, internal
JSON
→ [{ "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#

TypeScript
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.