{"version":3,"sources":["../src/index.ts","../src/postgres-lock.ts","../src/postgres-schema.ts"],"sourcesContent":["/**\n * awaitly-postgres\n *\n * PostgreSQL persistence adapter for awaitly workflows.\n * Provides ready-to-use SnapshotStore backed by PostgreSQL.\n * Supports both WorkflowSnapshot and ResumeState (serialized via serializeResumeState).\n */\n\nimport { Pool as PgPool } from \"pg\";\nimport type { WorkflowSnapshot, SnapshotStore } from \"awaitly/durable\";\nimport type { WorkflowLock } from \"awaitly/durable\";\nimport {\n  type ResumeState,\n  type StoreSaveInput,\n  type StoreLoadResult,\n  isWorkflowSnapshot,\n  isResumeState,\n  isSerializedResumeState,\n  serializeResumeState,\n  deserializeResumeState,\n} from \"awaitly/durable\";\nimport { createPostgresLock, type PostgresLockOptions } from \"./postgres-lock\";\nimport { createSchemaInitializer } from \"./postgres-schema\";\n\nexport type { SnapshotStore, WorkflowSnapshot } from \"awaitly/durable\";\nexport type { WorkflowLock } from \"awaitly/durable\";\nexport type { PostgresLockOptions } from \"./postgres-lock\";\nexport type { StoreSaveInput, StoreLoadResult } from \"awaitly/durable\";\n\n// =============================================================================\n// PostgresOptions\n// =============================================================================\n\n/**\n * Options for the postgres() shorthand function.\n */\nexport interface PostgresOptions {\n  /** PostgreSQL connection URL. */\n  url: string;\n  /** Table name for snapshots. @default 'awaitly_snapshots' */\n  table?: string;\n  /** Key prefix for IDs. @default '' */\n  prefix?: string;\n  /** Bring your own pool. */\n  pool?: PgPool;\n  /** Report background errors on idle connections in an internally owned pool. */\n  onPoolError?: (error: Error) => void;\n  /** Auto-create table on first use. @default true */\n  autoCreateTable?: boolean;\n  /** Cross-process lock options. When set, the store implements WorkflowLock. */\n  lock?: PostgresLockOptions;\n}\n\n// =============================================================================\n// postgres() - One-liner Snapshot Store Setup\n// =============================================================================\n\nconst SAFE_TABLE_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/**\n * Create a snapshot store backed by PostgreSQL.\n * Save accepts WorkflowSnapshot or ResumeState; load returns whichever was stored.\n * Use loadResumeState(id) for type-safe restore, or toResumeState(await store.load(id)).\n *\n * @example\n * ```typescript\n * import { postgres } from 'awaitly-postgres';\n * import { createWorkflow } from 'awaitly';\n * import { toResumeState } from 'awaitly/durable';\n *\n * const store = postgres('postgresql://localhost/mydb');\n * const workflow = createWorkflow(deps);\n *\n * // Run and persist resume state\n * const { result, resumeState } = await workflow.runWithState(fn);\n * await store.save('wf-123', resumeState);\n *\n * // Restore\n * const resumeState = await store.loadResumeState('wf-123');\n * if (resumeState) await workflow.run(fn, { resumeState });\n * ```\n *\n * @example\n * ```typescript\n * // With options including cross-process locking\n * const store = postgres({\n *   url: 'postgresql://localhost/mydb',\n *   table: 'my_workflow_snapshots',\n *   prefix: 'orders:',\n *   lock: { lockTableName: 'my_workflow_locks' },\n * });\n * ```\n */\n/** Postgres store with widened save/load for WorkflowSnapshot and ResumeState. Compatible with SnapshotStore for snapshot-only usage. */\nexport interface PostgresStore extends Partial<WorkflowLock> {\n  save(id: string, state: StoreSaveInput): Promise<void>;\n  load(id: string): Promise<StoreLoadResult>;\n  loadResumeState(id: string): Promise<ResumeState | null>;\n  delete(id: string): Promise<void>;\n  list(options?: { prefix?: string; limit?: number }): Promise<Array<{ id: string; updatedAt: string }>>;\n  close(): Promise<void>;\n}\n\nexport function postgres(urlOrOptions: string | PostgresOptions): PostgresStore {\n  const opts = typeof urlOrOptions === \"string\" ? { url: urlOrOptions } : urlOrOptions;\n  const tableName = opts.table ?? \"awaitly_snapshots\";\n\n  if (!SAFE_TABLE_NAME.test(tableName)) {\n    throw new Error(`Invalid table name: ${tableName}. Must be alphanumeric with underscores.`);\n  }\n\n  const prefix = opts.prefix ?? \"\";\n  const autoCreateTable = opts.autoCreateTable ?? true;\n\n  const ownPool = !opts.pool;\n  const pool = opts.pool ?? new PgPool({ connectionString: opts.url });\n  if (ownPool) {\n    // pg evicts broken idle clients itself, but an unhandled pool error also\n    // terminates Node. Queries still reject normally and future calls reconnect.\n    // A caller supplying a pool owns its error handling and lifecycle.\n    pool.on(\"error\", (error) => opts.onPoolError?.(error));\n  }\n  const initializeTable = createSchemaInitializer(pool, tableName, `\n      CREATE TABLE IF NOT EXISTS ${tableName} (\n        id TEXT PRIMARY KEY,\n        snapshot JSONB NOT NULL,\n        updated_at TIMESTAMPTZ DEFAULT NOW()\n      );\n      CREATE INDEX IF NOT EXISTS ${tableName}_updated_at_idx ON ${tableName} (updated_at DESC);\n  `);\n  const ensureTable = (): Promise<void> => autoCreateTable ? initializeTable() : Promise.resolve();\n\n  const lock = opts.lock ? createPostgresLock(pool, opts.lock) : null;\n\n  const store: PostgresStore = {\n    async save(id: string, state: StoreSaveInput): Promise<void> {\n      await ensureTable();\n      const fullId = prefix + id;\n      const toStore = isResumeState(state) ? serializeResumeState(state) : state;\n      await pool.query(\n        `INSERT INTO ${tableName} (id, snapshot, updated_at)\n         VALUES ($1, $2, NOW())\n         ON CONFLICT (id) DO UPDATE SET snapshot = $2, updated_at = NOW()`,\n        [fullId, JSON.stringify(toStore)]\n      );\n    },\n\n    async load(id: string): Promise<StoreLoadResult> {\n      await ensureTable();\n      const fullId = prefix + id;\n      const result = await pool.query(\n        `SELECT snapshot FROM ${tableName} WHERE id = $1`,\n        [fullId]\n      );\n      if (result.rows.length === 0) return null;\n      const raw = result.rows[0].snapshot;\n      if (isSerializedResumeState(raw)) return deserializeResumeState(raw);\n      if (isWorkflowSnapshot(raw)) return raw;\n      return raw as WorkflowSnapshot;\n    },\n\n    async loadResumeState(id: string): Promise<ResumeState | null> {\n      const loaded = await store.load(id);\n      if (loaded === null) return null;\n      if (isResumeState(loaded)) return loaded;\n      return null;\n    },\n\n    async delete(id: string): Promise<void> {\n      await ensureTable();\n      const fullId = prefix + id;\n      await pool.query(`DELETE FROM ${tableName} WHERE id = $1`, [fullId]);\n    },\n\n    async list(options?: { prefix?: string; limit?: number }): Promise<Array<{ id: string; updatedAt: string }>> {\n      await ensureTable();\n      const filterPrefix = prefix + (options?.prefix ?? \"\");\n      const limit = options?.limit ?? 100;\n\n      const result = await pool.query(\n        `SELECT id, updated_at FROM ${tableName}\n         WHERE id LIKE $1\n         ORDER BY updated_at DESC\n         LIMIT $2`,\n        [filterPrefix + \"%\", limit]\n      );\n\n      return result.rows.map(row => ({\n        id: (row.id as string).slice(prefix.length),\n        updatedAt: (row.updated_at as Date).toISOString(),\n      }));\n    },\n\n    async close(): Promise<void> {\n      if (ownPool) {\n        await pool.end();\n      }\n    },\n  };\n\n  if (lock) {\n    store.tryAcquire = lock.tryAcquire.bind(lock);\n    store.release = lock.release.bind(lock);\n    store.renew = lock.renew.bind(lock);\n  }\n\n  return store;\n}\n","/**\n * PostgreSQL workflow lock (lease) for cross-process concurrency control.\n * Uses a lease (TTL) + owner token; release verifies the token.\n */\n\nimport type { Pool } from \"pg\";\nimport { randomUUID } from \"node:crypto\";\nimport { createSchemaInitializer } from \"./postgres-schema\";\n\nexport interface PostgresLockOptions {\n  /**\n   * Table name for workflow locks.\n   * @default 'awaitly_workflow_lock'\n   */\n  lockTableName?: string;\n}\n\n/**\n * Create tryAcquire and release functions that use a PostgreSQL lock table.\n * Caller must pass the same pool used for state (so one connection pool).\n */\nexport function createPostgresLock(\n  pool: Pool,\n  options: PostgresLockOptions = {}\n): {\n  tryAcquire(\n    id: string,\n    opts?: { ttlMs?: number }\n  ): Promise<{ ownerToken: string } | null>;\n  release(id: string, ownerToken: string): Promise<void>;\n  renew(id: string, ownerToken: string, opts?: { ttlMs?: number }): Promise<boolean>;\n  ensureLockTable(): Promise<void>;\n} {\n  const lockTableName = options.lockTableName ?? \"awaitly_workflow_lock\";\n\n  const safeIndexName = `idx_${lockTableName.replace(/[^a-zA-Z0-9_]/g, \"_\")}_expires_at`;\n\n  const ensureLockTable = createSchemaInitializer(pool, lockTableName, `\n      CREATE TABLE IF NOT EXISTS ${lockTableName} (\n        workflow_id TEXT PRIMARY KEY,\n        owner_token TEXT NOT NULL,\n        expires_at TIMESTAMPTZ NOT NULL\n      );\n      CREATE INDEX IF NOT EXISTS ${safeIndexName} ON ${lockTableName}(expires_at);\n  `);\n\n  async function tryAcquire(\n    id: string,\n    opts?: { ttlMs?: number }\n  ): Promise<{ ownerToken: string } | null> {\n    const ttlMs = opts?.ttlMs ?? 60_000;\n    const ownerToken = randomUUID();\n    const expiresAt = new Date(Date.now() + ttlMs);\n\n    await ensureLockTable();\n\n    // Insert new row or update only if current row is expired (or missing).\n    const result = await pool.query(\n      `\n      INSERT INTO ${lockTableName} (workflow_id, owner_token, expires_at)\n      VALUES ($1, $2, $3)\n      ON CONFLICT (workflow_id) DO UPDATE SET\n        owner_token = EXCLUDED.owner_token,\n        expires_at = EXCLUDED.expires_at\n      WHERE ${lockTableName}.expires_at < NOW()\n      RETURNING owner_token\n    `,\n      [id, ownerToken, expiresAt]\n    );\n\n    if (result.rowCount === 1 && result.rows[0].owner_token === ownerToken) {\n      return { ownerToken };\n    }\n    return null;\n  }\n\n  async function release(id: string, ownerToken: string): Promise<void> {\n    await pool.query(\n      `DELETE FROM ${lockTableName} WHERE workflow_id = $1 AND owner_token = $2`,\n      [id, ownerToken]\n    );\n  }\n\n  async function renew(\n    id: string,\n    ownerToken: string,\n    opts?: { ttlMs?: number }\n  ): Promise<boolean> {\n    const ttlMs = opts?.ttlMs ?? 60_000;\n    const expiresAt = new Date(Date.now() + ttlMs);\n\n    const result = await pool.query(\n      `UPDATE ${lockTableName} SET expires_at = $1 WHERE workflow_id = $2 AND owner_token = $3`,\n      [expiresAt, id, ownerToken]\n    );\n\n    return (result.rowCount ?? 0) > 0;\n  }\n\n  return { tryAcquire, release, renew, ensureLockTable };\n}\n","import type { Pool } from \"pg\";\n\n/** Serialize first-use DDL across workers, then cache successful initialization. */\nexport function createSchemaInitializer(pool: Pool, name: string, sql: string): () => Promise<void> {\n  let initialized: Promise<void> | undefined;\n\n  const initialize = async (): Promise<void> => {\n    const client = await pool.connect();\n    try {\n      await client.query(\"BEGIN\");\n      // IF NOT EXISTS alone can still race in PostgreSQL's system catalogs.\n      // A transaction-scoped lock is released even if the worker disconnects.\n      await client.query(\"SELECT pg_advisory_xact_lock(hashtext($1))\", [`awaitly-schema:${name}`]);\n      await client.query(sql);\n      await client.query(\"COMMIT\");\n    } catch (error) {\n      await client.query(\"ROLLBACK\").catch(() => {});\n      throw error;\n    } finally {\n      client.release();\n    }\n  };\n\n  return () => {\n    initialized ??= initialize().catch((error: unknown) => {\n      initialized = undefined; // An outage must not permanently poison this store.\n      throw error;\n    });\n    return initialized;\n  };\n}\n"],"mappings":"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","names":["index_exports","__export","postgres","__toCommonJS","import_pg","import_durable","import_node_crypto","createSchemaInitializer","pool","name","sql","initialized","initialize","client","error","createPostgresLock","pool","options","lockTableName","safeIndexName","ensureLockTable","createSchemaInitializer","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","release","renew","SAFE_TABLE_NAME","postgres","urlOrOptions","opts","tableName","prefix","autoCreateTable","ownPool","pool","PgPool","error","initializeTable","createSchemaInitializer","ensureTable","lock","createPostgresLock","store","id","state","fullId","toStore","result","raw","loaded","options","filterPrefix","limit","row"]}