Advanced Store Patterns

Deep dive into advanced store techniques including store-to-store communication, persistence, lifecycle management, and comprehensive API reference.

📚 Prerequisites: This page covers advanced store patterns. If you're new to stores, start with the Basic Stores first.

Store-to-Store Communication

Stores can communicate with each other for complex state management, enabling sophisticated workflows and real-time updates:

// Shopping Cart Store - Central e-commerce state management
wildflower.store('cart', {
    state: {
        items: [],
        discount: 0,
        promoCode: '',
        shipping: 0,
        tax: 0,
        taxRate: 0.08
    },

    computed: {
        itemCount() {
            return this.items.reduce((sum, item) => sum + item.quantity, 0)
        },

        subtotal() {
            return this.items.reduce((sum, item) =>
                sum + (item.price * item.quantity), 0
            )
        },

        // Formatted versions for display
        subtotalFormatted() {
            return this.subtotal.toFixed(2)
        },

        taxFormatted() {
            return this.tax.toFixed(2)
        },

        total() {
            return Math.max(0, this.subtotal - this.discount + this.shipping + this.tax).toFixed(2)
        },

        isEmpty() {
            return this.items.length === 0
        }
    },

    // Methods (unified paradigm - at top level)
    addItem(product) {
        const existingItem = this.items.find(item => item.productId === product.id)

        if (existingItem) {
            existingItem.quantity += 1
        } else {
            this.items.push({
                id: Date.now(),
                productId: product.id,
                name: product.name,
                price: product.price,
                quantity: 1,
                addedAt: new Date()
            })
        }

        // Notify analytics store
        const analyticsStore = wildflower.getStore('analytics')
        analyticsStore.trackEvent('item_added', {
            productId: product.id,
            productName: product.name,
            price: product.price,
            cartTotal: this.subtotal
        })

        // Check for promotional discounts
        this.updatePromotions()
    },

    removeItem(itemId) {
        const item = this.items.find(item => item.id === itemId)
        if (item) {
            // Track removal
            const analyticsStore = wildflower.getStore('analytics')
            analyticsStore.trackEvent('item_removed', {
                productId: item.productId,
                productName: item.name,
                price: item.price,
                quantity: item.quantity
            })

            // Remove from cart
            const index = this.items.findIndex(item => item.id === itemId)
            this.items.splice(index, 1)

            // Recalculate promotions
            this.updatePromotions()
        }
    },

    updateQuantity(itemId, quantity) {
        const item = this.items.find(item => item.id === itemId)
        if (item && quantity > 0) {
            item.quantity = quantity
            this.updatePromotions()
        }
    },

    applyPromoCode(code) {
        this.promoCode = code
        this.updatePromotions()
    },

    updatePromotions() {
        const promoStore = wildflower.getStore('promotions')
        const discount = promoStore.calculateDiscount(this.items, this.promoCode)
        this.discount = discount

        // Calculate shipping
        this.shipping = this.subtotal >= 50 ? 0 : 5.99

        // Calculate tax
        this.tax = (this.subtotal - this.discount) * this.taxRate
    },

    clearCart() {
        const analyticsStore = wildflower.getStore('analytics')
        analyticsStore.trackEvent('cart_cleared', {
            itemCount: this.items.length,
            totalValue: this.subtotal
        })

        this.items = []
        this.discount = 0
        this.promoCode = ''
        this.shipping = 0
        this.tax = 0
    }
})
// Analytics Store - Track user behavior and generate insights
wildflower.store('analytics', {
    state: {
        events: [],
        sessionId: null,
        startTime: null,
        pageViews: 0,
        conversionFunnel: {
            views: 0,
            addToCart: 0,
            checkout: 0,
            purchase: 0
        }
    },

    computed: {
        sessionDuration() {
            return Math.round((new Date() - this.startTime) / 1000)
        },

        conversionRate() {
            return this.conversionFunnel.views > 0
                ? Math.round((this.conversionFunnel.addToCart / this.conversionFunnel.views) * 100)
                : 0
        },

        recentEvents() {
            return this.events.slice(-10).reverse()
        }
    },

    init() {
        this.sessionId = 'session_' + Date.now()
        this.startTime = new Date()
        this.trackEvent('session_start', {
            timestamp: this.startTime,
            userAgent: navigator.userAgent
        })
    },

    // Methods (unified paradigm - at top level)
    trackEvent(eventName, data) {
        const event = {
            id: Date.now(),
            event: eventName,
            data: data,
            formattedTime: new Date().toLocaleTimeString(),
            sessionId: this.sessionId
        }

        this.events.push(event)

        // Update conversion funnel
        this.updateConversionFunnel(eventName, data)

        // Send to analytics service (simulated)
        this.sendToAnalytics(event)

        // Keep events list manageable
        if (this.events.length > 100) {
            this.events = this.events.slice(-50)
        }
    },

    updateConversionFunnel(eventName, data) {
        switch (eventName) {
            case 'item_added':
                this.conversionFunnel.addToCart++
                break
            case 'checkout_started':
                this.conversionFunnel.checkout++
                break
            case 'purchase_completed':
                this.conversionFunnel.purchase++
                break
        }
    },

    trackPageView(page) {
        this.pageViews++
        this.conversionFunnel.views++
        this.trackEvent('page_view', {
            page: page,
            totalViews: this.pageViews
        })
    },

    async sendToAnalytics(event) {
        try {
            // Simulate API call
            console.log('📊 Analytics Event:', event)

            // In a real app, you'd send to your analytics service
            // await fetch('/api/analytics', {
            //     method: 'POST',
            //     headers: { 'Content-Type': 'application/json' },
            //     body: JSON.stringify(event)
            // })
        } catch (error) {
            console.error('Analytics error:', error)
        }
    },

    clearEvents() {
        this.events = []
    }
})
// Promotions Store - Handle discounts and special offers
wildflower.store('promotions', {
    state: {
        availablePromotions: [
            { code: 'SAVE10', discount: 0.10, minAmount: 25, description: '10% off orders over $25' },
            { code: 'WELCOME', discount: 5, minAmount: 0, description: '$5 off first order' },
            { code: 'BULK20', discount: 0.20, minAmount: 100, description: '20% off orders over $100' }
        ],
        activePromotions: [],
        seasonalMultiplier: 1.0
    },
    
    computed: {
        activePromoCodes() {
            return this.availablePromotions.map(p => p.code)
        }
    },

    // Methods at top level (Unified Entity Paradigm)
    calculateDiscount(cartItems, promoCode) {
        const subtotal = cartItems.reduce((sum, item) =>
            sum + (item.price * item.quantity), 0
        )

        let discount = 0

        // Apply promo code discount
        if (promoCode) {
            const promo = this.availablePromotions.find(p => p.code === promoCode)
            if (promo && subtotal >= promo.minAmount) {
                if (promo.discount < 1) {
                    // Percentage discount
                    discount += subtotal * promo.discount
                } else {
                    // Fixed amount discount
                    discount += promo.discount
                }
            }
        }

        // Apply automatic quantity discounts
        const itemCount = cartItems.reduce((sum, item) => sum + item.quantity, 0)
        if (itemCount >= 5) {
            discount += subtotal * 0.05 // 5% bulk discount
        }

        // Apply seasonal multiplier
        discount *= this.seasonalMultiplier

        // Track promotion usage
        if (discount > 0) {
            const analyticsStore = wildflower.getStore('analytics')
            analyticsStore.trackEvent('promotion_applied', {
                promoCode: promoCode,
                discount: discount,
                subtotal: subtotal,
                itemCount: itemCount
            })
        }

        return Math.round(discount * 100) / 100
    },

    validatePromoCode(code) {
        const promo = this.availablePromotions.find(p => p.code === code)
        return promo ? promo : null
    },

    addPromotion(promotion) {
        this.availablePromotions.push(promotion)
    },

    setSeasonalMultiplier(multiplier) {
        this.seasonalMultiplier = multiplier

        // Notify about seasonal changes
        const analyticsStore = wildflower.getStore('analytics')
        analyticsStore.trackEvent('seasonal_promotion_updated', {
            multiplier: multiplier
        })
    }
})
<div data-component="store-communication-demo">
    <p class="text-muted">Demonstrates how stores communicate and share data in real-time.</p>

    <div class="row">
        <div class="col-md-6">
            <!-- Shopping Cart -->
            <div class="card mb-3">
                <div class="card-header d-flex justify-content-between">
                    <h6 class="mb-0">Shopping Cart</h6>
                    <span class="badge bg-primary" data-bind="$cart.itemCount">0</span>
                </div>
                <div class="card-body">
                    <!-- Product Selection -->
                    <div class="mb-3">
                        <h6>Add Products:</h6>
                        <div class="d-grid gap-2">
                            <button data-action="addWidget" class="btn btn-primary btn-sm">
                                Add Widget ($19.99)
                            </button>
                            <button data-action="addGadget" class="btn btn-primary btn-sm">
                                Add Gadget ($29.99)
                            </button>
                            <button data-action="addTool" class="btn btn-primary btn-sm">
                                Add Tool ($39.99)
                            </button>
                        </div>
                    </div>
                    
                    <!-- Cart Items -->
                    <div data-show="!$cart.isEmpty">
                        <div class="mb-3">
                            <h6>Cart Items:</h6>
                            <div data-list="$cart.items">
                                <template>
                                    <div class="d-flex justify-content-between align-items-center py-2 border-bottom">
                                        <div>
                                            <strong data-bind="name"></strong>
                                            <br>
                                            <small class="text-muted">
                                                $<span data-bind="price"></span> x <span data-bind="quantity"></span>
                                            </small>
                                        </div>
                                        <button data-action="removeFromCart" class="btn btn-sm btn-danger">
                                            Remove
                                        </button>
                                    </div>
                                </template>
                            </div>
                        </div>
                        
                        <!-- Promo Code -->
                        <div class="mb-3">
                            <label class="form-label">Promo Code:</label>
                            <div class="d-flex gap-2">
                                <input type="text" data-model="promoCode" class="form-control"
                                       placeholder="Enter code (try SAVE10)">
                                <button data-action="applyPromo" class="btn btn-secondary">Apply</button>
                            </div>
                            <small class="text-muted">Try: SAVE10, WELCOME, BULK20</small>
                        </div>
                        
                        <!-- Cart Summary -->
                        <div class="border-top pt-3">
                            <div class="d-flex justify-content-between">
                                <span>Subtotal:</span>
                                <span>$<span data-bind="$cart.subtotalFormatted"></span></span>
                            </div>
                            <div class="d-flex justify-content-between" data-show="$cart.discount > 0">
                                <span class="text-success">Discount:</span>
                                <span class="text-success">-$<span data-bind="$cart.discount"></span></span>
                            </div>
                            <div class="d-flex justify-content-between">
                                <span>Shipping:</span>
                                <span>$<span data-bind="$cart.shipping"></span></span>
                            </div>
                            <div class="d-flex justify-content-between">
                                <span>Tax:</span>
                                <span>$<span data-bind="$cart.taxFormatted"></span></span>
                            </div>
                            <div class="d-flex justify-content-between fw-bold border-top pt-2">
                                <span>Total:</span>
                                <span>$<span data-bind="$cart.total"></span></span>
                            </div>
                        </div>
                        
                        <button data-action="clearCart" class="btn btn-danger mt-3 w-100">
                            Clear Cart
                        </button>
                    </div>
                    
                    <div data-show="$cart.isEmpty" class="text-center text-muted">
                        Cart is empty
                    </div>
                </div>
            </div>
        </div>
        
        <div class="col-md-6">
            <!-- Analytics Dashboard -->
            <div class="card mb-3">
                <div class="card-header">
                    <h6 class="mb-0">Analytics Dashboard</h6>
                </div>
                <div class="card-body">
                    <div class="row text-center mb-3">
                        <div class="col-6">
                            <div class="fw-bold" data-bind="$analytics.sessionDuration">0</div>
                            <small class="text-muted">Session (seconds)</small>
                        </div>
                        <div class="col-6">
                            <div class="fw-bold" data-bind="$analytics.conversionRate">0</div>
                            <small class="text-muted">Conversion %</small>
                        </div>
                    </div>
                    
                    <div class="mb-3">
                        <h6>Conversion Funnel:</h6>
                        <div class="small">
                            <div>Views: <span data-bind="$analytics.conversionFunnel.views"></span></div>
                            <div>Add to Cart: <span data-bind="$analytics.conversionFunnel.addToCart"></span></div>
                            <div>Checkout: <span data-bind="$analytics.conversionFunnel.checkout"></span></div>
                            <div>Purchase: <span data-bind="$analytics.conversionFunnel.purchase"></span></div>
                        </div>
                    </div>
                    
                    <div>
                        <h6>Recent Events:</h6>
                        <div style="max-height: 200px; overflow-y: auto;">
                            <div data-list="$analytics.recentEvents">
                                <template>
                                    <div class="small border-bottom py-1">
                                        <div class="d-flex justify-content-between gap-2">
                                            <span data-bind="event" class="fw-bold"></span>
                                            <span data-bind="formattedTime" class="text-muted text-nowrap"></span>
                                        </div>
                                    </div>
                                </template>
                            </div>
                        </div>
                    </div>
                    
                    <button data-action="clearAnalytics" class="btn btn-secondary btn-sm mt-3">
                        Clear Events
                    </button>
                </div>
            </div>
        </div>
    </div>
</div>
wildflower.component('store-communication-demo', {
    state: {
        promoCode: ''
    },

    // Subscribe to stores for this.stores auto-injection
    subscribe: {
        cart: ['items', 'discount'],
        analytics: ['events']
    },

    init() {
        // Initialize page view tracking using this.stores
        this.stores.analytics.trackPageView('store-communication-demo')
    },

    // Product addition actions
    addWidget() {
        this.stores.cart.addItem({
            id: 'widget',
            name: 'Widget',
            price: 19.99
        })
    },

    addGadget() {
        this.stores.cart.addItem({
            id: 'gadget',
            name: 'Gadget',
            price: 29.99
        })
    },

    addTool() {
        this.stores.cart.addItem({
            id: 'tool',
            name: 'Tool',
            price: 39.99
        })
    },

    // Cart management actions
    removeFromCart(event, element, details) {
        const itemId = details.item.id
        this.stores.cart.removeItem(itemId)
    },

    applyPromo() {
        this.stores.cart.applyPromoCode(this.promoCode.toUpperCase())
        this.promoCode = '' // Clear input
    },

    clearCart() {
        this.stores.cart.clearCart()
    },

    // Analytics actions
    clearAnalytics() {
        this.stores.analytics.clearEvents()
    }
})
Live Preview

Built-in Persistence (storageKey & autoSave)

WildflowerJS provides built-in localStorage persistence for stores. Simply add storageKey and autoSave options to your store definition:

// Automatic persistence - no manual localStorage code needed!
wildflower.store('app-settings', {
    storageKey: 'my-app-settings',  // localStorage key
    autoSave: true,                  // Auto-save on ANY state change

    state: {
        theme: 'light',
        fontSize: 'medium',
        notifications: true,
        recentItems: []
    },

    setTheme(theme) {
        this.theme = theme;
        // Automatically saved to localStorage!
    },

    addRecentItem(item) {
        this.recentItems = [...this.recentItems, item].slice(-10);
        // Nested changes are also auto-saved!
    }
});

// On page reload, state is automatically restored from localStorage
💡 Key Points:
  • storageKey - The localStorage key where state will be saved
  • autoSave: true - Automatically saves state on ANY change, including nested properties
  • State is automatically restored from localStorage when the store is created
  • Works with all state types: primitives, arrays, nested objects

When to use built-in persistence vs manual:

  • Built-in - Simple use cases where you want the entire store state persisted automatically
  • Manual - When you need selective persistence, encryption, or complex storage logic (see below)

Manual Store Persistence

For more control, implement custom persistence with lifecycle hooks. This demo saves your name and theme to localStorage. Change them, then refresh the page to see them persist:

// Preferences store: loads from localStorage on init, saves on change
wildflower.store('prefs', {
    state: { name: 'Guest', theme: 'light' },

    init() {
        const saved = localStorage.getItem('prefs');
        if (saved) {
            try {
                const data = JSON.parse(saved);
                if (data.name) this.name = data.name;
                if (data.theme) this.theme = data.theme;
            } catch (e) { /* ignore bad data */ }
        }
    },

    setName(name) {
        this.name = name;
        this._save();
    },

    setTheme(theme) {
        this.theme = theme;
        this._save();
    },

    _save() {
        localStorage.setItem('prefs', JSON.stringify({
            name: this.name, theme: this.theme
        }));
    },

    clearAll() {
        this.name = 'Guest';
        this.theme = 'light';
        localStorage.removeItem('prefs');
    }
});

Components bind to the store with $prefs.name and $prefs.theme. Changes persist across page reloads. The store's init() restores from localStorage on startup.

Store Lifecycle

Stores support full component lifecycle methods for initialization and cleanup, enabling sophisticated background operations and resource management:

// Data Service Store - Advanced lifecycle management with connections and retries
wildflower.store('data-service', {
    state: {
        connected: false,
        connecting: false,
        data: null,
        error: null,
        retryCount: 0,
        maxRetries: 5,
        connectionHistory: [],
        syncStatus: 'idle', // idle, syncing, success, error
        connectionQuality: 100, // 0-100 percentage
        bandwidth: 'high', // low, medium, high
        latency: 0
    },
    
    computed: {
        connectionStatus() {
            if (this.connecting) return 'connecting'
            if (this.connected) return 'connected'
            if (this.error) return 'error'
            return 'disconnected'
        },

        qualityLevel() {
            if (this.connectionQuality >= 80) return 'excellent'
            if (this.connectionQuality >= 60) return 'good'
            if (this.connectionQuality >= 40) return 'fair'
            return 'poor'
        },

        recentHistory() {
            return this.connectionHistory.slice(0, 10)
        }
    },

    // Initialize when store is created
    init() {
        console.log('🚀 Data Service Store initializing...')
        this.initializeConnectionHistory()
        this.connect()
        this.setupPeriodicSync()
        this.setupConnectionMonitoring()
    },

    // Cleanup when store is destroyed
    destroy() {
        console.log('🛑 Data Service Store destroying...')

        if (this.syncInterval) {
            clearInterval(this.syncInterval)
        }

        if (this.monitorInterval) {
            clearInterval(this.monitorInterval)
        }

        this.disconnect()
        this.logConnection('destroyed', 'Store destroyed')
    },

    // Helper methods
    delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms))
    },

    // Methods at top level (Unified Entity Paradigm)
    initializeConnectionHistory() {
        this.connectionHistory = []
        this.logConnection('initialization', 'Store initialized')
    },

    async connect() {
        if (this.connecting || this.connected) return

        try {
            this.connecting = true
            this.error = null
            this.logConnection('connecting', 'Attempting connection...')

            // Simulate connection negotiation
            await this.delay(Math.random() * 1000 + 500)

            // Simulate occasional connection failures
            if (Math.random() < 0.15) {
                throw new Error('Connection timeout')
            }

            this.connected = true
            this.connecting = false
            this.retryCount = 0
            this.connectionQuality = Math.round(Math.random() * 30 + 70)
            this.bandwidth = ['low', 'medium', 'high'][Math.floor(Math.random() * 3)]
            this.latency = Math.round(Math.random() * 100 + 20)

            this.logConnection('connected', 'Connection established successfully')
            this.loadInitialData()

        } catch (error) {
            this.connected = false
            this.connecting = false
            this.error = error.message
            this.retryCount++

            this.logConnection('error', `Connection failed: ${error.message}`)

            // Retry with exponential backoff
            if (this.retryCount < this.maxRetries) {
                const delay = Math.pow(2, this.retryCount) * 1000
                this.logConnection('retry', `Retrying in ${delay}ms (attempt ${this.retryCount + 1})`)
                setTimeout(() => this.connect(), delay)
            } else {
                this.logConnection('failed', 'Max retries exceeded')
            }
        }
    },

    async loadInitialData() {
        if (!this.connected) return

        try {
            this.syncStatus = 'syncing'
            this.logConnection('sync', 'Loading data...')

            // Simulate data loading with variable delay
            await this.delay(Math.random() * 1000 + 300)

            // Simulate occasional data errors
            if (Math.random() < 0.1) {
                throw new Error('Data fetch failed')
            }

            this.data = {
                lastSync: new Date(),
                items: this.generateSampleData(),
                version: Math.floor(Math.random() * 1000),
                checksum: this.generateChecksum()
            }

            this.syncStatus = 'success'
            this.logConnection('sync_success', `Data loaded: ${this.data.items.length} items`)

        } catch (error) {
            this.error = error.message
            this.syncStatus = 'error'
            this.logConnection('sync_error', `Data load failed: ${error.message}`)
        }
    },

    async refreshData() {
        if (!this.connected) {
            this.connect()
            return
        }

        await this.loadInitialData()
    },

    setupPeriodicSync() {
        this.syncInterval = setInterval(() => {
            if (this.connected && this.syncStatus !== 'syncing') {
                this.loadInitialData()
            }
        }, 15000) // Sync every 15 seconds
    },

    setupConnectionMonitoring() {
        this.monitorInterval = setInterval(() => {
            if (this.connected) {
                // Simulate connection quality changes
                const qualityChange = Math.random() * 20 - 10
                this.connectionQuality = Math.round(Math.max(0, Math.min(100,
                    this.connectionQuality + qualityChange)))

                // Simulate latency changes
                this.latency = Math.round(Math.max(10,
                    this.latency + Math.random() * 40 - 20))

                // Simulate disconnection on poor quality
                if (this.connectionQuality < 20) {
                    this.disconnect()
                }
            }
        }, 5000) // Monitor every 5 seconds
    },

    disconnect() {
        if (this.connected) {
            this.connected = false
            this.connecting = false
            this.connectionQuality = 0
            this.logConnection('disconnected', 'Connection closed')

            // Auto-reconnect after a delay
            setTimeout(() => {
                if (!this.connected && !this.connecting) {
                    this.connect()
                }
            }, 3000)
        }
    },

    forceReconnect() {
        this.disconnect()
        setTimeout(() => this.connect(), 1000)
    },

    clearHistory() {
        this.connectionHistory = []
        this.logConnection('history_cleared', 'Connection history cleared')
    },

    logConnection(type, message) {
        const entry = {
            id: Date.now(),
            type,
            message,
            timestamp: new Date().toLocaleTimeString(),
            quality: this.connectionQuality,
            latency: this.latency
        }

        this.connectionHistory.unshift(entry)

        // Keep history manageable
        if (this.connectionHistory.length > 50) {
            this.connectionHistory = this.connectionHistory.slice(0, 50)
        }
    },

    generateSampleData() {
        const items = []
        const count = Math.floor(Math.random() * 10) + 5

        for (let i = 0; i < count; i++) {
            items.push({
                id: i + 1,
                name: `Item ${i + 1}`,
                value: Math.floor(Math.random() * 1000),
                type: ['A', 'B', 'C'][Math.floor(Math.random() * 3)]
            })
        }

        return items
    },

    generateChecksum() {
        return Math.random().toString(36).substring(7).toUpperCase()
    }
})
// Background Tasks Store - Task scheduling and lifecycle management
wildflower.store('background-tasks', {
    state: {
        tasks: [],
        runningTasks: 0,
        maxConcurrentTasks: 3,
        totalCompleted: 0,
        totalFailed: 0,
        scheduler: null,
        paused: false
    },
    
    computed: {
        pendingTasks() {
            return this.tasks.filter(t => t.status === 'pending')
        },

        runningTaskList() {
            return this.tasks.filter(t => t.status === 'running')
        },

        completedTasks() {
            return this.tasks.filter(t => t.status === 'completed')
        },

        failedTasks() {
            return this.tasks.filter(t => t.status === 'failed')
        },

        successRate() {
            const total = this.totalCompleted + this.totalFailed
            return total > 0 ? Math.round((this.totalCompleted / total) * 100) : 0
        }
    },

    init() {
        console.log('⏰ Background Tasks Store initializing...')
        this.startScheduler()
        this.addSampleTasks()
    },

    destroy() {
        console.log('🛑 Background Tasks Store destroying...')

        if (this.scheduler) {
            clearInterval(this.scheduler)
        }

        // Cancel all pending tasks
        this.tasks.forEach(task => {
            if (task.status === 'pending') {
                task.status = 'cancelled'
            }
        })
    },

    delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms))
    },

    // Methods at top level (Unified Entity Paradigm)
    addTask(task) {
        const newTask = {
            id: Date.now() + Math.random(),
            name: task.name,
            type: task.type || 'generic',
            priority: task.priority || 'normal',
            status: 'pending',
            progress: 0,
            duration: task.duration || 3000,
            createdAt: new Date(),
            startedAt: null,
            completedAt: null,
            error: null,
            result: null
        }

        this.tasks.push(newTask)
        this.sortTasksByPriority()
    },

    addSampleTasks() {
        const sampleTasks = [
            { name: 'Data Backup', type: 'backup', priority: 'high', duration: 5000 },
            { name: 'Cache Cleanup', type: 'maintenance', priority: 'normal', duration: 2000 },
            { name: 'Report Generation', type: 'report', priority: 'low', duration: 8000 },
            { name: 'Log Rotation', type: 'maintenance', priority: 'normal', duration: 1500 },
            { name: 'Security Scan', type: 'security', priority: 'high', duration: 7000 }
        ]

        sampleTasks.forEach(task => this.addTask(task))
    },

    sortTasksByPriority() {
        const priorityOrder = { 'high': 0, 'normal': 1, 'low': 2 }
        this.tasks.sort((a, b) => {
            if (a.status === 'pending' && b.status !== 'pending') return -1
            if (a.status !== 'pending' && b.status === 'pending') return 1
            return priorityOrder[a.priority] - priorityOrder[b.priority]
        })
    },

    async runTask(taskId) {
        const task = this.tasks.find(t => t.id === taskId)
        if (!task || task.status !== 'pending') return

        task.status = 'running'
        task.startedAt = new Date()
        task.progress = 0
        this.runningTasks++

        try {
            // Simulate task execution with progress updates.
            // Roll failure ONCE per task (~10%), not per step — a per-step roll
            // compounds to a ~65% task failure rate across 10 steps.
            const steps = 10
            const failAtStep = Math.random() < 0.1 ? Math.floor(Math.random() * steps) : -1
            for (let i = 0; i < steps; i++) {
                // Pause freezes RUNNING tasks mid-flight, not just the
                // scheduler's pickup of pending ones — progress bars hold
                // in place until Resume.
                while (this.paused) {
                    await this.delay(150)
                }
                await this.delay(task.duration / steps)
                task.progress = Math.round(((i + 1) / steps) * 100)

                if (i === failAtStep) {
                    throw new Error('Task execution failed')
                }
            }

            task.status = 'completed'
            task.completedAt = new Date()
            task.result = `Task completed successfully in ${task.duration}ms`
            this.totalCompleted++

        } catch (error) {
            task.status = 'failed'
            task.error = error.message
            task.completedAt = new Date()
            this.totalFailed++
        }

        this.runningTasks--
    },

    startScheduler() {
        if (this.scheduler) return

        this.scheduler = setInterval(() => {
            if (this.paused) return

            // Find pending tasks that can be started
            const pendingTasks = this.tasks.filter(t => t.status === 'pending')
            const availableSlots = this.maxConcurrentTasks - this.runningTasks

            if (pendingTasks.length > 0 && availableSlots > 0) {
                const tasksToStart = pendingTasks.slice(0, availableSlots)
                tasksToStart.forEach(task => this.runTask(task.id))
            }
        }, 1000)
    },

    pauseScheduler() {
        this.paused = true
    },

    resumeScheduler() {
        this.paused = false
    },

    cancelTask(taskId) {
        const task = this.tasks.find(t => t.id === taskId)
        if (task && task.status === 'pending') {
            task.status = 'cancelled'
            task.completedAt = new Date()
        }
    },

    clearCompleted() {
        this.tasks = this.tasks.filter(t =>
            !['completed', 'failed', 'cancelled'].includes(t.status)
        )
    },

    addRandomTask() {
        const types = ['backup', 'maintenance', 'report', 'security', 'sync']
        const priorities = ['high', 'normal', 'low']

        this.addTask({
            name: `Random Task ${Math.floor(Math.random() * 1000)}`,
            type: types[Math.floor(Math.random() * types.length)],
            priority: priorities[Math.floor(Math.random() * priorities.length)],
            duration: Math.floor(Math.random() * 5000) + 1000
        })
    },

    resetStats() {
        this.totalCompleted = 0
        this.totalFailed = 0
    }
})
<div data-component="lifecycle-demo">
    <p class="text-muted">Demonstrates advanced store lifecycle management with background tasks and data services.</p>

    <div class="row">
        <div class="col-md-6">
            <!-- Data Service Status -->
            <div class="card mb-3">
                <div class="card-header d-flex justify-content-between align-items-center">
                    <h6 class="mb-0">Data Service</h6>
                    <span data-bind="$data-service.connectionStatus" 
                          data-bind-class="$data-service.connected ? 'badge bg-success' : $data-service.connecting ? 'badge bg-warning' : 'badge bg-danger'">
                        disconnected
                    </span>
                </div>
                <div class="card-body">
                    <!-- Connection Info -->
                    <div class="row mb-3">
                        <div class="col-4">
                            <small class="text-muted">Quality</small>
                            <div class="fw-bold">
                                <span data-bind="$data-service.connectionQuality">0</span>%
                                <span data-bind="$data-service.qualityLevel" 
                                      data-bind-class="$data-service.qualityLevel === 'excellent' ? 'text-success' : $data-service.qualityLevel === 'good' ? 'text-info' : $data-service.qualityLevel === 'fair' ? 'text-warning' : 'text-danger'">
                                    (poor)
                                </span>
                            </div>
                        </div>
                        <div class="col-4">
                            <small class="text-muted">Latency</small>
                            <div class="fw-bold"><span data-bind="$data-service.latency">0</span>ms</div>
                        </div>
                        <div class="col-4">
                            <small class="text-muted">Bandwidth</small>
                            <div class="fw-bold text-capitalize" data-bind="$data-service.bandwidth">high</div>
                        </div>
                    </div>
                    
                    <!-- Data Info -->
                    <div data-show="$data-service.data">
                        <div class="mb-3">
                            <h6>Data Status:</h6>
                            <div class="row">
                                <div class="col-6">
                                    <small class="text-muted">Items</small>
                                    <div class="fw-bold"><span data-bind="$data-service.data ? $data-service.data.items.length : 0">0</span></div>
                                </div>
                            </div>
                        </div>
                    </div>
                    
                    <!-- Error Display -->
                    <div data-show="$data-service.error" class="alert alert-danger py-2">
                        <small>Error: <span data-bind="$data-service.error"></span></small>
                    </div>
                    
                    <!-- Actions -->
                    <div class="d-grid gap-2">
                        <button data-action="refreshData" class="btn btn-primary btn-sm">
                            Refresh Data
                        </button>
                        <button data-action="forceReconnect" class="btn btn-warning btn-sm">
                            Force Reconnect
                        </button>
                        <button data-action="clearServiceHistory" class="btn btn-secondary btn-sm">
                            Clear History
                        </button>
                    </div>
                </div>
            </div>
            
            <!-- Connection History -->
            <div class="card">
                <div class="card-header">
                    <h6 class="mb-0">Connection History</h6>
                </div>
                <div class="card-body p-0">
                    <div style="max-height: 200px; overflow-y: auto;">
                        <div data-list="$data-service.recentHistory">
                            <template>
                                <div class="px-3 py-2 border-bottom small">
                                    <div class="d-flex justify-content-between">
                                        <span data-bind="type" class="fw-bold text-capitalize"></span>
                                        <span data-bind="timestamp" class="text-muted"></span>
                                    </div>
                                    <div data-bind="message" class="text-muted"></div>
                                </div>
                            </template>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        
        <div class="col-md-6">
            <!-- Background Tasks -->
            <div class="card mb-3">
                <div class="card-header d-flex justify-content-between align-items-center">
                    <h6 class="mb-0">Background Tasks</h6>
                    <span data-bind="$background-tasks.runningTasks" class="badge bg-info">0</span>
                </div>
                <div class="card-body">
                    <!-- Task Stats -->
                    <div class="row mb-3">
                        <div class="col-4">
                            <small class="text-muted">Pending</small>
                            <div class="fw-bold"><span data-bind="$background-tasks.pendingTasks.length">0</span></div>
                        </div>
                        <div class="col-4">
                            <small class="text-muted">Success Rate</small>
                            <div class="fw-bold"><span data-bind="$background-tasks.successRate">0</span>%</div>
                        </div>
                        <div class="col-4">
                            <small class="text-muted">Failed</small>
                            <div class="fw-bold text-danger"><span data-bind="$background-tasks.totalFailed">0</span></div>
                        </div>
                    </div>
                    
                    <!-- Scheduler Controls -->
                    <div class="mb-3">
                        <div class="d-flex justify-content-between align-items-center mb-2">
                            <span>Scheduler:</span>
                            <span data-bind="$background-tasks.paused ? 'PAUSED' : 'RUNNING'" 
                                  data-bind-class="$background-tasks.paused ? 'text-warning' : 'text-success'">
                                RUNNING
                            </span>
                        </div>
                            <button data-action="pauseScheduler" class="btn btn-warning btn-sm me-2">
                                Pause
                            </button>
                            <button data-action="resumeScheduler" class="btn btn-success btn-sm">
                                Resume
                            </button>
                    </div>
                    
                    <!-- Task Actions -->
                    <div class="d-grid gap-2">
                        <button data-action="addRandomTask" class="btn btn-primary btn-sm">
                            Add Random Task
                        </button>
                        <button data-action="clearCompleted" class="btn btn-secondary btn-sm">
                            Clear Completed
                        </button>
                        <button data-action="resetTaskStats" class="btn btn-danger btn-sm">
                            Reset Stats
                        </button>
                    </div>
                </div>
            </div>
            
            <!-- Running Tasks -->
            <div class="card">
                <div class="card-header">
                    <h6 class="mb-0">Active Tasks</h6>
                </div>
                <div class="card-body p-0">
                    <div style="max-height: 200px; overflow-y: auto;">
                        <div data-list="$background-tasks.runningTaskList">
                            <template>
                                <div class="px-3 py-2 border-bottom">
                                    <div class="d-flex justify-content-between align-items-center">
                                        <div>
                                            <strong data-bind="name"></strong>
                                            <br>
                                            <small class="text-muted">
                                                <span data-bind="type" class="text-capitalize"></span> • 
                                                <span data-bind="priority" class="text-capitalize"></span>
                                            </small>
                                        </div>
                                        <div class="text-end">
                                            <div data-bind="progress" class="fw-bold">0</div>
                                            <small class="text-muted">%</small>
                                        </div>
                                    </div>
                                    <div class="progress mt-2" style="height: 12px;">
                                        <div class="progress-bar bg-primary"
                                             data-bind-style="{ width: progress + '%' }">
                                        </div>
                                    </div>
                                </div>
                            </template>
                        </div>
                        <div data-show="$background-tasks.runningTaskList.length === 0" class="text-center p-3 text-muted">
                            No active tasks
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
wildflower.component('lifecycle-demo', {
    // Data Service actions
    refreshData() {
        const dataService = wildflower.getStore('data-service')
        dataService.refreshData()
    },
    
    forceReconnect() {
        const dataService = wildflower.getStore('data-service')
        dataService.forceReconnect()
    },
    
    clearServiceHistory() {
        const dataService = wildflower.getStore('data-service')
        dataService.clearHistory()
    },
    
    // Background Tasks actions
    addRandomTask() {
        const taskStore = wildflower.getStore('background-tasks')
        taskStore.addRandomTask()
    },
    
    pauseScheduler() {
        const taskStore = wildflower.getStore('background-tasks')
        taskStore.pauseScheduler()
    },
    
    resumeScheduler() {
        const taskStore = wildflower.getStore('background-tasks')
        taskStore.resumeScheduler()
    },
    
    clearCompleted() {
        const taskStore = wildflower.getStore('background-tasks')
        taskStore.clearCompleted()
    },
    
    resetTaskStats() {
        const taskStore = wildflower.getStore('background-tasks')
        taskStore.resetStats()
    }
})
Live Preview

Headless Simulations: tick() in a Store

Stores can implement tick(dt) to drive per-frame work that happens outside any component. The store owns the simulation state and advances it each animation frame; one or more components subscribe and render. Since the store persists after any view is destroyed, you can switch routes, swap viewports, or render the same simulation in multiple components, and the simulation keeps running.

See the physarum-store demo for a complete example: a physics store with no DOM owns the agent grid and pheromone field, and a separate viewport component subscribes and renders. tick(dt) signature, dt clamping, and ordering rules are documented on the Lifecycle Hooks page.

Virtual Component Protection

Store components are automatically protected from garbage collection during DOM changes:

⚠️ Important: Store components have the isVirtual: true flag that protects them from being destroyed during modal operations, route changes, or other DOM manipulations. This ensures persistent global state.
// Internal store component structure
const storeInstance = {
    id: instanceId,
    name: storeName,
    state: reactiveState,
    stateManager: stateManager,
    definition: storeDefinition,
    context: storeContext,
    isVirtual: true  // This flag protects from DOM-based cleanup
}

// Framework garbage collection excludes virtual components
if (instance.isVirtual || instance.isPersistent) {
    // Don't garbage collect virtual/persistent components
    return
}

Store API Quick Reference

API Purpose
wildflower.store(name, config)Create a global store
wildflower.getStore(name)Get store instance
$storeName.propertyBind to store state in HTML
subscribe: ['storeName']Wait for store + enable this.stores
subscribe: { store: ['path'] }Wait + receive onStoreUpdate notifications
store.subscribe(path, callback)Programmatic subscription
watch: { 'store:name.path': fn }Watch store changes in component
Full API details: See Store API Reference for complete documentation of all store methods, subscription patterns, the $ shorthand, onStoreUpdate, and subscribe-wait timing.

Store Initialization and Readiness

Stores with async init() provide readiness APIs. Components using subscribe: automatically wait for stores before their init() runs. See Store API Reference for isReady(), waitForReady(), and timeout configuration.

Store-Backed List Patterns

When rendering lists from store data, you have three options. All three work correctly with full reactivity:

Option 1: $ Shorthand (Recommended)

The most direct approach - bind the list directly to store data:

<!-- Bind directly to store items -->
<div data-list="$cart.items">
    <template>
        <div class="item">
            <span data-bind="name"></span>
            <span data-bind="price"></span>
        </div>
    </template>
</div>

<!-- Bind to a computed store property -->
<div data-list="$inventory.availableItems">
    <template>...</template>
</div>

Option 2: Computed Property

Use a computed property that returns store data:

<div data-list="items">
    <template>
        <div class="item" data-bind="name"></div>
    </template>
</div>
wildflower.component('item-list', {
    state: {},
    computed: {
        items() {
            return wildflower.getStore('myStore').items
        }
    }
})

Option 3: Subscribe Pattern

Sync store data to component state using subscriptions:

wildflower.component('synced-list', {
    state: {
        items: []
    },
    init() {
        const store = wildflower.getStore('myStore')
        // Initial sync
        this.items = [...store.items]
        // Subscribe to changes - callback receives (newValue, oldValue)
        store.subscribe('items', (newItems) => {
            if (Array.isArray(newItems)) {
                this.items = [...newItems]
            }
        })
    }
})
Which pattern to choose?
  • $entity.path - Simplest for read-only bindings, no extra component code needed
  • computed - Clean when you need to derive values from store data
  • subscribe - Required for two-way sync when component has editable local state that mirrors store data
⚠️ When Subscribe is Required: If your component has a local editing state (like form inputs) that needs to stay in sync with store changes from other sources, you must use the subscribe pattern. The $entity.path syntax only provides one-way display binding.

Example: A settings panel inside a list item component that edits column names. When the store updates (from another component), the input field values need to update too.

Best Practices

✅ Do
  • Create focused, single-responsibility stores
  • Use computed properties for derived data
  • Implement store persistence for user settings
  • Use store-to-store communication for complex workflows
  • Leverage lifecycle hooks for initialization and cleanup
  • Trust the framework's virtual component protection
❌ Don't
  • Create monolithic stores mixing unrelated data
  • Mutate store state directly from components
  • Forget to handle async operations properly
  • Create circular dependencies between stores
  • Store temporary UI state in global stores
  • Manually manage store cleanup (framework handles it)
Lite Analytics Dashboard

Multi-store architecture with real-time chart updates, computed metrics, and theme switching.