import type { Expense, Settlement } from '../../domain/types'; import type { LedgerEvent } from './events'; export interface FoldedState { expenses: Map; settlements: Map; /** Winning event per entity id (may be a tombstone). */ latest: Map; /** All events per entity id, sorted by version. */ history: Map; eventIds: Set; } export function newFoldedState(): FoldedState { return { expenses: new Map(), settlements: new Map(), latest: new Map(), history: new Map(), eventIds: new Set() }; } /** >0 if a should win over b. Higher version wins; ties by ts, then id. */ export function compareEvents(a: LedgerEvent, b: LedgerEvent): number { if (a.version !== b.version) return a.version - b.version; if (a.ts !== b.ts) return a.ts < b.ts ? -1 : 1; return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; } /** Applies one event. Returns false if the event id was already applied. */ export function applyEvent(state: FoldedState, event: LedgerEvent): boolean { if (state.eventIds.has(event.id)) return false; state.eventIds.add(event.id); const hist = state.history.get(event.entityId) ?? []; hist.push(event); hist.sort(compareEvents); state.history.set(event.entityId, hist); const current = state.latest.get(event.entityId); if (current && compareEvents(event, current) <= 0) return true; state.latest.set(event.entityId, event); if (event.kind === 'expense') { if (event.op === 'upsert') state.expenses.set(event.entityId, event.data); else state.expenses.delete(event.entityId); } else { if (event.op === 'upsert') state.settlements.set(event.entityId, event.data); else state.settlements.delete(event.entityId); } return true; } export function foldEvents(events: Iterable): FoldedState { const state = newFoldedState(); for (const e of events) applyEvent(state, e); return state; }