Skip to main content

WebSocket Communication

The HTTP control API is request/response: you ask, the page answers. WebSocket adds the other direction — the page pushes bridge events as they happen, so a driver can react to a modal opening or a workflow step completing instead of polling for it.

WebSocket is additive, not an alternative transport. The same handler set serves both, and turning WebSocket on does not turn the HTTP routes off.

Server Side​

StandaloneServer speaks both. Build a handler set, then start the server with websocket: true:

import { getGlobalRegistry, createActionExecutor } from '@qontinui/ui-bridge';
import { createHandlers } from '@qontinui/ui-bridge-server';
import { createStandaloneServer } from '@qontinui/ui-bridge-server/standalone';

const registry = getGlobalRegistry();
const handlers = createHandlers(registry, createActionExecutor(registry));

const server = await createStandaloneServer(handlers, {
host: 'localhost',
port: 9876,
websocket: true,
websocketPort: 9876, // defaults to `port`
});

console.log(server.getAddress()); // { host: 'localhost', port: 9876 }

createStandaloneServer starts the server before it resolves — there is no separate server.start() call to make after it. Use new StandaloneServer(…) directly if you want to construct and start in separate steps.

The server can push to every connected client:

server.broadcastEvent({ type: 'element:registered', data: { elementId: 'save-btn' }, timestamp: Date.now() });
server.broadcast({ type: 'event', channel: 'custom', data: { cartTotal: 42 }, timestamp: Date.now() });

await server.stop();

broadcastEvent takes a BridgeEvent; broadcast takes a raw WebSocketMessage whose type is one of subscribe, unsubscribe, event, snapshot, action or error.

Mounting on Your Own Server​

If you already run a ws (or compatible) server, use UIBridgeWSHandler directly instead of StandaloneServer. It has no transport of its own — you hand it socket objects:

import { UIBridgeWSHandler } from '@qontinui/ui-bridge-server';

const wsHandler = new UIBridgeWSHandler(handlers, { verbose: true });

wss.on('connection', (ws, req) => {
const tabId = new URL(req.url!, 'http://localhost').searchParams.get('tabId') ?? undefined;
const clientId = wsHandler.handleConnection(ws, tabId);

ws.on('close', () => wsHandler.handleDisconnect(clientId, ws));
});

Passing the client's own stable id as preferredId is what keeps a browser tab identified across reconnects rather than minting a new id each time. wsHandler.broadcastEvent(event) fans an event out to subscribers, and wsHandler.disconnectAll() closes every socket.

A running StandaloneServer exposes its handler via server.getWSHandler().

Client Side​

import { createWSClient } from '@qontinui/ui-bridge';

const client = createWSClient({
url: 'ws://localhost:9876',
tabId: 'my-tab',
autoReconnect: true,
reconnectDelay: 1000,
maxReconnectAttempts: 10, // 0 = infinite
pingInterval: 30000, // 0 = disabled
connectionTimeout: 10000,
});

await client.connect();

Those seven fields are the entire WSClientConfig. There is no transport option (the client is WebSocket-only), no auth block and no fallback block: authentication belongs to whatever fronts the socket, and falling back to HTTP means calling the HTTP routes, which are always there. new UIBridgeWSClient(config) is the class form of createWSClient.

Subscribing to Events​

Subscribe by event type, and optionally narrow to specific elements or components:

await client.subscribe({
events: ['element:stateChanged', 'action:completed', 'navigation:change'],
elementIds: ['cart-total'],
});

const unsubscribe = client.onEvent('element:stateChanged', (event) => {
console.log(event.type, event.data);
});

client.onEvent('*', (event) => console.log('any event', event.type));

await client.unsubscribe(['navigation:change']); // or unsubscribe() for all

BridgeEventType covers element lifecycle (element:registered, element:unregistered, element:stateChanged), components (component:registered, component:unregistered), actions (action:started, action:completed, action:failed), workflows (workflow:started, workflow:stepCompleted, workflow:completed, workflow:failed), idle detection (app:busy/app:idle, network:*, dom:mutating/dom:settled, loading:*, form:*), navigation:change, toasts (toast:appeared, toast:dismissed), captured browser events (browser:error, browser:warning, browser:crash), render:snapshot and error.

Requests Over the Socket​

The client is not read-only — the same operations the HTTP API offers are available as awaited requests:

const snapshot = await client.getSnapshot();
const elements = await client.find({ interactiveOnly: true, includeState: true });
const element = await client.getElement('save-btn');

await client.executeAction('save-btn', { action: 'click' });
await client.executeComponentAction('login-form', 'login', { email, password });

Workflows can stream their progress, which is the main thing the socket buys you over HTTP:

const result = await client.executeWorkflow('checkout', { coupon: 'SAVE10' }, (progress) => {
console.log(`${progress.currentStep}/${progress.totalSteps}: ${progress.step.id} ${progress.step.status}`);
});

Connection Management​

Reconnection is built in and on by default. Observe it rather than reimplementing it:

client.onConnectionChange((state) => {
// 'connecting' | 'connected' | 'disconnected' | 'reconnecting'
console.log('ws', state);
});

client.onError((err) => console.error(err));

client.reconnectNow(); // force an immediate reconnect attempt
client.disconnect(); // stop, and stop auto-reconnecting

reconnectDelay backs off between attempts up to maxReconnectAttempts (0 meaning "keep trying"), and pingInterval keeps an idle connection alive. Subscriptions are re-applied on reconnect, so listeners registered with onEvent keep working across a drop.

In React​

UIBridgeProvider creates and manages the client when the WebSocket config is set — you do not construct one yourself:

import { UIBridgeProvider, useUIBridgeContext } from '@qontinui/ui-bridge';

function App({ children }: { children: React.ReactNode }) {
return (
<UIBridgeProvider config={{ websocket: true, websocketPort: 9876 }}>
{children}
</UIBridgeProvider>
);
}

function ConnectionBadge() {
const { wsClient, wsConnectionState } = useUIBridgeContext();
return <span data-connected={wsConnectionState === 'connected'}>{wsConnectionState}</span>;
}

wsClient is undefined when WebSocket is not enabled, so guard on it before calling into it.