Refactor: Implement non-disruptive auto-refresh

This commit is contained in:
gpt-engineer-app[bot]
2025-10-06 18:33:00 +00:00
parent 63a7877314
commit b1112e6261

159
src/lib/smartStateUpdate.ts Normal file
View File

@@ -0,0 +1,159 @@
/**
* Smart State Update Utility
*
* Provides intelligent array diffing and merging to prevent
* unnecessary re-renders and preserve user interaction state.
*/
export interface SmartMergeOptions<T> {
compareFields?: (keyof T)[];
preserveOrder?: boolean;
addToTop?: boolean;
}
export interface MergeChanges<T> {
added: T[];
removed: T[];
updated: T[];
}
export interface SmartMergeResult<T> {
items: T[];
changes: MergeChanges<T>;
hasChanges: boolean;
}
/**
* Performs intelligent array diffing and merging
*
* @param currentItems - The current items in state
* @param newItems - The new items fetched from the server
* @param options - Configuration options
* @returns Merged items with change information
*/
export function smartMergeArray<T extends { id: string }>(
currentItems: T[],
newItems: T[],
options?: SmartMergeOptions<T>
): SmartMergeResult<T> {
const {
compareFields,
preserveOrder = false,
addToTop = true,
} = options || {};
// Create ID maps for quick lookup
const currentMap = new Map(currentItems.map(item => [item.id, item]));
const newMap = new Map(newItems.map(item => [item.id, item]));
// Detect changes
const added: T[] = [];
const removed: T[] = [];
const updated: T[] = [];
// Find added and updated items
for (const newItem of newItems) {
const currentItem = currentMap.get(newItem.id);
if (!currentItem) {
// New item
added.push(newItem);
} else if (hasItemChanged(currentItem, newItem, compareFields)) {
// Item has changed
updated.push(newItem);
}
}
// Find removed items
for (const currentItem of currentItems) {
if (!newMap.has(currentItem.id)) {
removed.push(currentItem);
}
}
const hasChanges = added.length > 0 || removed.length > 0 || updated.length > 0;
// If no changes, return current items (preserves object references)
if (!hasChanges) {
return {
items: currentItems,
changes: { added: [], removed: [], updated: [] },
hasChanges: false,
};
}
// Build merged array
let mergedItems: T[];
if (preserveOrder) {
// Preserve the order of current items
mergedItems = currentItems
.filter(item => !removed.some(r => r.id === item.id))
.map(item => {
const updatedItem = updated.find(u => u.id === item.id);
return updatedItem || item; // Use updated version if available, otherwise keep current
});
// Add new items at top or bottom
if (addToTop) {
mergedItems = [...added, ...mergedItems];
} else {
mergedItems = [...mergedItems, ...added];
}
} else {
// Use the order from newItems
mergedItems = newItems.map(newItem => {
const currentItem = currentMap.get(newItem.id);
// If item exists in current state and hasn't changed, preserve reference
if (currentItem && !updated.some(u => u.id === newItem.id)) {
return currentItem;
}
return newItem;
});
}
return {
items: mergedItems,
changes: { added, removed, updated },
hasChanges: true,
};
}
/**
* Checks if an item has changed by comparing specific fields
*/
function hasItemChanged<T>(
currentItem: T,
newItem: T,
compareFields?: (keyof T)[]
): boolean {
if (!compareFields || compareFields.length === 0) {
// Deep comparison if no specific fields provided
return JSON.stringify(currentItem) !== JSON.stringify(newItem);
}
// Compare only specified fields
for (const field of compareFields) {
if (currentItem[field] !== newItem[field]) {
return true;
}
}
return false;
}
/**
* Creates a stable ID set for tracking interactions
*/
export function createStableIdSet(ids: string[]): Set<string> {
return new Set(ids);
}
/**
* Checks if an ID is in an interaction set
*/
export function isInteractingWith(id: string, interactionSet: Set<string>): boolean {
return interactionSet.has(id);
}