/** * Safe localStorage wrapper with proper error handling and validation * * Handles: * - Private browsing mode (localStorage unavailable) * - Storage quota exceeded * - JSON parse errors * - Cross-origin restrictions */ import { logger } from './logger'; export class LocalStorageError extends Error { constructor(message: string, public readonly cause?: Error) { super(message); this.name = 'LocalStorageError'; } } /** * Check if localStorage is available */ export function isLocalStorageAvailable(): boolean { try { const testKey = '__localStorage_test__'; localStorage.setItem(testKey, 'test'); localStorage.removeItem(testKey); return true; } catch { return false; } } /** * Safely get an item from localStorage */ export function getItem(key: string): string | null { try { if (!isLocalStorageAvailable()) { return null; } return localStorage.getItem(key); } catch (error) { logger.warn(`Failed to get localStorage item: ${key}`, { error }); return null; } } /** * Safely set an item in localStorage */ export function setItem(key: string, value: string): boolean { try { if (!isLocalStorageAvailable()) { return false; } localStorage.setItem(key, value); return true; } catch (error) { if (error instanceof Error && error.name === 'QuotaExceededError') { logger.warn('localStorage quota exceeded', { key }); } else { logger.warn(`Failed to set localStorage item: ${key}`, { error }); } return false; } } /** * Safely remove an item from localStorage */ export function removeItem(key: string): boolean { try { if (!isLocalStorageAvailable()) { return false; } localStorage.removeItem(key); return true; } catch (error) { logger.warn(`Failed to remove localStorage item: ${key}`, { error }); return false; } } /** * Safely clear all localStorage */ export function clear(): boolean { try { if (!isLocalStorageAvailable()) { return false; } localStorage.clear(); return true; } catch (error) { logger.warn('Failed to clear localStorage', { error }); return false; } } /** * Get and parse a JSON object from localStorage */ export function getJSON(key: string, defaultValue: T): T { try { const item = getItem(key); if (!item) { return defaultValue; } const parsed = JSON.parse(item); return parsed as T; } catch (error) { logger.warn(`Failed to parse localStorage JSON for key: ${key}`, { error }); // Remove corrupted data removeItem(key); return defaultValue; } } /** * Stringify and set a JSON object in localStorage */ export function setJSON(key: string, value: T): boolean { try { const serialized = JSON.stringify(value); return setItem(key, serialized); } catch (error) { logger.warn(`Failed to stringify localStorage JSON for key: ${key}`, { error }); return false; } } /** * Check if a key exists in localStorage */ export function hasItem(key: string): boolean { return getItem(key) !== null; } /** * Get multiple items at once */ export function getItems(keys: string[]): Record { const result: Record = {}; for (const key of keys) { result[key] = getItem(key); } return result; } /** * Set multiple items at once */ export function setItems(items: Record): boolean { let allSuccessful = true; for (const [key, value] of Object.entries(items)) { if (!setItem(key, value)) { allSuccessful = false; } } return allSuccessful; } /** * Remove multiple items at once */ export function removeItems(keys: string[]): boolean { let allSuccessful = true; for (const key of keys) { if (!removeItem(key)) { allSuccessful = false; } } return allSuccessful; }