File size: 1,923 Bytes
a896df9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import type { Expense, Settlement } from '../../domain/types';
import type { LedgerEvent } from './events';

export interface FoldedState {
	expenses: Map<string, Expense>;
	settlements: Map<string, Settlement>;
	/** Winning event per entity id (may be a tombstone). */
	latest: Map<string, LedgerEvent>;
	/** All events per entity id, sorted by version. */
	history: Map<string, LedgerEvent[]>;
	eventIds: Set<string>;
}

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<LedgerEvent>): FoldedState {
	const state = newFoldedState();
	for (const e of events) applyEvent(state, e);
	return state;
}