wxt-zustand
Version:
High-performance Zustand state management for WXT web extensions with seamless cross-tab synchronization and sub-10ms React re-renders
107 lines • 4.23 kB
JavaScript
import { createStorageKey, createStorageWatcher, createVersionedItemWatcher, defineVersionedStoreItem, } from '../storage';
import { shallowDiff } from '../utils';
import { mergeStatePreservingFunctions, stripFunctionProps, } from '../utils/stateTransforms';
import { getBackendService } from './connect';
import { isExtensionContextInvalidated, reloadPageOnInvalidation, } from './errors';
/**
* Set up bidirectional sync between a local Zustand store and the background.
* - Local → Background: subscribe to local changes and dispatch full state.
* - Background → Local: watch WXT storage and apply remote state.
* - Loop prevention: temporarily unsubscribe local listener when applying remote state.
*
* Uses WXT-native storage.watch for background→local updates
* instead of proxy service callbacks for optimal performance.
*/
export function setupBidirectionalSync(storeName, store, config = {}) {
// Resolve service without probing; caller should ensure readiness via initial sync.
const service = getBackendService(storeName);
// Prepare storage key and (de)serializers.
const area = config.storageStrategy || 'local';
const usingVersioned = typeof config.storageVersion === 'number';
const storageKey = usingVersioned
? undefined
: createStorageKey(storeName, area);
const deserializer = (config.deserializer ||
JSON.parse);
// Local → Background
const localCallback = (state /*, prev: S */) => {
// Dispatch full state. Background handles broadcast via storage.
// We intentionally don't await to keep UI responsive.
const serializable = stripFunctionProps(state);
service
.dispatch({ type: '__WXT_ZUSTAND_SYNC__', state: serializable })
.catch((err) => {
if (isExtensionContextInvalidated(err)) {
reloadPageOnInvalidation();
return;
}
console.error('WXT Zustand: dispatch error', err);
});
};
let unsubscribeLocal = store.subscribe(localCallback);
// Background → Local via storage watcher or versioned item watcher
const unwatchRemote = (() => {
const onRemote = (newValue) => {
if (newValue === undefined)
return;
const curr = store.getState();
try {
const diff = shallowDiff(curr, newValue);
if (!diff || diff.length === 0)
return;
}
catch { }
unsubscribeLocal();
try {
const merged = mergeStatePreservingFunctions(curr, newValue);
store.setState(merged, true);
}
finally {
unsubscribeLocal = store.subscribe(localCallback);
}
};
if (usingVersioned) {
const version = config.storageVersion;
const item = defineVersionedStoreItem(storeName, {
area,
version,
...(config.storageMigrations && {
migrations: config.storageMigrations,
}),
...(config.storageFallback !== undefined && {
fallback: config.storageFallback,
}),
});
return createVersionedItemWatcher(item, (n) => onRemote(n)).unwatch;
}
// String-key watcher fallback (non-versioned)
const watcher = createStorageWatcher(storageKey, (n) => onRemote(n), deserializer);
return watcher.unwatch;
})();
// Unified cleanup (idempotent) for component/page unmount.
let cleaned = false;
const cleanup = () => {
if (cleaned)
return;
cleaned = true;
try {
unsubscribeLocal?.();
}
catch (err) {
console.warn('WXT Zustand: error during local unsubscribe', err);
}
try {
unwatchRemote?.();
}
catch (err) {
console.warn('WXT Zustand: error during remote unwatch', err);
}
};
return {
service,
unsubscribeLocal,
unwatchRemote,
cleanup,
};
}
//# sourceMappingURL=sync.js.map