Skip to content

@statesync/electron

Transport adapter for Electron applications.

Installation

bash
npm install @statesync/electron @statesync/core

Why the bridge?

Electron's contextBridge does not preserve callback identity. Each function crossing the bridge gets a new proxy, which breaks ipcRenderer.removeListener() because the callback reference differs.

createElectronBridge() solves this by returning an unsubscribe closure from on(), which captures the exact listener reference in preload scope.

Quick start

1. Preload script

typescript
// preload.ts
const { contextBridge, ipcRenderer } = require('electron');
const { createElectronBridge } = require('@statesync/electron');

contextBridge.exposeInMainWorld('statesync', createElectronBridge(ipcRenderer));

TypeScript setup

Declare the global bridge type in your renderer:

typescript
// src/window.d.ts
import type { ElectronStateSyncBridge } from '@statesync/electron';

declare global {
  interface Window {
    statesync: ElectronStateSyncBridge;
  }
}

export {};

2. Main process

typescript
// main.ts
import { createElectronBroadcaster, createElectronSnapshotHandler } from '@statesync/electron';
import { ipcMain, BrowserWindow } from 'electron';

let state = { theme: 'dark', lang: 'en' };
let rev = 0;

const broadcaster = createElectronBroadcaster({
  topic: 'settings',
  getTargets: () => BrowserWindow.getAllWindows().map(w => w.webContents),
});

const handler = createElectronSnapshotHandler({
  topic: 'settings',
  getSnapshot: () => ({ revision: String(rev), data: state }),
  handle: ipcMain.handle.bind(ipcMain),
  removeHandler: ipcMain.removeHandler.bind(ipcMain),
});

// On state change:
function updateSettings(newState: typeof state) {
  state = newState;
  rev++;
  broadcaster.invalidate(String(rev));
}

3. Renderer

typescript
// renderer.ts
import { createElectronRevisionSync } from '@statesync/electron';

const sync = createElectronRevisionSync({
  topic: 'settings',
  bridge: window.statesync,
  applier: {
    apply(snapshot) {
      console.log('New state:', snapshot.data);
    },
  },
});
await sync.start();

API

High-level (DX sugar)

  • createElectronRevisionSync(options) — ready-made wiring of bridge + transport + core engine

Low-level (building blocks)

  • createElectronBridge(ipcRenderer) — creates the preload bridge object
  • createElectronBroadcaster(options) — broadcasts invalidation events to webContents
  • createElectronSnapshotHandler(options) — registers ipcMain.handle() for snapshot requests
  • createElectronInvalidationSubscriber(options) — creates subscriber from bridge events
  • createElectronSnapshotProvider(options) — creates provider from bridge invoke

Channel helpers

  • invalidationChannel(topic) — returns statesync:${topic}:invalidated
  • snapshotChannel(topic) — returns statesync:${topic}:snapshot

Options reference

createElectronRevisionSync

OptionTypeRequiredDescription
topicstringYesTopic identifier
bridgeElectronStateSyncBridgeYesBridge object from window.statesync
applierSnapshotApplierYesApplier to update local state
invalidationChannelstringNoOverride default channel
snapshotChannelstringNoOverride default channel
shouldRefresh(event: InvalidationEvent) => booleanNoFilter invalidation events
throttlingInvalidationThrottlingOptionsNoControl refresh rate
onError(ctx: SyncErrorContext) => voidNoError callback with phase context
loggerLoggerNoLogger instance

Full API reference

See the generated TypeDoc API for complete type signatures.

Peer dependencies

electron is declared as an optional peer dependency. This allows tests to run without Electron installed.

Released under the MIT License.