import { SchemaBase } from './dsl.js'; import type { PageActionSchema } from './page-action.js'; import { Page, getRegisteredPages, clearRegisteredPages } from './page.js'; import type { PageDef } from './page-def.js'; import type { NavigationAction } from './navigation.js'; // Page-driven flow: every node is a page, and a page belongs to an app. // Leaf nodes are pages too — a journey starts at a page and ends at a page. // Page definitions (PageSchema/Page/actions) live in page.ts; this file // models the flow graph between pages. export interface PageEdge extends SchemaBase { /** Trigger page action; undefined = default path (success/normal). */ when?: PageActionSchema; start: Page; end: Page; } export interface PageFlow extends SchemaBase { /** Entry page. */ start: Page; /** All pages explicitly declared by the caller. */ pages: Page[]; edges: PageEdge[]; } export function pageEdge(start: Page, end: Page, when?: PageActionSchema, description?: string): PageEdge { // Auto name for uniformity with SchemaBase; `when` stays the branch marker. return { name: `${start.name}->${end.name}`, start, end, when, description }; } export function definePageFlow( name: string, schema: { start: Page; pages: Page[]; edges: PageEdge[]; description?: string; }, ): PageFlow { const { start, pages, edges: explicitEdges, description } = schema; // Resolve the back action from any page's actions list (PageSchema at runtime) const backAction = findBackAction(pages); // Auto-generate back edges: for every explicit edge A→B (not itself a back), // if B has no explicit back edge and no existing edge B→A, add B→A with back. const hasExplicitBack = new Set(); for (const e of explicitEdges) { if (e.when && e.when.name === 'back') hasExplicitBack.add(e.start); } const hasReverse = new Set(); for (const e of explicitEdges) { hasReverse.add(`${e.start.name}->${e.end.name}`); } const generatedBackEdges: PageEdge[] = []; if (backAction) { for (const e of explicitEdges) { if (e.when && e.when.name === 'back') continue; if (hasExplicitBack.has(e.end)) continue; if (hasReverse.has(`${e.end.name}->${e.start.name}`)) continue; generatedBackEdges.push({ name: `${e.end.name}->${e.start.name}`, start: e.end, end: e.start, when: backAction, description: 'auto-generated back edge', }); hasExplicitBack.add(e.end); } } const edges = [...explicitEdges, ...generatedBackEdges]; // Validate: every registered page must be in the flow's pages const registered = getRegisteredPages(); const orphaned: string[] = []; for (const rp of registered) { // Compare by object identity — caller passes the same reference if (!pages.includes(rp as unknown as Page)) { orphaned.push(rp.name); } } if (orphaned.length > 0) { throw new Error( `[definePageFlow "${name}"] orphaned pages (defined but not in flow): ${orphaned.join(', ')}`, ); } clearRegisteredPages(); // Validate: start must be in pages if (!pages.includes(start)) { throw new Error( `[definePageFlow "${name}"] start page "${start.name}" not in pages`, ); } // Validate: every page appears in at least one edge const inEdge = new Set(); for (const e of edges) { inEdge.add(e.start); inEdge.add(e.end); } const isolated: string[] = []; for (const p of pages) { if (!inEdge.has(p)) isolated.push(p.name); } if (isolated.length > 0) { throw new Error( `[definePageFlow "${name}"] isolated pages (no edges): ${isolated.join(', ')}`, ); } // Validate: all pages reachable from start const adj = new Map(); for (const p of pages) adj.set(p, []); for (const e of edges) { const neighbors = adj.get(e.start); if (neighbors) neighbors.push(e.end); } const visited = new Set(); const stack: Page[] = [start]; while (stack.length > 0) { const cur = stack.pop()!; if (visited.has(cur)) continue; visited.add(cur); for (const next of adj.get(cur) ?? []) { if (!visited.has(next)) stack.push(next); } } const unreachable: string[] = []; for (const p of pages) { if (!visited.has(p)) unreachable.push(p.name); } if (unreachable.length > 0) { throw new Error( `[definePageFlow "${name}"] unreachable pages: ${unreachable.join(', ')}`, ); } return { name, description, start, pages, edges }; } /** Walk all pages' pageDef.actions (PageSchema runtime shape) to find a NavigationAction with method 'back'. */ function findBackAction(pages: Page[]): NavigationAction | undefined { for (const p of pages) { const pageDef = (p as unknown as Record).pageDef as PageDef | undefined; if (!pageDef?.actions) continue; for (const a of pageDef.actions) { const na = a as NavigationAction; if (na.type === 'navigation' && na.method === 'back') return na; } } return undefined; }