import { Listener, Store, StoreOptions } from "./types"; import { BroadcastStrategy } from "./enums"; import { globalMiddlewares } from "./middleware"; import { persistState, getPersistedState, resolveStorage } from "./storage"; /** * Global registry for all defined stores. * @internal */ const globalStoreRegistry = new Map>(); /** * Defines a new global store. * @template T * @param {string} name - Unique store name. * @param {T} initialState - Initial state. * @param {StoreOptions} [options={}] - Store options. * @returns {Store} The created store instance. * @throws If the store name is already registered. */ export function defineGlobalStore( name: string, initialState: T, options: StoreOptions = {} ): Store { if (globalStoreRegistry.has(name)) { throw new Error(`[stadojs] Store "${name}" is already registered.`); } const storage = resolveStorage(options.storageType); let state: T; (async () => { state = options.persist ? await getPersistedState(name, initialState, storage, options) : initialState; })(); const listeners = new Set>(); const shouldBroadcastTab = options.broadcast === BroadcastStrategy.CrossTab || options.broadcast === BroadcastStrategy.All; const channel = shouldBroadcastTab ? new BroadcastChannel(`store:${name}`) : null; /** * Notifies all listeners of a state change. */ function notifyListeners() { // Execute global middlewares globalMiddlewares.forEach((mw) => mw({ name, state })); // Notify registered listeners listeners.forEach((listener) => listener(state)); } /** * Broadcasts state changes to other tabs or locally. */ function broadcastState() { if ( options.broadcast === BroadcastStrategy.Local || options.broadcast === BroadcastStrategy.All ) { window.dispatchEvent(new CustomEvent(`store:${name}`, { detail: state })); } if (channel) { channel.postMessage(state); } } /** * Persists the current state if enabled. */ function persistCurrentState() { if (options.persist) { persistState(name, state, storage, options); } } const store: Store = { get: () => state, set: (update) => { state = { ...state, ...update }; notifyListeners(); broadcastState(); persistCurrentState(); }, on: (listener) => { listeners.add(listener); return () => listeners.delete(listener); }, }; globalStoreRegistry.set(name, store); return store; } /** * Retrieves a global store by name. * @template T * @param {string} name - Store name. * @returns {Store} The store instance. * @throws If the store is not found. */ export function useGlobalStore(name: string): Store { const store = globalStoreRegistry.get(name); if (!store) { throw new Error(`[stadojs] Store "${name}" not found.`); } return store as Store; } /** * Emits a global store event to listeners or other tabs. * @template T * @param {string} name - Store name. * @param {T} data - Data to emit. * @param {BroadcastStrategy} [target=BroadcastStrategy.CrossTab] - Broadcast target. */ export function emitGlobalStoreEvent( name: string, data: T, target: BroadcastStrategy = BroadcastStrategy.CrossTab ): void { if (target === BroadcastStrategy.Local || target === BroadcastStrategy.All) { window.dispatchEvent(new CustomEvent(`store:${name}`, { detail: data })); } if ( target === BroadcastStrategy.CrossTab || target === BroadcastStrategy.All ) { const channel = new BroadcastChannel(`store:${name}`); channel.postMessage(data); } }