Pattern Overview

These patterns help you build robust applications that handle real-world scenarios like slow networks, offline usage, and data structure changes.

💾

Local Caching

Reduce API calls with localStorage caching

Optimistic Updates

Instant UI feedback while saving

📋

Data Versioning

Track and migrate data structure changes

🔀

Merge Strategy

Handle concurrent updates gracefully

Pattern 1: UserDataManager Class

A complete data manager that handles caching, saving, and provides a clean API for your app:

UserDataManager.js
class UserDataManager {
    constructor(defaults = {}) {
        this.defaults = defaults;
        this.cache = null;
        this.cacheKey = 'gammaltech_userdata_cache';
        this.listeners = [];
    }
    
    // Get data with caching
    async get() {
        // Return cache if available
        if (this.cache) return this.cache;
        
        // Try localStorage first (offline support)
        const cached = localStorage.getItem(this.cacheKey);
        if (cached) {
            this.cache = JSON.parse(cached);
        }
        
        // Fetch fresh data if logged in
        if (GammalTech.isLoggedIn()) {
            try {
                const remote = await GammalTech.user.get();
                this.cache = { ...this.defaults, ...remote };
                this._saveToLocalStorage();
            } catch (e) {
                console.warn('Failed to fetch remote data', e);
            }
        }
        
        return this.cache || this.defaults;
    }
    
    // Update specific fields
    async update(updates) {
        const current = await this.get();
        const updated = { ...current, ...updates };
        return this.save(updated);
    }
    
    // Save with optimistic update
    async save(data) {
        // Optimistic: update cache immediately
        this.cache = data;
        this._saveToLocalStorage();
        this._notifyListeners();
        
        // Save to server
        if (GammalTech.isLoggedIn()) {
            try {
                await GammalTech.user.save(data);
            } catch (e) {
                console.error('Failed to save to server', e);
                // Data is still in localStorage, will sync later
            }
        }
        
        return data;
    }
    
    // Subscribe to changes
    subscribe(callback) {
        this.listeners.push(callback);
        return () => {
            this.listeners = this.listeners.filter(l => l !== callback);
        };
    }
    
    // Clear cache (on logout)
    clear() {
        this.cache = null;
        localStorage.removeItem(this.cacheKey);
    }
    
    _saveToLocalStorage() {
        localStorage.setItem(this.cacheKey, JSON.stringify(this.cache));
    }
    
    _notifyListeners() {
        this.listeners.forEach(cb => cb(this.cache));
    }
}

// Usage
const userData = new UserDataManager({
    theme: 'light',
    language: 'en'
});

// Get data
const data = await userData.get();

// Update single field
await userData.update({ theme: 'dark' });

// Subscribe to changes
userData.subscribe((data) => {
    console.log('Data changed:', data);
});

Pattern 2: Data Versioning & Migration

When your data structure evolves, use versioning to migrate old data automatically:

Data migration system
const CURRENT_VERSION = 3;

const migrations = {
    // v1 -> v2: Renamed 'darkMode' to 'theme'
    1: (data) => ({
        ...data,
        theme: data.darkMode ? 'dark' : 'light',
        darkMode: undefined
    }),
    
    // v2 -> v3: Added settings object
    2: (data) => ({
        ...data,
        settings: {
            theme: data.theme || 'light',
            language: data.language || 'en'
        },
        theme: undefined,
        language: undefined
    })
};

async function getDataWithMigration() {
    let data = await GammalTech.user.get();
    const version = data._version || 1;
    
    // Run migrations if needed
    if (version < CURRENT_VERSION) {
        for (let v = version; v < CURRENT_VERSION; v++) {
            if (migrations[v]) {
                data = migrations[v](data);
                console.log(`Migrated from v${v} to v${v + 1}`);
            }
        }
        
        // Save migrated data
        data._version = CURRENT_VERSION;
        await GammalTech.user.save(data);
    }
    
    return data;
}

// Always save with version
async function saveData(data) {
    data._version = CURRENT_VERSION;
    await GammalTech.user.save(data);
}
Migration Best Practice

Always keep old migration functions even after all users have migrated. A user who hasn't logged in for months might still have v1 data.

Pattern 3: Debounced Auto-Save

For frequently changing data (like form inputs), debounce saves to avoid excessive API calls:

Auto-save with debounce
class AutoSaveManager {
    constructor(delay = 1000) {
        this.delay = delay;
        this.timeout = null;
        this.pendingData = null;
        this.isSaving = false;
    }
    
    // Queue data for saving
    save(data) {
        this.pendingData = data;
        
        // Clear existing timeout
        if (this.timeout) {
            clearTimeout(this.timeout);
        }
        
        // Schedule save
        this.timeout = setTimeout(() => {
            this._doSave();
        }, this.delay);
        
        // Update UI to show "saving..."
        this._showSavingIndicator();
    }
    
    // Force immediate save
    async flush() {
        if (this.timeout) {
            clearTimeout(this.timeout);
        }
        if (this.pendingData) {
            await this._doSave();
        }
    }
    
    async _doSave() {
        if (!this.pendingData || this.isSaving) return;
        
        this.isSaving = true;
        const dataToSave = this.pendingData;
        this.pendingData = null;
        
        try {
            await GammalTech.user.save(dataToSave);
            this._showSavedIndicator();
        } catch (e) {
            console.error('Auto-save failed', e);
            this._showErrorIndicator();
        } finally {
            this.isSaving = false;
        }
    }
    
    _showSavingIndicator() {
        document.getElementById('saveStatus').textContent = 'Saving...';
    }
    
    _showSavedIndicator() {
        document.getElementById('saveStatus').textContent = '✓ Saved';
    }
    
    _showErrorIndicator() {
        document.getElementById('saveStatus').textContent = '⚠ Save failed';
    }
}

// Usage
const autoSave = new AutoSaveManager(1500); // 1.5s delay

// On every form change
document.getElementById('bio').addEventListener('input', async (e) => {
    const current = await GammalTech.user.get();
    current.bio = e.target.value;
    autoSave.save(current);
});

// Before leaving page
window.addEventListener('beforeunload', () => {
    autoSave.flush();
});

Pattern 4: Deep Nested Updates

Safely update deeply nested properties without losing other data:

Deep update helper
// Deep merge utility
function deepMerge(target, source) {
    const result = { ...target };
    
    for (const key in source) {
        if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
            result[key] = deepMerge(result[key] || {}, source[key]);
        } else {
            result[key] = source[key];
        }
    }
    
    return result;
}

// Update at path (like "settings.notifications.email")
function setAtPath(obj, path, value) {
    const parts = path.split('.');
    const result = { ...obj };
    let current = result;
    
    for (let i = 0; i < parts.length - 1; i++) {
        current[parts[i]] = { ...current[parts[i]] };
        current = current[parts[i]];
    }
    
    current[parts[parts.length - 1]] = value;
    return result;
}

// Usage examples
async function updateNestedSetting(path, value) {
    const data = await GammalTech.user.get();
    const updated = setAtPath(data, path, value);
    await GammalTech.user.save(updated);
}

// Update just email notifications
await updateNestedSetting('settings.notifications.email', false);

// Merge multiple nested changes
const data = await GammalTech.user.get();
const updated = deepMerge(data, {
    settings: {
        theme: 'dark',
        notifications: {
            push: true
        }
    }
});
await GammalTech.user.save(updated);

Pattern 5: Managing Collections

Add, remove, and update items in arrays (favorites, cart, history):

Collection helpers
class UserCollection {
    constructor(collectionName) {
        this.name = collectionName;
    }
    
    async getAll() {
        const data = await GammalTech.user.get();
        return data[this.name] || [];
    }
    
    async add(item) {
        const data = await GammalTech.user.get();
        const collection = data[this.name] || [];
        
        // Add with timestamp
        collection.push({
            ...item,
            _id: Date.now().toString(),
            _addedAt: new Date().toISOString()
        });
        
        data[this.name] = collection;
        await GammalTech.user.save(data);
        return collection;
    }
    
    async remove(itemId) {
        const data = await GammalTech.user.get();
        data[this.name] = (data[this.name] || []).filter(i => i._id !== itemId);
        await GammalTech.user.save(data);
        return data[this.name];
    }
    
    async update(itemId, updates) {
        const data = await GammalTech.user.get();
        data[this.name] = (data[this.name] || []).map(item => 
            item._id === itemId ? { ...item, ...updates } : item
        );
        await GammalTech.user.save(data);
        return data[this.name];
    }
    
    async has(predicate) {
        const items = await this.getAll();
        return items.some(predicate);
    }
}

// Usage: Favorites collection
const favorites = new UserCollection('favorites');

// Add to favorites
await favorites.add({ productId: 'prod_123', name: 'Cool Product' });

// Check if favorited
const isFav = await favorites.has(f => f.productId === 'prod_123');

// Remove from favorites
await favorites.remove('1704450000000');

// Get all favorites
const allFavs = await favorites.getAll();

Pattern 6: Sync on Authentication

Merge local guest data with server data when user logs in:

Auth sync strategy
const LOCAL_KEY = 'guest_data';

// Save locally when not logged in
function saveLocalData(data) {
    localStorage.setItem(LOCAL_KEY, JSON.stringify(data));
}

// Get local guest data
function getLocalData() {
    const stored = localStorage.getItem(LOCAL_KEY);
    return stored ? JSON.parse(stored) : {};
}

// Handle login - merge local with remote
GammalTech.onAuth(
    async (token) => {
        // User logged in
        const localData = getLocalData();
        const remoteData = await GammalTech.user.get();
        
        // Merge strategy: remote wins for conflicts, keep local additions
        const merged = {
            ...localData,        // Local as base
            ...remoteData,       // Remote overwrites
            // Special handling for arrays (combine, dedupe)
            favorites: [
                ...(remoteData.favorites || []),
                ...(localData.favorites || []).filter(local => 
                    !(remoteData.favorites || []).some(r => r.productId === local.productId)
                )
            ]
        };
        
        // Save merged data
        await GammalTech.user.save(merged);
        
        // Clear local storage
        localStorage.removeItem(LOCAL_KEY);
        
        console.log('Data synced!');
    },
    () => {
        // User logged out - continue with local storage
        console.log('Using local storage');
    }
);

Best Practices Summary

Do

• Cache data locally for offline support and speed
• Use versioning from day one
• Debounce frequent saves
• Handle merge conflicts gracefully
• Clear cache on logout

⚠️ Don't

• Don't save on every keystroke without debouncing
• Don't assume data structure never changes
• Don't forget to handle offline scenarios
• Don't store sensitive data (passwords, tokens)

🤖

AI Prompt for Vibe Coding

Advanced Patterns

Copy this prompt for help with advanced data patterns:

I need help with advanced Gammal Tech user data patterns. Available Methods: - GammalTech.user.get() → Promise - GammalTech.user.save(data) → Promise - save() REPLACES all data (not merge) Common Patterns: 1. UserDataManager - Caching + optimistic updates 2. Data Versioning - _version field + migrations 3. Debounced Auto-Save - Reduce API calls 4. Deep Updates - setAtPath() helper 5. Collections - add/remove/update arrays 6. Auth Sync - Merge guest data on login Key Concepts: - Cache in localStorage for offline - Always include _version for migrations - Debounce saves (1-2 seconds) - Deep merge for nested objects - Clear cache on logout Please help me implement: [DESCRIBE YOUR ADVANCED DATA PATTERN]