244 lines
6.2 KiB
TypeScript
244 lines
6.2 KiB
TypeScript
/**
|
|
* 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<T> {
|
|
byId: Record<string, T>;
|
|
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<T extends { id: string }>(
|
|
items: T[],
|
|
): NormalizedState<T> {
|
|
const byId: Record<string, T> = {};
|
|
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<T>(normalized: NormalizedState<T>): 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<T extends { id: string }>(
|
|
normalized: NormalizedState<T>,
|
|
item: T,
|
|
position?: number,
|
|
): NormalizedState<T> {
|
|
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<T extends { id: string }>(
|
|
normalized: NormalizedState<T>,
|
|
itemId: string,
|
|
updates: Partial<T>,
|
|
): NormalizedState<T> {
|
|
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<T>(
|
|
normalized: NormalizedState<T>,
|
|
itemId: string,
|
|
): NormalizedState<T> {
|
|
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<T extends { id: string }>(
|
|
_normalized: NormalizedState<T>,
|
|
items: T[],
|
|
): NormalizedState<T> {
|
|
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<T extends { id: string }>(
|
|
normalized: NormalizedState<T>,
|
|
items: T[],
|
|
): NormalizedState<T> {
|
|
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<T>(
|
|
normalized: NormalizedState<T>,
|
|
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<T>(
|
|
normalized: NormalizedState<T>,
|
|
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<T>(
|
|
normalized: NormalizedState<T>,
|
|
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<T>(normalized: NormalizedState<T>): number {
|
|
return normalized.allIds.length;
|
|
}
|
|
|
|
/**
|
|
* Create an empty normalized state
|
|
* @returns Empty normalized state
|
|
*/
|
|
export function createEmptyNormalized<T>(): NormalizedState<T> {
|
|
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<T>(
|
|
normalized: NormalizedState<T>,
|
|
fromIndex: number,
|
|
toIndex: number,
|
|
): NormalizedState<T> {
|
|
const newAllIds = [...normalized.allIds];
|
|
const [removed] = newAllIds.splice(fromIndex, 1);
|
|
newAllIds.splice(toIndex, 0, removed);
|
|
|
|
return {
|
|
...normalized,
|
|
allIds: newAllIds,
|
|
};
|
|
}
|