Scalable, transport-agnostic state synchronization for multi-window and multi-process apps -- fully typed TypeScript with framework adapters for Redux, Zustand, Jotai, MobX, Pinia, Valtio, Svelte, and Vue.
Documentation | Comparison | GitHub
You provide:
- a subscriber (an invalidation signal: "something changed, refresh!")
- a provider (fetches a snapshot:
{ revision, data }) - an applier (applies the snapshot to your local state)
state-sync provides:
- a small engine that handles common hard parts: coalescing, retry hooks, lifecycle, and avoiding many race conditions around refresh/apply.
- What is this?
- Why state-sync?
- How it works (concepts)
- Packages
- Install
- Quickstart (core)
- Quickstart (Tauri)
- Framework adapters
- Docs & examples
- Development
- Rust crate
- License
state-sync is a framework-agnostic, transport-agnostic way to keep one logical piece of state consistent across multiple runtimes.
Typical use cases:
- Tauri: sync settings/auth/cache between windows
- Web: sync state between tabs/iframes (planned via BroadcastChannel/adapters)
- Any IPC where you can emit an invalidation + fetch a snapshot
Non-goals:
- Realtime CRDT merging or fine-grained patches. The model here is "invalidate -> fetch canonical snapshot -> apply".
- Tiny footprint. The core engine is 3.1 KB gzipped; each framework adapter adds roughly 0.8 KB.
- No vendor lock-in. Works with any transport (Tauri events, BroadcastChannel, WebSocket, custom IPC) and any state library.
- One API for every framework. The same
createRevisionSynccall works with Redux, Zustand, Jotai, MobX, Pinia, Valtio, Svelte, and Vue -- swap one line to switch adapters.
See the full comparison with alternatives for details.
- Topic: string identifier for a resource (
'settings','profile','cache:user:123') - Revision: monotonic-ish version identifier for ordering (string)
- Invalidation event: "your snapshot may be stale" (subscriber emits these)
- Snapshot provider: returns an envelope
{ revision, data } - Applier: takes the snapshot and mutates your local state
If invalidations come in fast (or the transport drops/duplicates), the engine aims to behave well:
- it coalesces refresh requests
- it keeps a consistent lifecycle (
start(),stop(),refresh()) - it exposes structured error hooks to help observe retry/refresh failures
| Package | What it is |
|---|---|
@statesync/core |
Engine + revision protocol + types |
@statesync/persistence |
Persistence layer (localStorage, IndexedDB, cross-tab sync) |
@statesync/redux |
Redux snapshot applier adapter (HOF reducer wrapping) |
@statesync/pinia |
Pinia snapshot applier adapter |
@statesync/zustand |
Zustand snapshot applier adapter |
@statesync/jotai |
Jotai snapshot applier adapter (atom-based) |
@statesync/mobx |
MobX snapshot applier adapter (in-place mutation) |
@statesync/valtio |
Valtio snapshot applier adapter |
@statesync/svelte |
Svelte snapshot applier adapter |
@statesync/vue |
Vue (reactive/ref) snapshot applier adapter |
@statesync/tauri |
Tauri transport adapters (subscriber + provider) |
state-sync (Rust) |
Shared protocol types for Rust backends (Revision, SnapshotEnvelope<T>, InvalidationEvent) |
npm install @statesync/core
# Persistence (optional):
npm install @statesync/persistence # localStorage, IndexedDB, cross-tab sync
# Framework adapters (pick one):
npm install @statesync/redux # Redux / RTK
npm install @statesync/pinia # Pinia
npm install @statesync/zustand # Zustand
npm install @statesync/jotai # Jotai
npm install @statesync/mobx # MobX
npm install @statesync/valtio # Valtio
npm install @statesync/svelte # Svelte
npm install @statesync/vue # Vue (reactive / ref)
# Transport adapter:
npm install @statesync/tauri # Tauri v2import { createConsoleLogger, createRevisionSync } from '@statesync/core';
const handle = createRevisionSync({
topic: 'settings',
subscriber: myInvalidationSubscriber, // emits "changed" events
provider: mySnapshotProvider, // returns { revision, data }
applier: {
apply(snapshot) {
// snapshot.data is your payload
console.log('Apply:', snapshot.revision, snapshot.data);
},
},
logger: createConsoleLogger({ debug: true }),
onError(ctx) {
// ctx.phase: 'subscribe' | 'getSnapshot' | 'apply' | ...
console.error(`[${ctx.phase}]`, ctx.error);
},
});
await handle.start();Use Tauri events for invalidation and invoke for snapshots.
import { createRevisionSync } from '@statesync/core';
import {
createTauriInvalidationSubscriber,
createTauriSnapshotProvider,
} from '@statesync/tauri';
import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/core';
const handle = createRevisionSync({
topic: 'settings',
subscriber: createTauriInvalidationSubscriber({
listen,
eventName: 'state-sync:invalidation',
}),
provider: createTauriSnapshotProvider({
invoke,
commandName: 'get_snapshot',
args: { topic: 'settings' },
}),
applier: myApplier, // use any framework adapter below
});
await handle.start();Each adapter creates a SnapshotApplier for a specific state management library. All adapters share the same options pattern: mode ('patch' | 'replace'), pickKeys/omitKeys, toState mapping, and strict mode.
import { createPiniaSnapshotApplier } from '@statesync/pinia';
const applier = createPiniaSnapshotApplier(myPiniaStore, {
mode: 'patch',
omitKeys: ['localUiFlag'],
});Every framework adapter follows the same pattern -- swap createPiniaSnapshotApplier for createReduxSnapshotApplier, createZustandSnapshotApplier, createJotaiSnapshotApplier, createMobXSnapshotApplier, createValtioSnapshotApplier, createSvelteSnapshotApplier, or createVueSnapshotApplier. See the adapter documentation for full examples of each.
| Option | Type | Default | Description |
|---|---|---|---|
mode |
'patch' | 'replace' |
'patch' |
Merge vs full replacement |
pickKeys |
string[] |
-- | Only sync these keys |
omitKeys |
string[] |
-- | Never sync these keys |
toState |
(data, ctx) => State |
identity | Map snapshot data to state shape |
strict |
boolean |
true |
Throw if toState returns non-object |
Full documentation is hosted at 777genius.github.io/state-sync.
- Getting started
- Protocol
- Multi-window guide
- Lifecycle contract
- Compatibility
- Troubleshooting
- Examples
pnpm install
pnpm lint
pnpm typecheck
pnpm test
pnpm buildThe state-sync Rust crate provides shared protocol types for Tauri (or any Rust) backends:
Revision-- monotonicu64counter with saturating arithmeticSnapshotEnvelope<T>-- generic{ revision, data }envelope (serde-ready)InvalidationEvent--{ topic, revision }change notification (serde-ready)compare_revisions()-- canonicalu64string comparison
# src-tauri/Cargo.toml
[dependencies]
state-sync = "0.1"use state_sync::{InvalidationEvent, Revision, SnapshotEnvelope};
let rev = Revision::new(1);
let envelope = SnapshotEnvelope {
revision: rev.to_string(),
data: my_app_state,
};The crate is versioned independently from the npm packages.
cd crates/state-sync
cargo test
cargo clippy --all-targets -- -D warningsMIT (see LICENSE).