mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 07:51:13 -05:00
190 lines
4.8 KiB
TypeScript
190 lines
4.8 KiB
TypeScript
/**
|
|
* Utility functions for performing "smart" updates on arrays
|
|
*/
|
|
|
|
/**
|
|
* Creates a stable content hash for comparison
|
|
*/
|
|
function hashContent(obj: any): string {
|
|
if (obj === null || obj === undefined) return 'null';
|
|
if (typeof obj !== 'object') return String(obj);
|
|
|
|
// Handle arrays
|
|
if (Array.isArray(obj)) {
|
|
return `[${obj.map(hashContent).join(',')}]`;
|
|
}
|
|
|
|
// Sort keys for stable hashing (CRITICAL for nested objects!)
|
|
const sortedKeys = Object.keys(obj).sort();
|
|
const parts = sortedKeys.map(key => `${key}:${hashContent(obj[key])}`);
|
|
return parts.join('|');
|
|
}
|
|
|
|
/**
|
|
* Checks if content has meaningfully changed (not just object reference)
|
|
*/
|
|
function hasContentChanged(current: any, next: any): boolean {
|
|
return hashContent(current) !== hashContent(next);
|
|
}
|
|
|
|
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) {
|
|
// If no fields specified, use content hash comparison
|
|
return hasContentChanged(currentItem, newItem);
|
|
}
|
|
|
|
// Compare only specified fields
|
|
for (const field of compareFields) {
|
|
const currentValue = currentItem[field];
|
|
const newValue = newItem[field];
|
|
|
|
// Handle nested objects/arrays with content hash
|
|
if (typeof currentValue === 'object' && typeof newValue === 'object') {
|
|
if (hasContentChanged(currentValue, newValue)) {
|
|
return true;
|
|
}
|
|
} else if (currentValue !== newValue) {
|
|
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);
|
|
}
|