# FX Standalone Mode - Migration Guide ## Overview FX can now run completely standalone in the browser without any server dependencies. This guide helps you migrate from server-dependent mode to the fully self-sufficient standalone mode. ## Table of Contents 1. [Quick Start](#quick-start) 2. [Service Worker Setup](#service-worker-setup) 3. [Migrating fx-orm to IndexedDB](#migrating-fx-orm-to-indexeddb) 4. [Migrating fx-flow to Web Workers](#migrating-fx-flow-to-web-workers) 5. [API Migration](#api-migration) 6. [Troubleshooting](#troubleshooting) 7. [Performance Considerations](#performance-considerations) ## Quick Start ### 1. Enable Standalone Mode Add this to your HTML before loading FX: ```html
``` ### 2. Copy Service Worker File Place the Service Worker at your web root: ```bash cp fx/workers/fx-service-worker.js /path/to/webroot/fx-service-worker.js ``` ## Service Worker Setup ### Installation The Service Worker automatically installs when you import the registration module: ```javascript import { registerFXServiceWorker } from '/fx/plugins/fx-service-worker-register.js'; // Configure Service Worker const swManager = registerFXServiceWorker({ scope: '/', // Service Worker scope autoUpdate: true, // Auto-check for updates autoUpdateInterval: 60000, // Check every minute onUpdateFound: () => { console.log('New version available!'); } }); ``` ### Features #### Module Proxy The Service Worker proxies cross-origin module requests: ```javascript // This will work even with CORS restrictions import React from 'https://esm.sh/react'; // Service Worker intercepts and proxies the request const module = await import('https://unpkg.com/lodash-es'); ``` #### Offline Caching Pre-cache critical modules: ```javascript await swManager.cacheModules([ '/fx/fx.js', '/fx/plugins/fx-orm.js', '/fx/plugins/fx-flow.js', 'https://esm.sh/react', 'https://esm.sh/vue' ]); ``` #### Cache Management ```javascript // Get cache status const status = await swManager.getCacheStatus(); console.log('Cached modules:', status); // Clear specific cache await swManager.clearCache('fx-modules-v1'); // Clear all caches await swManager.clearCache(); ``` ### Configuration Configure allowed proxy origins in `fx-service-worker.ts`: ```typescript const ALLOWED_ORIGINS = [ 'https://unpkg.com', 'https://cdn.jsdelivr.net', 'https://esm.sh', 'https://cdn.skypack.dev', 'https://jspm.dev', // Add your CDNs here ]; ``` ## Migrating fx-orm to IndexedDB ### Before (Server-dependent) ```javascript // Old way - requires server endpoint const users = await $db.query('SELECT * FROM users'); const user = await $db.users.create({ name: 'John' }); ``` ### After (Standalone with IndexedDB) ```javascript import { createIndexedDBAdapter } from '/fx/plugins/fx-orm-indexeddb.js'; // Create adapter const dbAdapter = createIndexedDBAdapter('my-app-db'); await dbAdapter.init(); // Use same syntax - works locally! const users = await dbAdapter.query('SELECT * FROM users'); const user = await dbAdapter.table('users').create({ name: 'John' }); ``` ### Schema Definition Define your schemas upfront: ```javascript // Add custom schemas dbAdapter.addSchema({ name: 'products', keyPath: 'id', autoIncrement: true, indexes: [ { name: 'name', keyPath: 'name' }, { name: 'category', keyPath: 'category' }, { name: 'price', keyPath: 'price' }, { name: 'created_at', keyPath: 'created_at' } ] }); // Schema is automatically created/migrated ``` ### Query Examples ```javascript // SQL-like queries const results = await dbAdapter.query( 'SELECT * FROM products WHERE category = "electronics" ORDER BY price LIMIT 10' ); // ORM-style access const products = dbAdapter.table('products'); // Create const product = await products.create({ name: 'Laptop', category: 'electronics', price: 999.99 }); // Read const found = await products.find(product.id); const all = await products.all(); const filtered = await products.where({ category: 'electronics' }); // Update await products.update(product.id, { price: 899.99 }); // Delete await products.delete(product.id); // CSS-like selectors const expensive = await products.select('[price>500]:limit(5)'); ``` ### Reactive Updates ```javascript // Subscribe to changes const unsubscribe = products.onChange((event) => { console.log('Change:', event.action, event.data); // Update UI }); // Changes trigger notifications await products.create({ name: 'New Product' }); // Triggers 'insert' await products.update(id, { price: 100 }); // Triggers 'update' await products.delete(id); // Triggers 'delete' ``` ### Data Migration Export from server and import locally: ```javascript // Export from server const serverData = await fetch('/api/export').then(r => r.json()); // Import to IndexedDB await dbAdapter.import(serverData); // Or migrate table by table for (const [table, records] of Object.entries(serverData.tables)) { for (const record of records) { await dbAdapter.table(table).create(record); } } ``` ## Migrating fx-flow to Web Workers ### Before (Server-executed) ```javascript // Old way - executes on server const result = await $flow.execute(flowGraph); ``` ### After (Worker-executed) ```javascript import { createFlowExecutor } from '/fx/plugins/fx-flow-worker.js'; // Create executor with worker pool const executor = createFlowExecutor(4); // 4 workers // Execute nodes locally in workers const result = await executor.executeNode(node, context); // Execute entire flow graph const flowResult = await executor.executeFlow(nodes, edges); ``` ### Flow Node Definition ```javascript const nodes = [ { id: 'input', type: 'input', code: ` // Runs in isolated worker const data = await fetch('/api/data').then(r => r.json()); return data; ` }, { id: 'transform', type: 'transform', code: ` const input = inputs.get('data'); return input.map(item => ({ ...item, processed: true, timestamp: Date.now() })); ` }, { id: 'output', type: 'output', code: ` const data = inputs.get('data'); // Store in IndexedDB for (const item of data) { await db.table('results').create(item); } return { count: data.length }; ` } ]; const edges = [ { source: 'input', target: 'transform' }, { source: 'transform', target: 'output' } ]; // Execute flow const result = await executor.executeFlow(nodes, edges); ``` ### Handling Suspension ```javascript const suspendableNode = { id: 'async-task', type: 'custom', code: ` // Check if we need to wait if (!state.get('dataReady')) { // Suspend execution return suspend({ waiting: true }); } // Continue after resumption const data = state.get('data'); return processData(data); ` }; // Execute with suspension support const result = await executor.executeNode(suspendableNode, context); if (result.suspended) { // Handle suspension console.log('Node suspended:', result.state); // Resume later context.state.set('dataReady', true); context.state.set('data', loadedData); const resumed = await executor.executeNode(suspendableNode, context); } ``` ### Worker Pool Management ```javascript // Monitor worker pool const stats = executor.getStats(); console.log('Workers:', stats.totalWorkers); console.log('Busy:', stats.busyWorkers); console.log('Queue:', stats.queueLength); // Adjust pool size const executor = createFlowExecutor(navigator.hardwareConcurrency); // Clean up when done executor.terminate(); ``` ## API Migration ### Proxied Requests The Service Worker automatically proxies API requests: ```javascript // Before - Direct API call (may fail with CORS) const response = await fetch('https://api.external.com/data'); // After - Proxied through Service Worker const response = await fetch('/fx/proxy?url=' + encodeURIComponent('https://api.external.com/data')); ``` ### Using fx-api Plugin ```javascript import FX from '/fx/fx.js'; // Configure API to use proxy FX.use('fx-api', { baseUrl: '/fx/proxy', interceptors: { request: [(config) => { // Transform URLs for proxy if (config.url.startsWith('http')) { config.url = `/fx/proxy?url=${encodeURIComponent(config.url)}`; } return config; }] } }); // Now API calls are automatically proxied const data = await $api.get('https://api.github.com/users/octocat'); ``` ## Troubleshooting ### Cross-Origin Isolation If you see warnings about Cross-Origin Isolation: 1. **Add required headers to your server:** ```nginx # Nginx configuration add_header Cross-Origin-Embedder-Policy "require-corp"; add_header Cross-Origin-Opener-Policy "same-origin"; ``` ```javascript // Express.js app.use((req, res, next) => { res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); next(); }); ``` 2. **Or use credentialless mode (limited functionality):** ```html ``` ### Service Worker Not Registering 1. **Check HTTPS:** Service Workers require HTTPS (or localhost) 2. **Check scope:** Ensure Service Worker is at root or adjust scope 3. **Clear cache:** Try unregistering old Service Workers ```javascript // Unregister all Service Workers navigator.serviceWorker.getRegistrations().then(registrations => { for (let registration of registrations) { registration.unregister(); } }); ``` ### IndexedDB Quota Exceeded ```javascript // Check storage estimate const estimate = await navigator.storage.estimate(); console.log('Usage:', estimate.usage, 'Quota:', estimate.quota); // Request persistent storage const persistent = await navigator.storage.persist(); console.log('Persistent storage:', persistent); // Clear old data await dbAdapter.deleteDatabase(); ``` ### Worker Execution Errors ```javascript // Add error handling executor.onLog((level, args) => { if (level === 'error') { console.error('Worker error:', ...args); // Handle error } }); // Wrap execution try { const result = await executor.executeNode(node, context); } catch (error) { console.error('Execution failed:', error); // Fallback logic } ``` ## Performance Considerations ### Service Worker Caching ```javascript // Pre-cache critical modules on first load if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then(async (registration) => { const swManager = getFXServiceWorker(); // Cache core modules await swManager.cacheModules([ '/fx/fx.js', '/fx/plugins/fx-orm-indexeddb.js', '/fx/plugins/fx-flow-worker.js' ]); // Cache frequently used external modules await swManager.cacheModules([ 'https://esm.sh/react@18', 'https://esm.sh/vue@3', 'https://unpkg.com/lodash-es' ]); }); } ``` ### IndexedDB Optimization ```javascript // Use indexes for frequent queries dbAdapter.addSchema({ name: 'logs', keyPath: 'id', autoIncrement: true, indexes: [ { name: 'timestamp', keyPath: 'timestamp' }, { name: 'level', keyPath: 'level' }, { name: 'user_id', keyPath: 'user_id' }, { name: 'timestamp_level', keyPath: ['timestamp', 'level'] } // Compound index ] }); // Batch operations const records = Array.from({ length: 1000 }, (_, i) => ({ name: `Item ${i}`, value: Math.random() })); // Use transaction for batch insert const results = await Promise.all( records.map(record => table.create(record)) ); ``` ### Worker Pool Sizing ```javascript // Optimize worker pool based on workload const cpuCores = navigator.hardwareConcurrency || 4; // CPU-intensive tasks: use all cores const heavyExecutor = createFlowExecutor(cpuCores); // I/O-bound tasks: use more workers const ioExecutor = createFlowExecutor(cpuCores * 2); // Light tasks: use fewer workers const lightExecutor = createFlowExecutor(2); ``` ### Memory Management ```javascript // Clean up unused resources window.addEventListener('beforeunload', () => { // Terminate workers executor.terminate(); // Close database connections dbAdapter.close(); // Clear large caches if needed swManager.clearCache('fx-api-v1'); }); // Monitor memory usage if (performance.memory) { console.log('Memory usage:', { used: (performance.memory.usedJSHeapSize / 1048576).toFixed(2) + ' MB', total: (performance.memory.totalJSHeapSize / 1048576).toFixed(2) + ' MB', limit: (performance.memory.jsHeapSizeLimit / 1048576).toFixed(2) + ' MB' }); } ``` ## Migration Checklist - [ ] Enable Cross-Origin Isolation headers - [ ] Copy Service Worker to web root - [ ] Import standalone adapter modules - [ ] Initialize Service Worker registration - [ ] Replace server DB calls with IndexedDB adapter - [ ] Replace server flow execution with Worker executor - [ ] Update API calls to use proxy - [ ] Test offline functionality - [ ] Configure caching strategy - [ ] Add error handling - [ ] Test in production environment ## Support For issues or questions about standalone mode: 1. Check browser console for errors 2. Verify Cross-Origin Isolation status: `console.log(self.crossOriginIsolated)` 3. Check Service Worker status: `navigator.serviceWorker.controller` 4. Review network tab for failed requests 5. File issues at: https://github.com/fx-framework/fx/issues ## Next Steps - Explore advanced caching strategies - Implement custom Worker nodes - Build offline-first applications - Integrate with PWA features - Optimize for performance