Skip to content

Custom transports

state-sync is transport-agnostic. You provide two interfaces:

PartSignatureRole
Subscribersubscribe(handler: (e: InvalidationEvent) => void): Promise<Unsubscribe>Delivers invalidation events
ProvidergetSnapshot(): Promise<SnapshotEnvelope<T>>Returns the latest snapshot

WebSocket

typescript
import { createRevisionSync } from '@statesync/core';

const subscriber = {
  async subscribe(handler: (event: { topic: string; revision: string }) => void) {
    const ws = new WebSocket('wss://api.example.com/sync');

    ws.onmessage = (e) => {
      const event = JSON.parse(e.data);
      handler(event); // { topic: 'settings', revision: '42' }
    };

    // Return cleanup function
    return () => ws.close();
  },
};

const provider = {
  async getSnapshot() {
    const res = await fetch('https://api.example.com/settings');
    return res.json(); // { revision: '42', data: { theme: 'dark' } }
  },
};

const sync = createRevisionSync({
  topic: 'settings',
  subscriber,
  provider,
  applier: myApplier,
});

Server-Sent Events (SSE)

typescript
const subscriber = {
  async subscribe(handler) {
    const source = new EventSource('/api/events');

    source.addEventListener('invalidation', (e) => {
      handler(JSON.parse(e.data));
    });

    return () => source.close();
  },
};

Electron IPC

typescript
// In renderer process
import { ipcRenderer } from 'electron';

const subscriber = {
  async subscribe(handler) {
    const listener = (_event, data) => handler(data);
    ipcRenderer.on('state:invalidated', listener);
    return () => ipcRenderer.removeListener('state:invalidated', listener);
  },
};

const provider = {
  async getSnapshot() {
    return ipcRenderer.invoke('get-state');
  },
};

BroadcastChannel (browser tabs)

typescript
const channel = new BroadcastChannel('my-sync');

const subscriber = {
  async subscribe(handler) {
    const listener = (e: MessageEvent) => handler(e.data);
    channel.addEventListener('message', listener);
    return () => channel.removeEventListener('message', listener);
  },
};

Handling reconnection

state-sync does not manage transport connections. After a reconnect, call sync.refresh() to re-fetch the latest snapshot:

typescript
ws.onopen = () => {
  sync.refresh();
};

Filtering events with shouldRefresh

Skip unnecessary refreshes via the shouldRefresh callback:

typescript
const sync = createRevisionSync({
  topic: 'settings',
  subscriber,
  provider,
  applier,
  shouldRefresh(event) {
    return event.sourceId !== myWindowId; 
  },
});

The callback receives a validated InvalidationEvent with topic, revision, and optional sourceId / timestampMs.

Retrying failed snapshots

Wrap the provider with withRetry for automatic retries with exponential backoff:

typescript
import { createRevisionSync, withRetry } from '@statesync/core';

const sync = createRevisionSync({
  topic: 'settings',
  subscriber,
  provider: withRetry(provider, {
    maxAttempts: 3,
    initialDelayMs: 500,
    backoffMultiplier: 2,
    maxDelayMs: 10_000,
  }),
  applier,
});

See also

Released under the MIT License.