import type { SerializedError } from '@pikku/core/errors'; import { PikkuWorkflowService } from '@pikku/core/workflow'; import type { WorkflowPlannedStep, WorkflowQueueOptions, WorkflowRun, WorkflowRunWire, StepState, StepStatus, WorkflowStatus, WorkflowVersionStatus } from '@pikku/core/workflow'; import { type Kysely } from 'kysely'; import type { KyselyPikkuDB } from './kysely-tables.js'; export declare class KyselyWorkflowService extends PikkuWorkflowService { protected db: Kysely; private initialized; private runService; constructor(db: Kysely, options?: WorkflowQueueOptions); init(): Promise; protected createRunImpl(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: { deterministic?: boolean; plannedSteps?: WorkflowPlannedStep[]; }): Promise; getRun(id: string): Promise; protected updateRunStatusImpl(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise; protected insertStepStateImpl(runId: string, stepName: string, rpcName: string | null, data: any, stepOptions?: { retries?: number; retryDelay?: string | number; }, fromStepName?: string): Promise; getStepState(runId: string, stepName: string): Promise; protected listStepStates(runId: string): Promise>; getRunHistory(runId: string): Promise>; protected setStepRunningImpl(stepId: string): Promise; protected setStepScheduledImpl(stepId: string): Promise; /** * Move a step and its current history attempt to the same status in one * transaction. * * Both halves matter: a crash between them used to leave the step row saying * `succeeded` while its history row still said `running`, which silently * corrupts `getRunHistory` and every timeline reconstruction built on it. * The history row is targeted by its `attempt` rather than by a `LIMIT 1` * over `created_at`, so a retry that lands in the same millisecond as the * attempt it replaces still resolves the newer row. */ private writeStepTransition; private insertHistoryRecord; private getTimestampFieldForStatus; protected setStepChildRunIdImpl(stepId: string, childRunId: string): Promise; protected setStepResultImpl(stepId: string, result: any): Promise; protected setStepErrorImpl(stepId: string, error: Error): Promise; protected createRetryAttemptImpl(stepId: string, status: 'pending' | 'running'): Promise; withRunLock(_id: string, fn: () => Promise): Promise; /** * A pass-through, and deliberately so: the one decision that must exclude — * claiming a step to execute it — is made by `claimStepForExecution` below as * a single conditional write, which needs no lock to be exclusive. * * What is left are the suspend and approval sections in the engine, where the * lock is genuine mutual exclusion rather than a claim. A dialect with a real * primitive overrides this to cover them — `kysely-postgres` with * `pg_advisory_xact_lock`, `kysely-mysql` with `GET_LOCK`. */ withStepLock(_runId: string, _stepName: string, fn: () => Promise): Promise; /** * Claim the step with a status-guarded `UPDATE` and read the affected-row * count: the database decides the winner in one statement, so two dispatches * racing for the same step cannot both proceed. * * This replaces the read-then-write the base engine does under `withStepLock`, * which is only as exclusive as that lock — and every dialect but Postgres and * MySQL inherits a pass-through. A conditional update needs no advisory-lock * primitive, so every SQL dialect gets the same guarantee. * * The winner then goes through the ordinary transition methods, so history * rows and the mirror see exactly what they saw before. */ protected claimStepForExecution(runId: string, stepName: string, rpcName: string): Promise; /** * Move a step to `running` only if it is still in one of `from`, reporting * whether this caller is the one that moved it. */ private claimStepStatus; getCompletedGraphState(runId: string): Promise<{ completedNodeIds: string[]; failedNodeIds: string[]; branchKeys: Record; }>; getStepInstances(runId: string): Promise>; getNodeResults(runId: string, nodeIds: string[]): Promise>; protected setBranchTakenImpl(stepId: string, branchKey: string): Promise; /** * Merge one key into the run's JSON state, as a single expression evaluated * by the database. * * The SQL is dialect-specific, so subclasses override this rather than the * caller. The default is the SQLite form; MySQL and Postgres differ in how * they cast a JSON literal. * * @param path - JSON path to the key, already quoted (`$."key"`) * @param json - The value as JSON text */ protected jsonSetState(path: string, json: string): import("kysely").RawBuilder; /** * A JSON path for an arbitrary key. State keys are user-supplied (a graph's * `setState` name, an approval's hex-encoded reason), so the key is quoted * rather than interpolated bare — an unquoted `$.a.b` would silently address * a nested path instead of the key literally called `a.b`. */ private jsonPathFor; protected updateRunStateImpl(runId: string, name: string, value: unknown): Promise; /** * The undispatched-step query, shared by every dialect. * * It used to be deliberately NOT an override, because this class's * `withStepLock` is a pass-through: a subclass inheriting it (kysely-sqlite) * had no atomic claim, so the relay's redundant dispatches would have become * double executions, and only `kysely-postgres` and `kysely-mysql` opted in * on the strength of their real locks. `claimStepForExecution` no longer * needs that lock — the claim is a status-guarded `UPDATE`, atomic in every * dialect — so the relay is safe here for all of them and the override lives * in the base. */ protected findUndispatchedSteps(before: Date, limit: number): Promise>; protected findStalledRunIds(before: Date, limit: number): Promise; getRunState(runId: string): Promise>; protected upsertWorkflowVersionImpl(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise; protected updateWorkflowVersionStatusImpl(name: string, graphHash: string, status: WorkflowVersionStatus): Promise; getWorkflowVersion(name: string, graphHash: string): Promise<{ graph: any; source: string; } | null>; close(): Promise; }