Multi-window patterns
Best practices for syncing state across multiple browser tabs or Tauri windows.
Architecture overview
Source of truth
Rule: One authoritative source per topic. All windows pull from the backend, never from each other.
// All windows fetch from the same backend
const provider = {
async getSnapshot() {
return invoke('get_settings');
}
};Windows syncing from each other creates circular dependencies and race conditions.
Handling self-echo
When Window A updates state, it receives its own invalidation event. This is normal but can cause unnecessary refreshes.
Option 1: Ignore via sourceId
const windowId = crypto.randomUUID();
const sync = createRevisionSync({
topic: 'settings',
subscriber,
provider,
applier,
shouldRefresh(event) {
// Skip if this window originated the change
return event.sourceId !== windowId;
},
});Option 2: Let the revision gate handle it
The engine skips events whose revision is <= the local revision, so the self-echo never triggers a refresh:
// Window A: applies revision 5 → localRevision = "5"
// Window A: receives self-echo with revision "5"
// Engine: "5" <= "5" → skip (no network call)Topic naming
Keep topics domain-oriented, not UI-oriented:
// Good: Domain concepts
'auth-state'
'app-settings'
'user-preferences'
// Bad: UI concepts
'settings-window'
'main-window-state'
'popup-data'Example: Tauri multi-window
Each window runs its own sync handle. The Rust backend is the source of truth.
import { createTauriRevisionSync } from '@statesync/tauri';
import { createPiniaSnapshotApplier } from '@statesync/pinia';
import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/core';
import { useSettingsStore } from './stores/settings';
export function setupSettingsSync() {
const store = useSettingsStore();
const sync = createTauriRevisionSync({
topic: 'settings',
listen,
invoke,
eventName: 'settings:invalidated',
commandName: 'get_settings',
applier: createPiniaSnapshotApplier(store, { mode: 'patch' }),
});
sync.start();
return () => sync.stop();
}Full Tauri app
For the complete Rust backend + multi-window setup, see Vue + Pinia + Tauri example.
Browser tabs (BroadcastChannel)
For browser tab sync, use @statesync/persistence which handles BroadcastChannel automatically:
import { createRevisionSync } from '@statesync/core';
import {
createPersistenceApplier,
createLocalStorageBackend,
} from '@statesync/persistence';
const applier = createPersistenceApplier({
storage: createLocalStorageBackend({ key: 'settings' }),
applier: innerApplier,
crossTabSync: {
channelName: 'settings-sync',
receiveUpdates: true, // apply snapshots from other tabs
broadcastSaves: true, // broadcast saves to other tabs
},
});
// Tabs automatically sync via BroadcastChannel.
// Call applier.dispose() on cleanup to close the channel.Debugging multi-window issues
Enable debug logging to trace sync flow:
import { createConsoleLogger, tagLogger } from '@statesync/core';
const windowId = new URLSearchParams(location.search).get('window') || 'main';
const logger = tagLogger(
createConsoleLogger({ debug: true }),
{ windowId }
);
const sync = createRevisionSync({
topic: 'settings',
subscriber,
provider,
applier,
logger, // Logs include windowId for each entry
});Output:
[debug] [windowId=main] subscribed
[debug] [windowId=main] snapshot applied { revision: "5" }
[debug] [windowId=settings] subscribed
[debug] [windowId=settings] snapshot applied { revision: "5" }See also
- Writing state — UI → backend write patterns
- Custom transports — build your own subscriber/provider
- Vue + Pinia + Tauri example — full multi-window app
