/** * State Normalization Utilities * FE-STATE-009: Normalize nested state structures for better performance * * Provides utilities to normalize nested state structures (arrays of objects) * into flat structures with indexed lookups (byId + allIds pattern). * This improves performance by enabling O(1) lookups instead of O(n) array searches. */ /** * Normalized state structure * Instead of storing items as arrays: [item1, item2, item3] * We store them as: { byId: { id1: item1, id2: item2, id3: item3 }, allIds: ['id1', 'id2', 'id3'] } */ export interface NormalizedState { byId: Record; allIds: string[]; } /** * Normalize an array of items into a normalized state structure * @param items Array of items with an 'id' property * @returns Normalized state with byId and allIds */ export function normalize( items: T[], ): NormalizedState { const byId: Record = {}; const allIds: string[] = []; for (const item of items) { if (item.id) { byId[item.id] = item; allIds.push(item.id); } } return { byId, allIds }; } /** * Denormalize a normalized state back into an array * @param normalized Normalized state structure * @returns Array of items in the order of allIds */ export function denormalize(normalized: NormalizedState): T[] { return normalized.allIds .map((id) => normalized.byId[id]) .filter((item): item is T => item !== undefined); } /** * Add an item to normalized state * @param normalized Current normalized state * @param item Item to add * @param position Optional position to insert at (default: append) * @returns New normalized state */ export function addToNormalized( normalized: NormalizedState, item: T, position?: number, ): NormalizedState { const { byId, allIds } = normalized; const newById = { ...byId, [item.id]: item }; let newAllIds: string[]; if (position === undefined || position >= allIds.length) { // Append to end newAllIds = [...allIds, item.id]; } else if (position <= 0) { // Prepend to start newAllIds = [item.id, ...allIds]; } else { // Insert at position newAllIds = [ ...allIds.slice(0, position), item.id, ...allIds.slice(position), ]; } return { byId: newById, allIds: newAllIds }; } /** * Update an item in normalized state * @param normalized Current normalized state * @param itemId ID of item to update * @param updates Partial updates to apply * @returns New normalized state */ export function updateInNormalized( normalized: NormalizedState, itemId: string, updates: Partial, ): NormalizedState { const existing = normalized.byId[itemId]; if (!existing) { return normalized; } return { ...normalized, byId: { ...normalized.byId, [itemId]: { ...existing, ...updates }, }, }; } /** * Remove an item from normalized state * @param normalized Current normalized state * @param itemId ID of item to remove * @returns New normalized state */ export function removeFromNormalized( normalized: NormalizedState, itemId: string, ): NormalizedState { const { [itemId]: removed, ...byId } = normalized.byId; const allIds = normalized.allIds.filter((id) => id !== itemId); return { byId, allIds }; } /** * Replace all items in normalized state * @param normalized Current normalized state * @param items New array of items * @returns New normalized state */ export function replaceNormalized( _normalized: NormalizedState, items: T[], ): NormalizedState { return normalize(items); } /** * Merge items into normalized state (add new, update existing) * @param normalized Current normalized state * @param items Items to merge * @returns New normalized state */ export function mergeNormalized( normalized: NormalizedState, items: T[], ): NormalizedState { const newById = { ...normalized.byId }; const newAllIds = [...normalized.allIds]; const existingIds = new Set(normalized.allIds); for (const item of items) { if (item.id) { newById[item.id] = item; if (!existingIds.has(item.id)) { newAllIds.push(item.id); existingIds.add(item.id); } } } return { byId: newById, allIds: newAllIds }; } /** * Get an item by ID from normalized state * @param normalized Normalized state * @param itemId ID to look up * @returns Item or undefined */ export function getById( normalized: NormalizedState, itemId: string, ): T | undefined { return normalized.byId[itemId]; } /** * Get multiple items by IDs from normalized state * @param normalized Normalized state * @param itemIds Array of IDs to look up * @returns Array of items (may contain undefined for missing IDs) */ export function getByIds( normalized: NormalizedState, itemIds: string[], ): (T | undefined)[] { return itemIds.map((id) => normalized.byId[id]); } /** * Check if an item exists in normalized state * @param normalized Normalized state * @param itemId ID to check * @returns True if item exists */ export function hasId( normalized: NormalizedState, itemId: string, ): boolean { return itemId in normalized.byId; } /** * Get the count of items in normalized state * @param normalized Normalized state * @returns Number of items */ export function getCount(normalized: NormalizedState): number { return normalized.allIds.length; } /** * Create an empty normalized state * @returns Empty normalized state */ export function createEmptyNormalized(): NormalizedState { return { byId: {}, allIds: [] }; } /** * Reorder items in normalized state * @param normalized Current normalized state * @param fromIndex Source index * @param toIndex Destination index * @returns New normalized state with reordered allIds */ export function reorderNormalized( normalized: NormalizedState, fromIndex: number, toIndex: number, ): NormalizedState { const newAllIds = [...normalized.allIds]; const [removed] = newAllIds.splice(fromIndex, 1); newAllIds.splice(toIndex, 0, removed); return { ...normalized, allIds: newAllIds, }; }