new structure
This commit is contained in:
952
packages/docs/api/storage.md
Normal file
952
packages/docs/api/storage.md
Normal file
@@ -0,0 +1,952 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user