import type { DesignScreenAssertion, DesignScreenCaptureReport, DesignScreenDesiredState, DesignScreenInteraction, DesignScreenPreset } from './designImport.js'; import type { RoutePreviewCache } from './routePreview.js'; import { type StateSwitchMetrics } from './stateSwitchMetrics.js'; import type { EditorDeclarations } from './declarations.js'; export interface ResolvedFlowWaypoint { screenId: string; /** * Route-preview store key the snapshot publishes under. An empty key marks * a pass-through waypoint: its steps advance the instance so LATER * waypoints stay reachable, but nothing publishes and no delivery status * is tracked (viewport-variant paths walk their prefix this way). */ cacheKey: string; /** Viewport of this waypoint's frame; the instance resizes when it differs. */ width: number; height: number; steps: readonly DesignScreenInteraction[]; /** Complete host-owned UI state applied before this waypoint's steps. */ desiredState?: DesignScreenDesiredState; /** * This waypoint is a synchronous host render over an already reproduced * screen. Interactive supply may publish it after paint without repeating * capture-only assertions and geometry waits. Authoritative capture still * performs the full contract. */ fastDesiredState?: boolean; /** Stable live-presentation slot; sibling direct states share one document. */ interactiveSurfaceId?: string; /** Activation warm-up may prepare a surface before an option is selected. */ prewarmSurfacePageId?: string; /** The waypoint's own capture assertions, checked after its steps settle. */ assertions?: readonly DesignScreenAssertion[]; /** * Runs against the settled live instance after the snapshot publishes, * while the iframe is still mounted. Viewport-variant deliveries commit * their layer import here: the walk iframe is a TRUE viewport, so width * media queries hold — unlike canvas shadow mounts, which evaluate them * against the editor window. */ afterSerialize?: (iframe: HTMLIFrameElement) => Promise; } export interface ResolvedFlowPath { id: string; /** Boot URL with route and declarative environment already encoded. */ url: string; /** Authored route, forwarded to the capture-preparation spec. */ route: string; /** * Shared screen preset of every waypoint on this path (mock APIs, identity, * timers). Applied through the host executor with the FIRST waypoint's * preparation; the instance keeps it for the rest of the walk. */ preset?: DesignScreenPreset; waypoints: readonly ResolvedFlowWaypoint[]; } export type FlowScreenStatus = 'pending' | 'running' | 'delivered' | 'failed'; export interface FlowScreenState { status: FlowScreenStatus; pathId: string; /** Failure reason, present only for failed screens. */ reason?: string; } export interface FlowSessionRunnerOptions { /** * Concurrent live instances. The default is 1: mocked app backends * (STT sockets, summary generators) are commonly single-session, and two * concurrent walks contending for them time out deterministically. Raise * it only for apps whose fixtures are proven concurrency-safe — measured * on one such app, two walks filled a canvas in half the time with no * more long tasks on the editor thread, and four in a quarter. */ concurrency?: number; /** * How long a boot may take before the walk gives up on the instance. A * load that never fires neither resolves nor rejects, and one such walk * holds a lane for the rest of the session. */ bootTimeoutMs?: number; /** * How long one path may walk in total. A path whose steps keep timing out * (an assertion the app never reaches) is failed at the next waypoint * boundary instead of holding its lane through every remaining step. */ walkBudgetMs?: number; /** Clock for the walk budget; injected so the budget can be tested without wall time. */ now?: () => number; /** Names this supply in the boot counters, so a stray boot is attributable. */ label?: string; createIframe?: () => HTMLIFrameElement; resolveContainer?: () => (Node & { appendChild(node: Node): unknown; }) | null; /** Navigates the instance and resolves when the document loaded. */ boot?: (iframe: HTMLIFrameElement, url: string) => Promise; /** * Prepares one waypoint on the instance: preset (host executor) -> desired * state (host executor) -> interactions -> assertions, exactly the capture * pipeline's contract. * Defaults to the design-import controller's prepareScreenCapture. */ prepare?: (iframe: HTMLIFrameElement, spec: { pageId: string; route?: string; preset?: DesignScreenPreset; desiredState?: DesignScreenDesiredState; interactions?: readonly DesignScreenInteraction[]; assertions?: readonly DesignScreenAssertion[]; }, waypoint: ResolvedFlowWaypoint) => Promise; /** Extra settle before serializing a waypoint (fonts, trailing paints). */ settle?: (iframe: HTMLIFrameElement, ms: number) => Promise; /** Layout-stability wait before serializing. Defaults to waitForGeometryStability. */ waitForStability?: (iframe: HTMLIFrameElement) => Promise; /** * The preview cache a walked screen is published into. Required unless * both `serialize` and `publish` are supplied, as tests do. */ previews?: Pick; /** Where walk phases are recorded; the runtime passes its own metrics. */ metrics?: Pick; serialize?: (iframe: HTMLIFrameElement, fallbackUrl: string) => string | null; /** Writes one waypoint snapshot into the route preview store. */ publish?: (cacheKey: string, snapshot: string) => boolean; /** Releases a live document after its delivery finishes or is abandoned. */ onWaypointFinish?: (iframe: HTMLIFrameElement, waypoint: ResolvedFlowWaypoint, outcome: 'completed' | 'failed' | 'cancelled') => void; /** Keeps a parked document alive while another surface is presenting it. */ keepWarmInstanceAlive?: (iframe: HTMLIFrameElement) => boolean; onError?: (pathId: string, error: unknown) => void; } /** * Resolves when the document's layout stops moving: two consecutive * animation frames whose element geometry matches. JS-driven entrance * animations (rAF springs writing inline transforms) are invisible to the * frozen stylesheet's animation:none — serializing mid-flight pins * intermediate rects as overlapping boxes, so the walk waits them out. * Movement owned by effectively-infinite animations (spinners, pulses, * loops) is exempt from the convergence check instead of blocking it, so * screens with perpetual motion settle as fast as still ones; the timeout * stays as the backstop for documents that never converge either way. * The policy lives in ./geometryStability, where it is tested as pure data. */ export declare function waitForGeometryStability(iframe: HTMLIFrameElement, timeoutMs?: number): Promise; export declare function createFlowSessionIframe(): HTMLIFrameElement; export interface FlowSessionsDeps { /** The cache every runner of this editor publishes into unless told otherwise. */ previews?: FlowSessionRunnerOptions['previews']; /** Where every runner of this editor records its walk phases. */ metrics?: FlowSessionRunnerOptions['metrics']; /** The host declarations a runner prepares waypoints against. */ declarations?: EditorDeclarations; } /** * One editor's flow sessions: the warm pool of parked application instances * every runner shares, the runner factory bound to that pool, and the * canonical runner that fills the canvas. Every runtime owns one. */ export declare function createFlowSessions(deps?: FlowSessionsDeps): { setFlowSessionWarmTtl: (ms: number) => void; __flowSessionWarmDebug: { boots: number; reuses: number; parks: number; evictions: number; /** Warming walks answered by renewing an already-parked instance's lease. */ refreshes: number; /** Which supply booted, so a stray second boot is attributable. */ bootsByRunner: Record; }; evictFlowSessionWarmInstances: (ownerLabel?: string, options?: { preservePinned?: boolean; }) => void; warmInstanceServesPath: (path: ResolvedFlowPath) => boolean; warmInstanceAtPathEnd: (path: ResolvedFlowPath) => HTMLIFrameElement | null; createFlowSessionRunner: (options?: FlowSessionRunnerOptions) => { /** * Queues paths for materialization. Screens already delivered, failed, or * claimed by a queued path are skipped — callers re-request freely. */ materialize(paths: readonly ResolvedFlowPath[]): number; /** * Applies a path's terminal state on a known live document. The caller * guarantees that this iframe already reached the path's route and base * prefix; all gesture steps are therefore skipped and only the target's * complete desired-state map is prepared. FIFO ordering keeps a document * exposed by the current walk from being mutated until that walk finishes. */ materializeOnInstance(path: ResolvedFlowPath, iframe: HTMLIFrameElement): number; /** * Drops queued paths the predicate rejects, releasing their pending * screens so a later materialize can queue them again. Running walks are * untouched. Called when the designer moves to another canvas: the * paths of the canvas they left should not run ahead of the one they * are looking at. */ pruneQueue(keep: (path: ResolvedFlowPath) => boolean): number; /** Live instances that may walk at once; takes effect at the next pump. */ setConcurrency(next: number): void; /** Session state of one screen, or null when no session claims it. */ screenState(screenId: string): FlowScreenState | null; /** * Forgets a terminal delivery whose external snapshot was evicted. A * running or queued screen keeps its claim and cannot be invalidated. */ forgetScreen(screenId: string): boolean; /** True while the session still owes this screen its snapshot. */ willDeliver(screenId: string): boolean; /** * True when nothing is queued or walking. Speculative callers read this * before enqueueing: the runner is FIFO, so speculative work queued ahead * of a designer's real request would delay it — speculation may only * enter an empty runner. */ isIdle(): boolean; subscribe(listener: () => void): () => void; /** * Drops all session state. Running paths notice the round change at their * next waypoint boundary and stop; their remaining screens re-queue on * the next materialize call. */ reset(): void; /** Test/diagnostic snapshot of every tracked screen. */ debugScreenStates(): Record; /** How many finished walks currently have an instance parked for reuse. */ debugWarmInstancesParked(): number; }; getFlowSessionRunner: () => ReturnType<(options?: FlowSessionRunnerOptions) => { /** * Queues paths for materialization. Screens already delivered, failed, or * claimed by a queued path are skipped — callers re-request freely. */ materialize(paths: readonly ResolvedFlowPath[]): number; /** * Applies a path's terminal state on a known live document. The caller * guarantees that this iframe already reached the path's route and base * prefix; all gesture steps are therefore skipped and only the target's * complete desired-state map is prepared. FIFO ordering keeps a document * exposed by the current walk from being mutated until that walk finishes. */ materializeOnInstance(path: ResolvedFlowPath, iframe: HTMLIFrameElement): number; /** * Drops queued paths the predicate rejects, releasing their pending * screens so a later materialize can queue them again. Running walks are * untouched. Called when the designer moves to another canvas: the * paths of the canvas they left should not run ahead of the one they * are looking at. */ pruneQueue(keep: (path: ResolvedFlowPath) => boolean): number; /** Live instances that may walk at once; takes effect at the next pump. */ setConcurrency(next: number): void; /** Session state of one screen, or null when no session claims it. */ screenState(screenId: string): FlowScreenState | null; /** * Forgets a terminal delivery whose external snapshot was evicted. A * running or queued screen keeps its claim and cannot be invalidated. */ forgetScreen(screenId: string): boolean; /** True while the session still owes this screen its snapshot. */ willDeliver(screenId: string): boolean; /** * True when nothing is queued or walking. Speculative callers read this * before enqueueing: the runner is FIFO, so speculative work queued ahead * of a designer's real request would delay it — speculation may only * enter an empty runner. */ isIdle(): boolean; subscribe(listener: () => void): () => void; /** * Drops all session state. Running paths notice the round change at their * next waypoint boundary and stop; their remaining screens re-queue on * the next materialize call. */ reset(): void; /** Test/diagnostic snapshot of every tracked screen. */ debugScreenStates(): Record; /** How many finished walks currently have an instance parked for reuse. */ debugWarmInstancesParked(): number; }>; setFlowSessionConcurrency: (concurrency: number) => void; resetFlowSessionRunner: () => void; }; export type FlowSessions = ReturnType; export type FlowSessionRunner = ReturnType;