From b3d6bb0772b0ff0ca39dbc533c438d1d0b094eb2 Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Wed, 28 Jan 2026 19:49:45 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20utility=20functions=20for=20J?= =?UTF-8?q?SON=20handling=20and=20performance=20optimization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/utilities/json-utils.js | 134 ++++++++++++++++++ .../src/utilities/performance-utils.js | 91 ++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 code/websites/pokedex.online/src/utilities/json-utils.js create mode 100644 code/websites/pokedex.online/src/utilities/performance-utils.js diff --git a/code/websites/pokedex.online/src/utilities/json-utils.js b/code/websites/pokedex.online/src/utilities/json-utils.js new file mode 100644 index 0000000..0097e2c --- /dev/null +++ b/code/websites/pokedex.online/src/utilities/json-utils.js @@ -0,0 +1,134 @@ +/** + * JSON Path Extraction Utility + * Extracts property paths from JSON objects for filtering + */ + +/** + * Extract all JSON paths from an object recursively + * @param {Object} obj - Object to extract paths from + * @param {string} prefix - Current path prefix + * @param {Set} paths - Accumulated paths + * @param {number} maxDepth - Maximum recursion depth + * @param {number} currentDepth - Current recursion depth + */ +function extractPathsRecursive(obj, prefix = '', paths = new Set(), maxDepth = 5, currentDepth = 0) { + if (currentDepth >= maxDepth || obj === null || typeof obj !== 'object') { + return; + } + + Object.keys(obj).forEach(key => { + const path = prefix ? `${prefix}.${key}` : key; + paths.add(path); + + if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) { + extractPathsRecursive(obj[key], path, paths, maxDepth, currentDepth + 1); + } + }); +} + +/** + * Extract JSON paths from an array of objects + * @param {Array} data - Array of objects + * @param {number} sampleSize - Number of items to sample (default: 100) + * @returns {Array} Array of {path, breadcrumb} objects + */ +export function extractJsonPaths(data, sampleSize = 100) { + const paths = new Set(); + const sample = data.slice(0, Math.min(sampleSize, data.length)); + + sample.forEach(item => { + extractPathsRecursive(item, '', paths); + }); + + return Array.from(paths) + .sort() + .map(path => ({ + path, + breadcrumb: path.replace(/\./g, ' › ') + })); +} + +/** + * Continue extracting paths from remaining items (lazy loading) + * @param {Array} data - Full data array + * @param {number} startIndex - Where to start processing + * @param {Set} existingPaths - Existing paths to add to + * @param {Function} callback - Callback when new paths found + * @param {number} chunkSize - Items to process per chunk + */ +export function extractJsonPathsLazy(data, startIndex, existingPaths, callback, chunkSize = 100) { + if (startIndex >= data.length) { + return; + } + + const processChunk = (index) => { + const end = Math.min(index + chunkSize, data.length); + const chunk = data.slice(index, end); + const newPaths = new Set(existingPaths); + + chunk.forEach(item => { + extractPathsRecursive(item, '', newPaths); + }); + + const addedPaths = Array.from(newPaths).filter(p => !existingPaths.has(p)); + + if (addedPaths.length > 0) { + addedPaths.forEach(p => existingPaths.add(p)); + callback(addedPaths.map(path => ({ + path, + breadcrumb: path.replace(/\./g, ' › ') + }))); + } + + if (end < data.length) { + requestIdleCallback(() => processChunk(end)); + } + }; + + if (typeof requestIdleCallback !== 'undefined') { + requestIdleCallback(() => processChunk(startIndex)); + } else { + setTimeout(() => processChunk(startIndex), 0); + } +} + +/** + * Get value from object using dot notation path + * @param {Object} obj - Object to get value from + * @param {string} path - Dot notation path (e.g., 'data.pokemonSettings.pokemonId') + * @returns {any} Value at path or undefined + */ +export function getValueByPath(obj, path) { + return path.split('.').reduce((current, key) => current?.[key], obj); +} + +/** + * Truncate text in the middle + * @param {string} text - Text to truncate + * @param {number} matchIndex - Index of match for context + * @param {number} maxLength - Maximum length + * @returns {string} Truncated text + */ +export function truncateMiddle(text, matchIndex = 0, maxLength = 100) { + if (text.length <= maxLength) { + return text; + } + + const halfLength = Math.floor(maxLength / 2); + const start = Math.max(0, matchIndex - halfLength); + const end = Math.min(text.length, matchIndex + halfLength); + + let result = ''; + + if (start > 0) { + result += '...'; + } + + result += text.substring(start, end); + + if (end < text.length) { + result += '...'; + } + + return result; +} diff --git a/code/websites/pokedex.online/src/utilities/performance-utils.js b/code/websites/pokedex.online/src/utilities/performance-utils.js new file mode 100644 index 0000000..383b081 --- /dev/null +++ b/code/websites/pokedex.online/src/utilities/performance-utils.js @@ -0,0 +1,91 @@ +/** + * Performance Monitoring Utility + * Wraps operations with console.time/timeEnd for development and production feature flag + */ + +/** + * Check if performance monitoring is enabled + * @returns {boolean} + */ +export function isPerfMonitoringEnabled() { + return ( + import.meta.env.DEV || + localStorage.getItem('enablePerfMonitoring') === 'true' + ); +} + +/** + * Monitor performance of an operation + * @param {string} label - Operation label + * @param {Function} fn - Function to monitor + * @returns {Promise|any} Result of the function + */ +export async function perfMonitor(label, fn) { + if (!isPerfMonitoringEnabled()) { + return typeof fn === 'function' ? await fn() : fn; + } + + const perfLabel = `⚡ ${label}`; + console.time(perfLabel); + + try { + const result = typeof fn === 'function' ? await fn() : fn; + console.timeEnd(perfLabel); + return result; + } catch (error) { + console.timeEnd(perfLabel); + console.error(`${perfLabel} failed:`, error); + throw error; + } +} + +/** + * Detect device performance characteristics + * @returns {Object} Performance info + */ +export function getDevicePerformance() { + const cores = navigator.hardwareConcurrency || 4; + const memory = navigator.deviceMemory || 4; // GB + + return { + cores, + memory, + isHighPerformance: cores >= 8 && memory >= 8, + isMediumPerformance: cores >= 4 && memory >= 4, + isLowPerformance: cores < 4 || memory < 4, + recommendedChunkSize: cores >= 8 ? 1000 : cores >= 4 ? 500 : 250 + }; +} + +/** + * Debounce function for performance optimization + * @param {Function} fn - Function to debounce + * @param {number} delay - Delay in milliseconds + * @returns {Function} Debounced function + */ +export function debounce(fn, delay = 300) { + let timeoutId; + + return function (...args) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), delay); + }; +} + +/** + * Throttle function for performance optimization + * @param {Function} fn - Function to throttle + * @param {number} limit - Limit in milliseconds + * @returns {Function} Throttled function + */ +export function throttle(fn, limit = 100) { + let inThrottle; + + return function (...args) { + if (!inThrottle) { + fn.apply(this, args); + inThrottle = true; + setTimeout(() => (inThrottle = false), limit); + } + }; +}