veza/apps/web/src/utils/stateNormalization.ts

248 lines
6.2 KiB
TypeScript
Raw Normal View History

/**
* 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,
};
}