import { Event, Variable } from "../schema"; import { assertExhaustive } from "../utils/exhaustive"; import { ChangeEvent, makeDeleteVariable, makeRevertVariable, RollbackChange, Subs, SubsSnapshot, } from "./subs"; export type EventEpoch = number & { __eventIndex: never }; function* flattenEvents(events: Event[]): Generator { for (const event of events) { yield event; switch (event.type) { case "Unification": { yield* flattenEvents(event.subevents); break; } case "VariableUnified": case "VariableSetDescriptor": break; default: assertExhaustive(event); } } } function getFlatEvents(events: Event[]): { flatEvents: Event[]; map: Map; } { const map = new Map(); const flatEvents = Array.from(flattenEvents(events)); let i = 0; for (const event of flatEvents) { map.set(event, i as EventEpoch); i++; } return { flatEvents, map }; } export class Engine { #eventIndexMap: Map; #events: Event[]; #subs: Subs = Subs.new(); #reverseEvents: Map = new Map(); #nextIndexForward: EventEpoch = 0 as EventEpoch; constructor(events: Event[]) { const { flatEvents, map } = getFlatEvents(events); this.#eventIndexMap = map; this.#events = flatEvents; } getEventIndex(event: Event): EventEpoch { const index = this.#eventIndexMap.get(event); if (index === undefined) { throw new Error("Event not found"); } return index; } get step(): EventEpoch { return this.#nextIndexForward; } stepTo(eventIndex: EventEpoch): void { while (this.#nextIndexForward <= eventIndex) { this.stepForward(this.#nextIndexForward); ++this.#nextIndexForward; } while (this.#nextIndexForward > eventIndex + 1) { --this.#nextIndexForward; this.stepBackward(this.#nextIndexForward); } if (this.#nextIndexForward !== eventIndex + 1) { throw new Error("Invalid event index"); } } get subs(): Readonly { return this.#subs; } subsSnapshot(): SubsSnapshot { return this.#subs.snapshot({ epoch: (this.#nextIndexForward - 1) as EventEpoch, }); } lastEventIndex(): EventEpoch { return (this.#events.length - 1) as EventEpoch; } private stepForward(eventIndex: EventEpoch): void { const event = this.#events[eventIndex]; if (!isApplicable(event)) { return; } if (!this.#reverseEvents.has(eventIndex)) { const variable = applicableVariable(event); const current = this.#subs.get(variable); let revert: RollbackChange; if (!current) { revert = makeDeleteVariable({ variable }); } else { revert = makeRevertVariable({ variable, to: current }); } this.#reverseEvents.set(eventIndex, revert); } this.#subs.apply(event); } private stepBackward(eventIndex: EventEpoch): void { const event = this.#events[eventIndex]; if (!isApplicable(event)) { return; } const revert = this.#reverseEvents.get(eventIndex); if (!revert) { throw new Error("No revert found"); } this.#subs.apply(revert); } } function isApplicable(event: Event): event is ChangeEvent { switch (event.type) { case "VariableUnified": case "VariableSetDescriptor": return true; case "Unification": return false; default: assertExhaustive(event); } } function applicableVariable(event: ChangeEvent): Variable { switch (event.type) { case "VariableUnified": return event.from; case "VariableSetDescriptor": return event.variable; default: assertExhaustive(event); } }