// Where the operator is in a flow, kept in sessionStorage beside the batch so // a refresh keeps their place. Pure, so tests/check-flows.mjs can hold a deep // link to a later step to landing on the first. import { paths, type Flow, type Step } from "./routes.ts"; export interface FlowState { /** The flow's base path. */ base: string; /** The steps the operator has reached, in any order. */ reached: Step[]; /** The rkey of the policy the flow writes, once it has one. */ policy?: string; /** The rkey of the binding the flow writes, once it has one. */ binding?: string; /** What the last deploy wrote, for the done step. Records' keys only. */ deployed?: { collection: string; rkey: string; op: string }[]; } const STORAGE_KEY = "policy-site:flow"; export function loadFlow(storage: Pick): FlowState | null { try { const saved = JSON.parse(storage.getItem(STORAGE_KEY) ?? "null") as FlowState | null; if (saved && typeof saved.base === "string" && Array.isArray(saved.reached)) return saved; } catch { // An unreadable place is no place. } return null; } export function saveFlow(storage: Pick, state: FlowState | null): void { if (state) storage.setItem(STORAGE_KEY, JSON.stringify(state)); else storage.removeItem(STORAGE_KEY); } /** A flow entered at `step`, as an entry action or a Next enters it. */ export function reach(state: FlowState | null, flow: Flow, step: Step): FlowState { const reached = state?.base === flow.base ? state.reached : []; return { ...(state?.base === flow.base ? state : {}), base: flow.base, reached: [...new Set([...reached, step])] }; } /** * Where a request for `step` of `flow` goes instead, or null when it may be * shown: a step the operator has not reached in this flow sends them to the * flow's first step. */ export function redirectFor(flow: Flow, step: Step, state: FlowState | null): string | null { const first = flow.steps[0]!; if (step === first) return null; if (state?.base === flow.base && state.reached.includes(step)) return null; return paths.step(flow, first); }