{"version":3,"file":"sqlite-node-builtin.mjs","names":[],"sources":["../../src/storage/sqlite-node-builtin.ts"],"sourcesContent":["/**\n * SQLite storage adapter for Node.js using the built-in `node:sqlite` module.\n * No native dependencies required — unlike the `better-sqlite3`-backed\n * `reflow-ts/sqlite-node` adapter.\n *\n * Requires **Node.js >= 22.5** (when `node:sqlite` was added). On Node 22.x and\n * 23.x before 23.4 it is gated behind the `--experimental-sqlite` flag; from\n * Node 23.4 it is available by default (still experimental). On older Node, or\n * on Bun, use `reflow-ts/sqlite-node` or `reflow-ts/sqlite-bun` instead.\n *\n * @example\n * ```ts\n * import { SQLiteStorage } from 'reflow-ts/sqlite-node-builtin'\n *\n * const storage = new SQLiteStorage('./reflow.db')\n * await storage.initialize()\n * ```\n */\n\nimport { randomUUID } from 'node:crypto'\nimport { createRequire } from 'node:module'\nimport type { DatabaseSync, StatementSync } from 'node:sqlite'\nimport type {\n  ClaimedRun,\n  CreateRunResult,\n  RunStatus,\n  StepResult,\n  StorageAdapter,\n  WorkflowRun,\n} from '../core/types'\nimport { deserializePersistedValue, serializePersistedValue } from './codec'\n\n// `node:sqlite` is loaded lazily through createRequire so importing this module\n// is safe on any runtime — only constructing SQLiteStorage requires the module\n// to be present (Node >= 22.5).\nconst nodeRequire = createRequire(import.meta.url)\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\nexport class SQLiteStorage implements StorageAdapter {\n  private db: DatabaseSync\n\n  constructor(path: string) {\n    const sqlite = nodeRequire('node:sqlite') as typeof import('node:sqlite')\n    this.db = new sqlite.DatabaseSync(path)\n    this.db.exec('PRAGMA journal_mode = WAL')\n    this.db.exec('PRAGMA synchronous = NORMAL')\n    this.db.exec('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 | null => {\n      if (!run.idempotencyKey) {\n        return null\n      }\n\n      return this.get<WorkflowRunRow>(\n        `SELECT * FROM workflow_runs\n         WHERE workflow = ? AND idempotency_key = ?\n         LIMIT 1`,\n        run.workflow,\n        run.idempotencyKey,\n      )\n    }\n\n    return this.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\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\n    return this.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.get<WorkflowRunRow>(\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        ...queryArgs,\n      )\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 changes = this.run(\n        `UPDATE workflow_runs\n         SET status = 'running', lease_id = ?, updated_at = ?\n         WHERE ${updateWhere}`,\n        ...updateArgs,\n      )\n\n      if (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\n  async heartbeatRun(runId: string, leaseId: string): Promise<boolean> {\n    const changes = this.run(\n      `UPDATE workflow_runs\n       SET updated_at = ?\n       WHERE id = ? AND status = 'running' AND lease_id = ?`,\n      Date.now(),\n      runId,\n      leaseId,\n    )\n\n    return changes > 0\n  }\n\n  async getRun(runId: string): Promise<WorkflowRun | null> {\n    const row = this.get<WorkflowRunRow>(`SELECT * FROM workflow_runs WHERE id = ?`, runId)\n    return row ? mapWorkflowRunRow(row) : null\n  }\n\n  async getStepResults(runId: string): Promise<StepResult[]> {\n    const rows = this.all<WorkflowStepRow>(\n      `SELECT * FROM workflow_steps WHERE run_id = ? ORDER BY created_at ASC`,\n      runId,\n    )\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    return this.transaction(() => {\n      if (leaseId) {\n        const run = this.get<{ _: number }>(\n          `SELECT 1 AS _ FROM workflow_runs\n           WHERE id = ? AND status = 'running' AND lease_id = ?`,\n          result.runId,\n          leaseId,\n        )\n\n        if (!run) {\n          return false\n        }\n      }\n\n      this.run(\n        `INSERT OR REPLACE INTO workflow_steps (id, run_id, name, status, output, error, attempts, created_at, updated_at)\n         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\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\n  async updateRunStatus(runId: string, status: RunStatus): Promise<boolean> {\n    const changes = this.run(\n      `UPDATE workflow_runs\n       SET status = ?, lease_id = ?, updated_at = ?\n       WHERE id = ?`,\n      status,\n      null,\n      Date.now(),\n      runId,\n    )\n\n    return changes > 0\n  }\n\n  async updateClaimedRunStatus(runId: string, leaseId: string, status: RunStatus): Promise<boolean> {\n    const changes = this.run(\n      `UPDATE workflow_runs\n       SET status = ?, lease_id = ?, updated_at = ?\n       WHERE id = ? AND status = 'running' AND lease_id = ?`,\n      status,\n      status === 'running' ? leaseId : null,\n      Date.now(),\n      runId,\n      leaseId,\n    )\n\n    return changes > 0\n  }\n\n  close(): void {\n    this.db.close()\n  }\n\n  // --- node:sqlite helpers -------------------------------------------------\n  // node:sqlite has no transaction() helper and run() returns { changes },\n  // so these wrap the StatementSync API to match the shape the methods expect.\n\n  private prepare(sql: string): StatementSync {\n    return this.db.prepare(sql)\n  }\n\n  private get<T>(sql: string, ...params: SqlParam[]): T | null {\n    return (this.prepare(sql).get(...params) as T | undefined) ?? null\n  }\n\n  private all<T>(sql: string, ...params: SqlParam[]): T[] {\n    return this.prepare(sql).all(...params) as T[]\n  }\n\n  private run(sql: string, ...params: SqlParam[]): number {\n    return Number(this.prepare(sql).run(...params).changes)\n  }\n\n  private transaction<T>(fn: () => T): T {\n    this.db.exec('BEGIN')\n    try {\n      const result = fn()\n      this.db.exec('COMMIT')\n      return result\n    } catch (error) {\n      this.db.exec('ROLLBACK')\n      throw error\n    }\n  }\n}\n\n// node:sqlite binds null as a value but typings model it via SupportedValueType.\ntype SqlParam = string | number | bigint | null | Uint8Array\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  const code = (error as { code?: string }).code\n  const errcode = (error as { errcode?: number }).errcode\n  return code === 'SQLITE_CONSTRAINT_UNIQUE'\n    || errcode === 2067 // SQLITE_CONSTRAINT_UNIQUE (extended result code)\n    || errcode === 19 // SQLITE_CONSTRAINT (primary result code)\n    || error.message.includes('UNIQUE constraint failed')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAM,cAAc,cAAc,OAAO,KAAK,IAAI;AAyBlD,IAAa,gBAAb,MAAqD;CACnD;CAEA,YAAY,MAAc;AAExB,OAAK,KAAK,KADK,YAAY,cAAc,EACpB,aAAa,KAAK;AACvC,OAAK,GAAG,KAAK,4BAA4B;AACzC,OAAK,GAAG,KAAK,8BAA8B;AAC3C,OAAK,GAAG,KAAK,6BAA6B;;CAG5C,MAAM,aAA4B;AAChC,OAAK,GAAG,KAAK;;;;;;;;;;;;;;;;;;;;;;;MAuBX;AAEF,OAAK,GAAG,KAAK;;;;;;MAMX;;CAGJ,MAAM,UAAU,KAA4C;EAC1D,MAAM,wBAA+C;AACnD,OAAI,CAAC,IAAI,eACP,QAAO;AAGT,UAAO,KAAK,IACV;;mBAGA,IAAI,UACJ,IAAI,eACL;;AAGH,SAAO,KAAK,kBAAmC;GAC7C,MAAM,WAAW,iBAAiB;AAClC,OAAI,SACF,QAAO;IACL,KAAK,kBAAkB,SAAS;IAChC,SAAS;IACV;GAGH,MAAM,kBAAkB,wBAAwB,IAAI,OAAO,iBAAiB;AAE5E,OAAI;AACF,SAAK,GACF,QACC;iDAED,CACA,IACC,IAAI,IACJ,IAAI,UACJ,iBACA,IAAI,gBACJ,IAAI,QACJ,IAAI,WACJ,IAAI,UACL;YACI,OAAO;AACd,QAAI,IAAI,kBAAkB,wBAAwB,MAAM,EAAE;KACxD,MAAM,gBAAgB,iBAAiB;AACvC,SAAI,cACF,QAAO;MACL,KAAK,kBAAkB,cAAc;MACrC,SAAS;MACV;;AAIL,UAAM;;AAGR,UAAO;IACL,KAAK;KACH,GAAG;KACH,OAAO,0BAA0B,gBAAgB;KAClD;IACD,SAAS;IACV;IACD;;CAGJ,MAAM,aAAa,eAAkC,aAAkD;AACrG,MAAI,cAAc,WAAW,EAC3B,QAAO;EAGT,MAAM,eAAe,cAAc,UAAU,IAAI,CAAC,KAAK,KAAK;AAE5D,SAAO,KAAK,kBAAkB;GAC5B,MAAM,gBAAgB,gBAAgB,KAAA,IAClC,uBACA;GACJ,MAAM,YAAY,gBAAgB,KAAA,IAC9B,CAAC,GAAG,cAAc,GAClB,CAAC,GAAG,eAAe,YAAY;GAEnC,MAAM,MAAM,KAAK,IACf;8BACsB,aAAa,QAAQ,cAAc;;mBAGzD,GAAG,UACJ;AAED,OAAI,CAAC,IACH,QAAO;GAGT,MAAM,MAAM,KAAK,KAAK;GACtB,MAAM,UAAU,YAAY;GAC5B,MAAM,cAAc,gBAAgB,KAAA,IAChC,kCACA;GACJ,MAAM,aAAa,gBAAgB,KAAA,IAC/B;IAAC;IAAS;IAAK,IAAI;IAAG,GACtB;IAAC;IAAS;IAAK,IAAI;IAAI;IAAY;AASvC,OAPgB,KAAK,IACnB;;iBAES,eACT,GAAG,WACJ,KAEe,EACd,QAAO;AAGT,UAAO;IACL,GAAG,kBAAkB,IAAI;IACzB,QAAQ;IACR,WAAW;IACX;IACD;IACD;;CAGJ,MAAM,aAAa,OAAe,SAAmC;AAUnE,SATgB,KAAK,IACnB;;8DAGA,KAAK,KAAK,EACV,OACA,QACD,GAEgB;;CAGnB,MAAM,OAAO,OAA4C;EACvD,MAAM,MAAM,KAAK,IAAoB,4CAA4C,MAAM;AACvF,SAAO,MAAM,kBAAkB,IAAI,GAAG;;CAGxC,MAAM,eAAe,OAAsC;AAMzD,SALa,KAAK,IAChB,yEACA,MACD,CAEW,KAAK,SAAqB;GACpC,IAAI,IAAI;GACR,OAAO,IAAI;GACX,MAAM,IAAI;GACV,QAAQ,IAAI;GACZ,QAAQ,IAAI,WAAW,OAAO,OAAO,0BAA0B,IAAI,OAAO;GAC1E,OAAO,IAAI;GACX,UAAU,IAAI;GACd,WAAW,IAAI;GACf,WAAW,IAAI;GAChB,EAAE;;CAGL,MAAM,eAAe,QAAoB,SAAoC;AAC3E,SAAO,KAAK,kBAAkB;AAC5B,OAAI;QAQE,CAPQ,KAAK,IACf;kEAEA,OAAO,OACP,QACD,CAGC,QAAO;;AAIX,QAAK,IACH;8CAEA,OAAO,IACP,OAAO,OACP,OAAO,MACP,OAAO,QACP,wBAAwB,OAAO,QAAQ,cAAc,EACrD,OAAO,OACP,OAAO,UACP,OAAO,WACP,OAAO,UACR;AAED,UAAO;IACP;;CAGJ,MAAM,gBAAgB,OAAe,QAAqC;AAWxE,SAVgB,KAAK,IACnB;;sBAGA,QACA,MACA,KAAK,KAAK,EACV,MACD,GAEgB;;CAGnB,MAAM,uBAAuB,OAAe,SAAiB,QAAqC;AAYhG,SAXgB,KAAK,IACnB;;8DAGA,QACA,WAAW,YAAY,UAAU,MACjC,KAAK,KAAK,EACV,OACA,QACD,GAEgB;;CAGnB,QAAc;AACZ,OAAK,GAAG,OAAO;;CAOjB,QAAgB,KAA4B;AAC1C,SAAO,KAAK,GAAG,QAAQ,IAAI;;CAG7B,IAAe,KAAa,GAAG,QAA8B;AAC3D,SAAQ,KAAK,QAAQ,IAAI,CAAC,IAAI,GAAG,OAAO,IAAsB;;CAGhE,IAAe,KAAa,GAAG,QAAyB;AACtD,SAAO,KAAK,QAAQ,IAAI,CAAC,IAAI,GAAG,OAAO;;CAGzC,IAAY,KAAa,GAAG,QAA4B;AACtD,SAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,QAAQ;;CAGzD,YAAuB,IAAgB;AACrC,OAAK,GAAG,KAAK,QAAQ;AACrB,MAAI;GACF,MAAM,SAAS,IAAI;AACnB,QAAK,GAAG,KAAK,SAAS;AACtB,UAAO;WACA,OAAO;AACd,QAAK,GAAG,KAAK,WAAW;AACxB,SAAM;;;;AAQZ,SAAS,kBAAkB,KAAkC;AAC3D,QAAO;EACL,IAAI,IAAI;EACR,UAAU,IAAI;EACd,OAAO,0BAA0B,IAAI,MAAM;EAC3C,gBAAgB,IAAI;EACpB,QAAQ,IAAI;EACZ,WAAW,IAAI;EACf,WAAW,IAAI;EAChB;;AAGH,SAAS,wBAAwB,OAAyB;AACxD,KAAI,EAAE,iBAAiB,OACrB,QAAO;CAGT,MAAM,OAAQ,MAA4B;CAC1C,MAAM,UAAW,MAA+B;AAChD,QAAO,SAAS,8BACX,YAAY,QACZ,YAAY,MACZ,MAAM,QAAQ,SAAS,2BAA2B"}