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:

TypeScript
const events = acme.eventBroker.connect();
const states = acme.stateBroker.connect();

Or directly, with a token your server gave the browser:

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

TypeScript
socket.onMessage(handleEvent); // register listeners before awaiting, so nothing is missed
await socket.ready;

EventBrokerSocket#

MemberDescription
ready: Promise<void>Resolves when open; rejects if the connection closes first.
open: booleanWhether 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: stringThe URL it connected to (token included: don't log it).
TypeScript
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#

MemberDescription
ready, open, close(), url, onFrame, onCloseAs 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.
TypeScript
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:

TypeScript
const stop = socket.onMessage(handle);
// later
stop();

Errors#

A failed operation rejects with EventBrokerSocketError or StateBrokerSocketError:

codeMeaning
forbiddenThe key's claim lacks the flag on this topic.
bad_requestInvalid topic, type, key or value (message explains).
not_foundtell or echo of a missing key (State Broker).
internalServer error; the op was not applied.
closedThe connection wasn't open, or closed before the reply arrived.
TypeScript
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:

TypeScript
socket.onFrame((direction, op) => console.debug(direction === 'out' ? '→' : '←', op));

The Playground's Wire log is built exactly this way.