Skip to content

Client API

New here? Start with the Developer Guide for integration-first setup, then use Tutorials for the full step-by-step build. For all constructor flags in one place, see Configuration Reference.

DatasoleClient

The main client class. Framework-agnostic — works with React, Vue 3, Svelte, React Native, or vanilla JS.

Constructor

typescript
const client = new DatasoleClient({
  url: 'wss://example.com', // Server URL (ws:// or wss://)
  path: '/__ds', // WebSocket path (default: /__ds)
  auth: {
    token: 'jwt-token', // Sent as ?token= query parameter on the WebSocket URL
  },
  useWorker: true, // Run WebSocket in Web Worker (default: true)
  workerUrl: '/__ds/datasole-worker.iife.min.js', // Worker script URL (default: `${path}/datasole-worker.iife.min.js`)
  useSharedArrayBuffer: false, // Zero-copy via SAB when available (default: false)
  reconnect: true, // Auto-reconnect on disconnect (default: true)
  reconnectInterval: 1000, // Base delay in ms between attempts (default: 1000)
  maxReconnectAttempts: 10, // Give up after N attempts (default: 10)
});

Note: Only auth.token is supported. It is sent as a query parameter (?token=…) because the browser WebSocket API does not allow custom headers during the upgrade handshake.

Connection

typescript
client.connect(); // Establish WebSocket connection
client.disconnect(); // Close connection
const state = client.getConnectionState(); // 'disconnected' | 'connecting' | 'connected' | 'reconnecting'

RPC — Call the Server

typescript
// Contract-first (recommended): use enum members from your shared AppContract as the method key
const user = await client.rpc(RpcMethod.GetUser, { userId: '123' });
console.log(user.name);

// With timeout
const result = await client.rpc(RpcMethod.SlowQuery, { q: 'test' }, { timeout: 10000 });

// Multiple calls in flight simultaneously — they're multiplexed over one WebSocket
const [a, b, c] = await Promise.all([
  client.rpc(RpcMethod.GetUser, { id: '1' }),
  client.rpc(RpcMethod.GetUser, { id: '2' }),
  client.rpc(RpcMethod.GetUser, { id: '3' }),
]);

Tutorial: RPC — Call the Server, Get a Response

Events — Send and Receive

typescript
// Subscribe to server-pushed events (use contract event enum members as names)
client.on(Event.Price, ({ data }) => {
  console.log(`${data.symbol}: $${data.price}`);
});

// Unsubscribe
client.off(Event.Price, handler);

// Send event to server (typically a client-originated name like Event.ChatSend)
client.emit(Event.ChatSend, { text: 'hello', username: 'alice' });

Tutorial: Server Events — A Live Stock Ticker

Live State — Server-Synced Data

typescript
// Subscribe to a state key — callback fires on every JSON Patch update
client.subscribeState(StateKey.Dashboard, (state) => {
  // state is the full, patched object — ready to render
  console.log(state.visitors, state.activeNow);
});

// Get current snapshot (synchronous)
const current = client.getState(StateKey.Dashboard);

Tutorial: Live State — A Server-Synced Dashboard — the most important pattern for most apps

CRDTs — Bidirectional Sync

typescript
import { CrdtStore } from 'datasole/client';
import { PNCounter, LWWMap } from 'datasole';
import { Event } from './shared/contract';

const store = new CrdtStore('unique-client-id');

// Register CRDT instances
const counter = store.register('votes', 'pn-counter');
const doc = store.register<string>('document', 'lww-map');

// Local mutation → immediate local update
const op = counter.increment();

// Send op to server
client.emit(Event.CrdtOp, op);

// Apply remote state
client.on(Event.CrdtState, ({ data }) => {
  store.mergeRemoteState('votes', data);
  console.log('Counter:', counter.value());
});

Tutorial: Bidirectional CRDT — A Shared Counter

Full Method Reference

MethodDescription
connect()Establish WebSocket connection
disconnect()Close connection
rpc<T>(method, params?, options?)Call server RPC method with typed response
on<T>(event, handler)Subscribe to server-pushed events
off<T>(event, handler)Unsubscribe
emit(event, data?)Send event to server
subscribeState<T>(key, handler)Subscribe to state changes (JSON Patch applied)
getState<T>(key)Get current state snapshot
getConnectionState()'disconnected' | 'connecting' | 'connected' | 'reconnecting'
registerCrdt(nodeId)Register a CrdtStore for this client (returns CrdtStore)
getCrdtStore()Get the registered CrdtStore (or null)

Framework Integration

React

typescript
import type { StateKeyName } from 'datasole';
import { DatasoleClient } from 'datasole/client';
import { useEffect, useRef, useState } from 'react';
import type { AppContract } from './contract';
import { StateKey } from './contract';

function useDatasole(url: string) {
  const clientRef = useRef(new DatasoleClient<AppContract>({ url }));
  useEffect(() => {
    clientRef.current.connect();
    return () => { clientRef.current.disconnect(); };
  }, [url]);
  return clientRef.current;
}

function useLiveState<T>(client: DatasoleClient<AppContract>, key: StateKeyName<AppContract>): T | null {
  const [state, setState] = useState<T | null>(null);
  useEffect(() => {
    const sub = client.subscribeState<T>(key, setState);
    return () => sub.unsubscribe();
  }, [client, key]);
  return state;
}

// Usage:
function Dashboard() {
  const ds = useDatasole('wss://example.com');
  const dashboard = useLiveState<{ visitors: number }>(ds, StateKey.Dashboard);
  if (!dashboard) return <p>Loading...</p>;
  return <p>Visitors: {dashboard.visitors}</p>;
}

Vue 3 SFC

vue
<script setup lang="ts">
import { DatasoleClient } from 'datasole/client';
import { onMounted, onUnmounted, ref } from 'vue';
import type { AppContract } from './contract';
import { StateKey } from './contract';

const client = new DatasoleClient<AppContract>({ url: 'wss://example.com' });
const dashboard = ref<Record<string, unknown>>({});
let stateSub: { unsubscribe(): void } | null = null;

onMounted(() => {
  client.connect();
  stateSub = client.subscribeState(StateKey.Dashboard, (s) => {
    dashboard.value = s;
  });
});

onUnmounted(() => {
  stateSub?.unsubscribe();
  client.disconnect();
});
</script>

<template>
  <p>Visitors: {{ dashboard.visitors }}</p>
</template>

Vue 3 Composable

typescript
import type { StateKeyName } from 'datasole';
import { DatasoleClient } from 'datasole/client';
import { onMounted, onUnmounted, ref, type Ref } from 'vue';
import type { AppContract } from './contract';

export function useDatasole(url: string) {
  const client = new DatasoleClient<AppContract>({ url });
  onMounted(() => client.connect());
  onUnmounted(() => client.disconnect());
  return client;
}

export function useLiveState<T>(
  client: DatasoleClient<AppContract>,
  key: StateKeyName<AppContract>,
): Ref<T | null> {
  const state = ref<T | null>(null) as Ref<T | null>;
  let sub: { unsubscribe(): void } | null = null;
  onMounted(() => {
    sub = client.subscribeState<T>(key, (s) => {
      state.value = s;
    });
  });
  onUnmounted(() => sub?.unsubscribe());
  return state;
}

React Native

Uses the fallback transport (no Web Workers). Set useWorker: false:

typescript
import { DatasoleClient } from 'datasole/client';

const client = new DatasoleClient({
  url: 'wss://example.com',
  useWorker: false,
});
client.connect();

Vanilla JS (Script Tag)

html
<script src="https://unpkg.com/datasole/dist/client/datasole.iife.min.js"></script>
<script type="module">
  import { StateKey } from './contract.mjs'; // shared enum/const map from your app
  const ds = new Datasole.DatasoleClient({ url: 'wss://example.com' });
  ds.connect();
  ds.subscribeState(StateKey.Dashboard, (state) => {
    document.getElementById('output').textContent = JSON.stringify(state);
  });
</script>

Worker Architecture

When useWorker: true (the default), the WebSocket connection runs in a Web Worker, keeping the main thread free for rendering. The worker script is loaded from workerUrl (default: ${path}/datasole-worker.iife.min.js, with default path /__ds). The server must serve this file — see the Demos for framework-specific examples.

Set useWorker: false for environments without Web Workers (React Native, SSR, Node.js).

  • SharedArrayBuffer (SAB): Zero-copy ring buffer when Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers are set.
  • postMessage with Transferable: Fallback when SAB is unavailable. ArrayBuffers are transferred (not copied).
  • No worker: Set useWorker: false for environments without Workers (React Native, SSR, Node.js test runners).