use-storage-persisted-state 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -233,3 +233,18 @@ Key differences include:
|
|
|
233
233
|
### How does the hook handle null and undefined values?
|
|
234
234
|
|
|
235
235
|
When the state is set to `null` or `undefined`, the hook will remove the corresponding item from the underlying storage (`localStorage`/`sessionStorage`). This means that subsequent reads will return the `defaultValue` provided to the hook until an explicit value is set.
|
|
236
|
+
|
|
237
|
+
## Publishing
|
|
238
|
+
|
|
239
|
+
Follow this checklist to publish a new version.
|
|
240
|
+
|
|
241
|
+
### One-time setup
|
|
242
|
+
|
|
243
|
+
- Ensure you have npm access to the package: `npm whoami` and `npm owner ls use-storage-persisted-state`.
|
|
244
|
+
|
|
245
|
+
### Release checklist
|
|
246
|
+
|
|
247
|
+
1. Bump the version: `npm version patch|minor|major` (this creates a git tag).
|
|
248
|
+
2. Run release checks and build the package:`npm run prepublishOnly`
|
|
249
|
+
3. Verify the tarball contents: `npm pack --dry-run`
|
|
250
|
+
4. Publish: `npm publish`
|
package/dist/index.d.mts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/useStoragePersistedState.ts","../src/codecs.ts","../src/storage.ts","../src/storagePersistedState.ts"],"sourcesContent":["export { useStoragePersistedState } from \"./useStoragePersistedState\";\nexport type {\n StoragePersistedStateOptions,\n StorageType,\n} from \"./useStoragePersistedState\";\nexport type { Codec } from \"./codecs\";\nexport {\n JsonCodec,\n StringCodec,\n BooleanCodec,\n NumberCodec,\n inferCodec,\n} from \"./codecs\";\nexport {\n readStoragePersistedState,\n setStoragePersistedState,\n} from \"./storagePersistedState\";\n","\"use client\";\n\nimport { useCallback, useMemo, useRef } from \"react\";\nimport { useSyncExternalStore } from \"use-sync-external-store/shim\";\nimport { Codec, inferCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\n\nexport type StorageType = \"localStorage\" | \"sessionStorage\" | \"memory\";\n\n/**\n * Options for the useStoragePersistedState hook\n */\nexport interface StoragePersistedStateOptions<T> {\n /**\n * Explicit codec for when defaultValue is null or undefined, for complex types, or special cases (e.g., data migration on read).\n * If not provided, codec is inferred from defaultValue type.\n */\n codec?: Codec<T>;\n /**\n * Storage type to use: 'localStorage' (default), 'sessionStorage', or 'memory'.\n */\n storageType?: StorageType;\n /**\n * Enable cross-tab synchronization via the StorageEvent. Note: disabling this will not stop other tabs from updating localStorage, but this hook will not automatically respond to those changes.\n * defaults to true.\n */\n crossTabSync?: boolean;\n /**\n * Polling interval in milliseconds for detecting storage changes made outside of React (e.g., DevTools, direct localStorage manipulation). Set to null to disable polling.\n * defaults to 2000ms.\n */\n pollingIntervalMs?: number | null;\n}\n\n// Overload 1: Default provided, T inferred\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): [T, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): [T | null, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Implementation\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager =\n options.storageType === \"sessionStorage\"\n ? sessionStorageSync\n : options.storageType === \"memory\"\n ? memoryStorageSync\n : localStorageSync;\n const adapter = syncManager.storage;\n\n // 1. Determine the codec.\n // If no explicit codec is passed, we try to infer it from defaultValue.\n // If defaultValue is undefined and no codec is passed, we fall back to JSON.\n const codec = useMemo(() => {\n if (options?.codec) return options.codec;\n return inferCodec(defaultValue);\n }, [defaultValue, options?.codec]);\n\n if ((defaultValue === undefined || defaultValue === null) && !options.codec) {\n console.warn(\n `useStorage: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n // Memoize the decoded value to prevent infinite loops in useSyncExternalStore\n // when the codec returns a new object reference (e.g. JSON.parse).\n const lastRaw = useRef<string | null>(null); // string | null, because storage returns null for missing keys\n const lastParsed = useRef<T | undefined>(undefined);\n\n const getSnapshot = useCallback(() => {\n const raw = adapter.getItem(key);\n\n // Return default if storage is missing the key\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n // If raw value matches cache, return cached object.\n if (raw === lastRaw.current) {\n if (lastParsed.current === undefined) {\n return null as T;\n }\n return lastParsed.current as T;\n }\n\n // Decode new raw value\n try {\n const decoded = codec.decode(raw);\n\n lastRaw.current = raw;\n lastParsed.current = decoded;\n return decoded;\n } catch (e) {\n console.error(`Error parsing storage key \"${key}\"`, e);\n return defaultValue as T;\n }\n }, [adapter, key, codec, defaultValue]);\n\n // 2. Subscribe to the external store (Local/SessionStorage + Polling/Events)\n // useSyncExternalStore handles the hydration mismatch automatically by\n // taking a `getServerSnapshot` (returning defaultValue).\n const value = useSyncExternalStore(\n (callback) =>\n syncManager.subscribe(key, callback, {\n crossTabSync: options.crossTabSync,\n pollingIntervalMs: options.pollingIntervalMs,\n }),\n getSnapshot,\n () => defaultValue as T, // Server Snapshot\n );\n\n // 3. Create the Setter\n const setValue = useCallback(\n (newValueOrFn: T | ((prev: T) => T)) => {\n try {\n const currentRaw = adapter.getItem(key);\n const current =\n currentRaw !== null ? codec.decode(currentRaw) : defaultValue;\n\n const newValue =\n newValueOrFn instanceof Function\n ? newValueOrFn(current)\n : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n lastRaw.current = encoded;\n lastParsed.current = newValue;\n\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n // Notify other hooks/tabs\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n },\n [adapter, key, codec, defaultValue, syncManager],\n );\n\n const removeItem = useCallback(() => {\n try {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error removing storage key \"${key}\":`, error);\n }\n }, [adapter, key, syncManager]);\n\n return [value, setValue, removeItem] as const;\n}\n","/**\n * Defines how to serialize/deserialize a value from localStorage.\n * null means the key doesn't exist.\n */\nexport interface Codec<T> {\n encode: (value: T) => string | null;\n decode: (value: string | null) => T;\n}\n\n/**\n * A robust JSON codec that handles parsing errors gracefully.\n * Works with objects, arrays, and other JSON-serializable values.\n *\n * @example\n * ```ts\n * const [user, setUser] = useStoragePersistedState<User | null>(\n * \"user\",\n * null,\n * { codec: JsonCodec }\n * );\n * ```\n */\nexport const JsonCodec: Codec<unknown> = {\n encode: (value) => {\n if (value === null) return null;\n return JSON.stringify(value);\n },\n decode: (value) => {\n if (value === null) return null;\n try {\n return JSON.parse(value);\n } catch (e) {\n console.warn(`LocalStorage parse error for value \"${value}\".`, e);\n throw e;\n }\n },\n};\n\n/**\n * Codec for string values. Stores strings as-is without any transformation.\n *\n * @example\n * ```ts\n * const [name, setName] = useStoragePersistedState(\"name\", \"Guest\");\n * // Automatically uses StringCodec because defaultValue is a string\n * ```\n */\nexport const StringCodec: Codec<string | null> = {\n encode: (value) => value ?? null,\n decode: (value) => value ?? null,\n};\n\n/**\n * Codec for boolean values. Stores as \"true\" or \"false\" strings.\n *\n * @example\n * ```ts\n * const [isDark, setIsDark] = useStoragePersistedState(\"darkMode\", false);\n * // Automatically uses BooleanCodec because defaultValue is a boolean\n * ```\n */\nexport const BooleanCodec: Codec<boolean> = {\n encode: (value) => (value ? \"true\" : \"false\"),\n decode: (value) => value === \"true\",\n};\n\n/**\n * Codec for number values. Handles NaN, Infinity, and -Infinity correctly.\n * Returns null for unparseable values (e.g., empty string, invalid number string).\n *\n * @example\n * ```ts\n * const [count, setCount] = useStoragePersistedState(\"count\", 0);\n * // Automatically uses NumberCodec because defaultValue is a number\n * ```\n */\nexport const NumberCodec: Codec<number | null> = {\n encode: (value) => {\n if (value === null) return null;\n return String(value);\n },\n decode: (value) => {\n if (value === null || value === \"\") return null;\n // Handle special numeric values\n if (value === \"NaN\") return NaN;\n if (value === \"Infinity\") return Infinity;\n if (value === \"-Infinity\") return -Infinity;\n const parsed = Number(value);\n return isNaN(parsed) ? null : parsed;\n },\n};\n\n/**\n * Infers the appropriate codec based on the default value's type.\n * Used internally when the user doesn't provide an explicit codec.\n *\n * - `boolean` → BooleanCodec\n * - `number` → NumberCodec\n * - `string` → StringCodec\n * - `object`, `array`, `undefined`, `null` → JsonCodec\n *\n * @param defaultValue - The default value to infer the codec from\n * @returns The inferred codec for the given value type\n */\nexport function inferCodec<T>(defaultValue: T): Codec<T> {\n const type = typeof defaultValue;\n\n // Type assertions through unknown are needed because we're doing runtime type inference\n // The actual codec returned will match the runtime type of defaultValue\n if (type === \"boolean\") return BooleanCodec as unknown as Codec<T>;\n if (type === \"number\") return NumberCodec as unknown as Codec<T>;\n if (type === \"string\") return StringCodec as unknown as Codec<T>;\n\n // Default to JSON for objects, arrays, or undefined\n return JsonCodec as unknown as Codec<T>;\n}\n","\"use client\";\n\n/**\n * Interface allows swapping LocalStorage for SessionStorage or Async Storage later\n */\nexport interface StorageAdapter {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface SubscribeOptions {\n crossTabSync?: boolean;\n pollingIntervalMs?: number | null;\n}\n\ninterface ListenerOptions {\n crossTabSync: boolean;\n pollingIntervalMs: number | null;\n}\n\ninterface StorageSyncManagerOptions {\n enableCrossTabSync?: boolean;\n enablePolling?: boolean;\n}\n\nclass MemoryStorageAdapter implements StorageAdapter {\n private store = new Map<string, string>();\n\n getItem(key: string) {\n return this.store.has(key) ? this.store.get(key)! : null;\n }\n\n setItem(key: string, value: string) {\n this.store.set(key, value);\n }\n\n removeItem(key: string) {\n this.store.delete(key);\n }\n}\n\nclass FallbackStorageAdapter implements StorageAdapter {\n constructor(\n private primary: StorageAdapter,\n private fallback: StorageAdapter,\n ) {}\n\n getItem(key: string) {\n const fallbackValue = this.fallback.getItem(key);\n if (fallbackValue !== null) return fallbackValue;\n return this.primary.getItem(key);\n }\n\n setItem(key: string, value: string) {\n try {\n this.primary.setItem(key, value);\n this.fallback.removeItem(key);\n } catch {\n this.fallback.setItem(key, value);\n }\n }\n\n removeItem(key: string) {\n try {\n this.primary.removeItem(key);\n } finally {\n this.fallback.removeItem(key);\n }\n }\n}\n\n// Singleton manager ensures we only have ONE listener per key globally\nclass StorageSyncManager {\n private listeners = new Map<string, Map<() => void, ListenerOptions>>();\n private pollingIntervalId: number | null = null;\n private pollingIntervalMsActive: number | null = null;\n private enableCrossTabSync: boolean;\n private enablePolling: boolean;\n\n constructor(\n public readonly storage: StorageAdapter,\n private defaultPollingIntervalMs = 2000,\n options: StorageSyncManagerOptions = {},\n ) {\n this.enableCrossTabSync = options.enableCrossTabSync ?? true;\n this.enablePolling = options.enablePolling ?? true;\n\n if (typeof window !== \"undefined\" && this.enableCrossTabSync) {\n // 1. Cross-tab sync (\"storage\" event is a built-in browser feature that fires\n // when localStorage/sessionStorage changes in ANOTHER tab)\n // We do not remove this listener because this manager is a singleton meant to last\n // the entire application lifecycle. It is effectively cleaned up when the page unloads.\n window.addEventListener(\"storage\", (e) => {\n if (e.key && this.listeners.has(e.key)) {\n this.notifyCrossTab(e.key);\n }\n });\n // 2. Start polling for DevTools or direct window.localStorage changes (robustness)\n // We only poll if there are listeners. Lazy start in subscribe.\n }\n }\n\n subscribe(key: string, callback: () => void, options: SubscribeOptions = {}) {\n if (!this.listeners.has(key)) {\n this.listeners.set(key, new Map());\n }\n const listenerOptions: ListenerOptions = {\n crossTabSync: this.enableCrossTabSync && (options.crossTabSync ?? true),\n pollingIntervalMs: this.enablePolling\n ? this.normalizePollingInterval(options.pollingIntervalMs)\n : null,\n };\n this.listeners.get(key)!.set(callback, listenerOptions);\n\n this.updatePollingInterval();\n\n return () => {\n const listenersForKey = this.listeners.get(key);\n listenersForKey?.delete(callback);\n if (listenersForKey?.size === 0) {\n this.listeners.delete(key);\n }\n this.updatePollingInterval();\n };\n }\n\n // Called when WE change the value in this tab\n notify(key: string) {\n this.notifyListeners(key);\n }\n\n // Keep a cache of values to detect changes during polling\n private snapshotCache = new Map<string, string | null>();\n\n private notifyListeners(\n key: string,\n predicate?: (options: ListenerOptions) => boolean,\n ) {\n this.listeners.get(key)?.forEach((options, cb) => {\n if (!predicate || predicate(options)) {\n cb();\n }\n });\n }\n\n private notifyCrossTab(key: string) {\n this.notifyListeners(key, (options) => options.crossTabSync);\n }\n\n private notifyPolling(key: string) {\n this.notifyListeners(key, (options) => options.pollingIntervalMs !== null);\n }\n\n private normalizePollingInterval(\n pollingIntervalMs: number | null | undefined,\n ) {\n if (pollingIntervalMs === null) return null;\n if (pollingIntervalMs === undefined) return this.defaultPollingIntervalMs;\n if (pollingIntervalMs <= 0 || Number.isNaN(pollingIntervalMs)) {\n return null;\n }\n return pollingIntervalMs;\n }\n\n private getMinPollingIntervalMs() {\n let minInterval: number | null = null;\n this.listeners.forEach((listenersForKey) => {\n listenersForKey.forEach((options) => {\n if (options.pollingIntervalMs === null) return;\n if (minInterval === null || options.pollingIntervalMs < minInterval) {\n minInterval = options.pollingIntervalMs;\n }\n });\n });\n return minInterval;\n }\n\n private updatePollingInterval() {\n if (typeof window === \"undefined\") return;\n if (!this.enablePolling) return;\n\n const nextInterval = this.getMinPollingIntervalMs();\n if (nextInterval === null) {\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n this.pollingIntervalId = null;\n this.pollingIntervalMsActive = null;\n this.snapshotCache.clear();\n return;\n }\n\n if (\n this.pollingIntervalId &&\n this.pollingIntervalMsActive === nextInterval\n ) {\n return;\n }\n\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n\n this.pollingIntervalId = window.setInterval(() => {\n this.listeners.forEach((listenersForKey, key) => {\n const shouldPoll = Array.from(listenersForKey.values()).some(\n (options) => options.pollingIntervalMs !== null,\n );\n if (!shouldPoll) return;\n\n const currentValue = this.storage.getItem(key);\n const lastValue = this.snapshotCache.get(key);\n\n if (currentValue !== lastValue) {\n this.snapshotCache.set(key, currentValue);\n this.notifyPolling(key);\n }\n });\n }, nextInterval);\n this.pollingIntervalMsActive = nextInterval;\n }\n}\n\n// Lazy-initialized singletons to avoid SSR crashes (window is not defined on server)\nconst localStorageFallback = new MemoryStorageAdapter();\nconst sessionStorageFallback = new MemoryStorageAdapter();\n\nlet _localStorageSync: StorageSyncManager | null = null;\nlet _sessionStorageSync: StorageSyncManager | null = null;\nlet _memoryStorageSync: StorageSyncManager | null = null;\n\nfunction getLocalStorageSync(): StorageSyncManager {\n if (!_localStorageSync) {\n if (typeof window !== \"undefined\") {\n _localStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(window.localStorage, localStorageFallback),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _localStorageSync;\n}\n\nfunction getSessionStorageSync(): StorageSyncManager {\n if (!_sessionStorageSync) {\n if (typeof window !== \"undefined\") {\n _sessionStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(\n window.sessionStorage,\n sessionStorageFallback,\n ),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _sessionStorageSync;\n}\n\nfunction getMemoryStorageSync(): StorageSyncManager {\n if (!_memoryStorageSync) {\n _memoryStorageSync = new StorageSyncManager(\n new MemoryStorageAdapter(),\n 2000,\n {\n enableCrossTabSync: false,\n enablePolling: false,\n },\n );\n }\n return _memoryStorageSync;\n}\n\n/**\n * Proxy object for localStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const localStorageSync = {\n get storage() {\n return getLocalStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getLocalStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getLocalStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for sessionStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const sessionStorageSync = {\n get storage() {\n return getSessionStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getSessionStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getSessionStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for memory storage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const memoryStorageSync = {\n get storage() {\n return getMemoryStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getMemoryStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getMemoryStorageSync().notify(key);\n },\n};\n","\"use client\";\n\nimport { Codec, inferCodec, JsonCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\nimport {\n StoragePersistedStateOptions,\n StorageType,\n} from \"./useStoragePersistedState\";\n\nfunction getSyncManager(storageType: StorageType | undefined) {\n if (storageType === \"sessionStorage\") return sessionStorageSync;\n if (storageType === \"memory\") return memoryStorageSync;\n return localStorageSync;\n}\n\nfunction resolveCodec<T>(\n key: string,\n valueHint: T,\n options?: StoragePersistedStateOptions<T>,\n): Codec<T> {\n if (options?.codec) return options.codec;\n\n if ((valueHint === undefined || valueHint === null) && !options?.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n return inferCodec(valueHint);\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Read a persisted value from storage using the same codec behavior as the hook.\n *\n * Returns the provided defaultValue when the key is missing or parsing fails.\n */\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): T;\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\n/**\n * Read a persisted value from storage using an explicit codec.\n *\n * Use this overload when defaultValue is null or undefined.\n */\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): T | null;\n\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n const codec = resolveCodec(key, defaultValue, options);\n const raw = adapter.getItem(key);\n\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n try {\n return codec.decode(raw);\n } catch (error) {\n console.error(`Error parsing storage key \"${key}\"`, error);\n return defaultValue as T;\n }\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Set a persisted value in storage and notify active hooks for the same key.\n *\n * Supports functional updates using the current decoded value.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): void;\n\n// Overload 2: Explicit Codec provided, newValue can be null or undefined\n/**\n * Set a persisted value in storage using an explicit codec and notify listeners.\n *\n * Use this overload when the new value is null or undefined or when you want custom serialization.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): void;\n\nexport function setStoragePersistedState<T>(\n key: string,\n newValueOrFn: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n\n let codec: Codec<T>;\n if (!(newValueOrFn instanceof Function)) {\n codec = resolveCodec(key, newValueOrFn, options);\n } else {\n // For functional updates, we cannot infer from newValue\n if (!options.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses functional update without explicit Codec. defaulting to JSON.`,\n );\n }\n codec = options.codec ?? (JsonCodec as unknown as Codec<T>);\n }\n\n try {\n const currentRaw = adapter.getItem(key);\n const current = currentRaw !== null ? codec.decode(currentRaw) : null;\n\n const newValue =\n newValueOrFn instanceof Function ? newValueOrFn(current) : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/useStoragePersistedState.ts","../src/codecs.ts","../src/storage.ts","../src/storagePersistedState.ts"],"sourcesContent":["export { useStoragePersistedState } from \"./useStoragePersistedState\";\nexport type {\n StoragePersistedStateOptions,\n StorageType,\n} from \"./useStoragePersistedState\";\nexport type { Codec } from \"./codecs\";\nexport {\n JsonCodec,\n StringCodec,\n BooleanCodec,\n NumberCodec,\n inferCodec,\n} from \"./codecs\";\nexport {\n readStoragePersistedState,\n setStoragePersistedState,\n} from \"./storagePersistedState\";\n","\"use client\";\n\nimport { useCallback, useMemo, useRef } from \"react\";\nimport { useSyncExternalStore } from \"use-sync-external-store/shim\";\nimport { Codec, inferCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\n\nexport type StorageType = \"localStorage\" | \"sessionStorage\" | \"memory\";\n\n/**\n * Options for the useStoragePersistedState hook\n */\nexport interface StoragePersistedStateOptions<T> {\n /**\n * Explicit codec for when defaultValue is null or undefined, for complex types, or special cases (e.g., data migration on read).\n * If not provided, codec is inferred from defaultValue type.\n */\n codec?: Codec<T>;\n /**\n * Storage type to use: 'localStorage' (default), 'sessionStorage', or 'memory'.\n */\n storageType?: StorageType;\n /**\n * Enable cross-tab synchronization via the StorageEvent. Note: disabling this will not stop other tabs from updating localStorage, but this hook will not automatically respond to those changes.\n * defaults to true.\n */\n crossTabSync?: boolean;\n /**\n * Polling interval in milliseconds for detecting storage changes made outside of React (e.g., DevTools, direct localStorage manipulation). Set to null to disable polling.\n * defaults to 2000ms.\n */\n pollingIntervalMs?: number | null;\n}\n\n// Overload 1: Default provided, T inferred\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): [T, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): [T | null, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Implementation\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager =\n options.storageType === \"sessionStorage\"\n ? sessionStorageSync\n : options.storageType === \"memory\"\n ? memoryStorageSync\n : localStorageSync;\n const adapter = syncManager.storage;\n\n // 1. Determine the codec.\n // If no explicit codec is passed, we try to infer it from defaultValue.\n // If defaultValue is undefined and no codec is passed, we fall back to JSON.\n const codec = useMemo(() => {\n if (options?.codec) return options.codec;\n return inferCodec(defaultValue);\n }, [defaultValue, options?.codec]);\n\n if ((defaultValue === undefined || defaultValue === null) && !options.codec) {\n console.warn(\n `useStorage: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n // Memoize the decoded value to prevent infinite loops in useSyncExternalStore\n // when the codec returns a new object reference (e.g. JSON.parse).\n const lastRaw = useRef<string | null>(null); // string | null, because storage returns null for missing keys\n const lastParsed = useRef<T | undefined>(undefined);\n\n const getSnapshot = useCallback(() => {\n const raw = adapter.getItem(key);\n\n // Return default if storage is missing the key\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n // If raw value matches cache, return cached object.\n if (raw === lastRaw.current) {\n if (lastParsed.current === undefined) {\n return null as T;\n }\n return lastParsed.current as T;\n }\n\n // Decode new raw value\n try {\n const decoded = codec.decode(raw);\n\n lastRaw.current = raw;\n lastParsed.current = decoded;\n return decoded;\n } catch (e) {\n console.error(`Error parsing storage key \"${key}\"`, e);\n return defaultValue as T;\n }\n }, [adapter, key, codec, defaultValue]);\n\n // 2. Subscribe to the external store (Local/SessionStorage + Polling/Events)\n // useSyncExternalStore handles the hydration mismatch automatically by\n // taking a `getServerSnapshot` (returning defaultValue).\n const value = useSyncExternalStore(\n (callback) =>\n syncManager.subscribe(key, callback, {\n crossTabSync: options.crossTabSync,\n pollingIntervalMs: options.pollingIntervalMs,\n }),\n getSnapshot,\n () => defaultValue as T, // Server Snapshot\n );\n\n // 3. Create the Setter\n const setValue = useCallback(\n (newValueOrFn: T | ((prev: T) => T)) => {\n try {\n const currentRaw = adapter.getItem(key);\n const current =\n currentRaw !== null ? codec.decode(currentRaw) : defaultValue;\n\n const newValue =\n newValueOrFn instanceof Function\n ? newValueOrFn(current)\n : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n lastRaw.current = encoded;\n lastParsed.current = newValue;\n\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n // Notify other hooks/tabs\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n },\n [adapter, key, codec, defaultValue, syncManager],\n );\n\n const removeItem = useCallback(() => {\n try {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error removing storage key \"${key}\":`, error);\n }\n }, [adapter, key, syncManager]);\n\n return [value, setValue, removeItem] as const;\n}\n","/**\n * Defines how to serialize/deserialize a value from localStorage.\n * null means the key doesn't exist.\n */\nexport interface Codec<T> {\n encode: (value: T) => string | null;\n decode: (value: string | null) => T;\n}\n\n/**\n * A robust JSON codec that handles parsing errors gracefully.\n * Works with objects, arrays, and other JSON-serializable values.\n *\n * @example\n * ```ts\n * const [user, setUser] = useStoragePersistedState<User | null>(\n * \"user\",\n * null,\n * { codec: JsonCodec }\n * );\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const JsonCodec: Codec<any> = {\n encode: (value) => {\n if (value === null) return null;\n return JSON.stringify(value);\n },\n decode: (value) => {\n if (value === null) return null;\n try {\n return JSON.parse(value);\n } catch (e) {\n console.warn(`LocalStorage parse error for value \"${value}\".`, e);\n throw e;\n }\n },\n};\n\n/**\n * Codec for string values. Stores strings as-is without any transformation.\n *\n * @example\n * ```ts\n * const [name, setName] = useStoragePersistedState(\"name\", \"Guest\");\n * // Automatically uses StringCodec because defaultValue is a string\n * ```\n */\nexport const StringCodec: Codec<string | null> = {\n encode: (value) => value ?? null,\n decode: (value) => value ?? null,\n};\n\n/**\n * Codec for boolean values. Stores as \"true\" or \"false\" strings.\n *\n * @example\n * ```ts\n * const [isDark, setIsDark] = useStoragePersistedState(\"darkMode\", false);\n * // Automatically uses BooleanCodec because defaultValue is a boolean\n * ```\n */\nexport const BooleanCodec: Codec<boolean> = {\n encode: (value) => (value ? \"true\" : \"false\"),\n decode: (value) => value === \"true\",\n};\n\n/**\n * Codec for number values. Handles NaN, Infinity, and -Infinity correctly.\n * Returns null for unparseable values (e.g., empty string, invalid number string).\n *\n * @example\n * ```ts\n * const [count, setCount] = useStoragePersistedState(\"count\", 0);\n * // Automatically uses NumberCodec because defaultValue is a number\n * ```\n */\nexport const NumberCodec: Codec<number | null> = {\n encode: (value) => {\n if (value === null) return null;\n return String(value);\n },\n decode: (value) => {\n if (value === null || value === \"\") return null;\n // Handle special numeric values\n if (value === \"NaN\") return NaN;\n if (value === \"Infinity\") return Infinity;\n if (value === \"-Infinity\") return -Infinity;\n const parsed = Number(value);\n return isNaN(parsed) ? null : parsed;\n },\n};\n\n/**\n * Infers the appropriate codec based on the default value's type.\n * Used internally when the user doesn't provide an explicit codec.\n *\n * - `boolean` → BooleanCodec\n * - `number` → NumberCodec\n * - `string` → StringCodec\n * - `object`, `array`, `undefined`, `null` → JsonCodec\n *\n * @param defaultValue - The default value to infer the codec from\n * @returns The inferred codec for the given value type\n */\nexport function inferCodec<T>(defaultValue: T): Codec<T> {\n const type = typeof defaultValue;\n\n // Type assertions through unknown are needed because we're doing runtime type inference\n // The actual codec returned will match the runtime type of defaultValue\n if (type === \"boolean\") return BooleanCodec as unknown as Codec<T>;\n if (type === \"number\") return NumberCodec as unknown as Codec<T>;\n if (type === \"string\") return StringCodec as unknown as Codec<T>;\n\n // Default to JSON for objects, arrays, or undefined\n return JsonCodec as unknown as Codec<T>;\n}\n","\"use client\";\n\n/**\n * Interface allows swapping LocalStorage for SessionStorage or Async Storage later\n */\nexport interface StorageAdapter {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface SubscribeOptions {\n crossTabSync?: boolean;\n pollingIntervalMs?: number | null;\n}\n\ninterface ListenerOptions {\n crossTabSync: boolean;\n pollingIntervalMs: number | null;\n}\n\ninterface StorageSyncManagerOptions {\n enableCrossTabSync?: boolean;\n enablePolling?: boolean;\n}\n\nclass MemoryStorageAdapter implements StorageAdapter {\n private store = new Map<string, string>();\n\n getItem(key: string) {\n return this.store.has(key) ? this.store.get(key)! : null;\n }\n\n setItem(key: string, value: string) {\n this.store.set(key, value);\n }\n\n removeItem(key: string) {\n this.store.delete(key);\n }\n}\n\nclass FallbackStorageAdapter implements StorageAdapter {\n constructor(\n private primary: StorageAdapter,\n private fallback: StorageAdapter,\n ) {}\n\n getItem(key: string) {\n const fallbackValue = this.fallback.getItem(key);\n if (fallbackValue !== null) return fallbackValue;\n return this.primary.getItem(key);\n }\n\n setItem(key: string, value: string) {\n try {\n this.primary.setItem(key, value);\n this.fallback.removeItem(key);\n } catch {\n this.fallback.setItem(key, value);\n }\n }\n\n removeItem(key: string) {\n try {\n this.primary.removeItem(key);\n } finally {\n this.fallback.removeItem(key);\n }\n }\n}\n\n// Singleton manager ensures we only have ONE listener per key globally\nclass StorageSyncManager {\n private listeners = new Map<string, Map<() => void, ListenerOptions>>();\n private pollingIntervalId: number | null = null;\n private pollingIntervalMsActive: number | null = null;\n private enableCrossTabSync: boolean;\n private enablePolling: boolean;\n\n constructor(\n public readonly storage: StorageAdapter,\n private defaultPollingIntervalMs = 2000,\n options: StorageSyncManagerOptions = {},\n ) {\n this.enableCrossTabSync = options.enableCrossTabSync ?? true;\n this.enablePolling = options.enablePolling ?? true;\n\n if (typeof window !== \"undefined\" && this.enableCrossTabSync) {\n // 1. Cross-tab sync (\"storage\" event is a built-in browser feature that fires\n // when localStorage/sessionStorage changes in ANOTHER tab)\n // We do not remove this listener because this manager is a singleton meant to last\n // the entire application lifecycle. It is effectively cleaned up when the page unloads.\n window.addEventListener(\"storage\", (e) => {\n if (e.key && this.listeners.has(e.key)) {\n this.notifyCrossTab(e.key);\n }\n });\n // 2. Start polling for DevTools or direct window.localStorage changes (robustness)\n // We only poll if there are listeners. Lazy start in subscribe.\n }\n }\n\n subscribe(key: string, callback: () => void, options: SubscribeOptions = {}) {\n if (!this.listeners.has(key)) {\n this.listeners.set(key, new Map());\n }\n const listenerOptions: ListenerOptions = {\n crossTabSync: this.enableCrossTabSync && (options.crossTabSync ?? true),\n pollingIntervalMs: this.enablePolling\n ? this.normalizePollingInterval(options.pollingIntervalMs)\n : null,\n };\n this.listeners.get(key)!.set(callback, listenerOptions);\n\n this.updatePollingInterval();\n\n return () => {\n const listenersForKey = this.listeners.get(key);\n listenersForKey?.delete(callback);\n if (listenersForKey?.size === 0) {\n this.listeners.delete(key);\n }\n this.updatePollingInterval();\n };\n }\n\n // Called when WE change the value in this tab\n notify(key: string) {\n this.notifyListeners(key);\n }\n\n // Keep a cache of values to detect changes during polling\n private snapshotCache = new Map<string, string | null>();\n\n private notifyListeners(\n key: string,\n predicate?: (options: ListenerOptions) => boolean,\n ) {\n this.listeners.get(key)?.forEach((options, cb) => {\n if (!predicate || predicate(options)) {\n cb();\n }\n });\n }\n\n private notifyCrossTab(key: string) {\n this.notifyListeners(key, (options) => options.crossTabSync);\n }\n\n private notifyPolling(key: string) {\n this.notifyListeners(key, (options) => options.pollingIntervalMs !== null);\n }\n\n private normalizePollingInterval(\n pollingIntervalMs: number | null | undefined,\n ) {\n if (pollingIntervalMs === null) return null;\n if (pollingIntervalMs === undefined) return this.defaultPollingIntervalMs;\n if (pollingIntervalMs <= 0 || Number.isNaN(pollingIntervalMs)) {\n return null;\n }\n return pollingIntervalMs;\n }\n\n private getMinPollingIntervalMs() {\n let minInterval: number | null = null;\n this.listeners.forEach((listenersForKey) => {\n listenersForKey.forEach((options) => {\n if (options.pollingIntervalMs === null) return;\n if (minInterval === null || options.pollingIntervalMs < minInterval) {\n minInterval = options.pollingIntervalMs;\n }\n });\n });\n return minInterval;\n }\n\n private updatePollingInterval() {\n if (typeof window === \"undefined\") return;\n if (!this.enablePolling) return;\n\n const nextInterval = this.getMinPollingIntervalMs();\n if (nextInterval === null) {\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n this.pollingIntervalId = null;\n this.pollingIntervalMsActive = null;\n this.snapshotCache.clear();\n return;\n }\n\n if (\n this.pollingIntervalId &&\n this.pollingIntervalMsActive === nextInterval\n ) {\n return;\n }\n\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n\n this.pollingIntervalId = window.setInterval(() => {\n this.listeners.forEach((listenersForKey, key) => {\n const shouldPoll = Array.from(listenersForKey.values()).some(\n (options) => options.pollingIntervalMs !== null,\n );\n if (!shouldPoll) return;\n\n const currentValue = this.storage.getItem(key);\n const lastValue = this.snapshotCache.get(key);\n\n if (currentValue !== lastValue) {\n this.snapshotCache.set(key, currentValue);\n this.notifyPolling(key);\n }\n });\n }, nextInterval);\n this.pollingIntervalMsActive = nextInterval;\n }\n}\n\n// Lazy-initialized singletons to avoid SSR crashes (window is not defined on server)\nconst localStorageFallback = new MemoryStorageAdapter();\nconst sessionStorageFallback = new MemoryStorageAdapter();\n\nlet _localStorageSync: StorageSyncManager | null = null;\nlet _sessionStorageSync: StorageSyncManager | null = null;\nlet _memoryStorageSync: StorageSyncManager | null = null;\n\nfunction getLocalStorageSync(): StorageSyncManager {\n if (!_localStorageSync) {\n if (typeof window !== \"undefined\") {\n _localStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(window.localStorage, localStorageFallback),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _localStorageSync;\n}\n\nfunction getSessionStorageSync(): StorageSyncManager {\n if (!_sessionStorageSync) {\n if (typeof window !== \"undefined\") {\n _sessionStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(\n window.sessionStorage,\n sessionStorageFallback,\n ),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _sessionStorageSync;\n}\n\nfunction getMemoryStorageSync(): StorageSyncManager {\n if (!_memoryStorageSync) {\n _memoryStorageSync = new StorageSyncManager(\n new MemoryStorageAdapter(),\n 2000,\n {\n enableCrossTabSync: false,\n enablePolling: false,\n },\n );\n }\n return _memoryStorageSync;\n}\n\n/**\n * Proxy object for localStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const localStorageSync = {\n get storage() {\n return getLocalStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getLocalStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getLocalStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for sessionStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const sessionStorageSync = {\n get storage() {\n return getSessionStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getSessionStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getSessionStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for memory storage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const memoryStorageSync = {\n get storage() {\n return getMemoryStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getMemoryStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getMemoryStorageSync().notify(key);\n },\n};\n","\"use client\";\n\nimport { Codec, inferCodec, JsonCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\nimport {\n StoragePersistedStateOptions,\n StorageType,\n} from \"./useStoragePersistedState\";\n\nfunction getSyncManager(storageType: StorageType | undefined) {\n if (storageType === \"sessionStorage\") return sessionStorageSync;\n if (storageType === \"memory\") return memoryStorageSync;\n return localStorageSync;\n}\n\nfunction resolveCodec<T>(\n key: string,\n valueHint: T,\n options?: StoragePersistedStateOptions<T>,\n): Codec<T> {\n if (options?.codec) return options.codec;\n\n if ((valueHint === undefined || valueHint === null) && !options?.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n return inferCodec(valueHint);\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Read a persisted value from storage using the same codec behavior as the hook.\n *\n * Returns the provided defaultValue when the key is missing or parsing fails.\n */\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): T;\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\n/**\n * Read a persisted value from storage using an explicit codec.\n *\n * Use this overload when defaultValue is null or undefined.\n */\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): T | null;\n\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n const codec = resolveCodec(key, defaultValue, options);\n const raw = adapter.getItem(key);\n\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n try {\n return codec.decode(raw);\n } catch (error) {\n console.error(`Error parsing storage key \"${key}\"`, error);\n return defaultValue as T;\n }\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Set a persisted value in storage and notify active hooks for the same key.\n *\n * Supports functional updates using the current decoded value.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): void;\n\n// Overload 2: Explicit Codec provided, newValue can be null or undefined\n/**\n * Set a persisted value in storage using an explicit codec and notify listeners.\n *\n * Use this overload when the new value is null or undefined or when you want custom serialization.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): void;\n\nexport function setStoragePersistedState<T>(\n key: string,\n newValueOrFn: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n\n let codec: Codec<T>;\n if (!(newValueOrFn instanceof Function)) {\n codec = resolveCodec(key, newValueOrFn, options);\n } else {\n // For functional updates, we cannot infer from newValue\n if (!options.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses functional update without explicit Codec. defaulting to JSON.`,\n );\n }\n codec = options.codec ?? (JsonCodec as unknown as Codec<T>);\n }\n\n try {\n const currentRaw = adapter.getItem(key);\n const current = currentRaw !== null ? codec.decode(currentRaw) : null;\n\n const newValue =\n newValueOrFn instanceof Function ? newValueOrFn(current) : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,mBAA6C;AAC7C,kBAAqC;;;ACoB9B,IAAM,YAAwB;AAAA,EACnC,QAAQ,CAAC,UAAU;AACjB,QAAI,UAAU,KAAM,QAAO;AAC3B,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B;AAAA,EACA,QAAQ,CAAC,UAAU;AACjB,QAAI,UAAU,KAAM,QAAO;AAC3B,QAAI;AACF,aAAO,KAAK,MAAM,KAAK;AAAA,IACzB,SAAS,GAAG;AACV,cAAQ,KAAK,uCAAuC,KAAK,MAAM,CAAC;AAChE,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAWO,IAAM,cAAoC;AAAA,EAC/C,QAAQ,CAAC,UAAU,SAAS;AAAA,EAC5B,QAAQ,CAAC,UAAU,SAAS;AAC9B;AAWO,IAAM,eAA+B;AAAA,EAC1C,QAAQ,CAAC,UAAW,QAAQ,SAAS;AAAA,EACrC,QAAQ,CAAC,UAAU,UAAU;AAC/B;AAYO,IAAM,cAAoC;AAAA,EAC/C,QAAQ,CAAC,UAAU;AACjB,QAAI,UAAU,KAAM,QAAO;AAC3B,WAAO,OAAO,KAAK;AAAA,EACrB;AAAA,EACA,QAAQ,CAAC,UAAU;AACjB,QAAI,UAAU,QAAQ,UAAU,GAAI,QAAO;AAE3C,QAAI,UAAU,MAAO,QAAO;AAC5B,QAAI,UAAU,WAAY,QAAO;AACjC,QAAI,UAAU,YAAa,QAAO;AAClC,UAAM,SAAS,OAAO,KAAK;AAC3B,WAAO,MAAM,MAAM,IAAI,OAAO;AAAA,EAChC;AACF;AAcO,SAAS,WAAc,cAA2B;AACvD,QAAM,OAAO,OAAO;AAIpB,MAAI,SAAS,UAAW,QAAO;AAC/B,MAAI,SAAS,SAAU,QAAO;AAC9B,MAAI,SAAS,SAAU,QAAO;AAG9B,SAAO;AACT;;;AC1FA,IAAM,uBAAN,MAAqD;AAAA,EAArD;AACE,wBAAQ,SAAQ,oBAAI,IAAoB;AAAA;AAAA,EAExC,QAAQ,KAAa;AACnB,WAAO,KAAK,MAAM,IAAI,GAAG,IAAI,KAAK,MAAM,IAAI,GAAG,IAAK;AAAA,EACtD;AAAA,EAEA,QAAQ,KAAa,OAAe;AAClC,SAAK,MAAM,IAAI,KAAK,KAAK;AAAA,EAC3B;AAAA,EAEA,WAAW,KAAa;AACtB,SAAK,MAAM,OAAO,GAAG;AAAA,EACvB;AACF;AAEA,IAAM,yBAAN,MAAuD;AAAA,EACrD,YACU,SACA,UACR;AAFQ;AACA;AAAA,EACP;AAAA,EAEH,QAAQ,KAAa;AACnB,UAAM,gBAAgB,KAAK,SAAS,QAAQ,GAAG;AAC/C,QAAI,kBAAkB,KAAM,QAAO;AACnC,WAAO,KAAK,QAAQ,QAAQ,GAAG;AAAA,EACjC;AAAA,EAEA,QAAQ,KAAa,OAAe;AAClC,QAAI;AACF,WAAK,QAAQ,QAAQ,KAAK,KAAK;AAC/B,WAAK,SAAS,WAAW,GAAG;AAAA,IAC9B,QAAQ;AACN,WAAK,SAAS,QAAQ,KAAK,KAAK;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,WAAW,KAAa;AACtB,QAAI;AACF,WAAK,QAAQ,WAAW,GAAG;AAAA,IAC7B,UAAE;AACA,WAAK,SAAS,WAAW,GAAG;AAAA,IAC9B;AAAA,EACF;AACF;AAGA,IAAM,qBAAN,MAAyB;AAAA,EAOvB,YACkB,SACR,2BAA2B,KACnC,UAAqC,CAAC,GACtC;AAHgB;AACR;AARV,wBAAQ,aAAY,oBAAI,IAA8C;AACtE,wBAAQ,qBAAmC;AAC3C,wBAAQ,2BAAyC;AACjD,wBAAQ;AACR,wBAAQ;AAuDR;AAAA,wBAAQ,iBAAgB,oBAAI,IAA2B;AAhDrD,SAAK,qBAAqB,QAAQ,sBAAsB;AACxD,SAAK,gBAAgB,QAAQ,iBAAiB;AAE9C,QAAI,OAAO,WAAW,eAAe,KAAK,oBAAoB;AAK5D,aAAO,iBAAiB,WAAW,CAAC,MAAM;AACxC,YAAI,EAAE,OAAO,KAAK,UAAU,IAAI,EAAE,GAAG,GAAG;AACtC,eAAK,eAAe,EAAE,GAAG;AAAA,QAC3B;AAAA,MACF,CAAC;AAAA,IAGH;AAAA,EACF;AAAA,EAEA,UAAU,KAAa,UAAsB,UAA4B,CAAC,GAAG;AAC3E,QAAI,CAAC,KAAK,UAAU,IAAI,GAAG,GAAG;AAC5B,WAAK,UAAU,IAAI,KAAK,oBAAI,IAAI,CAAC;AAAA,IACnC;AACA,UAAM,kBAAmC;AAAA,MACvC,cAAc,KAAK,uBAAuB,QAAQ,gBAAgB;AAAA,MAClE,mBAAmB,KAAK,gBACpB,KAAK,yBAAyB,QAAQ,iBAAiB,IACvD;AAAA,IACN;AACA,SAAK,UAAU,IAAI,GAAG,EAAG,IAAI,UAAU,eAAe;AAEtD,SAAK,sBAAsB;AAE3B,WAAO,MAAM;AACX,YAAM,kBAAkB,KAAK,UAAU,IAAI,GAAG;AAC9C,uBAAiB,OAAO,QAAQ;AAChC,UAAI,iBAAiB,SAAS,GAAG;AAC/B,aAAK,UAAU,OAAO,GAAG;AAAA,MAC3B;AACA,WAAK,sBAAsB;AAAA,IAC7B;AAAA,EACF;AAAA;AAAA,EAGA,OAAO,KAAa;AAClB,SAAK,gBAAgB,GAAG;AAAA,EAC1B;AAAA,EAKQ,gBACN,KACA,WACA;AACA,SAAK,UAAU,IAAI,GAAG,GAAG,QAAQ,CAAC,SAAS,OAAO;AAChD,UAAI,CAAC,aAAa,UAAU,OAAO,GAAG;AACpC,WAAG;AAAA,MACL;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,eAAe,KAAa;AAClC,SAAK,gBAAgB,KAAK,CAAC,YAAY,QAAQ,YAAY;AAAA,EAC7D;AAAA,EAEQ,cAAc,KAAa;AACjC,SAAK,gBAAgB,KAAK,CAAC,YAAY,QAAQ,sBAAsB,IAAI;AAAA,EAC3E;AAAA,EAEQ,yBACN,mBACA;AACA,QAAI,sBAAsB,KAAM,QAAO;AACvC,QAAI,sBAAsB,OAAW,QAAO,KAAK;AACjD,QAAI,qBAAqB,KAAK,OAAO,MAAM,iBAAiB,GAAG;AAC7D,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,0BAA0B;AAChC,QAAI,cAA6B;AACjC,SAAK,UAAU,QAAQ,CAAC,oBAAoB;AAC1C,sBAAgB,QAAQ,CAAC,YAAY;AACnC,YAAI,QAAQ,sBAAsB,KAAM;AACxC,YAAI,gBAAgB,QAAQ,QAAQ,oBAAoB,aAAa;AACnE,wBAAc,QAAQ;AAAA,QACxB;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AACD,WAAO;AAAA,EACT;AAAA,EAEQ,wBAAwB;AAC9B,QAAI,OAAO,WAAW,YAAa;AACnC,QAAI,CAAC,KAAK,cAAe;AAEzB,UAAM,eAAe,KAAK,wBAAwB;AAClD,QAAI,iBAAiB,MAAM;AACzB,UAAI,KAAK,mBAAmB;AAC1B,sBAAc,KAAK,iBAAiB;AAAA,MACtC;AACA,WAAK,oBAAoB;AACzB,WAAK,0BAA0B;AAC/B,WAAK,cAAc,MAAM;AACzB;AAAA,IACF;AAEA,QACE,KAAK,qBACL,KAAK,4BAA4B,cACjC;AACA;AAAA,IACF;AAEA,QAAI,KAAK,mBAAmB;AAC1B,oBAAc,KAAK,iBAAiB;AAAA,IACtC;AAEA,SAAK,oBAAoB,OAAO,YAAY,MAAM;AAChD,WAAK,UAAU,QAAQ,CAAC,iBAAiB,QAAQ;AAC/C,cAAM,aAAa,MAAM,KAAK,gBAAgB,OAAO,CAAC,EAAE;AAAA,UACtD,CAAC,YAAY,QAAQ,sBAAsB;AAAA,QAC7C;AACA,YAAI,CAAC,WAAY;AAEjB,cAAM,eAAe,KAAK,QAAQ,QAAQ,GAAG;AAC7C,cAAM,YAAY,KAAK,cAAc,IAAI,GAAG;AAE5C,YAAI,iBAAiB,WAAW;AAC9B,eAAK,cAAc,IAAI,KAAK,YAAY;AACxC,eAAK,cAAc,GAAG;AAAA,QACxB;AAAA,MACF,CAAC;AAAA,IACH,GAAG,YAAY;AACf,SAAK,0BAA0B;AAAA,EACjC;AACF;AAGA,IAAM,uBAAuB,IAAI,qBAAqB;AACtD,IAAM,yBAAyB,IAAI,qBAAqB;AAExD,IAAI,oBAA+C;AACnD,IAAI,sBAAiD;AACrD,IAAI,qBAAgD;AAEpD,SAAS,sBAA0C;AACjD,MAAI,CAAC,mBAAmB;AACtB,QAAI,OAAO,WAAW,aAAa;AACjC,0BAAoB,IAAI;AAAA,QACtB,IAAI,uBAAuB,OAAO,cAAc,oBAAoB;AAAA,MACtE;AAAA,IACF,OAAO;AAEL,aAAO,qBAAqB;AAAA,IAC9B;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,wBAA4C;AACnD,MAAI,CAAC,qBAAqB;AACxB,QAAI,OAAO,WAAW,aAAa;AACjC,4BAAsB,IAAI;AAAA,QACxB,IAAI;AAAA,UACF,OAAO;AAAA,UACP;AAAA,QACF;AAAA,MACF;AAAA,IACF,OAAO;AAEL,aAAO,qBAAqB;AAAA,IAC9B;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,uBAA2C;AAClD,MAAI,CAAC,oBAAoB;AACvB,yBAAqB,IAAI;AAAA,MACvB,IAAI,qBAAqB;AAAA,MACzB;AAAA,MACA;AAAA,QACE,oBAAoB;AAAA,QACpB,eAAe;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAMO,IAAM,mBAAmB;AAAA,EAC9B,IAAI,UAAU;AACZ,WAAO,oBAAoB,EAAE;AAAA,EAC/B;AAAA,EACA,UAAU,KAAa,UAAsB,SAA4B;AACvE,WAAO,oBAAoB,EAAE,UAAU,KAAK,UAAU,OAAO;AAAA,EAC/D;AAAA,EACA,OAAO,KAAa;AAClB,WAAO,oBAAoB,EAAE,OAAO,GAAG;AAAA,EACzC;AACF;AAMO,IAAM,qBAAqB;AAAA,EAChC,IAAI,UAAU;AACZ,WAAO,sBAAsB,EAAE;AAAA,EACjC;AAAA,EACA,UAAU,KAAa,UAAsB,SAA4B;AACvE,WAAO,sBAAsB,EAAE,UAAU,KAAK,UAAU,OAAO;AAAA,EACjE;AAAA,EACA,OAAO,KAAa;AAClB,WAAO,sBAAsB,EAAE,OAAO,GAAG;AAAA,EAC3C;AACF;AAMO,IAAM,oBAAoB;AAAA,EAC/B,IAAI,UAAU;AACZ,WAAO,qBAAqB,EAAE;AAAA,EAChC;AAAA,EACA,UAAU,KAAa,UAAsB,SAA4B;AACvE,WAAO,qBAAqB,EAAE,UAAU,KAAK,UAAU,OAAO;AAAA,EAChE;AAAA,EACA,OAAO,KAAa;AAClB,WAAO,qBAAqB,EAAE,OAAO,GAAG;AAAA,EAC1C;AACF;;;AF9QO,SAAS,yBACd,KACA,cACA,UAA2C,CAAC,GAC5C;AACA,QAAM,cACJ,QAAQ,gBAAgB,mBACpB,qBACA,QAAQ,gBAAgB,WACtB,oBACA;AACR,QAAM,UAAU,YAAY;AAK5B,QAAM,YAAQ,sBAAQ,MAAM;AAC1B,QAAI,SAAS,MAAO,QAAO,QAAQ;AACnC,WAAO,WAAW,YAAY;AAAA,EAChC,GAAG,CAAC,cAAc,SAAS,KAAK,CAAC;AAEjC,OAAK,iBAAiB,UAAa,iBAAiB,SAAS,CAAC,QAAQ,OAAO;AAC3E,YAAQ;AAAA,MACN,oBAAoB,GAAG;AAAA,IACzB;AAAA,EACF;AAIA,QAAM,cAAU,qBAAsB,IAAI;AAC1C,QAAM,iBAAa,qBAAsB,MAAS;AAElD,QAAM,kBAAc,0BAAY,MAAM;AACpC,UAAM,MAAM,QAAQ,QAAQ,GAAG;AAG/B,QAAI,QAAQ,QAAQ,iBAAiB,QAAW;AAC9C,aAAO;AAAA,IACT;AAGA,QAAI,QAAQ,QAAQ,SAAS;AAC3B,UAAI,WAAW,YAAY,QAAW;AACpC,eAAO;AAAA,MACT;AACA,aAAO,WAAW;AAAA,IACpB;AAGA,QAAI;AACF,YAAM,UAAU,MAAM,OAAO,GAAG;AAEhC,cAAQ,UAAU;AAClB,iBAAW,UAAU;AACrB,aAAO;AAAA,IACT,SAAS,GAAG;AACV,cAAQ,MAAM,8BAA8B,GAAG,KAAK,CAAC;AACrD,aAAO;AAAA,IACT;AAAA,EACF,GAAG,CAAC,SAAS,KAAK,OAAO,YAAY,CAAC;AAKtC,QAAM,YAAQ;AAAA,IACZ,CAAC,aACC,YAAY,UAAU,KAAK,UAAU;AAAA,MACnC,cAAc,QAAQ;AAAA,MACtB,mBAAmB,QAAQ;AAAA,IAC7B,CAAC;AAAA,IACH;AAAA,IACA,MAAM;AAAA;AAAA,EACR;AAGA,QAAM,eAAW;AAAA,IACf,CAAC,iBAAuC;AACtC,UAAI;AACF,cAAM,aAAa,QAAQ,QAAQ,GAAG;AACtC,cAAM,UACJ,eAAe,OAAO,MAAM,OAAO,UAAU,IAAI;AAEnD,cAAM,WACJ,wBAAwB,WACpB,aAAa,OAAO,IACpB;AAEN,YAAI,aAAa,UAAa,aAAa,MAAM;AAC/C,kBAAQ,UAAU;AAClB,qBAAW,UAAU;AACrB,kBAAQ,WAAW,GAAG;AAAA,QACxB,OAAO;AACL,gBAAM,UAAU,MAAM,OAAO,QAAQ;AACrC,kBAAQ,UAAU;AAClB,qBAAW,UAAU;AAErB,cAAI,YAAY,QAAQ,YAAY,QAAW;AAC7C,oBAAQ,WAAW,GAAG;AAAA,UACxB,OAAO;AACL,oBAAQ,QAAQ,KAAK,OAAO;AAAA,UAC9B;AAAA,QACF;AAGA,oBAAY,OAAO,GAAG;AAAA,MACxB,SAAS,OAAO;AACd,gBAAQ,MAAM,8BAA8B,GAAG,MAAM,KAAK;AAAA,MAC5D;AAAA,IACF;AAAA,IACA,CAAC,SAAS,KAAK,OAAO,cAAc,WAAW;AAAA,EACjD;AAEA,QAAM,iBAAa,0BAAY,MAAM;AACnC,QAAI;AACF,cAAQ,UAAU;AAClB,iBAAW,UAAU;AACrB,cAAQ,WAAW,GAAG;AACtB,kBAAY,OAAO,GAAG;AAAA,IACxB,SAAS,OAAO;AACd,cAAQ,MAAM,+BAA+B,GAAG,MAAM,KAAK;AAAA,IAC7D;AAAA,EACF,GAAG,CAAC,SAAS,KAAK,WAAW,CAAC;AAE9B,SAAO,CAAC,OAAO,UAAU,UAAU;AACrC;;;AGpKA,SAAS,eAAe,aAAsC;AAC5D,MAAI,gBAAgB,iBAAkB,QAAO;AAC7C,MAAI,gBAAgB,SAAU,QAAO;AACrC,SAAO;AACT;AAEA,SAAS,aACP,KACA,WACA,SACU;AACV,MAAI,SAAS,MAAO,QAAO,QAAQ;AAEnC,OAAK,cAAc,UAAa,cAAc,SAAS,CAAC,SAAS,OAAO;AACtE,YAAQ;AAAA,MACN,+BAA+B,GAAG;AAAA,IACpC;AAAA,EACF;AAEA,SAAO,WAAW,SAAS;AAC7B;AA0BO,SAAS,0BACd,KACA,cACA,UAA2C,CAAC,GAC5C;AACA,QAAM,cAAc,eAAe,QAAQ,WAAW;AACtD,QAAM,UAAU,YAAY;AAC5B,QAAM,QAAQ,aAAa,KAAK,cAAc,OAAO;AACrD,QAAM,MAAM,QAAQ,QAAQ,GAAG;AAE/B,MAAI,QAAQ,QAAQ,iBAAiB,QAAW;AAC9C,WAAO;AAAA,EACT;AAEA,MAAI;AACF,WAAO,MAAM,OAAO,GAAG;AAAA,EACzB,SAAS,OAAO;AACd,YAAQ,MAAM,8BAA8B,GAAG,KAAK,KAAK;AACzD,WAAO;AAAA,EACT;AACF;AA0BO,SAAS,yBACd,KACA,cACA,UAA2C,CAAC,GAC5C;AACA,QAAM,cAAc,eAAe,QAAQ,WAAW;AACtD,QAAM,UAAU,YAAY;AAE5B,MAAI;AACJ,MAAI,EAAE,wBAAwB,WAAW;AACvC,YAAQ,aAAa,KAAK,cAAc,OAAO;AAAA,EACjD,OAAO;AAEL,QAAI,CAAC,QAAQ,OAAO;AAClB,cAAQ;AAAA,QACN,+BAA+B,GAAG;AAAA,MACpC;AAAA,IACF;AACA,YAAQ,QAAQ,SAAU;AAAA,EAC5B;AAEA,MAAI;AACF,UAAM,aAAa,QAAQ,QAAQ,GAAG;AACtC,UAAM,UAAU,eAAe,OAAO,MAAM,OAAO,UAAU,IAAI;AAEjE,UAAM,WACJ,wBAAwB,WAAW,aAAa,OAAO,IAAI;AAE7D,QAAI,aAAa,UAAa,aAAa,MAAM;AAC/C,cAAQ,WAAW,GAAG;AAAA,IACxB,OAAO;AACL,YAAM,UAAU,MAAM,OAAO,QAAQ;AACrC,UAAI,YAAY,QAAQ,YAAY,QAAW;AAC7C,gBAAQ,WAAW,GAAG;AAAA,MACxB,OAAO;AACL,gBAAQ,QAAQ,KAAK,OAAO;AAAA,MAC9B;AAAA,IACF;AAEA,gBAAY,OAAO,GAAG;AAAA,EACxB,SAAS,OAAO;AACd,YAAQ,MAAM,8BAA8B,GAAG,MAAM,KAAK;AAAA,EAC5D;AACF;","names":[]}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/useStoragePersistedState.ts","../src/codecs.ts","../src/storage.ts","../src/storagePersistedState.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useMemo, useRef } from \"react\";\nimport { useSyncExternalStore } from \"use-sync-external-store/shim\";\nimport { Codec, inferCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\n\nexport type StorageType = \"localStorage\" | \"sessionStorage\" | \"memory\";\n\n/**\n * Options for the useStoragePersistedState hook\n */\nexport interface StoragePersistedStateOptions<T> {\n /**\n * Explicit codec for when defaultValue is null or undefined, for complex types, or special cases (e.g., data migration on read).\n * If not provided, codec is inferred from defaultValue type.\n */\n codec?: Codec<T>;\n /**\n * Storage type to use: 'localStorage' (default), 'sessionStorage', or 'memory'.\n */\n storageType?: StorageType;\n /**\n * Enable cross-tab synchronization via the StorageEvent. Note: disabling this will not stop other tabs from updating localStorage, but this hook will not automatically respond to those changes.\n * defaults to true.\n */\n crossTabSync?: boolean;\n /**\n * Polling interval in milliseconds for detecting storage changes made outside of React (e.g., DevTools, direct localStorage manipulation). Set to null to disable polling.\n * defaults to 2000ms.\n */\n pollingIntervalMs?: number | null;\n}\n\n// Overload 1: Default provided, T inferred\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): [T, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): [T | null, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Implementation\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager =\n options.storageType === \"sessionStorage\"\n ? sessionStorageSync\n : options.storageType === \"memory\"\n ? memoryStorageSync\n : localStorageSync;\n const adapter = syncManager.storage;\n\n // 1. Determine the codec.\n // If no explicit codec is passed, we try to infer it from defaultValue.\n // If defaultValue is undefined and no codec is passed, we fall back to JSON.\n const codec = useMemo(() => {\n if (options?.codec) return options.codec;\n return inferCodec(defaultValue);\n }, [defaultValue, options?.codec]);\n\n if ((defaultValue === undefined || defaultValue === null) && !options.codec) {\n console.warn(\n `useStorage: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n // Memoize the decoded value to prevent infinite loops in useSyncExternalStore\n // when the codec returns a new object reference (e.g. JSON.parse).\n const lastRaw = useRef<string | null>(null); // string | null, because storage returns null for missing keys\n const lastParsed = useRef<T | undefined>(undefined);\n\n const getSnapshot = useCallback(() => {\n const raw = adapter.getItem(key);\n\n // Return default if storage is missing the key\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n // If raw value matches cache, return cached object.\n if (raw === lastRaw.current) {\n if (lastParsed.current === undefined) {\n return null as T;\n }\n return lastParsed.current as T;\n }\n\n // Decode new raw value\n try {\n const decoded = codec.decode(raw);\n\n lastRaw.current = raw;\n lastParsed.current = decoded;\n return decoded;\n } catch (e) {\n console.error(`Error parsing storage key \"${key}\"`, e);\n return defaultValue as T;\n }\n }, [adapter, key, codec, defaultValue]);\n\n // 2. Subscribe to the external store (Local/SessionStorage + Polling/Events)\n // useSyncExternalStore handles the hydration mismatch automatically by\n // taking a `getServerSnapshot` (returning defaultValue).\n const value = useSyncExternalStore(\n (callback) =>\n syncManager.subscribe(key, callback, {\n crossTabSync: options.crossTabSync,\n pollingIntervalMs: options.pollingIntervalMs,\n }),\n getSnapshot,\n () => defaultValue as T, // Server Snapshot\n );\n\n // 3. Create the Setter\n const setValue = useCallback(\n (newValueOrFn: T | ((prev: T) => T)) => {\n try {\n const currentRaw = adapter.getItem(key);\n const current =\n currentRaw !== null ? codec.decode(currentRaw) : defaultValue;\n\n const newValue =\n newValueOrFn instanceof Function\n ? newValueOrFn(current)\n : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n lastRaw.current = encoded;\n lastParsed.current = newValue;\n\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n // Notify other hooks/tabs\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n },\n [adapter, key, codec, defaultValue, syncManager],\n );\n\n const removeItem = useCallback(() => {\n try {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error removing storage key \"${key}\":`, error);\n }\n }, [adapter, key, syncManager]);\n\n return [value, setValue, removeItem] as const;\n}\n","/**\n * Defines how to serialize/deserialize a value from localStorage.\n * null means the key doesn't exist.\n */\nexport interface Codec<T> {\n encode: (value: T) => string | null;\n decode: (value: string | null) => T;\n}\n\n/**\n * A robust JSON codec that handles parsing errors gracefully.\n * Works with objects, arrays, and other JSON-serializable values.\n *\n * @example\n * ```ts\n * const [user, setUser] = useStoragePersistedState<User | null>(\n * \"user\",\n * null,\n * { codec: JsonCodec }\n * );\n * ```\n */\nexport const JsonCodec: Codec<unknown> = {\n encode: (value) => {\n if (value === null) return null;\n return JSON.stringify(value);\n },\n decode: (value) => {\n if (value === null) return null;\n try {\n return JSON.parse(value);\n } catch (e) {\n console.warn(`LocalStorage parse error for value \"${value}\".`, e);\n throw e;\n }\n },\n};\n\n/**\n * Codec for string values. Stores strings as-is without any transformation.\n *\n * @example\n * ```ts\n * const [name, setName] = useStoragePersistedState(\"name\", \"Guest\");\n * // Automatically uses StringCodec because defaultValue is a string\n * ```\n */\nexport const StringCodec: Codec<string | null> = {\n encode: (value) => value ?? null,\n decode: (value) => value ?? null,\n};\n\n/**\n * Codec for boolean values. Stores as \"true\" or \"false\" strings.\n *\n * @example\n * ```ts\n * const [isDark, setIsDark] = useStoragePersistedState(\"darkMode\", false);\n * // Automatically uses BooleanCodec because defaultValue is a boolean\n * ```\n */\nexport const BooleanCodec: Codec<boolean> = {\n encode: (value) => (value ? \"true\" : \"false\"),\n decode: (value) => value === \"true\",\n};\n\n/**\n * Codec for number values. Handles NaN, Infinity, and -Infinity correctly.\n * Returns null for unparseable values (e.g., empty string, invalid number string).\n *\n * @example\n * ```ts\n * const [count, setCount] = useStoragePersistedState(\"count\", 0);\n * // Automatically uses NumberCodec because defaultValue is a number\n * ```\n */\nexport const NumberCodec: Codec<number | null> = {\n encode: (value) => {\n if (value === null) return null;\n return String(value);\n },\n decode: (value) => {\n if (value === null || value === \"\") return null;\n // Handle special numeric values\n if (value === \"NaN\") return NaN;\n if (value === \"Infinity\") return Infinity;\n if (value === \"-Infinity\") return -Infinity;\n const parsed = Number(value);\n return isNaN(parsed) ? null : parsed;\n },\n};\n\n/**\n * Infers the appropriate codec based on the default value's type.\n * Used internally when the user doesn't provide an explicit codec.\n *\n * - `boolean` → BooleanCodec\n * - `number` → NumberCodec\n * - `string` → StringCodec\n * - `object`, `array`, `undefined`, `null` → JsonCodec\n *\n * @param defaultValue - The default value to infer the codec from\n * @returns The inferred codec for the given value type\n */\nexport function inferCodec<T>(defaultValue: T): Codec<T> {\n const type = typeof defaultValue;\n\n // Type assertions through unknown are needed because we're doing runtime type inference\n // The actual codec returned will match the runtime type of defaultValue\n if (type === \"boolean\") return BooleanCodec as unknown as Codec<T>;\n if (type === \"number\") return NumberCodec as unknown as Codec<T>;\n if (type === \"string\") return StringCodec as unknown as Codec<T>;\n\n // Default to JSON for objects, arrays, or undefined\n return JsonCodec as unknown as Codec<T>;\n}\n","\"use client\";\n\n/**\n * Interface allows swapping LocalStorage for SessionStorage or Async Storage later\n */\nexport interface StorageAdapter {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface SubscribeOptions {\n crossTabSync?: boolean;\n pollingIntervalMs?: number | null;\n}\n\ninterface ListenerOptions {\n crossTabSync: boolean;\n pollingIntervalMs: number | null;\n}\n\ninterface StorageSyncManagerOptions {\n enableCrossTabSync?: boolean;\n enablePolling?: boolean;\n}\n\nclass MemoryStorageAdapter implements StorageAdapter {\n private store = new Map<string, string>();\n\n getItem(key: string) {\n return this.store.has(key) ? this.store.get(key)! : null;\n }\n\n setItem(key: string, value: string) {\n this.store.set(key, value);\n }\n\n removeItem(key: string) {\n this.store.delete(key);\n }\n}\n\nclass FallbackStorageAdapter implements StorageAdapter {\n constructor(\n private primary: StorageAdapter,\n private fallback: StorageAdapter,\n ) {}\n\n getItem(key: string) {\n const fallbackValue = this.fallback.getItem(key);\n if (fallbackValue !== null) return fallbackValue;\n return this.primary.getItem(key);\n }\n\n setItem(key: string, value: string) {\n try {\n this.primary.setItem(key, value);\n this.fallback.removeItem(key);\n } catch {\n this.fallback.setItem(key, value);\n }\n }\n\n removeItem(key: string) {\n try {\n this.primary.removeItem(key);\n } finally {\n this.fallback.removeItem(key);\n }\n }\n}\n\n// Singleton manager ensures we only have ONE listener per key globally\nclass StorageSyncManager {\n private listeners = new Map<string, Map<() => void, ListenerOptions>>();\n private pollingIntervalId: number | null = null;\n private pollingIntervalMsActive: number | null = null;\n private enableCrossTabSync: boolean;\n private enablePolling: boolean;\n\n constructor(\n public readonly storage: StorageAdapter,\n private defaultPollingIntervalMs = 2000,\n options: StorageSyncManagerOptions = {},\n ) {\n this.enableCrossTabSync = options.enableCrossTabSync ?? true;\n this.enablePolling = options.enablePolling ?? true;\n\n if (typeof window !== \"undefined\" && this.enableCrossTabSync) {\n // 1. Cross-tab sync (\"storage\" event is a built-in browser feature that fires\n // when localStorage/sessionStorage changes in ANOTHER tab)\n // We do not remove this listener because this manager is a singleton meant to last\n // the entire application lifecycle. It is effectively cleaned up when the page unloads.\n window.addEventListener(\"storage\", (e) => {\n if (e.key && this.listeners.has(e.key)) {\n this.notifyCrossTab(e.key);\n }\n });\n // 2. Start polling for DevTools or direct window.localStorage changes (robustness)\n // We only poll if there are listeners. Lazy start in subscribe.\n }\n }\n\n subscribe(key: string, callback: () => void, options: SubscribeOptions = {}) {\n if (!this.listeners.has(key)) {\n this.listeners.set(key, new Map());\n }\n const listenerOptions: ListenerOptions = {\n crossTabSync: this.enableCrossTabSync && (options.crossTabSync ?? true),\n pollingIntervalMs: this.enablePolling\n ? this.normalizePollingInterval(options.pollingIntervalMs)\n : null,\n };\n this.listeners.get(key)!.set(callback, listenerOptions);\n\n this.updatePollingInterval();\n\n return () => {\n const listenersForKey = this.listeners.get(key);\n listenersForKey?.delete(callback);\n if (listenersForKey?.size === 0) {\n this.listeners.delete(key);\n }\n this.updatePollingInterval();\n };\n }\n\n // Called when WE change the value in this tab\n notify(key: string) {\n this.notifyListeners(key);\n }\n\n // Keep a cache of values to detect changes during polling\n private snapshotCache = new Map<string, string | null>();\n\n private notifyListeners(\n key: string,\n predicate?: (options: ListenerOptions) => boolean,\n ) {\n this.listeners.get(key)?.forEach((options, cb) => {\n if (!predicate || predicate(options)) {\n cb();\n }\n });\n }\n\n private notifyCrossTab(key: string) {\n this.notifyListeners(key, (options) => options.crossTabSync);\n }\n\n private notifyPolling(key: string) {\n this.notifyListeners(key, (options) => options.pollingIntervalMs !== null);\n }\n\n private normalizePollingInterval(\n pollingIntervalMs: number | null | undefined,\n ) {\n if (pollingIntervalMs === null) return null;\n if (pollingIntervalMs === undefined) return this.defaultPollingIntervalMs;\n if (pollingIntervalMs <= 0 || Number.isNaN(pollingIntervalMs)) {\n return null;\n }\n return pollingIntervalMs;\n }\n\n private getMinPollingIntervalMs() {\n let minInterval: number | null = null;\n this.listeners.forEach((listenersForKey) => {\n listenersForKey.forEach((options) => {\n if (options.pollingIntervalMs === null) return;\n if (minInterval === null || options.pollingIntervalMs < minInterval) {\n minInterval = options.pollingIntervalMs;\n }\n });\n });\n return minInterval;\n }\n\n private updatePollingInterval() {\n if (typeof window === \"undefined\") return;\n if (!this.enablePolling) return;\n\n const nextInterval = this.getMinPollingIntervalMs();\n if (nextInterval === null) {\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n this.pollingIntervalId = null;\n this.pollingIntervalMsActive = null;\n this.snapshotCache.clear();\n return;\n }\n\n if (\n this.pollingIntervalId &&\n this.pollingIntervalMsActive === nextInterval\n ) {\n return;\n }\n\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n\n this.pollingIntervalId = window.setInterval(() => {\n this.listeners.forEach((listenersForKey, key) => {\n const shouldPoll = Array.from(listenersForKey.values()).some(\n (options) => options.pollingIntervalMs !== null,\n );\n if (!shouldPoll) return;\n\n const currentValue = this.storage.getItem(key);\n const lastValue = this.snapshotCache.get(key);\n\n if (currentValue !== lastValue) {\n this.snapshotCache.set(key, currentValue);\n this.notifyPolling(key);\n }\n });\n }, nextInterval);\n this.pollingIntervalMsActive = nextInterval;\n }\n}\n\n// Lazy-initialized singletons to avoid SSR crashes (window is not defined on server)\nconst localStorageFallback = new MemoryStorageAdapter();\nconst sessionStorageFallback = new MemoryStorageAdapter();\n\nlet _localStorageSync: StorageSyncManager | null = null;\nlet _sessionStorageSync: StorageSyncManager | null = null;\nlet _memoryStorageSync: StorageSyncManager | null = null;\n\nfunction getLocalStorageSync(): StorageSyncManager {\n if (!_localStorageSync) {\n if (typeof window !== \"undefined\") {\n _localStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(window.localStorage, localStorageFallback),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _localStorageSync;\n}\n\nfunction getSessionStorageSync(): StorageSyncManager {\n if (!_sessionStorageSync) {\n if (typeof window !== \"undefined\") {\n _sessionStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(\n window.sessionStorage,\n sessionStorageFallback,\n ),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _sessionStorageSync;\n}\n\nfunction getMemoryStorageSync(): StorageSyncManager {\n if (!_memoryStorageSync) {\n _memoryStorageSync = new StorageSyncManager(\n new MemoryStorageAdapter(),\n 2000,\n {\n enableCrossTabSync: false,\n enablePolling: false,\n },\n );\n }\n return _memoryStorageSync;\n}\n\n/**\n * Proxy object for localStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const localStorageSync = {\n get storage() {\n return getLocalStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getLocalStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getLocalStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for sessionStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const sessionStorageSync = {\n get storage() {\n return getSessionStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getSessionStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getSessionStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for memory storage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const memoryStorageSync = {\n get storage() {\n return getMemoryStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getMemoryStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getMemoryStorageSync().notify(key);\n },\n};\n","\"use client\";\n\nimport { Codec, inferCodec, JsonCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\nimport {\n StoragePersistedStateOptions,\n StorageType,\n} from \"./useStoragePersistedState\";\n\nfunction getSyncManager(storageType: StorageType | undefined) {\n if (storageType === \"sessionStorage\") return sessionStorageSync;\n if (storageType === \"memory\") return memoryStorageSync;\n return localStorageSync;\n}\n\nfunction resolveCodec<T>(\n key: string,\n valueHint: T,\n options?: StoragePersistedStateOptions<T>,\n): Codec<T> {\n if (options?.codec) return options.codec;\n\n if ((valueHint === undefined || valueHint === null) && !options?.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n return inferCodec(valueHint);\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Read a persisted value from storage using the same codec behavior as the hook.\n *\n * Returns the provided defaultValue when the key is missing or parsing fails.\n */\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): T;\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\n/**\n * Read a persisted value from storage using an explicit codec.\n *\n * Use this overload when defaultValue is null or undefined.\n */\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): T | null;\n\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n const codec = resolveCodec(key, defaultValue, options);\n const raw = adapter.getItem(key);\n\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n try {\n return codec.decode(raw);\n } catch (error) {\n console.error(`Error parsing storage key \"${key}\"`, error);\n return defaultValue as T;\n }\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Set a persisted value in storage and notify active hooks for the same key.\n *\n * Supports functional updates using the current decoded value.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): void;\n\n// Overload 2: Explicit Codec provided, newValue can be null or undefined\n/**\n * Set a persisted value in storage using an explicit codec and notify listeners.\n *\n * Use this overload when the new value is null or undefined or when you want custom serialization.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): void;\n\nexport function setStoragePersistedState<T>(\n key: string,\n newValueOrFn: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n\n let codec: Codec<T>;\n if (!(newValueOrFn instanceof Function)) {\n codec = resolveCodec(key, newValueOrFn, options);\n } else {\n // For functional updates, we cannot infer from newValue\n if (!options.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses functional update without explicit Codec. defaulting to JSON.`,\n );\n }\n codec = options.codec ?? (JsonCodec as unknown as Codec<T>);\n }\n\n try {\n const currentRaw = adapter.getItem(key);\n const current = currentRaw !== null ? codec.decode(currentRaw) : null;\n\n const newValue =\n newValueOrFn instanceof Function ? newValueOrFn(current) : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n}\n"],"mappings":";;;;;AAEA,SAAS,aAAa,SAAS,cAAc;AAC7C,SAAS,4BAA4B;;;ACmB9B,IAAM,YAA4B;AAAA,EACvC,QAAQ,CAAC,UAAU;AACjB,QAAI,UAAU,KAAM,QAAO;AAC3B,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B;AAAA,EACA,QAAQ,CAAC,UAAU;AACjB,QAAI,UAAU,KAAM,QAAO;AAC3B,QAAI;AACF,aAAO,KAAK,MAAM,KAAK;AAAA,IACzB,SAAS,GAAG;AACV,cAAQ,KAAK,uCAAuC,KAAK,MAAM,CAAC;AAChE,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAWO,IAAM,cAAoC;AAAA,EAC/C,QAAQ,CAAC,UAAU,SAAS;AAAA,EAC5B,QAAQ,CAAC,UAAU,SAAS;AAC9B;AAWO,IAAM,eAA+B;AAAA,EAC1C,QAAQ,CAAC,UAAW,QAAQ,SAAS;AAAA,EACrC,QAAQ,CAAC,UAAU,UAAU;AAC/B;AAYO,IAAM,cAAoC;AAAA,EAC/C,QAAQ,CAAC,UAAU;AACjB,QAAI,UAAU,KAAM,QAAO;AAC3B,WAAO,OAAO,KAAK;AAAA,EACrB;AAAA,EACA,QAAQ,CAAC,UAAU;AACjB,QAAI,UAAU,QAAQ,UAAU,GAAI,QAAO;AAE3C,QAAI,UAAU,MAAO,QAAO;AAC5B,QAAI,UAAU,WAAY,QAAO;AACjC,QAAI,UAAU,YAAa,QAAO;AAClC,UAAM,SAAS,OAAO,KAAK;AAC3B,WAAO,MAAM,MAAM,IAAI,OAAO;AAAA,EAChC;AACF;AAcO,SAAS,WAAc,cAA2B;AACvD,QAAM,OAAO,OAAO;AAIpB,MAAI,SAAS,UAAW,QAAO;AAC/B,MAAI,SAAS,SAAU,QAAO;AAC9B,MAAI,SAAS,SAAU,QAAO;AAG9B,SAAO;AACT;;;ACzFA,IAAM,uBAAN,MAAqD;AAAA,EAArD;AACE,wBAAQ,SAAQ,oBAAI,IAAoB;AAAA;AAAA,EAExC,QAAQ,KAAa;AACnB,WAAO,KAAK,MAAM,IAAI,GAAG,IAAI,KAAK,MAAM,IAAI,GAAG,IAAK;AAAA,EACtD;AAAA,EAEA,QAAQ,KAAa,OAAe;AAClC,SAAK,MAAM,IAAI,KAAK,KAAK;AAAA,EAC3B;AAAA,EAEA,WAAW,KAAa;AACtB,SAAK,MAAM,OAAO,GAAG;AAAA,EACvB;AACF;AAEA,IAAM,yBAAN,MAAuD;AAAA,EACrD,YACU,SACA,UACR;AAFQ;AACA;AAAA,EACP;AAAA,EAEH,QAAQ,KAAa;AACnB,UAAM,gBAAgB,KAAK,SAAS,QAAQ,GAAG;AAC/C,QAAI,kBAAkB,KAAM,QAAO;AACnC,WAAO,KAAK,QAAQ,QAAQ,GAAG;AAAA,EACjC;AAAA,EAEA,QAAQ,KAAa,OAAe;AAClC,QAAI;AACF,WAAK,QAAQ,QAAQ,KAAK,KAAK;AAC/B,WAAK,SAAS,WAAW,GAAG;AAAA,IAC9B,QAAQ;AACN,WAAK,SAAS,QAAQ,KAAK,KAAK;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,WAAW,KAAa;AACtB,QAAI;AACF,WAAK,QAAQ,WAAW,GAAG;AAAA,IAC7B,UAAE;AACA,WAAK,SAAS,WAAW,GAAG;AAAA,IAC9B;AAAA,EACF;AACF;AAGA,IAAM,qBAAN,MAAyB;AAAA,EAOvB,YACkB,SACR,2BAA2B,KACnC,UAAqC,CAAC,GACtC;AAHgB;AACR;AARV,wBAAQ,aAAY,oBAAI,IAA8C;AACtE,wBAAQ,qBAAmC;AAC3C,wBAAQ,2BAAyC;AACjD,wBAAQ;AACR,wBAAQ;AAuDR;AAAA,wBAAQ,iBAAgB,oBAAI,IAA2B;AAhDrD,SAAK,qBAAqB,QAAQ,sBAAsB;AACxD,SAAK,gBAAgB,QAAQ,iBAAiB;AAE9C,QAAI,OAAO,WAAW,eAAe,KAAK,oBAAoB;AAK5D,aAAO,iBAAiB,WAAW,CAAC,MAAM;AACxC,YAAI,EAAE,OAAO,KAAK,UAAU,IAAI,EAAE,GAAG,GAAG;AACtC,eAAK,eAAe,EAAE,GAAG;AAAA,QAC3B;AAAA,MACF,CAAC;AAAA,IAGH;AAAA,EACF;AAAA,EAEA,UAAU,KAAa,UAAsB,UAA4B,CAAC,GAAG;AAC3E,QAAI,CAAC,KAAK,UAAU,IAAI,GAAG,GAAG;AAC5B,WAAK,UAAU,IAAI,KAAK,oBAAI,IAAI,CAAC;AAAA,IACnC;AACA,UAAM,kBAAmC;AAAA,MACvC,cAAc,KAAK,uBAAuB,QAAQ,gBAAgB;AAAA,MAClE,mBAAmB,KAAK,gBACpB,KAAK,yBAAyB,QAAQ,iBAAiB,IACvD;AAAA,IACN;AACA,SAAK,UAAU,IAAI,GAAG,EAAG,IAAI,UAAU,eAAe;AAEtD,SAAK,sBAAsB;AAE3B,WAAO,MAAM;AACX,YAAM,kBAAkB,KAAK,UAAU,IAAI,GAAG;AAC9C,uBAAiB,OAAO,QAAQ;AAChC,UAAI,iBAAiB,SAAS,GAAG;AAC/B,aAAK,UAAU,OAAO,GAAG;AAAA,MAC3B;AACA,WAAK,sBAAsB;AAAA,IAC7B;AAAA,EACF;AAAA;AAAA,EAGA,OAAO,KAAa;AAClB,SAAK,gBAAgB,GAAG;AAAA,EAC1B;AAAA,EAKQ,gBACN,KACA,WACA;AACA,SAAK,UAAU,IAAI,GAAG,GAAG,QAAQ,CAAC,SAAS,OAAO;AAChD,UAAI,CAAC,aAAa,UAAU,OAAO,GAAG;AACpC,WAAG;AAAA,MACL;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,eAAe,KAAa;AAClC,SAAK,gBAAgB,KAAK,CAAC,YAAY,QAAQ,YAAY;AAAA,EAC7D;AAAA,EAEQ,cAAc,KAAa;AACjC,SAAK,gBAAgB,KAAK,CAAC,YAAY,QAAQ,sBAAsB,IAAI;AAAA,EAC3E;AAAA,EAEQ,yBACN,mBACA;AACA,QAAI,sBAAsB,KAAM,QAAO;AACvC,QAAI,sBAAsB,OAAW,QAAO,KAAK;AACjD,QAAI,qBAAqB,KAAK,OAAO,MAAM,iBAAiB,GAAG;AAC7D,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,0BAA0B;AAChC,QAAI,cAA6B;AACjC,SAAK,UAAU,QAAQ,CAAC,oBAAoB;AAC1C,sBAAgB,QAAQ,CAAC,YAAY;AACnC,YAAI,QAAQ,sBAAsB,KAAM;AACxC,YAAI,gBAAgB,QAAQ,QAAQ,oBAAoB,aAAa;AACnE,wBAAc,QAAQ;AAAA,QACxB;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AACD,WAAO;AAAA,EACT;AAAA,EAEQ,wBAAwB;AAC9B,QAAI,OAAO,WAAW,YAAa;AACnC,QAAI,CAAC,KAAK,cAAe;AAEzB,UAAM,eAAe,KAAK,wBAAwB;AAClD,QAAI,iBAAiB,MAAM;AACzB,UAAI,KAAK,mBAAmB;AAC1B,sBAAc,KAAK,iBAAiB;AAAA,MACtC;AACA,WAAK,oBAAoB;AACzB,WAAK,0BAA0B;AAC/B,WAAK,cAAc,MAAM;AACzB;AAAA,IACF;AAEA,QACE,KAAK,qBACL,KAAK,4BAA4B,cACjC;AACA;AAAA,IACF;AAEA,QAAI,KAAK,mBAAmB;AAC1B,oBAAc,KAAK,iBAAiB;AAAA,IACtC;AAEA,SAAK,oBAAoB,OAAO,YAAY,MAAM;AAChD,WAAK,UAAU,QAAQ,CAAC,iBAAiB,QAAQ;AAC/C,cAAM,aAAa,MAAM,KAAK,gBAAgB,OAAO,CAAC,EAAE;AAAA,UACtD,CAAC,YAAY,QAAQ,sBAAsB;AAAA,QAC7C;AACA,YAAI,CAAC,WAAY;AAEjB,cAAM,eAAe,KAAK,QAAQ,QAAQ,GAAG;AAC7C,cAAM,YAAY,KAAK,cAAc,IAAI,GAAG;AAE5C,YAAI,iBAAiB,WAAW;AAC9B,eAAK,cAAc,IAAI,KAAK,YAAY;AACxC,eAAK,cAAc,GAAG;AAAA,QACxB;AAAA,MACF,CAAC;AAAA,IACH,GAAG,YAAY;AACf,SAAK,0BAA0B;AAAA,EACjC;AACF;AAGA,IAAM,uBAAuB,IAAI,qBAAqB;AACtD,IAAM,yBAAyB,IAAI,qBAAqB;AAExD,IAAI,oBAA+C;AACnD,IAAI,sBAAiD;AACrD,IAAI,qBAAgD;AAEpD,SAAS,sBAA0C;AACjD,MAAI,CAAC,mBAAmB;AACtB,QAAI,OAAO,WAAW,aAAa;AACjC,0BAAoB,IAAI;AAAA,QACtB,IAAI,uBAAuB,OAAO,cAAc,oBAAoB;AAAA,MACtE;AAAA,IACF,OAAO;AAEL,aAAO,qBAAqB;AAAA,IAC9B;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,wBAA4C;AACnD,MAAI,CAAC,qBAAqB;AACxB,QAAI,OAAO,WAAW,aAAa;AACjC,4BAAsB,IAAI;AAAA,QACxB,IAAI;AAAA,UACF,OAAO;AAAA,UACP;AAAA,QACF;AAAA,MACF;AAAA,IACF,OAAO;AAEL,aAAO,qBAAqB;AAAA,IAC9B;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,uBAA2C;AAClD,MAAI,CAAC,oBAAoB;AACvB,yBAAqB,IAAI;AAAA,MACvB,IAAI,qBAAqB;AAAA,MACzB;AAAA,MACA;AAAA,QACE,oBAAoB;AAAA,QACpB,eAAe;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAMO,IAAM,mBAAmB;AAAA,EAC9B,IAAI,UAAU;AACZ,WAAO,oBAAoB,EAAE;AAAA,EAC/B;AAAA,EACA,UAAU,KAAa,UAAsB,SAA4B;AACvE,WAAO,oBAAoB,EAAE,UAAU,KAAK,UAAU,OAAO;AAAA,EAC/D;AAAA,EACA,OAAO,KAAa;AAClB,WAAO,oBAAoB,EAAE,OAAO,GAAG;AAAA,EACzC;AACF;AAMO,IAAM,qBAAqB;AAAA,EAChC,IAAI,UAAU;AACZ,WAAO,sBAAsB,EAAE;AAAA,EACjC;AAAA,EACA,UAAU,KAAa,UAAsB,SAA4B;AACvE,WAAO,sBAAsB,EAAE,UAAU,KAAK,UAAU,OAAO;AAAA,EACjE;AAAA,EACA,OAAO,KAAa;AAClB,WAAO,sBAAsB,EAAE,OAAO,GAAG;AAAA,EAC3C;AACF;AAMO,IAAM,oBAAoB;AAAA,EAC/B,IAAI,UAAU;AACZ,WAAO,qBAAqB,EAAE;AAAA,EAChC;AAAA,EACA,UAAU,KAAa,UAAsB,SAA4B;AACvE,WAAO,qBAAqB,EAAE,UAAU,KAAK,UAAU,OAAO;AAAA,EAChE;AAAA,EACA,OAAO,KAAa;AAClB,WAAO,qBAAqB,EAAE,OAAO,GAAG;AAAA,EAC1C;AACF;;;AF9QO,SAAS,yBACd,KACA,cACA,UAA2C,CAAC,GAC5C;AACA,QAAM,cACJ,QAAQ,gBAAgB,mBACpB,qBACA,QAAQ,gBAAgB,WACtB,oBACA;AACR,QAAM,UAAU,YAAY;AAK5B,QAAM,QAAQ,QAAQ,MAAM;AAC1B,QAAI,SAAS,MAAO,QAAO,QAAQ;AACnC,WAAO,WAAW,YAAY;AAAA,EAChC,GAAG,CAAC,cAAc,SAAS,KAAK,CAAC;AAEjC,OAAK,iBAAiB,UAAa,iBAAiB,SAAS,CAAC,QAAQ,OAAO;AAC3E,YAAQ;AAAA,MACN,oBAAoB,GAAG;AAAA,IACzB;AAAA,EACF;AAIA,QAAM,UAAU,OAAsB,IAAI;AAC1C,QAAM,aAAa,OAAsB,MAAS;AAElD,QAAM,cAAc,YAAY,MAAM;AACpC,UAAM,MAAM,QAAQ,QAAQ,GAAG;AAG/B,QAAI,QAAQ,QAAQ,iBAAiB,QAAW;AAC9C,aAAO;AAAA,IACT;AAGA,QAAI,QAAQ,QAAQ,SAAS;AAC3B,UAAI,WAAW,YAAY,QAAW;AACpC,eAAO;AAAA,MACT;AACA,aAAO,WAAW;AAAA,IACpB;AAGA,QAAI;AACF,YAAM,UAAU,MAAM,OAAO,GAAG;AAEhC,cAAQ,UAAU;AAClB,iBAAW,UAAU;AACrB,aAAO;AAAA,IACT,SAAS,GAAG;AACV,cAAQ,MAAM,8BAA8B,GAAG,KAAK,CAAC;AACrD,aAAO;AAAA,IACT;AAAA,EACF,GAAG,CAAC,SAAS,KAAK,OAAO,YAAY,CAAC;AAKtC,QAAM,QAAQ;AAAA,IACZ,CAAC,aACC,YAAY,UAAU,KAAK,UAAU;AAAA,MACnC,cAAc,QAAQ;AAAA,MACtB,mBAAmB,QAAQ;AAAA,IAC7B,CAAC;AAAA,IACH;AAAA,IACA,MAAM;AAAA;AAAA,EACR;AAGA,QAAM,WAAW;AAAA,IACf,CAAC,iBAAuC;AACtC,UAAI;AACF,cAAM,aAAa,QAAQ,QAAQ,GAAG;AACtC,cAAM,UACJ,eAAe,OAAO,MAAM,OAAO,UAAU,IAAI;AAEnD,cAAM,WACJ,wBAAwB,WACpB,aAAa,OAAO,IACpB;AAEN,YAAI,aAAa,UAAa,aAAa,MAAM;AAC/C,kBAAQ,UAAU;AAClB,qBAAW,UAAU;AACrB,kBAAQ,WAAW,GAAG;AAAA,QACxB,OAAO;AACL,gBAAM,UAAU,MAAM,OAAO,QAAQ;AACrC,kBAAQ,UAAU;AAClB,qBAAW,UAAU;AAErB,cAAI,YAAY,QAAQ,YAAY,QAAW;AAC7C,oBAAQ,WAAW,GAAG;AAAA,UACxB,OAAO;AACL,oBAAQ,QAAQ,KAAK,OAAO;AAAA,UAC9B;AAAA,QACF;AAGA,oBAAY,OAAO,GAAG;AAAA,MACxB,SAAS,OAAO;AACd,gBAAQ,MAAM,8BAA8B,GAAG,MAAM,KAAK;AAAA,MAC5D;AAAA,IACF;AAAA,IACA,CAAC,SAAS,KAAK,OAAO,cAAc,WAAW;AAAA,EACjD;AAEA,QAAM,aAAa,YAAY,MAAM;AACnC,QAAI;AACF,cAAQ,UAAU;AAClB,iBAAW,UAAU;AACrB,cAAQ,WAAW,GAAG;AACtB,kBAAY,OAAO,GAAG;AAAA,IACxB,SAAS,OAAO;AACd,cAAQ,MAAM,+BAA+B,GAAG,MAAM,KAAK;AAAA,IAC7D;AAAA,EACF,GAAG,CAAC,SAAS,KAAK,WAAW,CAAC;AAE9B,SAAO,CAAC,OAAO,UAAU,UAAU;AACrC;;;AGpKA,SAAS,eAAe,aAAsC;AAC5D,MAAI,gBAAgB,iBAAkB,QAAO;AAC7C,MAAI,gBAAgB,SAAU,QAAO;AACrC,SAAO;AACT;AAEA,SAAS,aACP,KACA,WACA,SACU;AACV,MAAI,SAAS,MAAO,QAAO,QAAQ;AAEnC,OAAK,cAAc,UAAa,cAAc,SAAS,CAAC,SAAS,OAAO;AACtE,YAAQ;AAAA,MACN,+BAA+B,GAAG;AAAA,IACpC;AAAA,EACF;AAEA,SAAO,WAAW,SAAS;AAC7B;AA0BO,SAAS,0BACd,KACA,cACA,UAA2C,CAAC,GAC5C;AACA,QAAM,cAAc,eAAe,QAAQ,WAAW;AACtD,QAAM,UAAU,YAAY;AAC5B,QAAM,QAAQ,aAAa,KAAK,cAAc,OAAO;AACrD,QAAM,MAAM,QAAQ,QAAQ,GAAG;AAE/B,MAAI,QAAQ,QAAQ,iBAAiB,QAAW;AAC9C,WAAO;AAAA,EACT;AAEA,MAAI;AACF,WAAO,MAAM,OAAO,GAAG;AAAA,EACzB,SAAS,OAAO;AACd,YAAQ,MAAM,8BAA8B,GAAG,KAAK,KAAK;AACzD,WAAO;AAAA,EACT;AACF;AA0BO,SAAS,yBACd,KACA,cACA,UAA2C,CAAC,GAC5C;AACA,QAAM,cAAc,eAAe,QAAQ,WAAW;AACtD,QAAM,UAAU,YAAY;AAE5B,MAAI;AACJ,MAAI,EAAE,wBAAwB,WAAW;AACvC,YAAQ,aAAa,KAAK,cAAc,OAAO;AAAA,EACjD,OAAO;AAEL,QAAI,CAAC,QAAQ,OAAO;AAClB,cAAQ;AAAA,QACN,+BAA+B,GAAG;AAAA,MACpC;AAAA,IACF;AACA,YAAQ,QAAQ,SAAU;AAAA,EAC5B;AAEA,MAAI;AACF,UAAM,aAAa,QAAQ,QAAQ,GAAG;AACtC,UAAM,UAAU,eAAe,OAAO,MAAM,OAAO,UAAU,IAAI;AAEjE,UAAM,WACJ,wBAAwB,WAAW,aAAa,OAAO,IAAI;AAE7D,QAAI,aAAa,UAAa,aAAa,MAAM;AAC/C,cAAQ,WAAW,GAAG;AAAA,IACxB,OAAO;AACL,YAAM,UAAU,MAAM,OAAO,QAAQ;AACrC,UAAI,YAAY,QAAQ,YAAY,QAAW;AAC7C,gBAAQ,WAAW,GAAG;AAAA,MACxB,OAAO;AACL,gBAAQ,QAAQ,KAAK,OAAO;AAAA,MAC9B;AAAA,IACF;AAEA,gBAAY,OAAO,GAAG;AAAA,EACxB,SAAS,OAAO;AACd,YAAQ,MAAM,8BAA8B,GAAG,MAAM,KAAK;AAAA,EAC5D;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/useStoragePersistedState.ts","../src/codecs.ts","../src/storage.ts","../src/storagePersistedState.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useMemo, useRef } from \"react\";\nimport { useSyncExternalStore } from \"use-sync-external-store/shim\";\nimport { Codec, inferCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\n\nexport type StorageType = \"localStorage\" | \"sessionStorage\" | \"memory\";\n\n/**\n * Options for the useStoragePersistedState hook\n */\nexport interface StoragePersistedStateOptions<T> {\n /**\n * Explicit codec for when defaultValue is null or undefined, for complex types, or special cases (e.g., data migration on read).\n * If not provided, codec is inferred from defaultValue type.\n */\n codec?: Codec<T>;\n /**\n * Storage type to use: 'localStorage' (default), 'sessionStorage', or 'memory'.\n */\n storageType?: StorageType;\n /**\n * Enable cross-tab synchronization via the StorageEvent. Note: disabling this will not stop other tabs from updating localStorage, but this hook will not automatically respond to those changes.\n * defaults to true.\n */\n crossTabSync?: boolean;\n /**\n * Polling interval in milliseconds for detecting storage changes made outside of React (e.g., DevTools, direct localStorage manipulation). Set to null to disable polling.\n * defaults to 2000ms.\n */\n pollingIntervalMs?: number | null;\n}\n\n// Overload 1: Default provided, T inferred\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): [T, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): [T | null, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Implementation\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager =\n options.storageType === \"sessionStorage\"\n ? sessionStorageSync\n : options.storageType === \"memory\"\n ? memoryStorageSync\n : localStorageSync;\n const adapter = syncManager.storage;\n\n // 1. Determine the codec.\n // If no explicit codec is passed, we try to infer it from defaultValue.\n // If defaultValue is undefined and no codec is passed, we fall back to JSON.\n const codec = useMemo(() => {\n if (options?.codec) return options.codec;\n return inferCodec(defaultValue);\n }, [defaultValue, options?.codec]);\n\n if ((defaultValue === undefined || defaultValue === null) && !options.codec) {\n console.warn(\n `useStorage: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n // Memoize the decoded value to prevent infinite loops in useSyncExternalStore\n // when the codec returns a new object reference (e.g. JSON.parse).\n const lastRaw = useRef<string | null>(null); // string | null, because storage returns null for missing keys\n const lastParsed = useRef<T | undefined>(undefined);\n\n const getSnapshot = useCallback(() => {\n const raw = adapter.getItem(key);\n\n // Return default if storage is missing the key\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n // If raw value matches cache, return cached object.\n if (raw === lastRaw.current) {\n if (lastParsed.current === undefined) {\n return null as T;\n }\n return lastParsed.current as T;\n }\n\n // Decode new raw value\n try {\n const decoded = codec.decode(raw);\n\n lastRaw.current = raw;\n lastParsed.current = decoded;\n return decoded;\n } catch (e) {\n console.error(`Error parsing storage key \"${key}\"`, e);\n return defaultValue as T;\n }\n }, [adapter, key, codec, defaultValue]);\n\n // 2. Subscribe to the external store (Local/SessionStorage + Polling/Events)\n // useSyncExternalStore handles the hydration mismatch automatically by\n // taking a `getServerSnapshot` (returning defaultValue).\n const value = useSyncExternalStore(\n (callback) =>\n syncManager.subscribe(key, callback, {\n crossTabSync: options.crossTabSync,\n pollingIntervalMs: options.pollingIntervalMs,\n }),\n getSnapshot,\n () => defaultValue as T, // Server Snapshot\n );\n\n // 3. Create the Setter\n const setValue = useCallback(\n (newValueOrFn: T | ((prev: T) => T)) => {\n try {\n const currentRaw = adapter.getItem(key);\n const current =\n currentRaw !== null ? codec.decode(currentRaw) : defaultValue;\n\n const newValue =\n newValueOrFn instanceof Function\n ? newValueOrFn(current)\n : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n lastRaw.current = encoded;\n lastParsed.current = newValue;\n\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n // Notify other hooks/tabs\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n },\n [adapter, key, codec, defaultValue, syncManager],\n );\n\n const removeItem = useCallback(() => {\n try {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error removing storage key \"${key}\":`, error);\n }\n }, [adapter, key, syncManager]);\n\n return [value, setValue, removeItem] as const;\n}\n","/**\n * Defines how to serialize/deserialize a value from localStorage.\n * null means the key doesn't exist.\n */\nexport interface Codec<T> {\n encode: (value: T) => string | null;\n decode: (value: string | null) => T;\n}\n\n/**\n * A robust JSON codec that handles parsing errors gracefully.\n * Works with objects, arrays, and other JSON-serializable values.\n *\n * @example\n * ```ts\n * const [user, setUser] = useStoragePersistedState<User | null>(\n * \"user\",\n * null,\n * { codec: JsonCodec }\n * );\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const JsonCodec: Codec<any> = {\n encode: (value) => {\n if (value === null) return null;\n return JSON.stringify(value);\n },\n decode: (value) => {\n if (value === null) return null;\n try {\n return JSON.parse(value);\n } catch (e) {\n console.warn(`LocalStorage parse error for value \"${value}\".`, e);\n throw e;\n }\n },\n};\n\n/**\n * Codec for string values. Stores strings as-is without any transformation.\n *\n * @example\n * ```ts\n * const [name, setName] = useStoragePersistedState(\"name\", \"Guest\");\n * // Automatically uses StringCodec because defaultValue is a string\n * ```\n */\nexport const StringCodec: Codec<string | null> = {\n encode: (value) => value ?? null,\n decode: (value) => value ?? null,\n};\n\n/**\n * Codec for boolean values. Stores as \"true\" or \"false\" strings.\n *\n * @example\n * ```ts\n * const [isDark, setIsDark] = useStoragePersistedState(\"darkMode\", false);\n * // Automatically uses BooleanCodec because defaultValue is a boolean\n * ```\n */\nexport const BooleanCodec: Codec<boolean> = {\n encode: (value) => (value ? \"true\" : \"false\"),\n decode: (value) => value === \"true\",\n};\n\n/**\n * Codec for number values. Handles NaN, Infinity, and -Infinity correctly.\n * Returns null for unparseable values (e.g., empty string, invalid number string).\n *\n * @example\n * ```ts\n * const [count, setCount] = useStoragePersistedState(\"count\", 0);\n * // Automatically uses NumberCodec because defaultValue is a number\n * ```\n */\nexport const NumberCodec: Codec<number | null> = {\n encode: (value) => {\n if (value === null) return null;\n return String(value);\n },\n decode: (value) => {\n if (value === null || value === \"\") return null;\n // Handle special numeric values\n if (value === \"NaN\") return NaN;\n if (value === \"Infinity\") return Infinity;\n if (value === \"-Infinity\") return -Infinity;\n const parsed = Number(value);\n return isNaN(parsed) ? null : parsed;\n },\n};\n\n/**\n * Infers the appropriate codec based on the default value's type.\n * Used internally when the user doesn't provide an explicit codec.\n *\n * - `boolean` → BooleanCodec\n * - `number` → NumberCodec\n * - `string` → StringCodec\n * - `object`, `array`, `undefined`, `null` → JsonCodec\n *\n * @param defaultValue - The default value to infer the codec from\n * @returns The inferred codec for the given value type\n */\nexport function inferCodec<T>(defaultValue: T): Codec<T> {\n const type = typeof defaultValue;\n\n // Type assertions through unknown are needed because we're doing runtime type inference\n // The actual codec returned will match the runtime type of defaultValue\n if (type === \"boolean\") return BooleanCodec as unknown as Codec<T>;\n if (type === \"number\") return NumberCodec as unknown as Codec<T>;\n if (type === \"string\") return StringCodec as unknown as Codec<T>;\n\n // Default to JSON for objects, arrays, or undefined\n return JsonCodec as unknown as Codec<T>;\n}\n","\"use client\";\n\n/**\n * Interface allows swapping LocalStorage for SessionStorage or Async Storage later\n */\nexport interface StorageAdapter {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface SubscribeOptions {\n crossTabSync?: boolean;\n pollingIntervalMs?: number | null;\n}\n\ninterface ListenerOptions {\n crossTabSync: boolean;\n pollingIntervalMs: number | null;\n}\n\ninterface StorageSyncManagerOptions {\n enableCrossTabSync?: boolean;\n enablePolling?: boolean;\n}\n\nclass MemoryStorageAdapter implements StorageAdapter {\n private store = new Map<string, string>();\n\n getItem(key: string) {\n return this.store.has(key) ? this.store.get(key)! : null;\n }\n\n setItem(key: string, value: string) {\n this.store.set(key, value);\n }\n\n removeItem(key: string) {\n this.store.delete(key);\n }\n}\n\nclass FallbackStorageAdapter implements StorageAdapter {\n constructor(\n private primary: StorageAdapter,\n private fallback: StorageAdapter,\n ) {}\n\n getItem(key: string) {\n const fallbackValue = this.fallback.getItem(key);\n if (fallbackValue !== null) return fallbackValue;\n return this.primary.getItem(key);\n }\n\n setItem(key: string, value: string) {\n try {\n this.primary.setItem(key, value);\n this.fallback.removeItem(key);\n } catch {\n this.fallback.setItem(key, value);\n }\n }\n\n removeItem(key: string) {\n try {\n this.primary.removeItem(key);\n } finally {\n this.fallback.removeItem(key);\n }\n }\n}\n\n// Singleton manager ensures we only have ONE listener per key globally\nclass StorageSyncManager {\n private listeners = new Map<string, Map<() => void, ListenerOptions>>();\n private pollingIntervalId: number | null = null;\n private pollingIntervalMsActive: number | null = null;\n private enableCrossTabSync: boolean;\n private enablePolling: boolean;\n\n constructor(\n public readonly storage: StorageAdapter,\n private defaultPollingIntervalMs = 2000,\n options: StorageSyncManagerOptions = {},\n ) {\n this.enableCrossTabSync = options.enableCrossTabSync ?? true;\n this.enablePolling = options.enablePolling ?? true;\n\n if (typeof window !== \"undefined\" && this.enableCrossTabSync) {\n // 1. Cross-tab sync (\"storage\" event is a built-in browser feature that fires\n // when localStorage/sessionStorage changes in ANOTHER tab)\n // We do not remove this listener because this manager is a singleton meant to last\n // the entire application lifecycle. It is effectively cleaned up when the page unloads.\n window.addEventListener(\"storage\", (e) => {\n if (e.key && this.listeners.has(e.key)) {\n this.notifyCrossTab(e.key);\n }\n });\n // 2. Start polling for DevTools or direct window.localStorage changes (robustness)\n // We only poll if there are listeners. Lazy start in subscribe.\n }\n }\n\n subscribe(key: string, callback: () => void, options: SubscribeOptions = {}) {\n if (!this.listeners.has(key)) {\n this.listeners.set(key, new Map());\n }\n const listenerOptions: ListenerOptions = {\n crossTabSync: this.enableCrossTabSync && (options.crossTabSync ?? true),\n pollingIntervalMs: this.enablePolling\n ? this.normalizePollingInterval(options.pollingIntervalMs)\n : null,\n };\n this.listeners.get(key)!.set(callback, listenerOptions);\n\n this.updatePollingInterval();\n\n return () => {\n const listenersForKey = this.listeners.get(key);\n listenersForKey?.delete(callback);\n if (listenersForKey?.size === 0) {\n this.listeners.delete(key);\n }\n this.updatePollingInterval();\n };\n }\n\n // Called when WE change the value in this tab\n notify(key: string) {\n this.notifyListeners(key);\n }\n\n // Keep a cache of values to detect changes during polling\n private snapshotCache = new Map<string, string | null>();\n\n private notifyListeners(\n key: string,\n predicate?: (options: ListenerOptions) => boolean,\n ) {\n this.listeners.get(key)?.forEach((options, cb) => {\n if (!predicate || predicate(options)) {\n cb();\n }\n });\n }\n\n private notifyCrossTab(key: string) {\n this.notifyListeners(key, (options) => options.crossTabSync);\n }\n\n private notifyPolling(key: string) {\n this.notifyListeners(key, (options) => options.pollingIntervalMs !== null);\n }\n\n private normalizePollingInterval(\n pollingIntervalMs: number | null | undefined,\n ) {\n if (pollingIntervalMs === null) return null;\n if (pollingIntervalMs === undefined) return this.defaultPollingIntervalMs;\n if (pollingIntervalMs <= 0 || Number.isNaN(pollingIntervalMs)) {\n return null;\n }\n return pollingIntervalMs;\n }\n\n private getMinPollingIntervalMs() {\n let minInterval: number | null = null;\n this.listeners.forEach((listenersForKey) => {\n listenersForKey.forEach((options) => {\n if (options.pollingIntervalMs === null) return;\n if (minInterval === null || options.pollingIntervalMs < minInterval) {\n minInterval = options.pollingIntervalMs;\n }\n });\n });\n return minInterval;\n }\n\n private updatePollingInterval() {\n if (typeof window === \"undefined\") return;\n if (!this.enablePolling) return;\n\n const nextInterval = this.getMinPollingIntervalMs();\n if (nextInterval === null) {\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n this.pollingIntervalId = null;\n this.pollingIntervalMsActive = null;\n this.snapshotCache.clear();\n return;\n }\n\n if (\n this.pollingIntervalId &&\n this.pollingIntervalMsActive === nextInterval\n ) {\n return;\n }\n\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n\n this.pollingIntervalId = window.setInterval(() => {\n this.listeners.forEach((listenersForKey, key) => {\n const shouldPoll = Array.from(listenersForKey.values()).some(\n (options) => options.pollingIntervalMs !== null,\n );\n if (!shouldPoll) return;\n\n const currentValue = this.storage.getItem(key);\n const lastValue = this.snapshotCache.get(key);\n\n if (currentValue !== lastValue) {\n this.snapshotCache.set(key, currentValue);\n this.notifyPolling(key);\n }\n });\n }, nextInterval);\n this.pollingIntervalMsActive = nextInterval;\n }\n}\n\n// Lazy-initialized singletons to avoid SSR crashes (window is not defined on server)\nconst localStorageFallback = new MemoryStorageAdapter();\nconst sessionStorageFallback = new MemoryStorageAdapter();\n\nlet _localStorageSync: StorageSyncManager | null = null;\nlet _sessionStorageSync: StorageSyncManager | null = null;\nlet _memoryStorageSync: StorageSyncManager | null = null;\n\nfunction getLocalStorageSync(): StorageSyncManager {\n if (!_localStorageSync) {\n if (typeof window !== \"undefined\") {\n _localStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(window.localStorage, localStorageFallback),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _localStorageSync;\n}\n\nfunction getSessionStorageSync(): StorageSyncManager {\n if (!_sessionStorageSync) {\n if (typeof window !== \"undefined\") {\n _sessionStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(\n window.sessionStorage,\n sessionStorageFallback,\n ),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _sessionStorageSync;\n}\n\nfunction getMemoryStorageSync(): StorageSyncManager {\n if (!_memoryStorageSync) {\n _memoryStorageSync = new StorageSyncManager(\n new MemoryStorageAdapter(),\n 2000,\n {\n enableCrossTabSync: false,\n enablePolling: false,\n },\n );\n }\n return _memoryStorageSync;\n}\n\n/**\n * Proxy object for localStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const localStorageSync = {\n get storage() {\n return getLocalStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getLocalStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getLocalStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for sessionStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const sessionStorageSync = {\n get storage() {\n return getSessionStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getSessionStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getSessionStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for memory storage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const memoryStorageSync = {\n get storage() {\n return getMemoryStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getMemoryStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getMemoryStorageSync().notify(key);\n },\n};\n","\"use client\";\n\nimport { Codec, inferCodec, JsonCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\nimport {\n StoragePersistedStateOptions,\n StorageType,\n} from \"./useStoragePersistedState\";\n\nfunction getSyncManager(storageType: StorageType | undefined) {\n if (storageType === \"sessionStorage\") return sessionStorageSync;\n if (storageType === \"memory\") return memoryStorageSync;\n return localStorageSync;\n}\n\nfunction resolveCodec<T>(\n key: string,\n valueHint: T,\n options?: StoragePersistedStateOptions<T>,\n): Codec<T> {\n if (options?.codec) return options.codec;\n\n if ((valueHint === undefined || valueHint === null) && !options?.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n return inferCodec(valueHint);\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Read a persisted value from storage using the same codec behavior as the hook.\n *\n * Returns the provided defaultValue when the key is missing or parsing fails.\n */\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): T;\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\n/**\n * Read a persisted value from storage using an explicit codec.\n *\n * Use this overload when defaultValue is null or undefined.\n */\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): T | null;\n\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n const codec = resolveCodec(key, defaultValue, options);\n const raw = adapter.getItem(key);\n\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n try {\n return codec.decode(raw);\n } catch (error) {\n console.error(`Error parsing storage key \"${key}\"`, error);\n return defaultValue as T;\n }\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Set a persisted value in storage and notify active hooks for the same key.\n *\n * Supports functional updates using the current decoded value.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): void;\n\n// Overload 2: Explicit Codec provided, newValue can be null or undefined\n/**\n * Set a persisted value in storage using an explicit codec and notify listeners.\n *\n * Use this overload when the new value is null or undefined or when you want custom serialization.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): void;\n\nexport function setStoragePersistedState<T>(\n key: string,\n newValueOrFn: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n\n let codec: Codec<T>;\n if (!(newValueOrFn instanceof Function)) {\n codec = resolveCodec(key, newValueOrFn, options);\n } else {\n // For functional updates, we cannot infer from newValue\n if (!options.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses functional update without explicit Codec. defaulting to JSON.`,\n );\n }\n codec = options.codec ?? (JsonCodec as unknown as Codec<T>);\n }\n\n try {\n const currentRaw = adapter.getItem(key);\n const current = currentRaw !== null ? codec.decode(currentRaw) : null;\n\n const newValue =\n newValueOrFn instanceof Function ? newValueOrFn(current) : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "use-storage-persisted-state",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "A robust, type-safe React hook for persisting state in localStorage/sessionStorage with cross-tab sync",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A robust, type-safe React hook for persisting state in localStorage/sessionStorage with cross-component and cross-tab sync",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Mikko Haapanen <mikko.h@iki.fi>",
|
|
7
7
|
"keywords": [
|
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
"check": "npm run typecheck && npm run lint",
|
|
51
51
|
"format": "prettier --write .",
|
|
52
52
|
"test": "vitest run",
|
|
53
|
-
"test:watch": "vitest"
|
|
53
|
+
"test:watch": "vitest",
|
|
54
|
+
"prepublishOnly": "npm run check && npm run test && npm run build"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
57
|
"@eslint/js": "^9.39.2",
|