/** * Lightweight sync structures for series comparison. * These interfaces mirror the backend SyncedSeries* types from Book.ts * but are used in the frontend for sync status detection. */ export interface SyncedSeriesBook { bookId: string; order: number; lastUpdate: number; } export interface SyncedSeriesCharacterAttribute { id: string; name: string; lastUpdate: number; } export interface SyncedSeriesCharacter { id: string; name: string; lastUpdate: number; attributes: SyncedSeriesCharacterAttribute[]; } export interface SyncedSeriesWorldElement { id: string; name: string; lastUpdate: number; } export interface SyncedSeriesWorld { id: string; name: string; lastUpdate: number; elements: SyncedSeriesWorldElement[]; } export interface SyncedSeriesLocationSubElement { id: string; name: string; lastUpdate: number; } export interface SyncedSeriesLocationElement { id: string; name: string; lastUpdate: number; subElements: SyncedSeriesLocationSubElement[]; } export interface SyncedSeriesLocation { id: string; name: string; lastUpdate: number; elements: SyncedSeriesLocationElement[]; } export interface SyncedSeriesSpell { id: string; name: string; lastUpdate: number; } export interface SyncedSeriesSpellTag { id: string; name: string; lastUpdate: number; } export interface SyncedSeries { id: string; name: string; description: string | null; lastUpdate: number; books: SyncedSeriesBook[]; characters: SyncedSeriesCharacter[]; worlds: SyncedSeriesWorld[]; locations: SyncedSeriesLocation[]; spells: SyncedSeriesSpell[]; spellTags: SyncedSeriesSpellTag[]; } /** * Comparison result containing IDs of changed entities. * Used for partial synchronization - only changed entities are transferred. */ export interface SeriesSyncCompare { id: string; books: string[]; characters: string[]; characterAttributes: string[]; worlds: string[]; worldElements: string[]; locations: string[]; locationElements: string[]; locationSubElements: string[]; spells: string[]; spellTags: string[]; } /** * Compares two versions of a series to find changed entities. * The "newer" series is compared against the "older" one to find * entities that have been added or modified. * * @param newerSeries - The series version with potentially newer data * @param olderSeries - The series version to compare against * @returns SeriesSyncCompare with IDs of changed entities, or null if no changes */ export function compareSeriesSyncs(newerSeries: SyncedSeries, olderSeries: SyncedSeries): SeriesSyncCompare | null { const changedBookIds: string[] = []; const changedCharacterIds: string[] = []; const changedCharacterAttributeIds: string[] = []; const changedWorldIds: string[] = []; const changedWorldElementIds: string[] = []; const changedLocationIds: string[] = []; const changedLocationElementIds: string[] = []; const changedLocationSubElementIds: string[] = []; const changedSpellIds: string[] = []; const changedSpellTagIds: string[] = []; // Compare books newerSeries.books.forEach((newerBook: SyncedSeriesBook): void => { const olderBook: SyncedSeriesBook | undefined = olderSeries.books.find( (book: SyncedSeriesBook): boolean => book.bookId === newerBook.bookId ); if (!olderBook || newerBook.lastUpdate > olderBook.lastUpdate) { changedBookIds.push(newerBook.bookId); } }); // Compare characters and their attributes newerSeries.characters.forEach((newerCharacter: SyncedSeriesCharacter): void => { const olderCharacter: SyncedSeriesCharacter | undefined = olderSeries.characters.find( (character: SyncedSeriesCharacter): boolean => character.id === newerCharacter.id ); if (!olderCharacter) { changedCharacterIds.push(newerCharacter.id); newerCharacter.attributes.forEach((attr: SyncedSeriesCharacterAttribute): void => { changedCharacterAttributeIds.push(attr.id); }); } else if (newerCharacter.lastUpdate > olderCharacter.lastUpdate) { changedCharacterIds.push(newerCharacter.id); } else { // Check attributes even if character hasn't changed newerCharacter.attributes.forEach((newerAttr: SyncedSeriesCharacterAttribute): void => { const olderAttr: SyncedSeriesCharacterAttribute | undefined = olderCharacter.attributes.find( (attr: SyncedSeriesCharacterAttribute): boolean => attr.id === newerAttr.id ); if (!olderAttr || newerAttr.lastUpdate > olderAttr.lastUpdate) { changedCharacterAttributeIds.push(newerAttr.id); } }); } }); // Compare worlds and their elements newerSeries.worlds.forEach((newerWorld: SyncedSeriesWorld): void => { const olderWorld: SyncedSeriesWorld | undefined = olderSeries.worlds.find( (world: SyncedSeriesWorld): boolean => world.id === newerWorld.id ); if (!olderWorld) { changedWorldIds.push(newerWorld.id); newerWorld.elements.forEach((element: SyncedSeriesWorldElement): void => { changedWorldElementIds.push(element.id); }); } else if (newerWorld.lastUpdate > olderWorld.lastUpdate) { changedWorldIds.push(newerWorld.id); } else { // Check elements even if world hasn't changed newerWorld.elements.forEach((newerElement: SyncedSeriesWorldElement): void => { const olderElement: SyncedSeriesWorldElement | undefined = olderWorld.elements.find( (element: SyncedSeriesWorldElement): boolean => element.id === newerElement.id ); if (!olderElement || newerElement.lastUpdate > olderElement.lastUpdate) { changedWorldElementIds.push(newerElement.id); } }); } }); // Compare locations, their elements, and sub-elements newerSeries.locations.forEach((newerLocation: SyncedSeriesLocation): void => { const olderLocation: SyncedSeriesLocation | undefined = olderSeries.locations.find( (location: SyncedSeriesLocation): boolean => location.id === newerLocation.id ); if (!olderLocation) { changedLocationIds.push(newerLocation.id); newerLocation.elements.forEach((element: SyncedSeriesLocationElement): void => { changedLocationElementIds.push(element.id); element.subElements.forEach((subElement: SyncedSeriesLocationSubElement): void => { changedLocationSubElementIds.push(subElement.id); }); }); } else if (newerLocation.lastUpdate > olderLocation.lastUpdate) { changedLocationIds.push(newerLocation.id); } else { // Check elements newerLocation.elements.forEach((newerElement: SyncedSeriesLocationElement): void => { const olderElement: SyncedSeriesLocationElement | undefined = olderLocation.elements.find( (element: SyncedSeriesLocationElement): boolean => element.id === newerElement.id ); if (!olderElement) { changedLocationElementIds.push(newerElement.id); newerElement.subElements.forEach((subElement: SyncedSeriesLocationSubElement): void => { changedLocationSubElementIds.push(subElement.id); }); } else if (newerElement.lastUpdate > olderElement.lastUpdate) { changedLocationElementIds.push(newerElement.id); } else { // Check sub-elements newerElement.subElements.forEach((newerSubElement: SyncedSeriesLocationSubElement): void => { const olderSubElement: SyncedSeriesLocationSubElement | undefined = olderElement.subElements.find( (subElement: SyncedSeriesLocationSubElement): boolean => subElement.id === newerSubElement.id ); if (!olderSubElement || newerSubElement.lastUpdate > olderSubElement.lastUpdate) { changedLocationSubElementIds.push(newerSubElement.id); } }); } }); } }); // Compare spells newerSeries.spells.forEach((newerSpell: SyncedSeriesSpell): void => { const olderSpell: SyncedSeriesSpell | undefined = olderSeries.spells.find( (spell: SyncedSeriesSpell): boolean => spell.id === newerSpell.id ); if (!olderSpell || newerSpell.lastUpdate > olderSpell.lastUpdate) { changedSpellIds.push(newerSpell.id); } }); // Compare spell tags newerSeries.spellTags.forEach((newerTag: SyncedSeriesSpellTag): void => { const olderTag: SyncedSeriesSpellTag | undefined = olderSeries.spellTags.find( (tag: SyncedSeriesSpellTag): boolean => tag.id === newerTag.id ); if (!olderTag || newerTag.lastUpdate > olderTag.lastUpdate) { changedSpellTagIds.push(newerTag.id); } }); // Check if there are any changes const hasChanges: boolean = changedBookIds.length > 0 || changedCharacterIds.length > 0 || changedCharacterAttributeIds.length > 0 || changedWorldIds.length > 0 || changedWorldElementIds.length > 0 || changedLocationIds.length > 0 || changedLocationElementIds.length > 0 || changedLocationSubElementIds.length > 0 || changedSpellIds.length > 0 || changedSpellTagIds.length > 0; if (!hasChanges) { return null; } return { id: newerSeries.id, books: changedBookIds, characters: changedCharacterIds, characterAttributes: changedCharacterAttributeIds, worlds: changedWorldIds, worldElements: changedWorldElementIds, locations: changedLocationIds, locationElements: changedLocationElementIds, locationSubElements: changedLocationSubElementIds, spells: changedSpellIds, spellTags: changedSpellTagIds }; }