import type { DebugTimeline, Stack } from "@starbeam/debug"; import type * as interfaces from "@starbeam/interfaces"; import { type UNINITIALIZED, REACTIVE } from "@starbeam/shared"; import { type Frame, ActiveFrame } from "./frame.js"; import { ReactiveProtocol } from "./protocol.js"; import type { Subscriptions } from "./subscriptions.js"; import type { Timeline } from "./timeline.js"; export class FrameStack { #current: ActiveFrame | null; #subscriptions: Subscriptions; #timeline: Timeline; static didConsumeCell( frames: FrameStack, reactive: ReactiveProtocol, caller: Stack ): void { frames.#debug.consumeCell(reactive[REACTIVE], caller); frames.#didConsumeReactive(reactive, caller); } static didConsumeFrame( frames: FrameStack, frame: interfaces.Frame, diff: interfaces.Diff, caller: Stack ): void { frames.#debug.consumeFrame(frame, diff, caller); frames.#didConsumeReactive(frame, caller); } static empty(timeline: Timeline, subscriptions: Subscriptions): FrameStack { return new FrameStack(subscriptions, null, timeline); } constructor( subscriptions: Subscriptions, current: ActiveFrame | null, timeline: Timeline ) { this.#subscriptions = subscriptions; this.#current = current; this.#timeline = timeline; } get currentFrame(): ActiveFrame | null { return this.#current; } get #debug(): DebugTimeline { return this.#timeline.log; } create(options: { evaluate: () => T; description: interfaces.Description; }): Frame; // FIXME Overloads shouldn't trigger member-ordering create(options: { description: interfaces.Description }): ActiveFrame; // FIXME Overloads shouldn't trigger member-ordering create({ evaluate, description, }: { evaluate?: () => T; description: interfaces.Description; }): Frame | ActiveFrame { const frame = this.#start(description) as ActiveFrame; if (evaluate) { try { const result = evaluate(); return this.#end(frame, result); } catch (e) { this.finally(frame); throw e; } } else { return frame; } } #didConsumeReactive(reactive: ReactiveProtocol, caller: Stack): void { const frame = this.currentFrame; if (frame) { frame.add(reactive); return; } else { const delegatesTo = ReactiveProtocol.subscribesTo(reactive).filter((r) => ReactiveProtocol.is(r, "mutable") ); for (const target of delegatesTo) { if (ReactiveProtocol.is(target, "mutable")) { this.#timeline.untrackedRead(target, caller); } } } } #end(active: ActiveFrame, value: T): Frame { const { prev, frame } = active.finalize(value, this.#timeline); this.#current = prev; return frame; } finally(prev: ActiveFrame | null): void { this.#current = prev; } #start( description: interfaces.Description, frame?: Frame ): ActiveFrame { const prev = this.#current; return (this.#current = ActiveFrame.create( frame ?? null, prev, description )); } update(options: { updating: Frame; evaluate: () => T; }): Frame; // FIXME Overloads shouldn't trigger member-ordering update({ updating, }: { updating: Frame; }): ActiveFrame; // FIXME Overloads shouldn't trigger member-ordering update({ updating, evaluate: callback, }: { updating: Frame; evaluate?: () => T; }): Frame | ActiveFrame { const activeFrame = this.#start( updating.description, updating ) as ActiveFrame; if (callback) { try { this.#timeline.willEvaluate(); const result = callback(); const frame = this.#end(activeFrame, result); this.#subscriptions.update(frame); return frame; } catch (e) { this.finally(activeFrame); throw e; } } else { return activeFrame; } } }