/** * Recipe composition — `requires` chaining. * * A recipe with `requires: ["auth/login"]` should run the dependency * first whenever it gets replayed in a fresh session. This module * resolves the transitive dependency graph (topologically sorted, * cycle-checked) and runs each piece in order, skipping anything that * already ran in this session. * * "Session" is per-Page: a WeakMap keyed by the Playwright `Page` * handle. New page = clean slate. This matches how scenarioLoad * workers and the chaos crawler treat browser context boundaries. * * Failure mode: if a dependency fails, the dependent is **not** * attempted — the caller's `RunWithRequiresResult.failedAt` carries * the name of the recipe that broke, so reporting is easy. */ import type { Page } from "playwright"; import type { RecipeStore } from "./store.js"; import type { RecipeVars } from "./templating.js"; import type { ActionRecipe, ReplayResult } from "./types.js"; export interface RunWithRequiresOptions { page: Page; recipe: ActionRecipe; store: RecipeStore; /** * Names of recipes already replayed in this session. The runner * skips anything in this set. Caller maintains the set across * iterations — typical pattern is a per-Page or per-context * `Set`. Passing `undefined` re-runs every dependency. */ alreadyRan?: Set; /** Fires before each recipe in the chain starts. */ onProgress?: (event: ChainProgressEvent) => void; /** * Variable bag forwarded to every replay in the chain. Templates * like `{{email}}` in any recipe's step values resolve from here. * The same bag is shared across the chain — `auth/login` and * `shop/checkout` see the same `{{email}}`. */ vars?: RecipeVars; /** * Storage-state snapshot policy (issue #89). When enabled, recipes * marked as snapshotable have their storage state captured after * the first successful replay; subsequent replays in fresh * contexts inject the snapshot and skip the chain step entirely. * * Pass `false` (default) to disable the optimisation. Pass `true` * to use the default TTL (30 min). Pass an object to customise. */ snapshot?: boolean | SnapshotPolicy; } export interface SnapshotPolicy { /** Time-to-live in milliseconds. Snapshots older than this are * discarded and the chain step replays normally. Default: 30 min. */ ttlMs?: number; /** * Predicate deciding which recipes are eligible for snapshot * capture. Defaults to "name starts with `auth/`" — the dominant * use case. Return false for recipes whose post-state is too * volatile or order-dependent to snapshot safely. */ eligible?: (recipe: ActionRecipe) => boolean; } export type ChainProgressEvent = { kind: "start"; recipe: string; index: number; total: number; } | { kind: "skip"; recipe: string; reason: "already-ran" | "snapshot-applied"; } | { kind: "complete"; recipe: string; result: ReplayResult; }; export interface RunWithRequiresResult { /** True when every recipe in the chain (skipped or run) succeeded. */ ok: boolean; /** Names in execution order. Includes "already-ran" skips. */ ranSequence: string[]; /** * Name of the recipe whose replay failed, if any. The dependent * recipe(s) after it are NOT attempted. `null` on full success. */ failedAt: string | null; /** Per-recipe replay results, keyed by name. */ results: Record; } /** * Replay `recipe` after running every transitive dependency in its * `requires` list. Mutates `alreadyRan` (if provided) so a caller * can amortise the chain cost across multiple replays in the same * session. * * Storage-state snapshots (issue #89): when `opts.snapshot` is * enabled and the chain link is eligible (default: `name.startsWith("auth/")`), * a fresh-context replay attempts to apply a cached snapshot first. * Snapshot hit → chain link is skipped with `reason: "snapshot-applied"`. * Snapshot miss → chain link runs normally and the runner captures * a new snapshot after success. */ export declare function runRecipeWithRequires(opts: RunWithRequiresOptions): Promise; /** * Topologically sort `recipe` + its transitive `requires`. Throws on * a cycle (recipes that depend on each other), an unresolved name * (recipe X requires Y but Y is not in the store), or a self-loop. * * Returns recipes in execution order: dependencies first, then the * recipe itself last. */ export declare function resolveDependencies(recipe: ActionRecipe, store: RecipeStore): ActionRecipe[]; //# sourceMappingURL=composition.d.ts.map