import { useCallback, useEffect, useState } from "react"; import { LOCAL_STORAGE_ADAPTER_CHANGED_EVENT, localStorageAdapter, } from "../../infrastructure/persistence/localStorageAdapter"; type StoredStringSetter = (nextValue: T | ((currentValue: T) => T)) => void; const canUseLocalStorage = () => typeof globalThis.localStorage !== "undefined"; const defaultIsAllowedString = (value: string | null): value is T => typeof value === "string"; export const readStoredStringValue = ( storageKey: string, fallback: T, isAllowedValue: (value: string | null) => value is T = defaultIsAllowedString, ): T => { if (!canUseLocalStorage()) return fallback; const stored = localStorageAdapter.readString(storageKey); const validator = isAllowedValue ?? defaultIsAllowedString; return validator(stored) ? stored : fallback; }; export const readOptionalStoredStringValue = ( storageKey: string, isAllowedValue: (value: string | null) => value is T = defaultIsAllowedString, ): T | null => { if (!canUseLocalStorage()) return null; const stored = localStorageAdapter.readString(storageKey); const validator = isAllowedValue ?? defaultIsAllowedString; return validator(stored) ? stored : null; }; export const resolveStoredStringUpdate = ( currentValue: T, nextValue: T | ((currentValue: T) => T), ): T => (typeof nextValue === "function" ? nextValue(currentValue) : nextValue); export const shouldSyncStoredStringEvent = (storageKey: string, event: Event): boolean => { const changedKey = event.type === "storage" ? (event as StorageEvent).key : (event as CustomEvent<{ key?: string }>).detail?.key; return changedKey === storageKey; }; export const createStoredStringSyncHandlers = ({ storageKey, fallback, isAllowedValue, onValue, }: { storageKey: string; fallback: T; isAllowedValue?: (value: string | null) => value is T; onValue: (value: T) => void; }) => { const validator = isAllowedValue ?? defaultIsAllowedString; const syncFromStorage = () => { onValue(readStoredStringValue(storageKey, fallback, validator)); }; return { handleAdapterChange(event: Event) { if (shouldSyncStoredStringEvent(storageKey, event)) syncFromStorage(); }, handleBrowserStorage(event: Event) { if (shouldSyncStoredStringEvent(storageKey, event)) syncFromStorage(); }, }; }; export const useStoredString = ( storageKey: string, fallback: T, isAllowedValue?: (value: string | null) => value is T, ) => { const validator = isAllowedValue ?? defaultIsAllowedString; const [value, setValue] = useState(() => readStoredStringValue( storageKey, fallback, validator, )); const setAndPersist = useCallback>((nextValue) => { setValue((currentValue) => { const resolvedValue = resolveStoredStringUpdate(currentValue, nextValue); if (canUseLocalStorage()) { localStorageAdapter.writeString(storageKey, resolvedValue); } return resolvedValue; }); }, [storageKey]); useEffect(() => { const target = globalThis as typeof globalThis & { addEventListener?: (type: string, listener: EventListener) => void; removeEventListener?: (type: string, listener: EventListener) => void; }; if (typeof target.addEventListener !== "function") return; const { handleAdapterChange, handleBrowserStorage, } = createStoredStringSyncHandlers({ storageKey, fallback, isAllowedValue: validator, onValue: setValue, }); target.addEventListener(LOCAL_STORAGE_ADAPTER_CHANGED_EVENT, handleAdapterChange); target.addEventListener("storage", handleBrowserStorage); return () => { target.removeEventListener?.(LOCAL_STORAGE_ADAPTER_CHANGED_EVENT, handleAdapterChange); target.removeEventListener?.("storage", handleBrowserStorage); }; }, [fallback, storageKey, validator]); return [value, setAndPersist] as const; };