// mean reducer calls = (((1 + cacheDistance) * cacheDistance) + cursor) / cacheDistance // delta(mean reducer calls) / delta(cacheDistance) = 0.5 * cacheDistance + cursor / cacheDistance // local minimum of mean reducer calls at [cacheDistance = sqrt(2 * cursor)] export const cacheDistance = (cursor: number) => Math.floor(Math.sqrt(2 * cursor)); export type WithHistory = { base: T; now: T; timeline: A[]; cursor: number; cache: { value: T; cursor: number; }; }; export function performWith(reducer: (data: T, action: A) => T) { return (data: WithHistory, action: A): WithHistory => { const { base, cache, cursor, now, timeline } = data; return { base, now: reducer(now, action), cursor: cursor + 1, cache: cursor - cache.cursor > cacheDistance(cursor) ? { value: reducer(cache.value, timeline[cache.cursor]), cursor: cache.cursor + 1, } : cache, timeline: timeline.slice(0, cursor).concat([action]), }; }; } export function undoWith(reducer: (data: T, action: A) => T) { return (data: WithHistory): WithHistory => { const { base, cache, cursor, timeline } = data; const newCursor = cursor - 1; if (newCursor < 0) return data; const rebuildCache = newCursor < cache.cursor; const cacheCursor = rebuildCache ? Math.max(0, newCursor - cacheDistance(newCursor)) : cache.cursor; const cacheValue = rebuildCache ? timeline.slice(0, cacheCursor).reduce(reducer, base) : cache.value; return { base, now: timeline.slice(cacheCursor, newCursor).reduce(reducer, cacheValue), cache: rebuildCache ? { value: cacheValue, cursor: cacheCursor } : cache, cursor: newCursor, timeline, }; }; } export function redoWith(reducer: (data: T, action: A) => T) { return (data: WithHistory): WithHistory => { const { base, cache, cursor, now, timeline } = data; if (cursor === timeline.length) return data; return { base, now: reducer(now, timeline[cursor]), cursor: cursor + 1, cache: cursor - cache.cursor > cacheDistance(cursor) ? { value: reducer(cache.value, timeline[cache.cursor]), cursor: cache.cursor + 1, } : cache, timeline, }; }; } export function freeze(data: WithHistory): WithHistory { return { base: data.now, cache: { value: data.now, cursor: 0, }, cursor: 0, now: data.now, timeline: [], }; } export function atWith(reducer: (data: T, action: A) => T) { return (data: WithHistory, cursor: number) => { if (cursor > data.cursor) return data.now; if (cursor > data.cache.cursor) return data.timeline.slice(data.cache.cursor, cursor).reduce(reducer, data.cache.value); return data.timeline.slice(0, cursor).reduce(reducer, data.base); }; } export function create(data: T) { return { base: data, cursor: 0, cache: { value: data, cursor: 0, }, now: data, timeline: [], }; }