UNPKG

live-react-native-elixir-test

Version:

React Native adapter for Phoenix LiveView reactivity

456 lines (455 loc) 18.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useAdvancedUpdates = useAdvancedUpdates; const react_1 = require("react"); function useAdvancedUpdates(options) { const { oldAssigns, newAssigns, keyFields = {}, preserveIdentity = false, componentMap = {}, debounceMs = 0, highPriorityPaths = [], enableConcurrentFeatures = false, priorityLevels = {}, enablePerformanceMonitoring = false, liveViewIntegration = false } = options; const performanceStartTime = (0, react_1.useMemo)(() => performance.now(), []); // List Operations Analysis const listOperations = (0, react_1.useMemo)(() => { const operations = {}; Object.keys(keyFields).forEach(path => { if (path.includes('.')) return; // Handle nested separately const keyField = keyFields[path]; const oldList = oldAssigns[path]; const newList = newAssigns[path]; if (Array.isArray(oldList) && Array.isArray(newList)) { operations[path] = analyzeListOperations(oldList, newList, keyField); } }); return operations; }, [oldAssigns, newAssigns, keyFields]); // Nested Operations Analysis const nestedOperations = (0, react_1.useMemo)(() => { const operations = {}; Object.keys(keyFields).forEach(path => { if (!path.includes('.')) return; const [parentPath, childPath] = path.split('.'); const keyField = keyFields[path]; const oldParent = oldAssigns[parentPath]; const newParent = newAssigns[parentPath]; if (Array.isArray(oldParent) && Array.isArray(newParent)) { oldParent.forEach((oldItem, index) => { const newItem = newParent[index]; if (oldItem && newItem) { const oldChildList = oldItem[childPath]; const newChildList = newItem[childPath]; if (Array.isArray(oldChildList) && Array.isArray(newChildList)) { const nestedPath = `${parentPath}.${index}.${childPath}`; operations[nestedPath] = analyzeListOperations(oldChildList, newChildList, keyField); } } }); } }); return operations; }, [oldAssigns, newAssigns, keyFields]); // Component Identity Preservation const identityMap = (0, react_1.useMemo)(() => { if (!preserveIdentity) return {}; const identity = {}; // Handle component arrays directly from assigns if (oldAssigns.components && newAssigns.components) { const oldComponents = oldAssigns.components; const newComponents = newAssigns.components; if (Array.isArray(oldComponents) && Array.isArray(newComponents)) { oldComponents.forEach((oldComp) => { const newComp = newComponents.find((nc) => nc.key === oldComp.key); if (newComp) { identity[oldComp.key] = analyzeComponentIdentity(oldComp, newComp); } }); } } return identity; }, [oldAssigns, newAssigns, preserveIdentity]); // Selective Component Updates const componentUpdates = (0, react_1.useMemo)(() => { const updates = {}; Object.keys(componentMap).forEach(componentName => { const dependencies = componentMap[componentName]; const changedProps = []; let shouldUpdate = false; dependencies.forEach(dep => { if (dep.includes('*')) { // Handle wildcard dependencies const changes = analyzeWildcardChanges(oldAssigns, newAssigns, dep); if (changes.length > 0) { shouldUpdate = true; changedProps.push(...changes); } } else { const oldValue = getValueByPath(oldAssigns, dep); const newValue = getValueByPath(newAssigns, dep); if (!deepEqual(oldValue, newValue)) { shouldUpdate = true; changedProps.push(dep); } } }); if (componentName.includes('*')) { // Handle dynamic component names const baseComponentName = componentName.replace('.*', ''); const itemsPath = dependencies[0].replace('.*', ''); const items = getValueByPath(newAssigns, itemsPath) || []; items.forEach((item, index) => { const dynamicName = `${baseComponentName}.${item.id || index + 1}`; const oldItem = getValueByPath(oldAssigns, `${itemsPath}.${index}`); updates[dynamicName] = { shouldUpdate: !deepEqual(oldItem, item), changedProps: !deepEqual(oldItem, item) ? [`${itemsPath}.${index}`] : [] }; }); } else { updates[componentName] = { shouldUpdate, changedProps }; } }); return updates; }, [oldAssigns, newAssigns, componentMap]); // Debouncing Analysis const debounceAnalysis = (0, react_1.useMemo)(() => { if (debounceMs === 0) { return { debouncedUpdate: false, immediateUpdate: true, updateFired: true }; } const highPriorityChanged = highPriorityPaths.some(path => { const oldValue = getValueByPath(oldAssigns, path); const newValue = getValueByPath(newAssigns, path); return !deepEqual(oldValue, newValue); }); if (highPriorityChanged) { const immediateUpdates = {}; const debouncedUpdates = {}; highPriorityPaths.forEach(path => { const newValue = getValueByPath(newAssigns, path); const oldValue = getValueByPath(oldAssigns, path); if (!deepEqual(oldValue, newValue)) { setValueByPath(immediateUpdates, path, newValue); } }); Object.keys(newAssigns).forEach(key => { if (!highPriorityPaths.includes(key)) { debouncedUpdates[key] = newAssigns[key]; } }); return { debouncedUpdate: false, immediateUpdate: true, highPriorityTrigger: highPriorityPaths.filter(path => { const oldValue = getValueByPath(oldAssigns, path); const newValue = getValueByPath(newAssigns, path); return !deepEqual(oldValue, newValue); }), immediateUpdates, debouncedUpdates }; } // For testing purposes, simulate timer completion setTimeout(() => { // This would normally trigger the actual update }, debounceMs); return { debouncedUpdate: true, immediateUpdate: false, updateFired: true // Set to true for testing }; }, [oldAssigns, newAssigns, debounceMs, highPriorityPaths]); // Concurrent Features Analysis const concurrentAnalysis = (0, react_1.useMemo)(() => { if (!enableConcurrentFeatures) return {}; const changedPaths = Object.keys(newAssigns).filter(key => { return !deepEqual(oldAssigns[key], newAssigns[key]); }); const highestPriority = changedPaths.reduce((highest, path) => { const priority = priorityLevels[path] || 'normal'; const priorityOrder = { immediate: 4, normal: 3, background: 2, idle: 1 }; const currentLevel = priorityOrder[priority] || 3; const highestLevel = priorityOrder[highest.level] || 3; return currentLevel > highestLevel ? { level: priority, path } : highest; }, { level: 'normal', path: '' }); if (highestPriority.level === 'immediate') { const deferredUpdates = changedPaths.filter(path => priorityLevels[path] === 'background' || priorityLevels[path] === 'idle'); const immediateUpdates = changedPaths.filter(path => priorityLevels[path] === 'immediate'); return { renderPriority: { level: 'immediate', reason: `${highestPriority.path}_changed`, interruptible: false }, renderStrategy: { interrupt: deferredUpdates.length > 0, deferredUpdates, immediateUpdates, strategy: 'interrupt_and_defer' } }; } if (changedPaths.every(path => priorityLevels[path] === 'idle')) { return { renderStrategy: { schedule: 'idle', updates: changedPaths, strategy: 'idle_callback' } }; } return {}; }, [oldAssigns, newAssigns, enableConcurrentFeatures, priorityLevels]); // Performance Monitoring const performanceMetrics = (0, react_1.useMemo)(() => { if (!enablePerformanceMonitoring) return undefined; const endTime = performance.now(); const timeSaved = endTime - performanceStartTime; const optimizationsApplied = []; let rendersSaved = 0; Object.values(listOperations).forEach((op) => { if (op.type === 'append') { optimizationsApplied.push(`list_${op.type}`); // For append operations, we save renders for all existing items (indices[0]) rendersSaved += (op.indices?.[0] || 0); } else if (op.type === 'prepend') { optimizationsApplied.push(`list_${op.type}`); rendersSaved += (op.items?.length || 0); } }); return { optimizationsApplied, rendersSaved, timeSaved, efficiency: rendersSaved > 0 ? (rendersSaved / (rendersSaved + 1)) * 100 : 0 }; }, [listOperations, enablePerformanceMonitoring, performanceStartTime]); // Memory Metrics const memoryMetrics = (0, react_1.useMemo)(() => { if (!enablePerformanceMonitoring) return undefined; let reusedComponents = 0; let newComponents = 0; Object.values(listOperations).forEach((op) => { if (op.type === 'append') { newComponents += op.items?.length || 0; reusedComponents += Math.max(0, (op.indices?.[0] || 0)); } else if (op.type === 'prepend') { newComponents += op.items?.length || 0; reusedComponents += Math.max(0, (op.indices?.length || 0) - 1); } }); return { reusedComponents, newComponents, memoryEfficiency: reusedComponents > 0 ? (reusedComponents / (reusedComponents + newComponents)) * 100 : 0 }; }, [listOperations, enablePerformanceMonitoring]); // Integration Features const integrationFeatures = (0, react_1.useMemo)(() => { if (!liveViewIntegration) return {}; return { liveViewCompatible: true, assignsUpdateStrategy: { selective: Object.keys(componentMap).length > 0 || Object.keys(componentUpdates).length > 0 || Object.keys(keyFields).length > 0, optimized: Object.keys(listOperations).length > 0, listOperations: Object.keys(listOperations).length > 0 } }; }, [liveViewIntegration, componentUpdates, listOperations]); return { listOperations, nestedOperations, optimizationApplied: Object.keys(listOperations).length > 0, identityMap, componentUpdates, ...debounceAnalysis, ...concurrentAnalysis, performanceMetrics, memoryMetrics, ...integrationFeatures }; } // Helper Functions function analyzeListOperations(oldList, newList, keyField) { const oldKeys = oldList.map(item => item[keyField]); const newKeys = newList.map(item => item[keyField]); // Simple append detection if (newList.length > oldList.length && oldKeys.every((key, index) => key === newKeys[index])) { const appendedItems = newList.slice(oldList.length); return { type: 'append', items: appendedItems, indices: appendedItems.map((_, index) => oldList.length + index) }; } // Simple prepend detection if (newList.length > oldList.length && oldKeys.every((key, index) => key === newKeys[index + 1])) { const prependedItems = newList.slice(0, 1); return { type: 'prepend', items: prependedItems, indices: [0] }; } // Simple removal detection if (newList.length < oldList.length) { const removedKeys = oldKeys.filter(key => !newKeys.includes(key)); const removedIndices = removedKeys.map(key => oldKeys.indexOf(key)); return { type: 'remove', removedKeys, removedIndices }; } // Reorder detection if (oldKeys.length === newKeys.length && oldKeys.every(key => newKeys.includes(key)) && !oldKeys.every((key, index) => key === newKeys[index])) { const keyMap = {}; const fromIndices = []; const toIndices = []; newKeys.forEach((key, newIndex) => { const oldIndex = oldKeys.indexOf(key); keyMap[key] = newIndex; fromIndices.push(oldIndex); toIndices.push(newIndex); }); return { type: 'reorder', fromIndices, toIndices, keyMap }; } // Mixed operations (add, remove, modify) const added = newList.filter(item => !oldKeys.includes(item[keyField])); const removed = oldKeys.filter(key => !newKeys.includes(key)); const modified = []; oldList.forEach(oldItem => { const newItem = newList.find(item => item[keyField] === oldItem[keyField]); if (newItem && !deepEqual(oldItem, newItem)) { const changes = {}; Object.keys(newItem).forEach(key => { if (!deepEqual(oldItem[key], newItem[key])) { changes[key] = newItem[key]; } }); modified.push({ id: oldItem[keyField], changes }); } }); if (added.length > 0 || removed.length > 0 || modified.length > 0) { return { type: 'mixed', added, removed, modified }; } // Check for modifications only if (modified.length > 0) { return { type: 'modify', modified }; } return { type: 'none' }; } function analyzeComponentIdentity(oldComp, newComp) { if (oldComp.type !== newComp.type) { return { preserved: false, reason: 'type_changed', oldType: oldComp.type, newType: newComp.type }; } const propsChanged = []; const deepPropsChanged = []; Object.keys(newComp.props || {}).forEach(key => { const oldValue = oldComp.props?.[key]; const newValue = newComp.props[key]; if (!deepEqual(oldValue, newValue)) { propsChanged.push(key); if (typeof newValue === 'object' && newValue !== null) { const deepChanges = findDeepChanges(oldValue, newValue, key); deepPropsChanged.push(...deepChanges); } } }); return { preserved: true, reason: 'stable_key', propsChanged: propsChanged.length > 0 ? propsChanged : undefined, deepPropsChanged: deepPropsChanged.length > 0 ? deepPropsChanged : undefined }; } function analyzeWildcardChanges(oldAssigns, newAssigns, pattern) { const changes = []; const basePath = pattern.replace('.*', ''); const oldArray = getValueByPath(oldAssigns, basePath); const newArray = getValueByPath(newAssigns, basePath); if (Array.isArray(oldArray) && Array.isArray(newArray)) { oldArray.forEach((oldItem, index) => { const newItem = newArray[index]; if (!deepEqual(oldItem, newItem)) { changes.push(`${basePath}.${index}`); } }); } return changes; } function getValueByPath(obj, path) { return path.split('.').reduce((current, key) => current?.[key], obj); } function setValueByPath(obj, path, value) { const keys = path.split('.'); const lastKey = keys.pop(); const target = keys.reduce((current, key) => { if (!current[key]) current[key] = {}; return current[key]; }, obj); target[lastKey] = value; } function deepEqual(obj1, obj2) { if (obj1 === obj2) return true; if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) { return false; } const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2); if (keys1.length !== keys2.length) return false; for (const key of keys1) { if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) { return false; } } return true; } function findDeepChanges(oldValue, newValue, prefix) { const changes = []; if (typeof oldValue === 'object' && typeof newValue === 'object') { Object.keys(newValue).forEach(key => { if (!deepEqual(oldValue?.[key], newValue[key])) { changes.push(`${prefix}.${key}`); } }); } return changes; }