/** * 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()( * broadcastSync( * (set, get) => ({ * // store implementation * }), * { channelName: 'my-store' } * ) * ); * ``` */ export function broadcastSync( config: StateCreator, options: BroadcastSyncOptions = {}, ): StateCreator { 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) => { 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()( * persist( * broadcastSync( * (set, get) => ({ * // store implementation * }), * { channelName: 'my-store' } * ), * { name: 'my-store-storage' } * ) * ); * ``` */ export function createSynchronizedStore( config: StateCreator, options: { persist?: { name: string; partialize?: (state: T) => Partial }; broadcast?: BroadcastSyncOptions; } = {}, ) { let store = config; // Apply broadcast sync if enabled if (options.broadcast?.enabled !== false) { store = broadcastSync(store, options.broadcast) as StateCreator; } // 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; } return store; }