workflows.ts127 lines · main
| 1 | import { brivenError } from '@briven/shared'; |
| 2 | |
| 3 | /** |
| 4 | * Workflows / durable execution — Phase 4 future-feature SKELETON. |
| 5 | * |
| 6 | * Like `branches.ts`, this file exists to pin the data model + API |
| 7 | * shape today so the dashboard and CLI can wire against it, and the |
| 8 | * full implementation lands incrementally without breaking consumers. |
| 9 | * |
| 10 | * The full implementation has four parts: |
| 11 | * |
| 12 | * 1. Meta-DB tables: `workflow_definitions` (versioned), |
| 13 | * `workflow_runs` (per-execution state), `workflow_steps` (per- |
| 14 | * step state with deterministic-replay seed). All cascade-delete |
| 15 | * with their parent project. |
| 16 | * |
| 17 | * 2. A `workflow()` wrapper in `@briven/cli/server` next to query / |
| 18 | * mutation / action. Authors declare a workflow as a function |
| 19 | * with explicit checkpoints — each `await ctx.step(...)` records |
| 20 | * its outcome so a crash mid-flight resumes from the last step |
| 21 | * instead of replaying side effects. |
| 22 | * |
| 23 | * 3. A worker process (`apps/workflow-worker` or as a mode on the |
| 24 | * runtime) that picks up `pending` runs from the queue, executes |
| 25 | * each step in a deno isolate, persists step outcomes, retries |
| 26 | * transient failures with exponential backoff. |
| 27 | * |
| 28 | * 4. Dashboard surface: list runs, step-by-step state inspector, |
| 29 | * manual retry / cancel buttons, audit-log integration. |
| 30 | * |
| 31 | * Today the code below is the type contract + a no-op stub. Calls |
| 32 | * throw `not_implemented` so the dashboard renders an "available in |
| 33 | * v2" banner instead of breaking. |
| 34 | * |
| 35 | * Design intent worth pinning before v1 ships: |
| 36 | * |
| 37 | * - Workflows are deterministic-replay, not event-sourced. Each |
| 38 | * `await ctx.step(name, fn)` records `(stepName, outcome)`. On |
| 39 | * resume the worker replays the workflow body and short-circuits |
| 40 | * each step that already has a recorded outcome. |
| 41 | * |
| 42 | * - External-system side effects (http calls, polar charges, email |
| 43 | * sends) MUST live inside a step — never inline in the workflow |
| 44 | * body. The worker hard-rejects a run that side-effects outside |
| 45 | * a step. |
| 46 | * |
| 47 | * - The wire format for step outcomes is JSON + 1MB cap. Outcomes |
| 48 | * beyond that get an opaque id; the actual payload lives in |
| 49 | * object storage. |
| 50 | */ |
| 51 | |
| 52 | export interface WorkflowDefinition { |
| 53 | readonly id: string; |
| 54 | readonly projectId: string; |
| 55 | readonly name: string; |
| 56 | readonly version: number; |
| 57 | readonly bundle: string; // path to the compiled workflow function |
| 58 | readonly createdAt: Date; |
| 59 | } |
| 60 | |
| 61 | export type WorkflowRunStatus = |
| 62 | | 'pending' |
| 63 | | 'running' |
| 64 | | 'succeeded' |
| 65 | | 'failed' |
| 66 | | 'cancelled'; |
| 67 | |
| 68 | export interface WorkflowRun { |
| 69 | readonly id: string; |
| 70 | readonly workflowId: string; |
| 71 | readonly projectId: string; |
| 72 | readonly status: WorkflowRunStatus; |
| 73 | readonly input: unknown; |
| 74 | readonly result: unknown; |
| 75 | readonly error: { code: string; message: string } | null; |
| 76 | readonly startedAt: Date; |
| 77 | readonly finishedAt: Date | null; |
| 78 | /** Step outcomes by name. Empty until the worker runs steps. */ |
| 79 | readonly steps: Readonly<Record<string, WorkflowStepOutcome>>; |
| 80 | } |
| 81 | |
| 82 | export interface WorkflowStepOutcome { |
| 83 | readonly status: 'pending' | 'running' | 'succeeded' | 'failed'; |
| 84 | readonly attempt: number; |
| 85 | readonly startedAt: string; |
| 86 | readonly finishedAt: string | null; |
| 87 | readonly result: unknown; |
| 88 | readonly error: { code: string; message: string } | null; |
| 89 | } |
| 90 | |
| 91 | export interface TriggerWorkflowInput { |
| 92 | readonly projectId: string; |
| 93 | readonly workflowName: string; |
| 94 | readonly input: unknown; |
| 95 | } |
| 96 | |
| 97 | export async function triggerWorkflow(_input: TriggerWorkflowInput): Promise<WorkflowRun> { |
| 98 | throw new brivenError( |
| 99 | 'not_implemented', |
| 100 | 'workflows are a Phase 4 feature — see docs/BUILD_PLAN.md', |
| 101 | { status: 501 }, |
| 102 | ); |
| 103 | } |
| 104 | |
| 105 | export async function listWorkflowRuns( |
| 106 | _projectId: string, |
| 107 | _options?: { workflowName?: string; status?: WorkflowRunStatus; limit?: number }, |
| 108 | ): Promise<readonly WorkflowRun[]> { |
| 109 | // Returns empty until the worker ships. Dashboard renders the |
| 110 | // "no runs yet" empty state, which is accurate. |
| 111 | return []; |
| 112 | } |
| 113 | |
| 114 | export async function getWorkflowRun( |
| 115 | _projectId: string, |
| 116 | _runId: string, |
| 117 | ): Promise<WorkflowRun | null> { |
| 118 | return null; |
| 119 | } |
| 120 | |
| 121 | export async function cancelWorkflowRun(_projectId: string, _runId: string): Promise<void> { |
| 122 | throw new brivenError( |
| 123 | 'not_implemented', |
| 124 | 'workflows are a Phase 4 feature — see docs/BUILD_PLAN.md', |
| 125 | { status: 501 }, |
| 126 | ); |
| 127 | } |