/** * Strata Store * Fast state management with encryption and cross-tab sync */ import { strata } from '../core/strata.sts'; type Subscriber = (state: T, prevState: T) => void; type Selector = (state: T) => R; interface StoreOptions { state: T; actions?: Record, ...args: any[]) => any>; encrypt?: boolean | string[]; // true = all, string[] = specific fields persist?: boolean | string[]; // true = all, string[] = specific fields shared?: boolean | string[]; // true = all, string[] = specific fields across tabs } interface StoreActions { $set: (partial: Partial) => void; $reset: () => void; $subscribe: (subscriber: Subscriber) => () => void; } export interface StrataStore extends StoreActions { getState: () => T; setState: (partial: Partial) => void; } const stores: Map> = new Map(); /** * Create a new store */ export function createStore( name: string, options: StoreOptions ): StrataStore & T { if (stores.has(name)) { return stores.get(name) as StrataStore & T; } const initialState = { ...options.state }; let state = { ...initialState }; const subscribers: Set> = new Set(); // Track which fields should be encrypted/persisted/shared const encryptFields = normalizeFields(options.encrypt); const persistFields = normalizeFields(options.persist); const sharedFields = normalizeFields(options.shared); function normalizeFields(opt: boolean | string[] | undefined): Set | 'all' | null { if (opt === true) return 'all'; if (Array.isArray(opt)) return new Set(opt); return null; } function shouldEncrypt(field: string): boolean { if (encryptFields === 'all') return true; if (encryptFields instanceof Set) return encryptFields.has(field); return false; } function shouldPersist(field: string): boolean { if (persistFields === 'all') return true; if (persistFields instanceof Set) return persistFields.has(field); return false; } function shouldShare(field: string): boolean { if (sharedFields === 'all') return true; if (sharedFields instanceof Set) return sharedFields.has(field); return false; } function notify(prevState: T) { for (const subscriber of subscribers) { subscriber(state, prevState); } } function setState(partial: Partial) { const prevState = { ...state }; const changes = Object.entries(partial); for (const [key, value] of changes) { (state as any)[key] = value; // Sync to SharedWorker if (shouldPersist(key) || shouldShare(key)) { strata.sendToWorker({ type: 'store:set', storeName: name, key, value, scope: shouldShare(key) ? 'shared' : 'tab', encrypt: shouldEncrypt(key), }); } } notify(prevState); } function getState(): T { return { ...state }; } // Core store object const store: StrataStore = { getState, setState, $set: setState, $reset: () => { setState(initialState as Partial); }, $subscribe: (subscriber: Subscriber) => { subscribers.add(subscriber); return () => subscribers.delete(subscriber); }, }; // Bind actions if (options.actions) { for (const [actionName, actionFn] of Object.entries(options.actions)) { (store as any)[actionName] = function (...args: any[]) { return actionFn.apply( new Proxy(state, { set(target, prop, value) { setState({ [prop]: value } as Partial); return true; }, get(target, prop) { if (prop === '$set') return setState; if (prop === '$reset') return store.$reset; return (target as any)[prop]; }, }), args ); }; } } // Create proxy for direct property access const proxy = new Proxy(store as StrataStore & T, { get(target, prop: string) { if (prop in target) return (target as any)[prop]; return state[prop as keyof T]; }, set(target, prop: string, value) { if (prop in state) { setState({ [prop]: value } as Partial); return true; } return false; }, }); // Listen for cross-tab updates strata.onBroadcast('store:changed', (data: any) => { if (data.storeName === name && shouldShare(data.key)) { const prevState = { ...state }; (state as any)[data.key] = data.value; notify(prevState); } }); // Load persisted state loadPersistedState(name, state, persistFields, sharedFields); stores.set(name, proxy); return proxy; } /** * Hook to use store with optional selector */ export function useStore( store: StrataStore, selector?: Selector ): R { const state = store.getState(); return selector ? selector(state) : (state as unknown as R); } /** * Subscribe to store changes with selector */ export function subscribeToStore( store: StrataStore, selector: Selector, callback: (value: R, prevValue: R) => void ): () => void { let prevValue = selector(store.getState()); return store.$subscribe((state, prevState) => { const newValue = selector(state); const oldValue = selector(prevState); // Only call if selected value changed if (!deepEqual(newValue, oldValue)) { callback(newValue, prevValue); prevValue = newValue; } }); } async function loadPersistedState( storeName: string, state: T, persistFields: Set | 'all' | null, sharedFields: Set | 'all' | null ): Promise { // Load from SharedWorker on init const fields = new Set(); if (persistFields === 'all' || sharedFields === 'all') { Object.keys(state as object).forEach((k) => fields.add(k)); } else { if (persistFields instanceof Set) persistFields.forEach((k) => fields.add(k)); if (sharedFields instanceof Set) sharedFields.forEach((k) => fields.add(k)); } // Request each field from worker // (In production, batch this into single request) for (const field of fields) { strata.sendToWorker({ type: 'store:get', storeName, key: field, scope: sharedFields === 'all' || (sharedFields instanceof Set && sharedFields.has(field)) ? 'shared' : 'tab', }); } } function deepEqual(a: any, b: any): boolean { if (a === b) return true; if (a === null || b === null) return false; if (typeof a !== typeof b) return false; if (typeof a === 'object') { if (Array.isArray(a) !== Array.isArray(b)) return false; const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (!deepEqual(a[key], b[key])) return false; } return true; } return false; }