A lightweight, fully type-safe unidirectional remote state synchronization library. Effortlessly sync complex states with fine-grained precision from a Provider (e.g., Server or Electron Main) to a Receiver (e.g., Web Client or Electron Renderer).
English | 中文
- Unidirectional Sync: Safely sync state from A to B with an efficient patch-based diffing system.
- Deep Reactivity & Proxies: Built-in, automatic support for intercepting deep changes across
Object,Map, andSetstructures. - Batch Patching: Automatically queues and batches rapid state mutations into singular, optimized events for high performance.
- Framework Agnostic Transport: Bring your own transport layer (WebSocket, Socket.io, Electron IPC, HTTP, etc).
- Vue Integration: Seamlessly connects to Vue's Reactivity system out of the box (
toValue(),toRef(),toShallowRef()).
npm install remote-state-syncServer (Provider)
import { SyncProvider } from 'remote-state-sync';
import { Hono } from 'hono';
import { Server } from 'socket.io';
const app = new Hono();
const io = new Server(3000);
const provider = new SyncProvider();
const usersNs = provider.register('users_space');
// Define a state
type UserState = {
connected: number;
history: string[];
};
// Initialize the state
const userState = usersNs.sync<UserState>('data', {
connected: 0,
history: [],
});
// 1. snapshotGetter implementation via HTTP endpoint
app.get('/snapshot/:namespace/:key', (c) => {
const ns = c.req.param('namespace');
const key = c.req.param('key');
return c.json(provider.getStateSnapshot(ns, key));
});
io.on('connection', (socket) => {
// Mutating deeply tracks changes and queues patches automatically
userState.set((state) => void state.connected++);
socket.on('disconnect', () => {
userState.set((state) => void state.connected--);
});
});
// 2. Broadcast patches triggered by deeper modifications over WebSocket
provider.bus.on('update', (namespace, patches) => {
io.emit('state-update', namespace, patches);
});Client (Receiver)
import { SyncReceiver } from 'remote-state-sync';
import { io } from 'socket.io-client';
const socket = io('ws://localhost:3000');
const receiver = new SyncReceiver({
// 1. Fetch the initial snapshot over HTTP
snapshotGetter: async (namespace, key) => {
const res = await fetch(`http://localhost:3000/snapshot/${namespace}/${key}`);
return res.json();
},
});
// 2. Receive and apply patches incrementally
socket.on('state-update', (namespace, patches) => {
receiver.applyPatches(namespace, patches);
});
async function main() {
const usersNs = await receiver.register('users_space');
type UserState = { connected: number; history: string[] };
const userState = await usersNs.sync<UserState>('data');
// Output: { connected: 1, history: [] }
console.log(userState.toValue());
// Listen to specific item changes!
userState.on('update', (newVal, oldVal, patches) => {
console.log('State updated!', newVal.connected);
});
}
main();Main Process (Provider)
import { SyncProvider } from 'remote-state-sync';
import { ipcMain, BrowserWindow } from 'electron';
const provider = new SyncProvider();
const appNs = provider.register('app_ns');
type SettingsState = {
theme: 'dark' | 'light';
version: string;
};
// Initialize the state
const settings = appNs.sync<SettingsState>('settings', {
theme: 'dark',
version: '1.0.0',
});
// 1. snapshotGetter via ipcMain.handle
ipcMain.handle('get-sync-snapshot', (_, namespace, key) => {
return provider.getStateSnapshot(namespace, key);
});
// 2. Broadcast patches to all renderer windows
provider.bus.on('update', (namespace, patches) => {
BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send('sync-update', namespace, patches);
});
});
// Example modification later down the line
setTimeout(() => {
settings.set((state) => void (state.theme = 'light'));
}, 5000);Renderer Process (Receiver with Vue)
import { SyncReceiver } from 'remote-state-sync';
import { ipcRenderer } from 'electron';
import { watch } from 'vue';
const receiver = new SyncReceiver({
// 1. Fetch the snapshot via ipcRenderer.invoke
snapshotGetter: (namespace, key) => ipcRenderer.invoke('get-sync-snapshot', namespace, key),
});
// 2. Listen for patches from Main process
ipcRenderer.on('sync-update', (_, namespace, patches) => {
receiver.applyPatches(namespace, patches);
});
async function setup() {
const appNs = await receiver.register('app_ns');
const settings = await appNs.sync<SettingsState>('settings');
// Vue Reactivity directly tied to the remote state!
const settingsRef = settings.toRef();
watch(
settingsRef,
(newSettings) => {
console.log('Renderer theme changed to:', newSettings.theme);
},
{ deep: true },
);
}
setup();