awaitly-postgres 17.0.0 → 19.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +13 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -13
- package/dist/index.d.ts +30 -13
- package/dist/index.js +13 -13
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
"use strict";var
|
|
2
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
1
|
+
"use strict";var S=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var _=Object.prototype.hasOwnProperty;var L=(r,t)=>{for(var e in t)S(r,e,{get:t[e],enumerable:!0})},N=(r,t,e,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of y(t))!_.call(r,n)&&n!==e&&S(r,n,{get:()=>t[n],enumerable:!(a=k(t,n))||a.enumerable});return r};var R=r=>N(S({},"__esModule",{value:!0}),r);var O={};L(O,{postgres:()=>A});module.exports=R(O);var P=require("pg"),i=require("awaitly/workflow");var f=require("crypto");function g(r,t={}){let e=t.lockTableName??"awaitly_workflow_lock",a=`idx_${e.replace(/[^a-zA-Z0-9_]/g,"_")}_expires_at`;async function n(){await r.query(`
|
|
2
|
+
CREATE TABLE IF NOT EXISTS ${e} (
|
|
3
3
|
workflow_id TEXT PRIMARY KEY,
|
|
4
4
|
owner_token TEXT NOT NULL,
|
|
5
5
|
expires_at TIMESTAMPTZ NOT NULL
|
|
6
6
|
);
|
|
7
|
-
CREATE INDEX IF NOT EXISTS ${
|
|
8
|
-
`)}async function w
|
|
9
|
-
INSERT INTO ${
|
|
7
|
+
CREATE INDEX IF NOT EXISTS ${a} ON ${e}(expires_at);
|
|
8
|
+
`)}async function m(w,u){let c=u?.ttlMs??6e4,p=(0,f.randomUUID)(),s=new Date(Date.now()+c);await n();let o=await r.query(`
|
|
9
|
+
INSERT INTO ${e} (workflow_id, owner_token, expires_at)
|
|
10
10
|
VALUES ($1, $2, $3)
|
|
11
11
|
ON CONFLICT (workflow_id) DO UPDATE SET
|
|
12
12
|
owner_token = EXCLUDED.owner_token,
|
|
13
13
|
expires_at = EXCLUDED.expires_at
|
|
14
|
-
WHERE ${
|
|
14
|
+
WHERE ${e}.expires_at < NOW()
|
|
15
15
|
RETURNING owner_token
|
|
16
|
-
`,[
|
|
17
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
16
|
+
`,[w,p,s]);return o.rowCount===1&&o.rows[0].owner_token===p?{ownerToken:p}:null}async function l(w,u){await r.query(`DELETE FROM ${e} WHERE workflow_id = $1 AND owner_token = $2`,[w,u])}return{tryAcquire:m,release:l,ensureLockTable:n}}var I=/^[a-zA-Z_][a-zA-Z0-9_]*$/;function A(r){let t=typeof r=="string"?{url:r}:r,e=t.table??"awaitly_snapshots";if(!I.test(e))throw new Error(`Invalid table name: ${e}. Must be alphanumeric with underscores.`);let a=t.prefix??"",n=t.autoCreateTable??!0,m=!t.pool,l=t.pool??new P.Pool({connectionString:t.url}),w=!1,u=async()=>{!n||w||(await l.query(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS ${e} (
|
|
18
18
|
id TEXT PRIMARY KEY,
|
|
19
19
|
snapshot JSONB NOT NULL,
|
|
20
20
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
21
21
|
)
|
|
22
|
-
`),await
|
|
23
|
-
CREATE INDEX IF NOT EXISTS ${
|
|
24
|
-
`),
|
|
22
|
+
`),await l.query(`
|
|
23
|
+
CREATE INDEX IF NOT EXISTS ${e}_updated_at_idx ON ${e} (updated_at DESC)
|
|
24
|
+
`),w=!0)},c=t.lock?g(l,t.lock):null,p={async save(s,o){await u();let E=a+s,d=(0,i.isResumeState)(o)?(0,i.serializeResumeState)(o):o;await l.query(`INSERT INTO ${e} (id, snapshot, updated_at)
|
|
25
25
|
VALUES ($1, $2, NOW())
|
|
26
|
-
ON CONFLICT (id) DO UPDATE SET snapshot = $2, updated_at = NOW()`,[E,JSON.stringify(
|
|
26
|
+
ON CONFLICT (id) DO UPDATE SET snapshot = $2, updated_at = NOW()`,[E,JSON.stringify(d)])},async load(s){await u();let o=a+s,E=await l.query(`SELECT snapshot FROM ${e} WHERE id = $1`,[o]);if(E.rows.length===0)return null;let d=E.rows[0].snapshot;return(0,i.isSerializedResumeState)(d)?(0,i.deserializeResumeState)(d):((0,i.isWorkflowSnapshot)(d),d)},async loadResumeState(s){let o=await p.load(s);return o===null?null:(0,i.isResumeState)(o)?o:null},async delete(s){await u();let o=a+s;await l.query(`DELETE FROM ${e} WHERE id = $1`,[o])},async list(s){await u();let o=a+(s?.prefix??""),E=s?.limit??100;return(await l.query(`SELECT id, updated_at FROM ${e}
|
|
27
27
|
WHERE id LIKE $1
|
|
28
28
|
ORDER BY updated_at DESC
|
|
29
|
-
LIMIT $2`,[
|
|
29
|
+
LIMIT $2`,[o+"%",E])).rows.map(T=>({id:T.id.slice(a.length),updatedAt:T.updated_at.toISOString()}))},async close(){m&&await l.end()}};return c&&(p.tryAcquire=c.tryAcquire.bind(c),p.release=c.release.bind(c)),p}0&&(module.exports={postgres});
|
|
30
30
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/postgres-lock.ts"],"sourcesContent":["/**\n * awaitly-postgres\n *\n * PostgreSQL persistence adapter for awaitly workflows.\n * Provides ready-to-use SnapshotStore backed by PostgreSQL.\n */\n\nimport { Pool as PgPool } from \"pg\";\nimport type { WorkflowSnapshot, SnapshotStore } from \"awaitly/persistence\";\nimport type { WorkflowLock } from \"awaitly/durable\";\nimport { createPostgresLock, type PostgresLockOptions } from \"./postgres-lock\";\n\n// Re-export types for convenience\nexport type { SnapshotStore, WorkflowSnapshot } from \"awaitly/persistence\";\nexport type { WorkflowLock } from \"awaitly/durable\";\nexport type { PostgresLockOptions } from \"./postgres-lock\";\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 /** 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 * This is the simplified one-liner API for workflow persistence.\n *\n * @example\n * ```typescript\n * import { postgres } from 'awaitly-postgres';\n *\n * // One-liner setup\n * const store = postgres('postgresql://localhost/mydb');\n *\n * // Execute + persist\n * const wf = createWorkflow(deps);\n * await wf(myWorkflowFn);\n * await store.save('wf-123', wf.getSnapshot());\n *\n * // Restore\n * const snapshot = await store.load('wf-123');\n * const wf2 = createWorkflow(deps, { snapshot });\n * await wf2(myWorkflowFn);\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 */\nexport function postgres(urlOrOptions: string | PostgresOptions): SnapshotStore & Partial<WorkflowLock> {\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 // Create or use existing pool\n const ownPool = !opts.pool;\n const pool = opts.pool ?? new PgPool({ connectionString: opts.url });\n let tableCreated = false;\n\n const ensureTable = async (): Promise<void> => {\n if (!autoCreateTable || tableCreated) return;\n await pool.query(`\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 `);\n await pool.query(`\n CREATE INDEX IF NOT EXISTS ${tableName}_updated_at_idx ON ${tableName} (updated_at DESC)\n `);\n tableCreated = true;\n };\n\n // Create lock if requested\n const lock = opts.lock ? createPostgresLock(pool, opts.lock) : null;\n\n const store: SnapshotStore & Partial<WorkflowLock> = {\n async save(id: string, snapshot: WorkflowSnapshot): Promise<void> {\n await ensureTable();\n const fullId = prefix + id;\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(snapshot)]\n );\n },\n\n async load(id: string): Promise<WorkflowSnapshot | null> {\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 return result.rows[0].snapshot as WorkflowSnapshot;\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 // Only end pool if we created it\n if (ownPool) {\n await pool.end();\n }\n },\n };\n\n // Add lock methods if lock is configured\n if (lock) {\n store.tryAcquire = lock.tryAcquire.bind(lock);\n store.release = lock.release.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\";\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 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 async function ensureLockTable(): Promise<void> {\n await pool.query(`\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\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 return { tryAcquire, release, ensureLockTable };\n}\n"],"mappings":"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","names":["index_exports","__export","postgres","__toCommonJS","import_pg","import_node_crypto","createPostgresLock","pool","options","lockTableName","safeIndexName","ensureLockTable","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","release","SAFE_TABLE_NAME","postgres","urlOrOptions","opts","tableName","prefix","autoCreateTable","ownPool","pool","PgPool","tableCreated","ensureTable","lock","createPostgresLock","store","id","snapshot","fullId","result","options","filterPrefix","limit","row"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/postgres-lock.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/persistence\";\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/workflow\";\nimport { createPostgresLock, type PostgresLockOptions } from \"./postgres-lock\";\n\n// Re-export types for convenience\nexport type { SnapshotStore, WorkflowSnapshot } from \"awaitly/persistence\";\nexport type { WorkflowLock } from \"awaitly/durable\";\nexport type { PostgresLockOptions } from \"./postgres-lock\";\nexport type { StoreSaveInput, StoreLoadResult } from \"awaitly/workflow\";\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 /** 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, toResumeState } from 'awaitly/workflow';\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 // Create or use existing pool\n const ownPool = !opts.pool;\n const pool = opts.pool ?? new PgPool({ connectionString: opts.url });\n let tableCreated = false;\n\n const ensureTable = async (): Promise<void> => {\n if (!autoCreateTable || tableCreated) return;\n await pool.query(`\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 `);\n await pool.query(`\n CREATE INDEX IF NOT EXISTS ${tableName}_updated_at_idx ON ${tableName} (updated_at DESC)\n `);\n tableCreated = true;\n };\n\n // Create lock if requested\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 // Only end pool if we created it\n if (ownPool) {\n await pool.end();\n }\n },\n };\n\n // Add lock methods if lock is configured\n if (lock) {\n store.tryAcquire = lock.tryAcquire.bind(lock);\n store.release = lock.release.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\";\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 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 async function ensureLockTable(): Promise<void> {\n await pool.query(`\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\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 return { tryAcquire, release, ensureLockTable };\n}\n"],"mappings":"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","names":["index_exports","__export","postgres","__toCommonJS","import_pg","import_workflow","import_node_crypto","createPostgresLock","pool","options","lockTableName","safeIndexName","ensureLockTable","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","release","SAFE_TABLE_NAME","postgres","urlOrOptions","opts","tableName","prefix","autoCreateTable","ownPool","pool","PgPool","tableCreated","ensureTable","lock","createPostgresLock","store","id","state","fullId","toStore","result","raw","loaded","options","filterPrefix","limit","row"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Pool } from 'pg';
|
|
2
|
-
import { SnapshotStore } from 'awaitly/persistence';
|
|
3
|
-
export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
|
|
4
2
|
import { WorkflowLock } from 'awaitly/durable';
|
|
5
3
|
export { WorkflowLock } from 'awaitly/durable';
|
|
4
|
+
import { StoreSaveInput, StoreLoadResult, ResumeState } from 'awaitly/workflow';
|
|
5
|
+
export { StoreLoadResult, StoreSaveInput } from 'awaitly/workflow';
|
|
6
|
+
export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* PostgreSQL workflow lock (lease) for cross-process concurrency control.
|
|
@@ -22,6 +23,7 @@ interface PostgresLockOptions {
|
|
|
22
23
|
*
|
|
23
24
|
* PostgreSQL persistence adapter for awaitly workflows.
|
|
24
25
|
* Provides ready-to-use SnapshotStore backed by PostgreSQL.
|
|
26
|
+
* Supports both WorkflowSnapshot and ResumeState (serialized via serializeResumeState).
|
|
25
27
|
*/
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -43,24 +45,24 @@ interface PostgresOptions {
|
|
|
43
45
|
}
|
|
44
46
|
/**
|
|
45
47
|
* Create a snapshot store backed by PostgreSQL.
|
|
46
|
-
*
|
|
48
|
+
* Save accepts WorkflowSnapshot or ResumeState; load returns whichever was stored.
|
|
49
|
+
* Use loadResumeState(id) for type-safe restore, or toResumeState(await store.load(id)).
|
|
47
50
|
*
|
|
48
51
|
* @example
|
|
49
52
|
* ```typescript
|
|
50
53
|
* import { postgres } from 'awaitly-postgres';
|
|
54
|
+
* import { createWorkflow, toResumeState } from 'awaitly/workflow';
|
|
51
55
|
*
|
|
52
|
-
* // One-liner setup
|
|
53
56
|
* const store = postgres('postgresql://localhost/mydb');
|
|
57
|
+
* const workflow = createWorkflow(deps);
|
|
54
58
|
*
|
|
55
|
-
* //
|
|
56
|
-
* const
|
|
57
|
-
* await wf
|
|
58
|
-
* await store.save('wf-123', wf.getSnapshot());
|
|
59
|
+
* // Run and persist resume state
|
|
60
|
+
* const { result, resumeState } = await workflow.runWithState(fn);
|
|
61
|
+
* await store.save('wf-123', resumeState);
|
|
59
62
|
*
|
|
60
63
|
* // Restore
|
|
61
|
-
* const
|
|
62
|
-
*
|
|
63
|
-
* await wf2(myWorkflowFn);
|
|
64
|
+
* const resumeState = await store.loadResumeState('wf-123');
|
|
65
|
+
* if (resumeState) await workflow.run(fn, { resumeState });
|
|
64
66
|
* ```
|
|
65
67
|
*
|
|
66
68
|
* @example
|
|
@@ -74,6 +76,21 @@ interface PostgresOptions {
|
|
|
74
76
|
* });
|
|
75
77
|
* ```
|
|
76
78
|
*/
|
|
77
|
-
|
|
79
|
+
/** Postgres store with widened save/load for WorkflowSnapshot and ResumeState. Compatible with SnapshotStore for snapshot-only usage. */
|
|
80
|
+
interface PostgresStore extends Partial<WorkflowLock> {
|
|
81
|
+
save(id: string, state: StoreSaveInput): Promise<void>;
|
|
82
|
+
load(id: string): Promise<StoreLoadResult>;
|
|
83
|
+
loadResumeState(id: string): Promise<ResumeState | null>;
|
|
84
|
+
delete(id: string): Promise<void>;
|
|
85
|
+
list(options?: {
|
|
86
|
+
prefix?: string;
|
|
87
|
+
limit?: number;
|
|
88
|
+
}): Promise<Array<{
|
|
89
|
+
id: string;
|
|
90
|
+
updatedAt: string;
|
|
91
|
+
}>>;
|
|
92
|
+
close(): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
declare function postgres(urlOrOptions: string | PostgresOptions): PostgresStore;
|
|
78
95
|
|
|
79
|
-
export { type PostgresLockOptions, type PostgresOptions, postgres };
|
|
96
|
+
export { type PostgresLockOptions, type PostgresOptions, type PostgresStore, postgres };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Pool } from 'pg';
|
|
2
|
-
import { SnapshotStore } from 'awaitly/persistence';
|
|
3
|
-
export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
|
|
4
2
|
import { WorkflowLock } from 'awaitly/durable';
|
|
5
3
|
export { WorkflowLock } from 'awaitly/durable';
|
|
4
|
+
import { StoreSaveInput, StoreLoadResult, ResumeState } from 'awaitly/workflow';
|
|
5
|
+
export { StoreLoadResult, StoreSaveInput } from 'awaitly/workflow';
|
|
6
|
+
export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* PostgreSQL workflow lock (lease) for cross-process concurrency control.
|
|
@@ -22,6 +23,7 @@ interface PostgresLockOptions {
|
|
|
22
23
|
*
|
|
23
24
|
* PostgreSQL persistence adapter for awaitly workflows.
|
|
24
25
|
* Provides ready-to-use SnapshotStore backed by PostgreSQL.
|
|
26
|
+
* Supports both WorkflowSnapshot and ResumeState (serialized via serializeResumeState).
|
|
25
27
|
*/
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -43,24 +45,24 @@ interface PostgresOptions {
|
|
|
43
45
|
}
|
|
44
46
|
/**
|
|
45
47
|
* Create a snapshot store backed by PostgreSQL.
|
|
46
|
-
*
|
|
48
|
+
* Save accepts WorkflowSnapshot or ResumeState; load returns whichever was stored.
|
|
49
|
+
* Use loadResumeState(id) for type-safe restore, or toResumeState(await store.load(id)).
|
|
47
50
|
*
|
|
48
51
|
* @example
|
|
49
52
|
* ```typescript
|
|
50
53
|
* import { postgres } from 'awaitly-postgres';
|
|
54
|
+
* import { createWorkflow, toResumeState } from 'awaitly/workflow';
|
|
51
55
|
*
|
|
52
|
-
* // One-liner setup
|
|
53
56
|
* const store = postgres('postgresql://localhost/mydb');
|
|
57
|
+
* const workflow = createWorkflow(deps);
|
|
54
58
|
*
|
|
55
|
-
* //
|
|
56
|
-
* const
|
|
57
|
-
* await wf
|
|
58
|
-
* await store.save('wf-123', wf.getSnapshot());
|
|
59
|
+
* // Run and persist resume state
|
|
60
|
+
* const { result, resumeState } = await workflow.runWithState(fn);
|
|
61
|
+
* await store.save('wf-123', resumeState);
|
|
59
62
|
*
|
|
60
63
|
* // Restore
|
|
61
|
-
* const
|
|
62
|
-
*
|
|
63
|
-
* await wf2(myWorkflowFn);
|
|
64
|
+
* const resumeState = await store.loadResumeState('wf-123');
|
|
65
|
+
* if (resumeState) await workflow.run(fn, { resumeState });
|
|
64
66
|
* ```
|
|
65
67
|
*
|
|
66
68
|
* @example
|
|
@@ -74,6 +76,21 @@ interface PostgresOptions {
|
|
|
74
76
|
* });
|
|
75
77
|
* ```
|
|
76
78
|
*/
|
|
77
|
-
|
|
79
|
+
/** Postgres store with widened save/load for WorkflowSnapshot and ResumeState. Compatible with SnapshotStore for snapshot-only usage. */
|
|
80
|
+
interface PostgresStore extends Partial<WorkflowLock> {
|
|
81
|
+
save(id: string, state: StoreSaveInput): Promise<void>;
|
|
82
|
+
load(id: string): Promise<StoreLoadResult>;
|
|
83
|
+
loadResumeState(id: string): Promise<ResumeState | null>;
|
|
84
|
+
delete(id: string): Promise<void>;
|
|
85
|
+
list(options?: {
|
|
86
|
+
prefix?: string;
|
|
87
|
+
limit?: number;
|
|
88
|
+
}): Promise<Array<{
|
|
89
|
+
id: string;
|
|
90
|
+
updatedAt: string;
|
|
91
|
+
}>>;
|
|
92
|
+
close(): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
declare function postgres(urlOrOptions: string | PostgresOptions): PostgresStore;
|
|
78
95
|
|
|
79
|
-
export { type PostgresLockOptions, type PostgresOptions, postgres };
|
|
96
|
+
export { type PostgresLockOptions, type PostgresOptions, type PostgresStore, postgres };
|
package/dist/index.js
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import{Pool as
|
|
2
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
1
|
+
import{Pool as g}from"pg";import{isWorkflowSnapshot as P,isResumeState as T,isSerializedResumeState as k,serializeResumeState as y,deserializeResumeState as _}from"awaitly/workflow";import{randomUUID as f}from"crypto";function S(l,r={}){let e=r.lockTableName??"awaitly_workflow_lock",u=`idx_${e.replace(/[^a-zA-Z0-9_]/g,"_")}_expires_at`;async function w(){await l.query(`
|
|
2
|
+
CREATE TABLE IF NOT EXISTS ${e} (
|
|
3
3
|
workflow_id TEXT PRIMARY KEY,
|
|
4
4
|
owner_token TEXT NOT NULL,
|
|
5
5
|
expires_at TIMESTAMPTZ NOT NULL
|
|
6
6
|
);
|
|
7
|
-
CREATE INDEX IF NOT EXISTS ${
|
|
8
|
-
`)}async function
|
|
9
|
-
INSERT INTO ${
|
|
7
|
+
CREATE INDEX IF NOT EXISTS ${u} ON ${e}(expires_at);
|
|
8
|
+
`)}async function E(d,i){let p=i?.ttlMs??6e4,a=f(),o=new Date(Date.now()+p);await w();let t=await l.query(`
|
|
9
|
+
INSERT INTO ${e} (workflow_id, owner_token, expires_at)
|
|
10
10
|
VALUES ($1, $2, $3)
|
|
11
11
|
ON CONFLICT (workflow_id) DO UPDATE SET
|
|
12
12
|
owner_token = EXCLUDED.owner_token,
|
|
13
13
|
expires_at = EXCLUDED.expires_at
|
|
14
|
-
WHERE ${
|
|
14
|
+
WHERE ${e}.expires_at < NOW()
|
|
15
15
|
RETURNING owner_token
|
|
16
|
-
`,[
|
|
17
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
16
|
+
`,[d,a,o]);return t.rowCount===1&&t.rows[0].owner_token===a?{ownerToken:a}:null}async function s(d,i){await l.query(`DELETE FROM ${e} WHERE workflow_id = $1 AND owner_token = $2`,[d,i])}return{tryAcquire:E,release:s,ensureLockTable:w}}var L=/^[a-zA-Z_][a-zA-Z0-9_]*$/;function x(l){let r=typeof l=="string"?{url:l}:l,e=r.table??"awaitly_snapshots";if(!L.test(e))throw new Error(`Invalid table name: ${e}. Must be alphanumeric with underscores.`);let u=r.prefix??"",w=r.autoCreateTable??!0,E=!r.pool,s=r.pool??new g({connectionString:r.url}),d=!1,i=async()=>{!w||d||(await s.query(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS ${e} (
|
|
18
18
|
id TEXT PRIMARY KEY,
|
|
19
19
|
snapshot JSONB NOT NULL,
|
|
20
20
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
21
21
|
)
|
|
22
|
-
`),await
|
|
23
|
-
CREATE INDEX IF NOT EXISTS ${
|
|
24
|
-
`),
|
|
22
|
+
`),await s.query(`
|
|
23
|
+
CREATE INDEX IF NOT EXISTS ${e}_updated_at_idx ON ${e} (updated_at DESC)
|
|
24
|
+
`),d=!0)},p=r.lock?S(s,r.lock):null,a={async save(o,t){await i();let c=u+o,n=T(t)?y(t):t;await s.query(`INSERT INTO ${e} (id, snapshot, updated_at)
|
|
25
25
|
VALUES ($1, $2, NOW())
|
|
26
|
-
ON CONFLICT (id) DO UPDATE SET snapshot = $2, updated_at = NOW()`,[
|
|
26
|
+
ON CONFLICT (id) DO UPDATE SET snapshot = $2, updated_at = NOW()`,[c,JSON.stringify(n)])},async load(o){await i();let t=u+o,c=await s.query(`SELECT snapshot FROM ${e} WHERE id = $1`,[t]);if(c.rows.length===0)return null;let n=c.rows[0].snapshot;return k(n)?_(n):(P(n),n)},async loadResumeState(o){let t=await a.load(o);return t===null?null:T(t)?t:null},async delete(o){await i();let t=u+o;await s.query(`DELETE FROM ${e} WHERE id = $1`,[t])},async list(o){await i();let t=u+(o?.prefix??""),c=o?.limit??100;return(await s.query(`SELECT id, updated_at FROM ${e}
|
|
27
27
|
WHERE id LIKE $1
|
|
28
28
|
ORDER BY updated_at DESC
|
|
29
|
-
LIMIT $2`,[
|
|
29
|
+
LIMIT $2`,[t+"%",c])).rows.map(m=>({id:m.id.slice(u.length),updatedAt:m.updated_at.toISOString()}))},async close(){E&&await s.end()}};return p&&(a.tryAcquire=p.tryAcquire.bind(p),a.release=p.release.bind(p)),a}export{x as postgres};
|
|
30
30
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/postgres-lock.ts"],"sourcesContent":["/**\n * awaitly-postgres\n *\n * PostgreSQL persistence adapter for awaitly workflows.\n * Provides ready-to-use SnapshotStore backed by PostgreSQL.\n */\n\nimport { Pool as PgPool } from \"pg\";\nimport type { WorkflowSnapshot, SnapshotStore } from \"awaitly/persistence\";\nimport type { WorkflowLock } from \"awaitly/durable\";\nimport { createPostgresLock, type PostgresLockOptions } from \"./postgres-lock\";\n\n// Re-export types for convenience\nexport type { SnapshotStore, WorkflowSnapshot } from \"awaitly/persistence\";\nexport type { WorkflowLock } from \"awaitly/durable\";\nexport type { PostgresLockOptions } from \"./postgres-lock\";\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 /** 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 * This is the simplified one-liner API for workflow persistence.\n *\n * @example\n * ```typescript\n * import { postgres } from 'awaitly-postgres';\n *\n * // One-liner setup\n * const store = postgres('postgresql://localhost/mydb');\n *\n * // Execute + persist\n * const wf = createWorkflow(deps);\n * await wf(myWorkflowFn);\n * await store.save('wf-123', wf.getSnapshot());\n *\n * // Restore\n * const snapshot = await store.load('wf-123');\n * const wf2 = createWorkflow(deps, { snapshot });\n * await wf2(myWorkflowFn);\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 */\nexport function postgres(urlOrOptions: string | PostgresOptions): SnapshotStore & Partial<WorkflowLock> {\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 // Create or use existing pool\n const ownPool = !opts.pool;\n const pool = opts.pool ?? new PgPool({ connectionString: opts.url });\n let tableCreated = false;\n\n const ensureTable = async (): Promise<void> => {\n if (!autoCreateTable || tableCreated) return;\n await pool.query(`\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 `);\n await pool.query(`\n CREATE INDEX IF NOT EXISTS ${tableName}_updated_at_idx ON ${tableName} (updated_at DESC)\n `);\n tableCreated = true;\n };\n\n // Create lock if requested\n const lock = opts.lock ? createPostgresLock(pool, opts.lock) : null;\n\n const store: SnapshotStore & Partial<WorkflowLock> = {\n async save(id: string, snapshot: WorkflowSnapshot): Promise<void> {\n await ensureTable();\n const fullId = prefix + id;\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(snapshot)]\n );\n },\n\n async load(id: string): Promise<WorkflowSnapshot | null> {\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 return result.rows[0].snapshot as WorkflowSnapshot;\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 // Only end pool if we created it\n if (ownPool) {\n await pool.end();\n }\n },\n };\n\n // Add lock methods if lock is configured\n if (lock) {\n store.tryAcquire = lock.tryAcquire.bind(lock);\n store.release = lock.release.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\";\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 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 async function ensureLockTable(): Promise<void> {\n await pool.query(`\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\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 return { tryAcquire, release, ensureLockTable };\n}\n"],"mappings":"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","names":["PgPool","randomUUID","createPostgresLock","pool","options","lockTableName","safeIndexName","ensureLockTable","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","release","SAFE_TABLE_NAME","postgres","urlOrOptions","opts","tableName","prefix","autoCreateTable","ownPool","pool","PgPool","tableCreated","ensureTable","lock","createPostgresLock","store","id","snapshot","fullId","result","options","filterPrefix","limit","row"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/postgres-lock.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/persistence\";\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/workflow\";\nimport { createPostgresLock, type PostgresLockOptions } from \"./postgres-lock\";\n\n// Re-export types for convenience\nexport type { SnapshotStore, WorkflowSnapshot } from \"awaitly/persistence\";\nexport type { WorkflowLock } from \"awaitly/durable\";\nexport type { PostgresLockOptions } from \"./postgres-lock\";\nexport type { StoreSaveInput, StoreLoadResult } from \"awaitly/workflow\";\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 /** 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, toResumeState } from 'awaitly/workflow';\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 // Create or use existing pool\n const ownPool = !opts.pool;\n const pool = opts.pool ?? new PgPool({ connectionString: opts.url });\n let tableCreated = false;\n\n const ensureTable = async (): Promise<void> => {\n if (!autoCreateTable || tableCreated) return;\n await pool.query(`\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 `);\n await pool.query(`\n CREATE INDEX IF NOT EXISTS ${tableName}_updated_at_idx ON ${tableName} (updated_at DESC)\n `);\n tableCreated = true;\n };\n\n // Create lock if requested\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 // Only end pool if we created it\n if (ownPool) {\n await pool.end();\n }\n },\n };\n\n // Add lock methods if lock is configured\n if (lock) {\n store.tryAcquire = lock.tryAcquire.bind(lock);\n store.release = lock.release.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\";\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 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 async function ensureLockTable(): Promise<void> {\n await pool.query(`\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\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 return { tryAcquire, release, ensureLockTable };\n}\n"],"mappings":"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","names":["PgPool","isWorkflowSnapshot","isResumeState","isSerializedResumeState","serializeResumeState","deserializeResumeState","randomUUID","createPostgresLock","pool","options","lockTableName","safeIndexName","ensureLockTable","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","release","SAFE_TABLE_NAME","postgres","urlOrOptions","opts","tableName","prefix","autoCreateTable","ownPool","pool","PgPool","tableCreated","ensureTable","lock","createPostgresLock","store","id","state","fullId","toStore","isResumeState","serializeResumeState","result","raw","isSerializedResumeState","deserializeResumeState","isWorkflowSnapshot","loaded","options","filterPrefix","limit","row"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "awaitly-postgres",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "19.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "PostgreSQL persistence adapter for awaitly workflows",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"awaitly": "^1.
|
|
41
|
+
"awaitly": "^1.29.0"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"pg": "^8.18.0"
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@total-typescript/ts-reset": "^0.6.1",
|
|
48
48
|
"@total-typescript/tsconfig": "^1.0.4",
|
|
49
|
-
"@types/node": "^25.
|
|
49
|
+
"@types/node": "^25.3.0",
|
|
50
50
|
"@types/pg": "^8.16.0",
|
|
51
51
|
"tsup": "^8.5.1",
|
|
52
52
|
"typescript": "^5.9.3",
|
|
53
53
|
"vitest": "^4.0.18",
|
|
54
|
-
"awaitly": "^1.
|
|
54
|
+
"awaitly": "^1.29.0"
|
|
55
55
|
},
|
|
56
56
|
"publishConfig": {
|
|
57
57
|
"access": "public",
|