{"version":3,"file":"sqlite-node.mjs","names":[],"sources":["../../src/storage/sqlite-node.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport Database from 'better-sqlite3'\nimport type {\n  ClaimedRun,\n  CreateRunResult,\n  RunStatus,\n  StepResult,\n  StorageAdapter,\n  WorkflowRun,\n} from '../core/types'\nimport { deserializePersistedValue, serializePersistedValue } from './codec'\n\ninterface WorkflowRunRow {\n  id: string\n  workflow: string\n  input: string\n  idempotency_key: string | null\n  lease_id: string | null\n  status: string\n  created_at: number\n  updated_at: number\n}\n\ninterface WorkflowStepRow {\n  id: string\n  run_id: string\n  name: string\n  status: string\n  output: string | null\n  error: string | null\n  attempts: number\n  created_at: number\n  updated_at: number\n}\n\n/**\n * SQLite-backed storage adapter. Uses WAL mode for concurrent reads and\n * transactional claiming for safe multi-process access.\n *\n * @example\n * ```ts\n * const storage = new SQLiteStorage('./reflow.db')\n * await storage.initialize()\n * ```\n */\nexport class SQLiteStorage implements StorageAdapter {\n  private db: Database.Database\n\n  constructor(path: string) {\n    this.db = new Database(path)\n    this.db.pragma('journal_mode = WAL')\n    this.db.pragma('synchronous = NORMAL')\n    this.db.pragma('busy_timeout = 5000')\n  }\n\n  async initialize(): Promise<void> {\n    this.db.exec(`\n      CREATE TABLE IF NOT EXISTS workflow_runs (\n        id               TEXT PRIMARY KEY,\n        workflow         TEXT NOT NULL,\n        input            TEXT NOT NULL,\n        idempotency_key  TEXT,\n        lease_id         TEXT,\n        status           TEXT NOT NULL,\n        created_at       INTEGER NOT NULL,\n        updated_at       INTEGER NOT NULL\n      );\n\n      CREATE TABLE IF NOT EXISTS workflow_steps (\n        id          TEXT PRIMARY KEY,\n        run_id      TEXT NOT NULL,\n        name        TEXT NOT NULL,\n        status      TEXT NOT NULL,\n        output      TEXT,\n        error       TEXT,\n        attempts    INTEGER DEFAULT 0,\n        created_at  INTEGER NOT NULL,\n        updated_at  INTEGER NOT NULL\n      );\n    `)\n\n    this.db.exec(`\n      CREATE INDEX IF NOT EXISTS idx_runs_status ON workflow_runs(status, workflow);\n      CREATE INDEX IF NOT EXISTS idx_steps_run_id ON workflow_steps(run_id);\n      CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_workflow_idempotency\n      ON workflow_runs(workflow, idempotency_key)\n      WHERE idempotency_key IS NOT NULL;\n    `)\n  }\n\n  async createRun(run: WorkflowRun): Promise<CreateRunResult> {\n    const findExistingRun = (): WorkflowRunRow | undefined => {\n      if (!run.idempotencyKey) {\n        return undefined\n      }\n\n      return this.db\n        .prepare(\n          `SELECT * FROM workflow_runs\n           WHERE workflow = ? AND idempotency_key = ?\n           LIMIT 1`,\n        )\n        .get(run.workflow, run.idempotencyKey) as WorkflowRunRow | undefined\n    }\n\n    const create = this.db.transaction((): CreateRunResult => {\n      const existing = findExistingRun()\n      if (existing) {\n        return {\n          run: mapWorkflowRunRow(existing),\n          created: false,\n        }\n      }\n\n      const serializedInput = serializePersistedValue(run.input, 'Workflow input')\n\n      try {\n        this.db\n          .prepare(\n            `INSERT INTO workflow_runs (id, workflow, input, idempotency_key, lease_id, status, created_at, updated_at)\n             VALUES (?, ?, ?, ?, NULL, ?, ?, ?)`,\n          )\n          .run(\n            run.id,\n            run.workflow,\n            serializedInput,\n            run.idempotencyKey,\n            run.status,\n            run.createdAt,\n            run.updatedAt,\n          )\n      } catch (error) {\n        if (run.idempotencyKey && isUniqueConstraintError(error)) {\n          const racedExisting = findExistingRun()\n          if (racedExisting) {\n            return {\n              run: mapWorkflowRunRow(racedExisting),\n              created: false,\n            }\n          }\n        }\n\n        throw error\n      }\n\n      return {\n        run: {\n          ...run,\n          input: deserializePersistedValue(serializedInput),\n        },\n        created: true,\n      }\n    })\n\n    return create()\n  }\n\n  async claimNextRun(workflowNames: readonly string[], staleBefore?: number): Promise<ClaimedRun | null> {\n    if (workflowNames.length === 0) {\n      return null\n    }\n\n    const placeholders = workflowNames.map(() => '?').join(', ')\n    const claim = this.db.transaction(() => {\n      const reclaimClause = staleBefore === undefined\n        ? `status = 'pending'`\n        : `(status = 'pending' OR (status = 'running' AND updated_at <= ?))`\n      const queryArgs = staleBefore === undefined\n        ? workflowNames\n        : [...workflowNames, staleBefore]\n\n      const row = this.db\n        .prepare(\n          `SELECT * FROM workflow_runs\n           WHERE workflow IN (${placeholders}) AND ${reclaimClause}\n           ORDER BY CASE status WHEN 'pending' THEN 0 ELSE 1 END ASC, created_at ASC, rowid ASC\n           LIMIT 1`,\n        )\n        .get(...queryArgs) as WorkflowRunRow | undefined\n\n      if (!row) {\n        return null\n      }\n\n      const now = Date.now()\n      const leaseId = randomUUID()\n      const updateWhere = staleBefore === undefined\n        ? `id = ? AND status = 'pending'`\n        : `id = ? AND (status = 'pending' OR (status = 'running' AND updated_at <= ?))`\n      const updateArgs = staleBefore === undefined\n        ? [leaseId, now, row.id]\n        : [leaseId, now, row.id, staleBefore]\n\n      const result = this.db\n        .prepare(\n          `UPDATE workflow_runs\n           SET status = 'running', lease_id = ?, updated_at = ?\n           WHERE ${updateWhere}`,\n        )\n        .run(...updateArgs)\n\n      if (result.changes === 0) {\n        return null\n      }\n\n      return {\n        ...mapWorkflowRunRow(row),\n        status: 'running' as RunStatus,\n        updatedAt: now,\n        leaseId,\n      }\n    })\n\n    return claim()\n  }\n\n  async heartbeatRun(runId: string, leaseId: string): Promise<boolean> {\n    const result = this.db\n      .prepare(\n        `UPDATE workflow_runs\n         SET updated_at = ?\n         WHERE id = ? AND status = 'running' AND lease_id = ?`,\n      )\n      .run(Date.now(), runId, leaseId)\n\n    return result.changes > 0\n  }\n\n  async getRun(runId: string): Promise<WorkflowRun | null> {\n    const row = this.db\n      .prepare(`SELECT * FROM workflow_runs WHERE id = ?`)\n      .get(runId) as WorkflowRunRow | undefined\n\n    return row ? mapWorkflowRunRow(row) : null\n  }\n\n  async getStepResults(runId: string): Promise<StepResult[]> {\n    const rows = this.db\n      .prepare(`SELECT * FROM workflow_steps WHERE run_id = ? ORDER BY created_at ASC`)\n      .all(runId) as WorkflowStepRow[]\n\n    return rows.map((row): StepResult => ({\n      id: row.id,\n      runId: row.run_id,\n      name: row.name,\n      status: row.status as StepResult['status'],\n      output: row.output === null ? null : deserializePersistedValue(row.output),\n      error: row.error,\n      attempts: row.attempts,\n      createdAt: row.created_at,\n      updatedAt: row.updated_at,\n    }))\n  }\n\n  async saveStepResult(result: StepResult, leaseId?: string): Promise<boolean> {\n    const save = this.db.transaction(() => {\n      if (leaseId) {\n        const run = this.db\n          .prepare(\n            `SELECT 1 FROM workflow_runs\n             WHERE id = ? AND status = 'running' AND lease_id = ?`,\n          )\n          .get(result.runId, leaseId)\n\n        if (!run) {\n          return false\n        }\n      }\n\n      this.db\n        .prepare(\n          `INSERT OR REPLACE INTO workflow_steps (id, run_id, name, status, output, error, attempts, created_at, updated_at)\n           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n        )\n        .run(\n          result.id,\n          result.runId,\n          result.name,\n          result.status,\n          serializePersistedValue(result.output, 'Step output'),\n          result.error,\n          result.attempts,\n          result.createdAt,\n          result.updatedAt,\n        )\n\n      return true\n    })\n\n    return save()\n  }\n\n  async updateRunStatus(runId: string, status: RunStatus): Promise<boolean> {\n    const result = this.db\n      .prepare(\n        `UPDATE workflow_runs\n         SET status = ?, lease_id = ?, updated_at = ?\n         WHERE id = ?`,\n      )\n      .run(status, null, Date.now(), runId)\n\n    return result.changes > 0\n  }\n\n  async updateClaimedRunStatus(runId: string, leaseId: string, status: RunStatus): Promise<boolean> {\n    const result = this.db\n      .prepare(\n        `UPDATE workflow_runs\n         SET status = ?, lease_id = ?, updated_at = ?\n         WHERE id = ? AND status = 'running' AND lease_id = ?`,\n      )\n      .run(status, status === 'running' ? leaseId : null, Date.now(), runId, leaseId)\n\n    return result.changes > 0\n  }\n\n  close(): void {\n    this.db.close()\n  }\n\n}\n\nfunction mapWorkflowRunRow(row: WorkflowRunRow): WorkflowRun {\n  return {\n    id: row.id,\n    workflow: row.workflow,\n    input: deserializePersistedValue(row.input),\n    idempotencyKey: row.idempotency_key,\n    status: row.status as RunStatus,\n    createdAt: row.created_at,\n    updatedAt: row.updated_at,\n  }\n}\n\nfunction isUniqueConstraintError(error: unknown): boolean {\n  if (!(error instanceof Error)) {\n    return false\n  }\n\n  return (error as { code?: string }).code === 'SQLITE_CONSTRAINT_UNIQUE'\n    || error.message.includes('UNIQUE constraint failed')\n}\n"],"mappings":"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"}