# Storage API 💾 | SigPro
Minimalist Reactive Library

Skip to content

# 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`
      <div>
        <p>Current theme: ${theme}</p>
        <button @click=${toggleTheme}>
          Toggle Theme
        </button>
      </div>
    `;

### 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`
      <form @submit=${handleSubmit}>
        <input
          type="text"
          :value=${() => draft().name}
          @input=${(e) => handleInput('name', e.target.value)}
          placeholder="Name"
        />
        <input
          type="email"
          :value=${() => draft().email}
          @input=${(e) => handleInput('email', e.target.value)}
          placeholder="Email"
        />
        <textarea
          :value=${() => draft().message}
          @input=${(e) => handleInput('message', e.target.value)}
          placeholder="Message"
        ></textarea>
        <button type="submit">Send</button>
      </form>
    `;

## 🚀 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`
        <div class="settings-panel">
          <h2>Settings</h2>

          ${Object.entries(settingsSchema).map(([key, config]) => {
            switch(config.type) {
              case 'select':
                return html`
                  <div class="setting">
                    <label>${key}:</label>
                    <select
                      :value=${() => settings()[key]}
                      @change=${(e) => updateSetting(key, e.target.value)}
                    >
                      ${config.options.map(opt => html`
                        <option value="${opt}" ?selected=${() => settings()[key] === opt}>
                          ${opt}
                        </option>
                      `)}
                    </select>
                  </div>
                `;

              case 'range':
                return html`
                  <div class="setting">
                    <label>${key}: ${() => settings()[key]}</label>
                    <input
                      type="range"
                      min="${config.min}"
                      max="${config.max}"
                      :value=${() => settings()[key]}
                      @input=${(e) => updateSetting(key, parseInt(e.target.value))}
                    />
                  </div>
                `;

              case 'checkbox':
                return html`
                  <div class="setting">
                    <label>
                      <input
                        type="checkbox"
                        :checked=${() => settings()[key]}
                        @change=${(e) => updateSetting(key, e.target.checked)}
                      />
                      ${key}
                    </label>
                  </div>
                `;
            }
          })}

          <button @click=${resetDefaults}>Reset to Defaults</button>
        </div>
      `;
    };

    // 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`
      <div class="cart">
        <h3>Shopping Cart (${itemCount} items)</h3>

        ${cartItems().map(item => html`
          <div class="cart-item">
            <span>${item.name}</span>
            <span>$${item.price} x ${item.quantity}</span>
            <span>$${item.price * item.quantity}</span>
            <button @click=${() => removeFromCart(item.id)}>Remove</button>
            <input
              type="number"
              min="1"
              :value=${item.quantity}
              @change=${(e) => updateQuantity(item.id, parseInt(e.target.value))}
            />
          </div>
        `)}

        <div class="cart-totals">
          <p>Subtotal: $${subtotal}</p>
          <p>Tax (10%): $${tax}</p>
          <p><strong>Total: $${total}</strong></p>
        </div>

        ${() => cartItems().length > 0 ? html`
          <button @click=${checkout}>Checkout</button>
          <button @click=${clearCart}>Clear Cart</button>
        ` : html`
          <p>Your cart is empty</p>
        `}
      </div>
    `;

### 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`
        <div class="search-container">
          <div class="search-box">
            <input
              type="search"
              :value=${searchInput}
              @keydown.enter=${handleSearch}
              placeholder="Search..."
            />
            <button @click=${handleSearch}>Search</button>
          </div>

          ${() => searchHistory().length > 0 ? html`
            <div class="search-history">
              <h4>Recent Searches</h4>
              ${searchHistory().map(item => html`
                <div class="history-item">
                  <button
                    class="history-query"
                    @click=${() => {
                      searchInput(item.query);
                      handleSearch();
                    }}
                  >
                    🔍 ${item.query}
                  </button>
                  <small>${new Date(item.timestamp).toLocaleString()}</small>
                  <button
                    class="remove-btn"
                    @click=${() => removeFromHistory(item.query)}
                  >
                    ✕
                  </button>
                </div>
              `)}
              <button class="clear-btn" @click=${clearHistory}>
                Clear History
              </button>
            </div>
          ` : ''}
        </div>
      `;
    };

### 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`
      <div class="profile-selector">
        <select
          :value=${() => profiles().current}
          @change=${(e) => switchProfile(e.target.value)}
        >
          ${Object.entries(profiles().list).map(([id, profile]) => html`
            <option value="${id}">${profile.name}</option>
          `)}
        </select>

        <button @click=${() => {
          const name = prompt('Enter profile name:');
          if (name) createProfile(name);
        }}>
          New Profile
        </button>
      </div>
    `;

## 🛡️ 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.