n8n/packages/@n8n/engine/src/execution/execution-store.ts
mfsiega a5b00eee2d
feat(engine): Add a service that cancels an execution (#39806)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 15:01:08 +00:00

82 lines
2.9 KiB
TypeScript

import type { WorkflowGraph } from '../graph';
import type { ResponseExpectation } from '../response-channel';
import type {
CallerContext,
ExecutionMode,
ExecutionStatus,
TriggerOutputs,
WorkflowDocument,
} from './execution.types';
/** The fields the write side supplies and the execution path reads back. */
interface BaseExecutionRecord {
/** Caller-minted id. The store never mints one. */
id: string;
workflowId: string;
status: ExecutionStatus;
mode: ExecutionMode;
graph: WorkflowGraph;
/** Stored for the read path only. Nothing on the execution path reads it. */
workflow: WorkflowDocument;
triggerOutputs: TriggerOutputs | null;
callerContext: CallerContext;
/** What kind of a response the caller expects. */
responseExpectation: ResponseExpectation;
}
/** A new execution to persist. Timestamps are assigned by the store. */
export type NewExecutionRecord = BaseExecutionRecord;
/**
* What running an execution needs of its row. No timing: the execution path
* decides on `status`, never on when anything happened. The read path has its
* own view (`ExecutionView`).
*/
export type ExecutionRecord = BaseExecutionRecord;
/** Thrown by `loadExecution` when no execution exists for the given id. */
export class ExecutionNotFoundError extends Error {
constructor(readonly executionId: string) {
super(`Execution not found: ${executionId}`);
this.name = 'ExecutionNotFoundError';
}
}
/** Persistence interface for executions. */
export interface ExecutionStore {
/** Persist a new execution record under the caller-minted `record.id`. */
createExecution(record: NewExecutionRecord): Promise<void>;
/** Load a full execution by id. Throws `ExecutionNotFoundError` if absent. */
loadExecution(id: string): Promise<ExecutionRecord>;
/**
* Compare-and-set status transition. Returns `true` iff this call performed
* the transition, so duplicate/redelivered events are handled idempotently.
*/
transitionStatus(id: string, from: ExecutionStatus, to: ExecutionStatus): Promise<boolean>;
/**
* Record an execution's outcome: writes the final status and the finish time
* together, as a compare-and-set on the live statuses, so they can't be
* observed apart.
*/
finishExecution(id: string, status: 'completed' | 'failed'): Promise<boolean>;
/** End an execution on request, from any status that has not ended. */
cancelExecution(id: string): Promise<boolean>;
/**
* Sets a live execution's status from the state of its steps: `waiting` when
* every step it still owes is suspended, `running` when one step can run.
*
* Call this when a step suspends and when a step settles. A resume does not
* call it, so the status can lag until the resumed step settles.
*
* The refresh takes no lock, so it can lose a race and confuse `running`
* with `waiting`. Callers may test whether the execution ended, as both step
* handlers do. Read the step rows to tell `running` from `waiting`.
*/
refreshLiveStatus(id: string): Promise<void>;
}