veza/apps/web/src/utils/broadcastSync.ts

214 lines
5.9 KiB
TypeScript
Raw Normal View History

/**
* BroadcastChannel State Synchronization
* FE-STATE-002: Synchronize Zustand state across browser tabs using BroadcastChannel
*
* This middleware allows Zustand stores to automatically sync their state
* across all open tabs/windows of the same origin.
*/
import { StateCreator, StoreMutatorIdentifier } from 'zustand';
/**
* Options for broadcast synchronization
*/
export interface BroadcastSyncOptions {
/** Channel name for BroadcastChannel (default: store name) */
channelName?: string;
/** Whether to enable synchronization (default: true) */
enabled?: boolean;
/** Function to filter which state changes should be synced */
shouldSync?: (state: any, prevState: any) => boolean;
}
/**
* BroadcastChannel message format
*/
interface BroadcastMessage {
type: 'state-update' | 'state-request' | 'state-response';
storeName: string;
state?: any;
timestamp: number;
}
/**
* Create a BroadcastChannel instance for a store
*/
function createBroadcastChannel(storeName: string): BroadcastChannel | null {
if (typeof window === 'undefined' || !window.BroadcastChannel) {
console.warn('[BroadcastSync] BroadcastChannel not supported in this environment');
return null;
}
try {
return new BroadcastChannel(`veza-store-${storeName}`);
} catch (error) {
console.warn(`[BroadcastSync] Failed to create BroadcastChannel for ${storeName}:`, error);
return null;
}
}
/**
* Zustand middleware for BroadcastChannel synchronization
*
* @example
* ```typescript
* export const useMyStore = create<MyState>()(
* broadcastSync(
* (set, get) => ({
* // store implementation
* }),
* { channelName: 'my-store' }
* )
* );
* ```
*/
export function broadcastSync<T extends object>(
config: StateCreator<T>,
options: BroadcastSyncOptions = {},
): StateCreator<T> {
return (set, get, api) => {
const storeName = options.channelName || 'default-store';
const enabled = options.enabled !== false;
const shouldSync = options.shouldSync || (() => true);
let channel: BroadcastChannel | null = null;
let isReceivingUpdate = false;
let lastState: T | null = null;
// Initialize BroadcastChannel
if (enabled) {
channel = createBroadcastChannel(storeName);
if (channel) {
// Listen for state updates from other tabs
channel.onmessage = (event: MessageEvent<BroadcastMessage>) => {
const message = event.data;
if (message.type === 'state-update' && message.state) {
// Ignore updates from the same tab
if (isReceivingUpdate) {
return;
}
// Check if we should sync this update
if (shouldSync(message.state, lastState)) {
isReceivingUpdate = true;
set(message.state as T);
lastState = message.state;
// Reset flag after a short delay
setTimeout(() => {
isReceivingUpdate = false;
}, 100);
}
} else if (message.type === 'state-request') {
// Another tab is requesting the current state
const currentState = get();
channel.postMessage({
type: 'state-response',
storeName,
state: currentState,
timestamp: Date.now(),
} as BroadcastMessage);
} else if (message.type === 'state-response' && message.state) {
// Received state from another tab (initial sync)
if (!lastState) {
isReceivingUpdate = true;
set(message.state as T);
lastState = message.state;
setTimeout(() => {
isReceivingUpdate = false;
}, 100);
}
}
};
// Request initial state from other tabs on startup
channel.postMessage({
type: 'state-request',
storeName,
timestamp: Date.now(),
} as BroadcastMessage);
}
}
// Create the store with wrapped set function
const store = config(
(...args) => {
if (!isReceivingUpdate) {
set(...args);
// Broadcast state update to other tabs
if (channel && enabled) {
const newState = get();
if (shouldSync(newState, lastState)) {
channel.postMessage({
type: 'state-update',
storeName,
state: newState,
timestamp: Date.now(),
} as BroadcastMessage);
lastState = newState;
}
}
} else {
// If we're receiving an update, just set without broadcasting
set(...args);
}
},
get,
api,
);
return store;
};
}
/**
* Helper to create a synchronized store with both persistence and broadcast sync
*
* @example
* ```typescript
* export const useMyStore = create<MyState>()(
* persist(
* broadcastSync(
* (set, get) => ({
* // store implementation
* }),
* { channelName: 'my-store' }
* ),
* { name: 'my-store-storage' }
* )
* );
* ```
*/
export function createSynchronizedStore<T extends object>(
config: StateCreator<T>,
options: {
persist?: { name: string; partialize?: (state: T) => Partial<T> };
broadcast?: BroadcastSyncOptions;
} = {},
) {
let store = config;
// Apply broadcast sync if enabled
if (options.broadcast?.enabled !== false) {
store = broadcastSync(store, options.broadcast) as StateCreator<T>;
}
// Apply persistence if configured
if (options.persist) {
const { persist: persistMiddleware } = require('zustand/middleware');
store = persistMiddleware(store, {
name: options.persist.name,
partialize: options.persist.partialize,
}) as StateCreator<T>;
}
return store;
}