| 1 | /** |
| 2 | * Guide runs this tab is waiting on, so the reader can leave and hear when one is done, and where |
| 3 | * the reader is in each guide. |
| 4 | * |
| 5 | * A run takes minutes and finishes on the server. `PullRequestGuideRunCoordinator` watches every |
| 6 | * run listed here, wherever the reader is in the app, and says when it ends. Progress is shared so |
| 7 | * a guide opened beside a new thread picks up at the same stop. Not persisted: a reload forgets |
| 8 | * both, and the guide is still there when the reader comes back. |
| 9 | */ |
| 10 | import type { EnvironmentId, PullRequestRef } from "@t3tools/contracts"; |
| 11 | import { create } from "zustand"; |
| 12 | |
| 13 | import { pullRequestReviewKey } from "../pullRequestReviewStore"; |
| 14 | |
| 15 | export interface GuideRunWatch { |
| 16 | readonly environmentId: EnvironmentId; |
| 17 | readonly reference: PullRequestRef; |
| 18 | /** The pull request's title, for the notice. */ |
| 19 | readonly title: string | null; |
| 20 | /** The guide and failure on record when the wait began; a different one means the run ended. */ |
| 21 | readonly previousGuideAt: string | null; |
| 22 | readonly previousFailedAt: string | null; |
| 23 | readonly since: number; |
| 24 | } |
| 25 | |
| 26 | /** Where a reader is in one guide, kept per guide head so any panel showing it agrees. */ |
| 27 | export interface GuideProgress { |
| 28 | readonly position: number; |
| 29 | /** Stops the reader has moved on from, which the rail checks off. */ |
| 30 | readonly seen: ReadonlySet<number>; |
| 31 | } |
| 32 | |
| 33 | const NO_PROGRESS: GuideProgress = { position: 0, seen: new Set() }; |
| 34 | |
| 35 | interface GuideRunStoreState { |
| 36 | readonly watches: Readonly<Record<string, GuideRunWatch>>; |
| 37 | /** The guide on screen right now, which needs no notice when it lands. */ |
| 38 | readonly shownKey: string | null; |
| 39 | /** A guide a notice asked to open; its panel switches to the Guide tab and clears this. */ |
| 40 | readonly openRequestKey: string | null; |
| 41 | /** Reading progress by guide, keyed by `guideProgressKey`. */ |
| 42 | readonly progress: Readonly<Record<string, GuideProgress>>; |
| 43 | readonly setProgress: (key: string, change: (current: GuideProgress) => GuideProgress) => void; |
| 44 | readonly watch: (watch: GuideRunWatch) => void; |
| 45 | readonly settle: (key: string) => void; |
| 46 | readonly setShown: (key: string | null) => void; |
| 47 | readonly requestOpen: (key: string | null) => void; |
| 48 | } |
| 49 | |
| 50 | export function guideRunKey(environmentId: EnvironmentId, reference: PullRequestRef): string { |
| 51 | return `${environmentId}:${pullRequestReviewKey(reference)}`; |
| 52 | } |
| 53 | |
| 54 | export const useGuideRunStore = create<GuideRunStoreState>()((set) => ({ |
| 55 | watches: {}, |
| 56 | shownKey: null, |
| 57 | openRequestKey: null, |
| 58 | progress: {}, |
| 59 | setProgress: (key, change) => |
| 60 | set((state) => ({ |
| 61 | progress: { ...state.progress, [key]: change(state.progress[key] ?? NO_PROGRESS) }, |
| 62 | })), |
| 63 | watch: (watch) => |
| 64 | set((state) => { |
| 65 | const key = guideRunKey(watch.environmentId, watch.reference); |
| 66 | if (state.watches[key] !== undefined) return state; |
| 67 | return { watches: { ...state.watches, [key]: watch } }; |
| 68 | }), |
| 69 | settle: (key) => |
| 70 | set((state) => { |
| 71 | if (state.watches[key] === undefined) return state; |
| 72 | const { [key]: _settled, ...watches } = state.watches; |
| 73 | return { watches }; |
| 74 | }), |
| 75 | setShown: (shownKey) => set({ shownKey }), |
| 76 | requestOpen: (openRequestKey) => set({ openRequestKey }), |
| 77 | })); |
| 78 | |
| 79 | /** One guide's progress: a new head is a new tour, since its stops are not the ones read. */ |
| 80 | export function guideProgressKey(runKey: string, headSha: string): string { |
| 81 | return `${runKey}:${headSha}`; |
| 82 | } |
| 83 | |
| 84 | export function useGuideProgress(key: string): GuideProgress { |
| 85 | return useGuideRunStore((state) => state.progress[key] ?? NO_PROGRESS); |
| 86 | } |