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

A store has no element and nothing to render, so it runs a shorter lifecycle than a component. init() runs once when the store registers, and may be async. beforeDestroy() and then destroy() run when the store is unregistered, which is where timers and connections opened in init() are closed. tick() runs on the shared frame loop, and watch: {} reacts to changes in the store's state.

The component render hooks beforeInit, beforeUpdate and onUpdate have no moment to run in a store, so a store ignores them, and development builds warn (WF-219). The demo below opens connections and timers in init() and closes them in destroy():

// 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.

Pools in a Store

A store can declare pools the same way a component does. Put the pool in a store when several components need the same collection, or when the collection must stay after the component showing it is destroyed. The store's tick(dt) steps the entities, and each component reads the pool and draws it its own way.

wildflower.store('swarm', {
    state: { running: true },
    pools: { bees: {} },
    computed: {
        count() { return this.pools.bees.length; }
    },
    init() {
        for (let i = 0; i < 200; i++) {
            this.pools.bees.push({ id: i, x: Math.random() * 400, y: Math.random() * 300, vx: 0, vy: 0 });
        }
    },
    tick(dt) {
        if (!this.running) return;
        const s = dt / 1000;
        for (const b of this.pools.bees) {
            b.vx += (Math.random() - 0.5) * 40 * s;
            b.vy += (Math.random() - 0.5) * 40 * s;
            b.x += b.vx * s;
            b.y += b.vy * s;
        }
    }
});

wildflower.component('swarm-view', {
    subscribe: ['swarm'],
    init() { this._g = this.find('canvas').getContext('2d'); },
    tick() {
        const g = this._g;
        g.clearRect(0, 0, 400, 300);
        for (const b of this.stores.swarm.pools.bees) g.fillRect(b.x, b.y, 2, 2);
    }
});
<div data-component="swarm-view">
    <canvas width="400" height="300"></canvas>
    <p>Bees: <span data-bind="$swarm.count"></span></p>
</div>
  • The same pool. The pools block takes the same options as a component's (entity, key, props, onAdd, onRemove, onClear), this.getPool(name, { onAdd, onRemove, onClear }) attaches hooks at run time as a component's does, and the pool has the same API: push, get, update, remove, clear, length, items, iteration, filter and the rest. Inside the store it is this.pools.name; a subscribed component or store reaches it as this.stores.storeName.pools.name, and any code as wildflower.getStore('storeName').pools.name.
  • Data only. A store has no DOM, so its pools have no container and no template. The DOM methods answer without one: getElement() returns undefined, at(i) is items[i], and swap() moves nothing (it still returns true when both keys exist, as a drawn pool's does).
  • A data-pool container renders its own component's pool only. Pointing one at a store's pool, by its name or as $storeName.name, renders nothing. A development build says so and identifies the store (WF-408 when the component declares pools of its own, WF-409 once the page settles otherwise). To show a store's entities, draw them in a component's tick() (canvas, SVG, or your own elements), or copy what a view needs into a pool that component declares.
  • Counts bind; entities don't. Pool entities are plain objects, not reactive state. A store computed that reads this.pools.name.length stays current, so $swarm.count binds as usual. A binding can't follow a single entity's fields; read those in a tick().
  • reset() empties them. A store's reset() returns it to its definition: state back to its state: values and every pool empty, with onClear firing. init() is not re-run for either.
  • Not persisted. storageKey saves and restores state only. Pool entities are plain objects that nothing tracks, so autoSave cannot see them change; a development build notes it when a store declares both (WF-911). To keep entities across a reload, copy what matters into state and refill the pool from it in init().
  • Plugins too. A plugin's pools block works the same way: data only, reached from its methods, stepped by its tick(), emptied by its reset().
  • Builds. Store pools need a build that carries pools. In one that doesn't, the store still registers without pool handles, and a development build says why.

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
watch: { path(newValue, oldValue, changedPath) {} } (in the store definition)React to the store's own state; the same keys as a component's watch
pools: { name: {} }Declare a data-only pool in the store
this.stores.storeName.pools.nameRead a store's pool from a subscribed 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.