Files
thrilltrack-explorer/src-old/lib/smartStateUpdate.ts

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);
}