# Storage API 💾 SigPro provides persistent signals that automatically synchronize with browser storage APIs. This allows you to create reactive state that survives page reloads and browser sessions with zero additional code. ## Core Concepts ### What is Persistent Storage? Persistent signals are special signals that: - **Initialize from storage** (localStorage/sessionStorage) if a saved value exists - **Auto-save** whenever the signal value changes - **Handle JSON serialization** automatically - **Clean up** when set to `null` or `undefined` ### Storage Types | Storage | Persistence | Use Case | |---------|-------------|----------| | `localStorage` | Forever (until cleared) | User preferences, themes, saved data | | `sessionStorage` | Until tab/window closes | Form drafts, temporary state | ## `$.storage(key, initialValue, [storage])` Creates a persistent signal that syncs with browser storage. ```javascript import { $ } from 'sigpro'; // localStorage (default) const theme = $.storage('theme', 'light'); const user = $.storage('user', null); const settings = $.storage('settings', { notifications: true }); // sessionStorage const draft = $.storage('draft', '', sessionStorage); const formData = $.storage('form', {}, sessionStorage); ``` ## 📋 API Reference ### Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `key` | `string` | required | Storage key name | | `initialValue` | `any` | required | Default value if none stored | | `storage` | `Storage` | `localStorage` | Storage type (`localStorage` or `sessionStorage`) | ### Returns | Return | Description | |--------|-------------| | `Function` | Signal function (getter/setter) with persistence | ## 🎯 Basic Examples ### Theme Preference ```javascript import { $, html } from 'sigpro'; // Persistent theme signal const theme = $.storage('theme', 'light'); // Apply theme to document $.effect(() => { document.body.className = `theme-${theme()}`; }); // Toggle theme const toggleTheme = () => { theme(t => t === 'light' ? 'dark' : 'light'); }; // Template html`

Current theme: ${theme}

`; ``` ### User Preferences ```javascript import { $ } from 'sigpro'; // Complex preferences object const preferences = $.storage('preferences', { language: 'en', fontSize: 'medium', notifications: true, compactView: false, sidebarOpen: true }); // Update single preference const setPreference = (key, value) => { preferences({ ...preferences(), [key]: value }); }; // Usage setPreference('language', 'es'); setPreference('fontSize', 'large'); console.log(preferences().language); // 'es' ``` ### Form Draft ```javascript import { $, html } from 'sigpro'; // Session-based draft (clears when tab closes) const draft = $.storage('contact-form', { name: '', email: '', message: '' }, sessionStorage); // Auto-save on input const handleInput = (field, value) => { draft({ ...draft(), [field]: value }); }; // Clear draft after submit const handleSubmit = async () => { await submitForm(draft()); draft(null); // Clears from storage }; // Template html`
draft().name} @input=${(e) => handleInput('name', e.target.value)} placeholder="Name" /> draft().email} @input=${(e) => handleInput('email', e.target.value)} placeholder="Email" />
`; ``` ## 🚀 Advanced Examples ### Authentication State ```javascript import { $, html } from 'sigpro'; // Persistent auth state const auth = $.storage('auth', { token: null, user: null, expiresAt: null }); // Computed helpers const isAuthenticated = $(() => { const { token, expiresAt } = auth(); if (!token || !expiresAt) return false; return new Date(expiresAt) > new Date(); }); const user = $(() => auth().user); // Login function const login = async (email, password) => { const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email, password }) }); if (response.ok) { const { token, user, expiresIn } = await response.json(); auth({ token, user, expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString() }); return true; } return false; }; // Logout const logout = () => { auth(null); // Clear from storage }; // Auto-refresh token $.effect(() => { if (!isAuthenticated()) return; const { expiresAt } = auth(); const expiresIn = new Date(expiresAt) - new Date(); const refreshTime = expiresIn - 60000; // 1 minute before expiry if (refreshTime > 0) { const timer = setTimeout(refreshToken, refreshTime); return () => clearTimeout(timer); } }); // Navigation guard $.effect(() => { if (!isAuthenticated() && window.location.pathname !== '/login') { $.router.go('/login'); } }); ``` ### Multi-tab Synchronization ```javascript import { $ } from 'sigpro'; // Storage key for cross-tab communication const STORAGE_KEY = 'app-state'; // Create persistent signal const appState = $.storage(STORAGE_KEY, { count: 0, lastUpdated: null }); // Listen for storage events (changes from other tabs) window.addEventListener('storage', (event) => { if (event.key === STORAGE_KEY && event.newValue) { try { // Update signal without triggering save loop const newValue = JSON.parse(event.newValue); appState(newValue); } catch (e) { console.error('Failed to parse storage event:', e); } } }); // Update state (syncs across all tabs) const increment = () => { appState({ count: appState().count + 1, lastUpdated: new Date().toISOString() }); }; // Tab counter const tabCount = $(1); // Track number of tabs open window.addEventListener('storage', (event) => { if (event.key === 'tab-heartbeat') { tabCount(parseInt(event.newValue) || 1); } }); // Send heartbeat setInterval(() => { localStorage.setItem('tab-heartbeat', tabCount()); }, 1000); ``` ### Settings Manager ```javascript import { $, html } from 'sigpro'; // Settings schema const settingsSchema = { theme: { type: 'select', options: ['light', 'dark', 'system'], default: 'system' }, fontSize: { type: 'range', min: 12, max: 24, default: 16 }, notifications: { type: 'checkbox', default: true }, language: { type: 'select', options: ['en', 'es', 'fr', 'de'], default: 'en' } }; // Persistent settings const settings = $.storage('app-settings', Object.entries(settingsSchema).reduce((acc, [key, config]) => ({ ...acc, [key]: config.default }), {}) ); // Settings component const SettingsPanel = () => { return html`

Settings

${Object.entries(settingsSchema).map(([key, config]) => { switch(config.type) { case 'select': return html`
`; case 'range': return html`
settings()[key]} @input=${(e) => updateSetting(key, parseInt(e.target.value))} />
`; case 'checkbox': return html`
`; } })}
`; }; // Helper functions const updateSetting = (key, value) => { settings({ ...settings(), [key]: value }); }; const resetDefaults = () => { const defaults = Object.entries(settingsSchema).reduce((acc, [key, config]) => ({ ...acc, [key]: config.default }), {}); settings(defaults); }; // Apply settings globally $.effect(() => { const { theme, fontSize } = settings(); // Apply theme document.documentElement.setAttribute('data-theme', theme); // Apply font size document.documentElement.style.fontSize = `${fontSize}px`; }); ``` ### Shopping Cart Persistence ```javascript import { $, html } from 'sigpro'; // Persistent shopping cart const cart = $.storage('shopping-cart', { items: [], lastUpdated: null }); // Computed values const cartItems = $(() => cart().items); const itemCount = $(() => cartItems().reduce((sum, item) => sum + item.quantity, 0)); const subtotal = $(() => cartItems().reduce((sum, item) => sum + (item.price * item.quantity), 0)); const tax = $(() => subtotal() * 0.1); const total = $(() => subtotal() + tax()); // Cart actions const addToCart = (product, quantity = 1) => { const existing = cartItems().findIndex(item => item.id === product.id); if (existing >= 0) { // Update quantity const newItems = [...cartItems()]; newItems[existing] = { ...newItems[existing], quantity: newItems[existing].quantity + quantity }; cart({ items: newItems, lastUpdated: new Date().toISOString() }); } else { // Add new item cart({ items: [...cartItems(), { ...product, quantity }], lastUpdated: new Date().toISOString() }); } }; const removeFromCart = (productId) => { cart({ items: cartItems().filter(item => item.id !== productId), lastUpdated: new Date().toISOString() }); }; const updateQuantity = (productId, quantity) => { if (quantity <= 0) { removeFromCart(productId); } else { const newItems = cartItems().map(item => item.id === productId ? { ...item, quantity } : item ); cart({ items: newItems, lastUpdated: new Date().toISOString() }); } }; const clearCart = () => { cart({ items: [], lastUpdated: new Date().toISOString() }); }; // Cart expiration (7 days) const CART_EXPIRY_DAYS = 7; $.effect(() => { const lastUpdated = cart().lastUpdated; if (lastUpdated) { const expiryDate = new Date(lastUpdated); expiryDate.setDate(expiryDate.getDate() + CART_EXPIRY_DAYS); if (new Date() > expiryDate) { clearCart(); } } }); // Cart display component const CartDisplay = () => html`

Shopping Cart (${itemCount} items)

${cartItems().map(item => html`
${item.name} $${item.price} x ${item.quantity} $${item.price * item.quantity} updateQuantity(item.id, parseInt(e.target.value))} />
`)}

Subtotal: $${subtotal}

Tax (10%): $${tax}

Total: $${total}

${() => cartItems().length > 0 ? html` ` : html`

Your cart is empty

`}
`; ``` ### Recent Searches History ```javascript import { $, html } from 'sigpro'; // Persistent search history (max 10 items) const searchHistory = $.storage('search-history', []); // Add search to history const addSearch = (query) => { if (!query.trim()) return; const current = searchHistory(); const newHistory = [ { query, timestamp: new Date().toISOString() }, ...current.filter(item => item.query !== query) ].slice(0, 10); // Keep only last 10 searchHistory(newHistory); }; // Clear history const clearHistory = () => { searchHistory([]); }; // Remove specific item const removeFromHistory = (query) => { searchHistory(searchHistory().filter(item => item.query !== query)); }; // Search component const SearchWithHistory = () => { const searchInput = $(''); const handleSearch = () => { const query = searchInput(); if (query) { addSearch(query); performSearch(query); searchInput(''); } }; return html`
${() => searchHistory().length > 0 ? html`

Recent Searches

${searchHistory().map(item => html`
${new Date(item.timestamp).toLocaleString()}
`)}
` : ''}
`; }; ``` ### Multiple Profiles / Accounts ```javascript import { $, html } from 'sigpro'; // Profile manager const profiles = $.storage('user-profiles', { current: 'default', list: { default: { name: 'Default', theme: 'light', preferences: {} } } }); // Switch profile const switchProfile = (profileId) => { profiles({ ...profiles(), current: profileId }); }; // Create profile const createProfile = (name) => { const id = `profile-${Date.now()}`; profiles({ current: id, list: { ...profiles().list, [id]: { name, theme: 'light', preferences: {}, createdAt: new Date().toISOString() } } }); return id; }; // Delete profile const deleteProfile = (profileId) => { if (profileId === 'default') return; // Can't delete default const newList = { ...profiles().list }; delete newList[profileId]; profiles({ current: 'default', list: newList }); }; // Get current profile data const currentProfile = $(() => { const { current, list } = profiles(); return list[current] || list.default; }); // Profile-aware settings const profileTheme = $(() => currentProfile().theme); const profilePreferences = $(() => currentProfile().preferences); // Update profile data const updateCurrentProfile = (updates) => { const { current, list } = profiles(); profiles({ current, list: { ...list, [current]: { ...list[current], ...updates } } }); }; // Profile selector component const ProfileSelector = () => html`
`; ``` ## 🛡️ Error Handling ### Storage Errors ```javascript import { $ } from 'sigpro'; // Safe storage wrapper const safeStorage = (key, initialValue, storage = localStorage) => { try { return $.storage(key, initialValue, storage); } catch (error) { console.warn(`Storage failed for ${key}, using in-memory fallback:`, error); return $(initialValue); } }; // Usage with fallback const theme = safeStorage('theme', 'light'); const user = safeStorage('user', null); ``` ### Quota Exceeded Handling ```javascript import { $ } from 'sigpro'; const createManagedStorage = (key, initialValue, maxSize = 1024 * 100) => { // 100KB limit const signal = $.storage(key, initialValue); // Monitor size const size = $(0); $.effect(() => { try { const value = signal(); const json = JSON.stringify(value); const bytes = new Blob([json]).size; size(bytes); if (bytes > maxSize) { console.warn(`Storage for ${key} exceeded ${maxSize} bytes`); // Could implement cleanup strategy here } } catch (e) { console.error('Size check failed:', e); } }); return { signal, size }; }; // Usage const { signal: largeData, size } = createManagedStorage('app-data', {}, 50000); ``` ## 📊 Storage Limits | Storage Type | Typical Limit | Notes | |--------------|---------------|-------| | `localStorage` | 5-10MB | Varies by browser | | `sessionStorage` | 5-10MB | Cleared when tab closes | | `cookies` | 4KB | Not recommended for SigPro | ## 🎯 Best Practices ### 1. Validate Stored Data ```javascript import { $ } from 'sigpro'; // Schema validation const createValidatedStorage = (key, schema, defaultValue, storage) => { const signal = $.storage(key, defaultValue, storage); // Wrap to validate on read/write const validated = (...args) => { if (args.length) { // Validate before writing const value = args[0]; if (typeof value === 'function') { // Handle functional updates return validated(validated()); } // Basic validation const isValid = Object.keys(schema).every(key => { const validator = schema[key]; return !validator || validator(value[key]); }); if (!isValid) { console.warn('Invalid data, skipping storage write'); return signal(); } } return signal(...args); }; return validated; }; // Usage const userSchema = { name: v => v && v.length > 0, age: v => v >= 18 && v <= 120, email: v => /@/.test(v) }; const user = createValidatedStorage('user', userSchema, { name: '', age: 25, email: '' }); ``` ### 2. Handle Versioning ```javascript import { $ } from 'sigpro'; const VERSION = 2; const createVersionedStorage = (key, migrations, storage) => { const raw = $.storage(key, { version: VERSION, data: {} }, storage); const migrate = (data) => { let current = data; const currentVersion = current.version || 1; for (let v = currentVersion; v < VERSION; v++) { const migrator = migrations[v]; if (migrator) { current = migrator(current); } } return current; }; // Migrate if needed const stored = raw(); if (stored.version !== VERSION) { const migrated = migrate(stored); raw(migrated); } return raw; }; // Usage const migrations = { 1: (old) => ({ version: 2, data: { ...old.data, preferences: old.preferences || {} } }) }; const settings = createVersionedStorage('app-settings', migrations); ``` ### 3. Encrypt Sensitive Data ```javascript import { $ } from 'sigpro'; // Simple encryption (use proper crypto in production) const encrypt = (text) => { return btoa(text); // Base64 - NOT secure, just example }; const decrypt = (text) => { try { return atob(text); } catch { return null; } }; const createSecureStorage = (key, initialValue, storage) => { const encryptedKey = `enc_${key}`; const signal = $.storage(encryptedKey, null, storage); const secure = (...args) => { if (args.length) { // Encrypt before storing const value = args[0]; const encrypted = encrypt(JSON.stringify(value)); return signal(encrypted); } // Decrypt when reading const encrypted = signal(); if (!encrypted) return initialValue; try { const decrypted = decrypt(encrypted); return decrypted ? JSON.parse(decrypted) : initialValue; } catch { return initialValue; } }; return secure; }; // Usage const secureToken = createSecureStorage('auth-token', null); secureToken('sensitive-data-123'); // Stored encrypted ``` ## 📈 Performance Considerations | Operation | Cost | Notes | |-----------|------|-------| | Initial read | O(1) | Single storage read | | Write | O(1) + JSON.stringify | Auto-save on change | | Large objects | O(n) | Stringify/parse overhead | | Multiple keys | O(k) | k = number of keys | --- > **Pro Tip:** Use `sessionStorage` for temporary data like form drafts, and `localStorage` for persistent user preferences. Always validate data when reading from storage to handle corrupted values gracefully.