| 1 | // The per-tab reads that index a session's persisted turns: the checkpoints its |
| 2 | // rewind menu offers and the boundaries its fork entries offer. A turn that |
| 3 | // commits, and a transcript that is replaced, move both, so the two are read |
| 4 | // together where a surface settles; each read keeps its own stale-response |
| 5 | // guard, because one answering late must not overwrite a newer answer. |
| 6 | |
| 7 | import { asArray } from "./array"; |
| 8 | import { app } from "./bridge"; |
| 9 | import { createForkTargetsRefresh, type ForkTurnAction } from "./forkTurn"; |
| 10 | import type { CheckpointMeta } from "./types"; |
| 11 | |
| 12 | /** The actions these reads dispatch into a tab's state. */ |
| 13 | export type TurnBoundaryAction = { type: "checkpoints"; checkpoints: CheckpointMeta[] } | ForkTurnAction; |
| 14 | |
| 15 | /** The turn-index reads one controller binds, dispatch included. */ |
| 16 | export interface TurnBoundaryReads { |
| 17 | /** Drops in-flight checkpoint and fork reads for a replaced session binding. */ |
| 18 | invalidateCheckpoints(tabId: string): void; |
| 19 | /** Re-reads a tab's checkpoints alone, for a reconcile that patches only its active turn. */ |
| 20 | refreshCheckpoints(tabId: string): Promise<void>; |
| 21 | /** Re-reads the checkpoints and the fork boundaries a settled turn changes. */ |
| 22 | refreshTurnBoundaries(tabId: string): Promise<void>; |
| 23 | /** |
| 24 | * Applies the checkpoints one hydration read returned and reads the fork |
| 25 | * boundaries that settle with them; undefined checkpoints dispatch nothing. |
| 26 | */ |
| 27 | settleCheckpoints(tabId: string, checkpoints: CheckpointMeta[] | undefined): Promise<void>; |
| 28 | } |
| 29 | |
| 30 | /** Binds the turn-index reads to one dispatch sink. */ |
| 31 | export function createTurnBoundaryReads(dispatch: (tabId: string, action: TurnBoundaryAction) => void): TurnBoundaryReads { |
| 32 | const checkpointSeq = new Map<string, number>(); |
| 33 | const bumpCheckpoints = (tabId: string): number => { |
| 34 | const seq = (checkpointSeq.get(tabId) ?? 0) + 1; |
| 35 | checkpointSeq.set(tabId, seq); |
| 36 | return seq; |
| 37 | }; |
| 38 | const refreshCheckpoints = async (tabId: string): Promise<void> => { |
| 39 | const seq = bumpCheckpoints(tabId); |
| 40 | const checkpoints = await app.CheckpointsForTab(tabId).catch(() => undefined); |
| 41 | if (checkpointSeq.get(tabId) !== seq || checkpoints === undefined) return; |
| 42 | dispatch(tabId, { type: "checkpoints", checkpoints: asArray(checkpoints) }); |
| 43 | }; |
| 44 | const forkTargets = createForkTargetsRefresh(dispatch); |
| 45 | return { |
| 46 | invalidateCheckpoints: (tabId) => { |
| 47 | bumpCheckpoints(tabId); |
| 48 | forkTargets.invalidate(tabId); |
| 49 | }, |
| 50 | refreshCheckpoints, |
| 51 | refreshTurnBoundaries: (tabId) => Promise.all([refreshCheckpoints(tabId), forkTargets.refresh(tabId)]).then(() => undefined), |
| 52 | settleCheckpoints: async (tabId, checkpoints) => { |
| 53 | if (checkpoints !== undefined) dispatch(tabId, { type: "checkpoints", checkpoints: asArray(checkpoints) }); |
| 54 | await forkTargets.refresh(tabId); |
| 55 | }, |
| 56 | }; |
| 57 | } |
| 58 |