Client library
Sockets
EventBrokerSocket and StateBrokerSocket — connecting, promise-based operations, listeners, errors and reconnection.
Both sockets wrap one WebSocket each. Every operation returns a promise that settles with the
server's reply, matched by a ref the socket adds for you.
Opening a socket#
From a domain client, with its token:
const events = acme.eventBroker.connect();
const states = acme.stateBroker.connect();Or directly, with a token your server gave the browser:
import { EventBrokerSocket, StateBrokerSocket } from '@thuum/client';
const grant = { baseUrl: 'https://api.thuum.dev', domain: 'acme', token };
const events = new EventBrokerSocket(grant);
const states = new StateBrokerSocket(grant);Then wait for ready. It rejects (with code closed) if the handshake is refused: invalid or
expired token, or no claim for this broker.
socket.onMessage(handleEvent); // register listeners before awaiting, so nothing is missed
await socket.ready;EventBrokerSocket#
| Member | Description |
|---|---|
ready: Promise<void> | Resolves when open; rejects if the connection closes first. |
open: boolean | Whether the connection is open now. |
sub(topic) | Subscribe to a topic's subtree. Needs S. |
unsub(topic) | Unsubscribe. |
pub(topic, type, data) | Publish. Resolves with { id, ts }. Needs P. |
onMessage(listener) | Each delivered event: { id, topic, type, data, ts }. |
onFrame(listener) | Every op in both directions: (direction: 'in' | 'out', op) => void. |
onClose(listener) | { code, reason, wasClean } when the connection closes. |
close() | Close normally (code 1000). |
url: string | The URL it connected to (token included: don't log it). |
const socket = acme.eventBroker.connect();
socket.onMessage((event) => {
if (event.topic === 'chat:lobby' && event.type === 'message') addMessage(event);
});
await socket.ready;
await socket.sub('chat:lobby');
const { id } = await socket.pub('chat:lobby', 'message', { text: 'Hi!' });StateBrokerSocket#
| Member | Description |
|---|---|
ready, open, close(), url, onFrame, onClose | As above. |
sub(topic), unsub(topic) | Change notifications for a topic's subtree. Needs S. |
set(topic, key, value) | Create or replace. Resolves with { value, version, ts }. Needs P. |
merge(topic, key, patch) | JSON Merge Patch. Resolves with the new { value, version, ts }. Needs P. |
del(topic, key) | Delete. Needs P. |
echo(topic, key) | Re-broadcast unchanged. Resolves with the state. Needs P. |
tell(topic, key) | The current value, to this connection. Needs R. |
onChange(listener) | Each change: { topic, key, value, version, ts }; value and version are null for a delete. |
const socket = acme.stateBroker.connect();
socket.onChange(({ topic, key, value, version }) => apply(topic, key, value, version));
await socket.ready;
await socket.sub('board:42');
const { version } = await socket.merge('board:42', 'item:7', { column: 'done' });Listeners#
Each on… method returns a function that removes the listener:
const stop = socket.onMessage(handle);
// later
stop();Errors#
A failed operation rejects with EventBrokerSocketError or StateBrokerSocketError:
code | Meaning |
|---|---|
forbidden | The key's claim lacks the flag on this topic. |
bad_request | Invalid topic, type, key or value (message explains). |
not_found | tell or echo of a missing key (State Broker). |
internal | Server error; the op was not applied. |
closed | The connection wasn't open, or closed before the reply arrived. |
import { StateBrokerSocketError } from '@thuum/client';
try {
return await socket.tell('board:42', 'meta');
} catch (e) {
if (e instanceof StateBrokerSocketError && e.code === 'not_found') return null;
throw e;
}Reconnecting#
A socket is one connection: once closed, it stays closed. To reconnect, create a new socket with a new token, register the listeners again, and subscribe again. Wrap it in a small class that owns the current socket; Build reliable clients has a complete one.
Debugging#
onFrame sees every op in both directions, which makes a wire log a one-liner:
socket.onFrame((direction, op) => console.debug(direction === 'out' ? '→' : '←', op));The Playground's Wire log is built exactly this way.