diff --git a/src/cohort-builder.utils.ts b/src/cohort-builder.utils.ts index 4e9bdbbe..ed1be960 100644 --- a/src/cohort-builder.utils.ts +++ b/src/cohort-builder.utils.ts @@ -1,3 +1,4 @@ +import { showToast } from '@openmrs/esm-framework'; import type { Column, Patient, Query } from './types'; export const composeJson = (searchParameters) => { @@ -106,16 +107,94 @@ export const addColumnsToDisplay = () => { return columnValues; }; -export const addToHistory = (description: string, patients: Patient[], parameters: {}) => { - const oldHistory = JSON.parse(window.sessionStorage.getItem('openmrsHistory')); - let newHistory = []; +const STORAGE_KEY = 'openmrsHistory'; +const MAX_HISTORY_ITEMS = 50; // LRU cache: Keep only most recent 50 searches +const MAX_PATIENTS_PER_SEARCH = 100; // Limit patient data to prevent storage bloat - if (oldHistory) { - newHistory = [...oldHistory, { description, patients, parameters }]; - } else { - newHistory = [{ description, patients, parameters }]; +/** + * Safely adds search to history with comprehensive error handling + * Implements LRU cache, quota management, and graceful degradation + * @param description - Human-readable description of the search + * @param patients - Array of patient results + * @param parameters - Search parameters used + * @returns boolean - true if successfully saved, false otherwise + */ +export const addToHistory = (description: string, patients: Patient[], parameters: {}): boolean => { + try { + // Validate inputs + if (!Array.isArray(patients) || typeof parameters !== 'object') { + return false; + } + + // Limit patient data to prevent storage bloat + const limitedPatients = patients.slice(0, MAX_PATIENTS_PER_SEARCH); + + // Safely retrieve existing history + let oldHistory: any[] = []; + const storedData = window.sessionStorage.getItem(STORAGE_KEY); + + if (storedData) { + try { + const parsed = JSON.parse(storedData); + oldHistory = Array.isArray(parsed) ? parsed : []; + } catch (parseError) { + // Reset corrupted data + window.sessionStorage.removeItem(STORAGE_KEY); + oldHistory = []; + } + } + + // Add new entry with timestamp + const newEntry = { + description, + patients: limitedPatients, + parameters, + timestamp: new Date().toISOString(), + }; + + // Implement LRU: Keep only most recent items + const newHistory = [...oldHistory, newEntry].slice(-MAX_HISTORY_ITEMS); + + // Try to save with quota handling + try { + const serialized = JSON.stringify(newHistory); + + window.sessionStorage.setItem(STORAGE_KEY, serialized); + return true; + } catch (storageError) { + if (storageError.name === 'QuotaExceededError') { + // Fallback: Keep only last 10 items + const reducedHistory = newHistory.slice(-10); + try { + window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(reducedHistory)); + showToast({ + title: 'Search History Limit Reached', + kind: 'warning', + description: 'Older search history has been cleared to save space.', + }); + return true; + } catch (retryError) { + // Complete failure - disable history + window.sessionStorage.removeItem(STORAGE_KEY); + showToast({ + title: 'Search History Unavailable', + kind: 'error', + description: 'Unable to save search history. Your searches will still work.', + }); + return false; + } + } + throw storageError; // Re-throw unexpected errors + } + } catch (error) { + // Don't break searching if history fails + showToast({ + title: 'History Error', + kind: 'error', + description: 'Could not save search to history, but your search completed successfully.', + }); + return false; } - window.sessionStorage.setItem('openmrsHistory', JSON.stringify(newHistory)); }; export const formatDate = (dateString: string) => { diff --git a/src/components/composition/composition.utils.ts b/src/components/composition/composition.utils.ts index 77292a4e..0aa962b2 100644 --- a/src/components/composition/composition.utils.ts +++ b/src/components/composition/composition.utils.ts @@ -23,16 +23,33 @@ export const createCompositionQuery = (compositionQuery: string) => { searchTokens.forEach((eachToken) => { if (eachToken.match(/\d/)) { - const history = JSON.parse(window.sessionStorage.getItem('openmrsHistory')); - const operandQuery = history[parseInt(eachToken) - 1]; - - const jsonRequestObject = operandQuery.parameters; - jsonRequestObject.customRowFilterCombination = formatFilterCombination( - jsonRequestObject.customRowFilterCombination, - query.rowFilters.length, - ); - query.customRowFilterCombination += `(${jsonRequestObject.customRowFilterCombination})`; - query.rowFilters = query.rowFilters.concat(jsonRequestObject.rowFilters); + try { + const storedData = window.sessionStorage.getItem('openmrsHistory'); + if (!storedData) { + return; + } + + const history = JSON.parse(storedData); + if (!Array.isArray(history)) { + return; + } + + const operandQuery = history[parseInt(eachToken) - 1]; + if (!operandQuery?.parameters) { + return; + } + + const jsonRequestObject = operandQuery.parameters; + jsonRequestObject.customRowFilterCombination = formatFilterCombination( + jsonRequestObject.customRowFilterCombination, + query.rowFilters.length, + ); + query.customRowFilterCombination += `(${jsonRequestObject.customRowFilterCombination})`; + query.rowFilters = query.rowFilters.concat(jsonRequestObject.rowFilters); + } catch (error) { + // Skip invalid history entry + return; + } } else { query.customRowFilterCombination += ` ${eachToken} `; } diff --git a/src/components/search-history/search-history.utils.ts b/src/components/search-history/search-history.utils.ts index b6f101ee..74fc2445 100644 --- a/src/components/search-history/search-history.utils.ts +++ b/src/components/search-history/search-history.utils.ts @@ -1,14 +1,48 @@ import { type SearchHistoryItem } from '../../types'; -export const getSearchHistory = () => { - const history = JSON.parse(window.sessionStorage.getItem('openmrsHistory')); - const searchHistory: SearchHistoryItem[] = []; - history?.map((historyItem, index) => - searchHistory.push({ - ...historyItem, - id: (index + 1).toString(), - results: historyItem.patients.length, - }), - ); - return searchHistory; +const STORAGE_KEY = 'openmrsHistory'; + +/** + * Safely retrieves search history from sessionStorage with error handling + * @returns Array of search history items, or empty array if data is corrupted/unavailable + */ +export const getSearchHistory = (): SearchHistoryItem[] => { + try { + const storedData = window.sessionStorage.getItem(STORAGE_KEY); + + if (!storedData) { + return []; + } + + const history = JSON.parse(storedData); + + // Validate that parsed data is an array + if (!Array.isArray(history)) { + window.sessionStorage.removeItem(STORAGE_KEY); + return []; + } + + const searchHistory: SearchHistoryItem[] = []; + + history.forEach((historyItem, index) => { + // Validate each history item has required properties + if (historyItem && historyItem.patients && Array.isArray(historyItem.patients)) { + searchHistory.push({ + ...historyItem, + id: (index + 1).toString(), + results: historyItem.patients.length, + }); + } + }); + + return searchHistory; + } catch (error) { + // Clear corrupted data to prevent repeated errors + try { + window.sessionStorage.removeItem(STORAGE_KEY); + } catch (removeError) { + // Silent failure - storage access denied + } + return []; + } };