awaitly-postgres 3.0.0 → 4.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/README.md CHANGED
@@ -103,23 +103,6 @@ WHERE expires_at IS NOT NULL;
103
103
 
104
104
  The table is created automatically on first use. You can customize the table name via the `tableName` option.
105
105
 
106
- ## Advanced Usage
107
-
108
- ### Direct KeyValueStore Access
109
-
110
- If you need more control, you can use the `PostgresKeyValueStore` class directly:
111
-
112
- ```typescript
113
- import { PostgresKeyValueStore } from 'awaitly-postgres';
114
- import { createStatePersistence } from 'awaitly/persistence';
115
-
116
- const store = new PostgresKeyValueStore({
117
- connectionString: process.env.DATABASE_URL,
118
- });
119
-
120
- const persistence = createStatePersistence(store, 'custom:prefix:');
121
- ```
122
-
123
106
  ## Features
124
107
 
125
108
  - ✅ Automatic table creation
package/dist/index.cjs CHANGED
@@ -1,59 +1,11 @@
1
- "use strict";var N=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var x=Object.prototype.hasOwnProperty;var I=(o,e)=>{for(var t in e)N(o,t,{get:e[t],enumerable:!0})},b=(o,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of _(e))!x.call(o,i)&&i!==t&&N(o,i,{get:()=>e[i],enumerable:!(s=O(e,i))||s.enumerable});return o};var R=o=>b(N({},"__esModule",{value:!0}),o);var A={};I(A,{PostgresKeyValueStore:()=>P,createPostgresPersistence:()=>$});module.exports=R(A);var f=require("pg");var h=require("pg"),P=class{pool;tableName;initialized=!1;initPromise=null;constructor(e){e.existingPool?this.pool=e.existingPool:e.connectionString?this.pool=new h.Pool({connectionString:e.connectionString,...e.pool}):this.pool=new h.Pool({host:e.host??"localhost",port:e.port??5432,database:e.database,user:e.user,password:e.password,...e.pool}),this.tableName=e.tableName??"awaitly_workflow_state"}async ensureInitialized(){if(!this.initialized)return this.initPromise?this.initPromise:(this.initPromise=(async()=>{try{await this.createTable(),this.initialized=!0}catch(e){throw this.initPromise=null,e}})(),this.initPromise)}async createTable(){await this.pool.query(`
2
- CREATE TABLE IF NOT EXISTS ${this.tableName} (
3
- key TEXT PRIMARY KEY,
4
- value TEXT NOT NULL,
5
- expires_at TIMESTAMP,
6
- updated_at TIMESTAMPTZ
7
- )
8
- `),await this.pool.query(`
9
- ALTER TABLE ${this.tableName}
10
- ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ
11
- `),await this.pool.query(`
12
- CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expires_at
13
- ON ${this.tableName}(expires_at)
14
- WHERE expires_at IS NOT NULL
15
- `),await this.pool.query(`
16
- CREATE INDEX IF NOT EXISTS idx_${this.tableName}_updated_at
17
- ON ${this.tableName}(updated_at)
18
- WHERE updated_at IS NOT NULL
19
- `)}patternToLike(e){return e.replace(/%/g,"\\%").replace(/_/g,"\\_").replace(/\*/g,"%")}async get(e){await this.ensureInitialized();let t=`
20
- SELECT value
21
- FROM ${this.tableName}
22
- WHERE key = $1
23
- AND (expires_at IS NULL OR expires_at > NOW())
24
- `,s=await this.pool.query(t,[e]);return s.rows.length===0?null:s.rows[0].value}async set(e,t,s){await this.ensureInitialized();let i=s?.ttl?new Date(Date.now()+s.ttl*1e3):null,n=`
25
- INSERT INTO ${this.tableName} (key, value, expires_at, updated_at)
26
- VALUES ($1, $2, $3, NOW())
27
- ON CONFLICT (key)
28
- DO UPDATE SET
29
- value = EXCLUDED.value,
30
- expires_at = EXCLUDED.expires_at,
31
- updated_at = NOW()
32
- `;await this.pool.query(n,[e,t,i])}async delete(e){await this.ensureInitialized();let t=`DELETE FROM ${this.tableName} WHERE key = $1`;return((await this.pool.query(t,[e])).rowCount??0)>0}async exists(e){await this.ensureInitialized();let t=`
33
- SELECT 1
34
- FROM ${this.tableName}
35
- WHERE key = $1
36
- AND (expires_at IS NULL OR expires_at > NOW())
37
- LIMIT 1
38
- `;return(await this.pool.query(t,[e])).rows.length>0}async keys(e){await this.ensureInitialized();let t=this.patternToLike(e),s=`
39
- SELECT key
40
- FROM ${this.tableName}
41
- WHERE key LIKE $1
42
- AND (expires_at IS NULL OR expires_at > NOW())
43
- `;return(await this.pool.query(s,[t])).rows.map(n=>n.key)}async listKeys(e,t={}){await this.ensureInitialized();let s=Math.min(Math.max(0,t.limit??100),1e4),i=Math.max(0,t.offset??0),n=t.orderBy==="key"?"key":"updated_at",g=t.orderDir==="asc"?"ASC":"DESC",p=this.patternToLike(e),u=["key LIKE $1","(expires_at IS NULL OR expires_at > NOW())"],d=[p],r=2;t.updatedBefore!=null&&(u.push(`updated_at < $${r}`),d.push(t.updatedBefore),r++),t.updatedAfter!=null&&(u.push(`updated_at > $${r}`),d.push(t.updatedAfter),r++);let l=u.join(" AND "),a=n==="updated_at"?" NULLS LAST":"",c=`
44
- SELECT key
45
- FROM ${this.tableName}
46
- WHERE ${l}
47
- ORDER BY ${n} ${g}${a}
48
- LIMIT $${r} OFFSET $${r+1}
49
- `,E=[...d,s,i],w=(await this.pool.query(c,E)).rows.map(T=>T.key),y;if(t.includeTotal===!0||i>0){let T=await this.pool.query(`SELECT COUNT(*) AS count FROM ${this.tableName} WHERE ${l}`,d);y=parseInt(T.rows[0]?.count??"0",10)}return{keys:w,total:y}}async deleteMany(e){return e.length===0?0:(await this.ensureInitialized(),(await this.pool.query(`DELETE FROM ${this.tableName} WHERE key = ANY($1::text[])`,[e])).rowCount??0)}async clear(){await this.ensureInitialized(),await this.pool.query(`TRUNCATE TABLE ${this.tableName}`)}async close(){await this.pool.end()}};var k=require("crypto");function L(o,e={}){let t=e.lockTableName??"awaitly_workflow_lock",s=`idx_${t.replace(/[^a-zA-Z0-9_]/g,"_")}_expires_at`;async function i(){await o.query(`
1
+ "use strict";var d=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var S=Object.prototype.hasOwnProperty;var y=(o,e)=>{for(var t in e)d(o,t,{get:e[t],enumerable:!0})},P=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of _(e))!S.call(o,s)&&s!==t&&d(o,s,{get:()=>e[s],enumerable:!(r=g(e,s))||r.enumerable});return o};var N=o=>P(d({},"__esModule",{value:!0}),o);var I={};y(I,{postgres:()=>O});module.exports=N(I);var m=require("pg");var f=require("crypto");function k(o,e={}){let t=e.lockTableName??"awaitly_workflow_lock",r=`idx_${t.replace(/[^a-zA-Z0-9_]/g,"_")}_expires_at`;async function s(){await o.query(`
50
2
  CREATE TABLE IF NOT EXISTS ${t} (
51
3
  workflow_id TEXT PRIMARY KEY,
52
4
  owner_token TEXT NOT NULL,
53
5
  expires_at TIMESTAMPTZ NOT NULL
54
6
  );
55
- CREATE INDEX IF NOT EXISTS ${s} ON ${t}(expires_at);
56
- `)}async function n(p,u){let d=u?.ttlMs??6e4,r=(0,k.randomUUID)(),l=new Date(Date.now()+d);await i();let a=await o.query(`
7
+ CREATE INDEX IF NOT EXISTS ${r} ON ${t}(expires_at);
8
+ `)}async function w(u,l){let c=l?.ttlMs??6e4,p=(0,f.randomUUID)(),n=new Date(Date.now()+c);await s();let i=await o.query(`
57
9
  INSERT INTO ${t} (workflow_id, owner_token, expires_at)
58
10
  VALUES ($1, $2, $3)
59
11
  ON CONFLICT (workflow_id) DO UPDATE SET
@@ -61,5 +13,18 @@
61
13
  expires_at = EXCLUDED.expires_at
62
14
  WHERE ${t}.expires_at < NOW()
63
15
  RETURNING owner_token
64
- `,[p,r,l]);return a.rowCount===1&&a.rows[0].owner_token===r?{ownerToken:r}:null}async function g(p,u){await o.query(`DELETE FROM ${t} WHERE workflow_id = $1 AND owner_token = $2`,[p,u])}return{tryAcquire:n,release:g,ensureLockTable:i}}var S=require("awaitly/persistence");async function $(o={}){let{prefix:e,lock:t,...s}=o,i=r=>r.slice((e??"workflow:state:").length),n=e??"workflow:state:",g=r=>`${n}${r}`,p=(r,l)=>Object.assign(r,{async listPage(a={}){let{keys:c,total:E}=await l.listKeys(`${n}*`,a),m=c.map(i),w=Math.min(Math.max(0,a.limit??100),1e4),y=m.length===w?(a.offset??0)+m.length:void 0;return{ids:m,total:E,nextOffset:y}},async deleteMany(a){if(a.length===0)return 0;let c=a.map(g);return l.deleteMany(c)},async clear(){return l.clear()}});if(t!==void 0){let r=s.existingPool??new f.Pool(s.connectionString?{connectionString:s.connectionString,...s.pool}:{host:s.host??"localhost",port:s.port??5432,database:s.database,user:s.user,password:s.password,...s.pool}),l=new P({...s,existingPool:r}),a=(0,S.createStatePersistence)(l,e),c=L(r,{lockTableName:t.lockTableName});return Object.assign(p(a,l),{tryAcquire:c.tryAcquire.bind(c),release:c.release.bind(c)})}let u=new P(s),d=(0,S.createStatePersistence)(u,e);return p(d,u)}0&&(module.exports={PostgresKeyValueStore,createPostgresPersistence});
16
+ `,[u,p,n]);return i.rowCount===1&&i.rows[0].owner_token===p?{ownerToken:p}:null}async function a(u,l){await o.query(`DELETE FROM ${t} WHERE workflow_id = $1 AND owner_token = $2`,[u,l])}return{tryAcquire:w,release:a,ensureLockTable:s}}var L=/^[a-zA-Z_][a-zA-Z0-9_]*$/;function O(o){let e=typeof o=="string"?{url:o}:o,t=e.table??"awaitly_snapshots";if(!L.test(t))throw new Error(`Invalid table name: ${t}. Must be alphanumeric with underscores.`);let r=e.prefix??"",s=e.autoCreateTable??!0,w=!e.pool,a=e.pool??new m.Pool({connectionString:e.url}),u=!1,l=async()=>{!s||u||(await a.query(`
17
+ CREATE TABLE IF NOT EXISTS ${t} (
18
+ id TEXT PRIMARY KEY,
19
+ snapshot JSONB NOT NULL,
20
+ updated_at TIMESTAMPTZ DEFAULT NOW()
21
+ )
22
+ `),await a.query(`
23
+ CREATE INDEX IF NOT EXISTS ${t}_updated_at_idx ON ${t} (updated_at DESC)
24
+ `),u=!0)},c=e.lock?k(a,e.lock):null,p={async save(n,i){await l();let E=r+n;await a.query(`INSERT INTO ${t} (id, snapshot, updated_at)
25
+ VALUES ($1, $2, NOW())
26
+ ON CONFLICT (id) DO UPDATE SET snapshot = $2, updated_at = NOW()`,[E,JSON.stringify(i)])},async load(n){await l();let i=r+n,E=await a.query(`SELECT snapshot FROM ${t} WHERE id = $1`,[i]);return E.rows.length===0?null:E.rows[0].snapshot},async delete(n){await l();let i=r+n;await a.query(`DELETE FROM ${t} WHERE id = $1`,[i])},async list(n){await l();let i=r+(n?.prefix??""),E=n?.limit??100;return(await a.query(`SELECT id, updated_at FROM ${t}
27
+ WHERE id LIKE $1
28
+ ORDER BY updated_at DESC
29
+ LIMIT $2`,[i+"%",E])).rows.map(T=>({id:T.id.slice(r.length),updatedAt:T.updated_at.toISOString()}))},async close(){w&&await a.end()}};return c&&(p.tryAcquire=c.tryAcquire.bind(c),p.release=c.release.bind(c)),p}0&&(module.exports={postgres});
65
30
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/postgres-store.ts","../src/postgres-lock.ts"],"sourcesContent":["/**\n * awaitly-postgres\n *\n * PostgreSQL persistence adapter for awaitly workflows.\n * Provides ready-to-use StatePersistence backed by PostgreSQL.\n */\n\nimport { Pool as PgPool } from \"pg\";\nimport { PostgresKeyValueStore, type PostgresKeyValueStoreOptions } from \"./postgres-store\";\nimport { createPostgresLock } from \"./postgres-lock\";\nimport {\n createStatePersistence,\n type StatePersistence,\n type SerializedState,\n type ListPageOptions,\n type ListPageResult,\n} from \"awaitly/persistence\";\nimport type { WorkflowLock } from \"awaitly/durable\";\n\n/**\n * Options for cross-process locking (lease + owner token).\n * When set, the returned store implements WorkflowLock so only one process\n * runs a given workflow ID at a time (when durable.run allowConcurrent is false).\n */\nexport interface PostgresLockOptions {\n /**\n * Table name for workflow locks.\n * @default 'awaitly_workflow_lock'\n */\n lockTableName?: string;\n}\n\n/**\n * Options for creating PostgreSQL persistence.\n */\nexport interface PostgresPersistenceOptions extends PostgresKeyValueStoreOptions {\n /**\n * Key prefix for state entries.\n * @default 'workflow:state:'\n */\n prefix?: string;\n\n /**\n * When set, the store implements WorkflowLock for cross-process concurrency control.\n * Uses a lease (TTL) + owner token; release verifies the token.\n */\n lock?: PostgresLockOptions;\n}\n\n/**\n * Create a StatePersistence instance backed by PostgreSQL.\n *\n * The table is automatically created on first use.\n *\n * @param options - PostgreSQL connection and configuration options\n * @returns StatePersistence instance ready to use with durable.run()\n *\n * @example\n * ```typescript\n * import { createPostgresPersistence } from 'awaitly-postgres';\n * import { durable } from 'awaitly/durable';\n *\n * const store = await createPostgresPersistence({\n * connectionString: process.env.DATABASE_URL,\n * });\n *\n * const result = await durable.run(\n * { fetchUser, createOrder },\n * async (step, { fetchUser, createOrder }) => {\n * const user = await step(() => fetchUser('123'), { key: 'fetch-user' });\n * const order = await step(() => createOrder(user), { key: 'create-order' });\n * return order;\n * },\n * {\n * id: 'checkout-123',\n * store,\n * }\n * );\n * ```\n *\n * @example\n * ```typescript\n * // Using individual connection options\n * const store = await createPostgresPersistence({\n * host: 'localhost',\n * port: 5432,\n * database: 'myapp',\n * user: 'postgres',\n * password: 'password',\n * tableName: 'custom_workflow_state',\n * });\n * ```\n */\nexport type PostgresStatePersistence = StatePersistence & {\n loadRaw(runId: string): Promise<SerializedState | undefined>;\n listPage(options?: ListPageOptions): Promise<ListPageResult>;\n deleteMany(ids: string[]): Promise<number>;\n clear(): Promise<void>;\n};\n\nexport type PostgresStatePersistenceWithLock = PostgresStatePersistence & WorkflowLock;\n\nexport async function createPostgresPersistence(\n options: PostgresPersistenceOptions = {}\n): Promise<PostgresStatePersistence | PostgresStatePersistenceWithLock> {\n const { prefix, lock: lockOptions, ...storeOptions } = options;\n\n const stripPrefix = (key: string): string =>\n key.slice((prefix ?? \"workflow:state:\").length);\n\n const effectivePrefix = prefix ?? \"workflow:state:\";\n const prefixKey = (runId: string): string => `${effectivePrefix}${runId}`;\n\n const addListPageAndDeleteMany = (\n statePersistence: StatePersistence & { loadRaw(runId: string): Promise<SerializedState | undefined> },\n store: PostgresKeyValueStore\n ): PostgresStatePersistence => {\n return Object.assign(statePersistence, {\n async listPage(options: ListPageOptions = {}): Promise<ListPageResult> {\n const { keys, total } = await store.listKeys(`${effectivePrefix}*`, options);\n const ids = keys.map(stripPrefix);\n const limit = Math.min(Math.max(0, options.limit ?? 100), 10_000);\n const nextOffset =\n ids.length === limit ? (options.offset ?? 0) + ids.length : undefined;\n return { ids, total, nextOffset };\n },\n async deleteMany(ids: string[]): Promise<number> {\n if (ids.length === 0) return 0;\n const keys = ids.map(prefixKey);\n return store.deleteMany(keys);\n },\n async clear(): Promise<void> {\n return store.clear();\n },\n });\n };\n\n if (lockOptions !== undefined) {\n const pool =\n storeOptions.existingPool ??\n new PgPool(\n storeOptions.connectionString\n ? { connectionString: storeOptions.connectionString, ...storeOptions.pool }\n : {\n host: storeOptions.host ?? \"localhost\",\n port: storeOptions.port ?? 5432,\n database: storeOptions.database,\n user: storeOptions.user,\n password: storeOptions.password,\n ...storeOptions.pool,\n }\n );\n const store = new PostgresKeyValueStore({ ...storeOptions, existingPool: pool });\n const statePersistence = createStatePersistence(store, prefix) as PostgresStatePersistence;\n const lock = createPostgresLock(pool, {\n lockTableName: lockOptions.lockTableName,\n });\n return Object.assign(addListPageAndDeleteMany(statePersistence, store), {\n tryAcquire: lock.tryAcquire.bind(lock),\n release: lock.release.bind(lock),\n });\n }\n\n const store = new PostgresKeyValueStore(storeOptions);\n const base = createStatePersistence(store, prefix);\n return addListPageAndDeleteMany(base as StatePersistence & { loadRaw(runId: string): Promise<SerializedState | undefined> }, store);\n}\n\n/**\n * PostgreSQL KeyValueStore implementation.\n * Use this directly if you need more control over the store.\n *\n * @example\n * ```typescript\n * import { PostgresKeyValueStore } from 'awaitly-postgres';\n * import { createStatePersistence } from 'awaitly/persistence';\n *\n * const store = new PostgresKeyValueStore({\n * connectionString: process.env.DATABASE_URL,\n * });\n *\n * const persistence = createStatePersistence(store, 'custom:prefix:');\n * ```\n */\nexport { PostgresKeyValueStore, type PostgresKeyValueStoreOptions };\n","/**\n * awaitly-postgres\n *\n * PostgreSQL KeyValueStore implementation for awaitly persistence.\n */\n\nimport type { Pool, PoolConfig, QueryResult } from \"pg\";\nimport { Pool as PgPool } from \"pg\";\nimport type { KeyValueStore, ListPageOptions } from \"awaitly/persistence\";\n\n/**\n * Options for PostgreSQL KeyValueStore.\n */\nexport interface PostgresKeyValueStoreOptions {\n /**\n * PostgreSQL connection string.\n * If provided, other connection options are ignored.\n *\n * @example 'postgresql://user:password@localhost:5432/dbname'\n */\n connectionString?: string;\n\n /**\n * Database host.\n * @default 'localhost'\n */\n host?: string;\n\n /**\n * Database port.\n * @default 5432\n */\n port?: number;\n\n /**\n * Database name.\n */\n database?: string;\n\n /**\n * Database user.\n */\n user?: string;\n\n /**\n * Database password.\n */\n password?: string;\n\n /**\n * Table name for storing key-value pairs.\n * @default 'awaitly_workflow_state'\n */\n tableName?: string;\n\n /**\n * Additional pool configuration options.\n * Ignored if `existingPool` is provided.\n */\n pool?: PoolConfig;\n\n /**\n * Existing PostgreSQL pool to use.\n * If provided, connection options are ignored.\n */\n existingPool?: Pool;\n}\n\n/**\n * PostgreSQL implementation of KeyValueStore.\n *\n * Automatically creates the required table on first use.\n * Supports TTL via expires_at column.\n */\nexport class PostgresKeyValueStore implements KeyValueStore {\n private pool: Pool;\n private tableName: string;\n private initialized: boolean = false;\n private initPromise: Promise<void> | null = null;\n\n constructor(options: PostgresKeyValueStoreOptions) {\n if (options.existingPool) {\n // Use provided pool\n this.pool = options.existingPool;\n } else if (options.connectionString) {\n // Create pool from connection string\n this.pool = new PgPool({\n connectionString: options.connectionString,\n ...options.pool,\n });\n } else {\n // Create pool from individual options\n this.pool = new PgPool({\n host: options.host ?? \"localhost\",\n port: options.port ?? 5432,\n database: options.database,\n user: options.user,\n password: options.password,\n ...options.pool,\n });\n }\n\n this.tableName = options.tableName ?? \"awaitly_workflow_state\";\n }\n\n /**\n * Initialize the store by creating the table if it doesn't exist.\n * This is called automatically on first use.\n */\n private async ensureInitialized(): Promise<void> {\n if (this.initialized) {\n return;\n }\n\n if (this.initPromise) {\n return this.initPromise;\n }\n\n this.initPromise = (async () => {\n try {\n await this.createTable();\n this.initialized = true;\n } catch (error) {\n this.initPromise = null;\n throw error;\n }\n })();\n\n return this.initPromise;\n }\n\n /**\n * Create the table if it doesn't exist.\n * Adds updated_at column to existing tables for listKeys ordering.\n */\n private async createTable(): Promise<void> {\n await this.pool.query(`\n CREATE TABLE IF NOT EXISTS ${this.tableName} (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n expires_at TIMESTAMP,\n updated_at TIMESTAMPTZ\n )\n `);\n\n // Add updated_at to existing tables that don't have it (before creating index)\n await this.pool.query(`\n ALTER TABLE ${this.tableName} \n ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ\n `);\n\n await this.pool.query(`\n CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expires_at \n ON ${this.tableName}(expires_at) \n WHERE expires_at IS NOT NULL\n `);\n\n await this.pool.query(`\n CREATE INDEX IF NOT EXISTS idx_${this.tableName}_updated_at \n ON ${this.tableName}(updated_at) \n WHERE updated_at IS NOT NULL\n `);\n }\n\n /**\n * Convert glob pattern to SQL LIKE pattern.\n * Supports * wildcard (matches any characters).\n */\n private patternToLike(pattern: string): string {\n // Escape SQL LIKE special characters and convert * to %\n return pattern.replace(/%/g, \"\\\\%\").replace(/_/g, \"\\\\_\").replace(/\\*/g, \"%\");\n }\n\n async get(key: string): Promise<string | null> {\n await this.ensureInitialized();\n\n const query = `\n SELECT value \n FROM ${this.tableName} \n WHERE key = $1 \n AND (expires_at IS NULL OR expires_at > NOW())\n `;\n\n const result: QueryResult<{ value: string }> = await this.pool.query(query, [key]);\n\n if (result.rows.length === 0) {\n return null;\n }\n\n return result.rows[0].value;\n }\n\n async set(key: string, value: string, options?: { ttl?: number }): Promise<void> {\n await this.ensureInitialized();\n\n const expiresAt = options?.ttl\n ? new Date(Date.now() + options.ttl * 1000)\n : null;\n\n const query = `\n INSERT INTO ${this.tableName} (key, value, expires_at, updated_at)\n VALUES ($1, $2, $3, NOW())\n ON CONFLICT (key) \n DO UPDATE SET \n value = EXCLUDED.value,\n expires_at = EXCLUDED.expires_at,\n updated_at = NOW()\n `;\n\n await this.pool.query(query, [key, value, expiresAt]);\n }\n\n async delete(key: string): Promise<boolean> {\n await this.ensureInitialized();\n\n const query = `DELETE FROM ${this.tableName} WHERE key = $1`;\n const result = await this.pool.query(query, [key]);\n\n return (result.rowCount ?? 0) > 0;\n }\n\n async exists(key: string): Promise<boolean> {\n await this.ensureInitialized();\n\n const query = `\n SELECT 1 \n FROM ${this.tableName} \n WHERE key = $1 \n AND (expires_at IS NULL OR expires_at > NOW())\n LIMIT 1\n `;\n\n const result = await this.pool.query(query, [key]);\n return result.rows.length > 0;\n }\n\n async keys(pattern: string): Promise<string[]> {\n await this.ensureInitialized();\n\n // Convert glob pattern to SQL LIKE\n const likePattern = this.patternToLike(pattern);\n\n const query = `\n SELECT key \n FROM ${this.tableName} \n WHERE key LIKE $1 \n AND (expires_at IS NULL OR expires_at > NOW())\n `;\n\n const result: QueryResult<{ key: string }> = await this.pool.query(query, [likePattern]);\n\n return result.rows.map((row) => row.key);\n }\n\n /**\n * List keys with pagination, filtering, and ordering.\n */\n async listKeys(\n pattern: string,\n options: ListPageOptions = {}\n ): Promise<{ keys: string[]; total?: number }> {\n await this.ensureInitialized();\n\n const limit = Math.min(Math.max(0, options.limit ?? 100), 10_000);\n const offset = Math.max(0, options.offset ?? 0);\n const orderBy = options.orderBy === \"key\" ? \"key\" : \"updated_at\";\n const orderDir = options.orderDir === \"asc\" ? \"ASC\" : \"DESC\";\n const likePattern = this.patternToLike(pattern);\n\n const conditions: string[] = [\n \"key LIKE $1\",\n \"(expires_at IS NULL OR expires_at > NOW())\",\n ];\n const args: unknown[] = [likePattern];\n let paramIndex = 2;\n\n if (options.updatedBefore != null) {\n conditions.push(`updated_at < $${paramIndex}`);\n args.push(options.updatedBefore);\n paramIndex++;\n }\n if (options.updatedAfter != null) {\n conditions.push(`updated_at > $${paramIndex}`);\n args.push(options.updatedAfter);\n paramIndex++;\n }\n\n const whereClause = conditions.join(\" AND \");\n const orderNulls = orderBy === \"updated_at\" ? \" NULLS LAST\" : \"\";\n\n const listQuery = `\n SELECT key \n FROM ${this.tableName} \n WHERE ${whereClause}\n ORDER BY ${orderBy} ${orderDir}${orderNulls}\n LIMIT $${paramIndex} OFFSET $${paramIndex + 1}\n `;\n const listArgs = [...args, limit, offset];\n\n const result: QueryResult<{ key: string }> = await this.pool.query(listQuery, listArgs);\n const keys = result.rows.map((row) => row.key);\n\n let total: number | undefined;\n if (options.includeTotal === true || offset > 0) {\n const countResult: QueryResult<{ count: string }> = await this.pool.query(\n `SELECT COUNT(*) AS count FROM ${this.tableName} WHERE ${whereClause}`,\n args\n );\n total = parseInt(countResult.rows[0]?.count ?? \"0\", 10);\n }\n\n return { keys, total };\n }\n\n /**\n * Delete multiple keys in one round-trip.\n */\n async deleteMany(keys: string[]): Promise<number> {\n if (keys.length === 0) return 0;\n await this.ensureInitialized();\n const result = await this.pool.query(\n `DELETE FROM ${this.tableName} WHERE key = ANY($1::text[])`,\n [keys]\n );\n return result.rowCount ?? 0;\n }\n\n /**\n * Remove all entries from the table (clear all workflow state).\n */\n async clear(): Promise<void> {\n await this.ensureInitialized();\n await this.pool.query(`TRUNCATE TABLE ${this.tableName}`);\n }\n\n /**\n * Close the database connection pool.\n * Call this when done with the store.\n */\n async close(): Promise<void> {\n await this.pool.end();\n }\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","PostgresKeyValueStore","createPostgresPersistence","__toCommonJS","import_pg","import_pg","PostgresKeyValueStore","options","PgPool","error","pattern","key","query","result","value","expiresAt","likePattern","row","limit","offset","orderBy","orderDir","conditions","args","paramIndex","whereClause","orderNulls","listQuery","listArgs","keys","total","countResult","import_node_crypto","createPostgresLock","pool","options","lockTableName","safeIndexName","ensureLockTable","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","release","import_persistence","createPostgresPersistence","options","prefix","lockOptions","storeOptions","stripPrefix","key","effectivePrefix","prefixKey","runId","addListPageAndDeleteMany","statePersistence","store","keys","total","ids","limit","nextOffset","pool","PgPool","PostgresKeyValueStore","lock","createPostgresLock","base"]}
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"]}
package/dist/index.d.cts CHANGED
@@ -1,203 +1,79 @@
1
- import { PoolConfig, Pool } from 'pg';
2
- import { KeyValueStore, ListPageOptions, StatePersistence, SerializedState, ListPageResult } from 'awaitly/persistence';
1
+ import { Pool } from 'pg';
2
+ import { SnapshotStore } from 'awaitly/persistence';
3
+ export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
3
4
  import { WorkflowLock } from 'awaitly/durable';
5
+ export { WorkflowLock } from 'awaitly/durable';
4
6
 
5
7
  /**
6
- * awaitly-postgres
7
- *
8
- * PostgreSQL KeyValueStore implementation for awaitly persistence.
8
+ * PostgreSQL workflow lock (lease) for cross-process concurrency control.
9
+ * Uses a lease (TTL) + owner token; release verifies the token.
9
10
  */
10
11
 
11
- /**
12
- * Options for PostgreSQL KeyValueStore.
13
- */
14
- interface PostgresKeyValueStoreOptions {
15
- /**
16
- * PostgreSQL connection string.
17
- * If provided, other connection options are ignored.
18
- *
19
- * @example 'postgresql://user:password@localhost:5432/dbname'
20
- */
21
- connectionString?: string;
22
- /**
23
- * Database host.
24
- * @default 'localhost'
25
- */
26
- host?: string;
27
- /**
28
- * Database port.
29
- * @default 5432
30
- */
31
- port?: number;
32
- /**
33
- * Database name.
34
- */
35
- database?: string;
36
- /**
37
- * Database user.
38
- */
39
- user?: string;
40
- /**
41
- * Database password.
42
- */
43
- password?: string;
44
- /**
45
- * Table name for storing key-value pairs.
46
- * @default 'awaitly_workflow_state'
47
- */
48
- tableName?: string;
49
- /**
50
- * Additional pool configuration options.
51
- * Ignored if `existingPool` is provided.
52
- */
53
- pool?: PoolConfig;
54
- /**
55
- * Existing PostgreSQL pool to use.
56
- * If provided, connection options are ignored.
57
- */
58
- existingPool?: Pool;
59
- }
60
- /**
61
- * PostgreSQL implementation of KeyValueStore.
62
- *
63
- * Automatically creates the required table on first use.
64
- * Supports TTL via expires_at column.
65
- */
66
- declare class PostgresKeyValueStore implements KeyValueStore {
67
- private pool;
68
- private tableName;
69
- private initialized;
70
- private initPromise;
71
- constructor(options: PostgresKeyValueStoreOptions);
72
- /**
73
- * Initialize the store by creating the table if it doesn't exist.
74
- * This is called automatically on first use.
75
- */
76
- private ensureInitialized;
77
- /**
78
- * Create the table if it doesn't exist.
79
- * Adds updated_at column to existing tables for listKeys ordering.
80
- */
81
- private createTable;
82
- /**
83
- * Convert glob pattern to SQL LIKE pattern.
84
- * Supports * wildcard (matches any characters).
85
- */
86
- private patternToLike;
87
- get(key: string): Promise<string | null>;
88
- set(key: string, value: string, options?: {
89
- ttl?: number;
90
- }): Promise<void>;
91
- delete(key: string): Promise<boolean>;
92
- exists(key: string): Promise<boolean>;
93
- keys(pattern: string): Promise<string[]>;
94
- /**
95
- * List keys with pagination, filtering, and ordering.
96
- */
97
- listKeys(pattern: string, options?: ListPageOptions): Promise<{
98
- keys: string[];
99
- total?: number;
100
- }>;
101
- /**
102
- * Delete multiple keys in one round-trip.
103
- */
104
- deleteMany(keys: string[]): Promise<number>;
105
- /**
106
- * Remove all entries from the table (clear all workflow state).
107
- */
108
- clear(): Promise<void>;
12
+ interface PostgresLockOptions {
109
13
  /**
110
- * Close the database connection pool.
111
- * Call this when done with the store.
14
+ * Table name for workflow locks.
15
+ * @default 'awaitly_workflow_lock'
112
16
  */
113
- close(): Promise<void>;
17
+ lockTableName?: string;
114
18
  }
115
19
 
116
20
  /**
117
21
  * awaitly-postgres
118
22
  *
119
23
  * PostgreSQL persistence adapter for awaitly workflows.
120
- * Provides ready-to-use StatePersistence backed by PostgreSQL.
24
+ * Provides ready-to-use SnapshotStore backed by PostgreSQL.
121
25
  */
122
26
 
123
27
  /**
124
- * Options for cross-process locking (lease + owner token).
125
- * When set, the returned store implements WorkflowLock so only one process
126
- * runs a given workflow ID at a time (when durable.run allowConcurrent is false).
28
+ * Options for the postgres() shorthand function.
127
29
  */
128
- interface PostgresLockOptions {
129
- /**
130
- * Table name for workflow locks.
131
- * @default 'awaitly_workflow_lock'
132
- */
133
- lockTableName?: string;
134
- }
135
- /**
136
- * Options for creating PostgreSQL persistence.
137
- */
138
- interface PostgresPersistenceOptions extends PostgresKeyValueStoreOptions {
139
- /**
140
- * Key prefix for state entries.
141
- * @default 'workflow:state:'
142
- */
30
+ interface PostgresOptions {
31
+ /** PostgreSQL connection URL. */
32
+ url: string;
33
+ /** Table name for snapshots. @default 'awaitly_snapshots' */
34
+ table?: string;
35
+ /** Key prefix for IDs. @default '' */
143
36
  prefix?: string;
144
- /**
145
- * When set, the store implements WorkflowLock for cross-process concurrency control.
146
- * Uses a lease (TTL) + owner token; release verifies the token.
147
- */
37
+ /** Bring your own pool. */
38
+ pool?: Pool;
39
+ /** Auto-create table on first use. @default true */
40
+ autoCreateTable?: boolean;
41
+ /** Cross-process lock options. When set, the store implements WorkflowLock. */
148
42
  lock?: PostgresLockOptions;
149
43
  }
150
44
  /**
151
- * Create a StatePersistence instance backed by PostgreSQL.
152
- *
153
- * The table is automatically created on first use.
154
- *
155
- * @param options - PostgreSQL connection and configuration options
156
- * @returns StatePersistence instance ready to use with durable.run()
45
+ * Create a snapshot store backed by PostgreSQL.
46
+ * This is the simplified one-liner API for workflow persistence.
157
47
  *
158
48
  * @example
159
49
  * ```typescript
160
- * import { createPostgresPersistence } from 'awaitly-postgres';
161
- * import { durable } from 'awaitly/durable';
50
+ * import { postgres } from 'awaitly-postgres';
162
51
  *
163
- * const store = await createPostgresPersistence({
164
- * connectionString: process.env.DATABASE_URL,
165
- * });
52
+ * // One-liner setup
53
+ * const store = postgres('postgresql://localhost/mydb');
54
+ *
55
+ * // Execute + persist
56
+ * const wf = createWorkflow(deps);
57
+ * await wf(myWorkflowFn);
58
+ * await store.save('wf-123', wf.getSnapshot());
166
59
  *
167
- * const result = await durable.run(
168
- * { fetchUser, createOrder },
169
- * async (step, { fetchUser, createOrder }) => {
170
- * const user = await step(() => fetchUser('123'), { key: 'fetch-user' });
171
- * const order = await step(() => createOrder(user), { key: 'create-order' });
172
- * return order;
173
- * },
174
- * {
175
- * id: 'checkout-123',
176
- * store,
177
- * }
178
- * );
60
+ * // Restore
61
+ * const snapshot = await store.load('wf-123');
62
+ * const wf2 = createWorkflow(deps, { snapshot });
63
+ * await wf2(myWorkflowFn);
179
64
  * ```
180
65
  *
181
66
  * @example
182
67
  * ```typescript
183
- * // Using individual connection options
184
- * const store = await createPostgresPersistence({
185
- * host: 'localhost',
186
- * port: 5432,
187
- * database: 'myapp',
188
- * user: 'postgres',
189
- * password: 'password',
190
- * tableName: 'custom_workflow_state',
68
+ * // With options including cross-process locking
69
+ * const store = postgres({
70
+ * url: 'postgresql://localhost/mydb',
71
+ * table: 'my_workflow_snapshots',
72
+ * prefix: 'orders:',
73
+ * lock: { lockTableName: 'my_workflow_locks' },
191
74
  * });
192
75
  * ```
193
76
  */
194
- type PostgresStatePersistence = StatePersistence & {
195
- loadRaw(runId: string): Promise<SerializedState | undefined>;
196
- listPage(options?: ListPageOptions): Promise<ListPageResult>;
197
- deleteMany(ids: string[]): Promise<number>;
198
- clear(): Promise<void>;
199
- };
200
- type PostgresStatePersistenceWithLock = PostgresStatePersistence & WorkflowLock;
201
- declare function createPostgresPersistence(options?: PostgresPersistenceOptions): Promise<PostgresStatePersistence | PostgresStatePersistenceWithLock>;
77
+ declare function postgres(urlOrOptions: string | PostgresOptions): SnapshotStore & Partial<WorkflowLock>;
202
78
 
203
- export { PostgresKeyValueStore, type PostgresKeyValueStoreOptions, type PostgresLockOptions, type PostgresPersistenceOptions, type PostgresStatePersistence, type PostgresStatePersistenceWithLock, createPostgresPersistence };
79
+ export { type PostgresLockOptions, type PostgresOptions, postgres };
package/dist/index.d.ts CHANGED
@@ -1,203 +1,79 @@
1
- import { PoolConfig, Pool } from 'pg';
2
- import { KeyValueStore, ListPageOptions, StatePersistence, SerializedState, ListPageResult } from 'awaitly/persistence';
1
+ import { Pool } from 'pg';
2
+ import { SnapshotStore } from 'awaitly/persistence';
3
+ export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
3
4
  import { WorkflowLock } from 'awaitly/durable';
5
+ export { WorkflowLock } from 'awaitly/durable';
4
6
 
5
7
  /**
6
- * awaitly-postgres
7
- *
8
- * PostgreSQL KeyValueStore implementation for awaitly persistence.
8
+ * PostgreSQL workflow lock (lease) for cross-process concurrency control.
9
+ * Uses a lease (TTL) + owner token; release verifies the token.
9
10
  */
10
11
 
11
- /**
12
- * Options for PostgreSQL KeyValueStore.
13
- */
14
- interface PostgresKeyValueStoreOptions {
15
- /**
16
- * PostgreSQL connection string.
17
- * If provided, other connection options are ignored.
18
- *
19
- * @example 'postgresql://user:password@localhost:5432/dbname'
20
- */
21
- connectionString?: string;
22
- /**
23
- * Database host.
24
- * @default 'localhost'
25
- */
26
- host?: string;
27
- /**
28
- * Database port.
29
- * @default 5432
30
- */
31
- port?: number;
32
- /**
33
- * Database name.
34
- */
35
- database?: string;
36
- /**
37
- * Database user.
38
- */
39
- user?: string;
40
- /**
41
- * Database password.
42
- */
43
- password?: string;
44
- /**
45
- * Table name for storing key-value pairs.
46
- * @default 'awaitly_workflow_state'
47
- */
48
- tableName?: string;
49
- /**
50
- * Additional pool configuration options.
51
- * Ignored if `existingPool` is provided.
52
- */
53
- pool?: PoolConfig;
54
- /**
55
- * Existing PostgreSQL pool to use.
56
- * If provided, connection options are ignored.
57
- */
58
- existingPool?: Pool;
59
- }
60
- /**
61
- * PostgreSQL implementation of KeyValueStore.
62
- *
63
- * Automatically creates the required table on first use.
64
- * Supports TTL via expires_at column.
65
- */
66
- declare class PostgresKeyValueStore implements KeyValueStore {
67
- private pool;
68
- private tableName;
69
- private initialized;
70
- private initPromise;
71
- constructor(options: PostgresKeyValueStoreOptions);
72
- /**
73
- * Initialize the store by creating the table if it doesn't exist.
74
- * This is called automatically on first use.
75
- */
76
- private ensureInitialized;
77
- /**
78
- * Create the table if it doesn't exist.
79
- * Adds updated_at column to existing tables for listKeys ordering.
80
- */
81
- private createTable;
82
- /**
83
- * Convert glob pattern to SQL LIKE pattern.
84
- * Supports * wildcard (matches any characters).
85
- */
86
- private patternToLike;
87
- get(key: string): Promise<string | null>;
88
- set(key: string, value: string, options?: {
89
- ttl?: number;
90
- }): Promise<void>;
91
- delete(key: string): Promise<boolean>;
92
- exists(key: string): Promise<boolean>;
93
- keys(pattern: string): Promise<string[]>;
94
- /**
95
- * List keys with pagination, filtering, and ordering.
96
- */
97
- listKeys(pattern: string, options?: ListPageOptions): Promise<{
98
- keys: string[];
99
- total?: number;
100
- }>;
101
- /**
102
- * Delete multiple keys in one round-trip.
103
- */
104
- deleteMany(keys: string[]): Promise<number>;
105
- /**
106
- * Remove all entries from the table (clear all workflow state).
107
- */
108
- clear(): Promise<void>;
12
+ interface PostgresLockOptions {
109
13
  /**
110
- * Close the database connection pool.
111
- * Call this when done with the store.
14
+ * Table name for workflow locks.
15
+ * @default 'awaitly_workflow_lock'
112
16
  */
113
- close(): Promise<void>;
17
+ lockTableName?: string;
114
18
  }
115
19
 
116
20
  /**
117
21
  * awaitly-postgres
118
22
  *
119
23
  * PostgreSQL persistence adapter for awaitly workflows.
120
- * Provides ready-to-use StatePersistence backed by PostgreSQL.
24
+ * Provides ready-to-use SnapshotStore backed by PostgreSQL.
121
25
  */
122
26
 
123
27
  /**
124
- * Options for cross-process locking (lease + owner token).
125
- * When set, the returned store implements WorkflowLock so only one process
126
- * runs a given workflow ID at a time (when durable.run allowConcurrent is false).
28
+ * Options for the postgres() shorthand function.
127
29
  */
128
- interface PostgresLockOptions {
129
- /**
130
- * Table name for workflow locks.
131
- * @default 'awaitly_workflow_lock'
132
- */
133
- lockTableName?: string;
134
- }
135
- /**
136
- * Options for creating PostgreSQL persistence.
137
- */
138
- interface PostgresPersistenceOptions extends PostgresKeyValueStoreOptions {
139
- /**
140
- * Key prefix for state entries.
141
- * @default 'workflow:state:'
142
- */
30
+ interface PostgresOptions {
31
+ /** PostgreSQL connection URL. */
32
+ url: string;
33
+ /** Table name for snapshots. @default 'awaitly_snapshots' */
34
+ table?: string;
35
+ /** Key prefix for IDs. @default '' */
143
36
  prefix?: string;
144
- /**
145
- * When set, the store implements WorkflowLock for cross-process concurrency control.
146
- * Uses a lease (TTL) + owner token; release verifies the token.
147
- */
37
+ /** Bring your own pool. */
38
+ pool?: Pool;
39
+ /** Auto-create table on first use. @default true */
40
+ autoCreateTable?: boolean;
41
+ /** Cross-process lock options. When set, the store implements WorkflowLock. */
148
42
  lock?: PostgresLockOptions;
149
43
  }
150
44
  /**
151
- * Create a StatePersistence instance backed by PostgreSQL.
152
- *
153
- * The table is automatically created on first use.
154
- *
155
- * @param options - PostgreSQL connection and configuration options
156
- * @returns StatePersistence instance ready to use with durable.run()
45
+ * Create a snapshot store backed by PostgreSQL.
46
+ * This is the simplified one-liner API for workflow persistence.
157
47
  *
158
48
  * @example
159
49
  * ```typescript
160
- * import { createPostgresPersistence } from 'awaitly-postgres';
161
- * import { durable } from 'awaitly/durable';
50
+ * import { postgres } from 'awaitly-postgres';
162
51
  *
163
- * const store = await createPostgresPersistence({
164
- * connectionString: process.env.DATABASE_URL,
165
- * });
52
+ * // One-liner setup
53
+ * const store = postgres('postgresql://localhost/mydb');
54
+ *
55
+ * // Execute + persist
56
+ * const wf = createWorkflow(deps);
57
+ * await wf(myWorkflowFn);
58
+ * await store.save('wf-123', wf.getSnapshot());
166
59
  *
167
- * const result = await durable.run(
168
- * { fetchUser, createOrder },
169
- * async (step, { fetchUser, createOrder }) => {
170
- * const user = await step(() => fetchUser('123'), { key: 'fetch-user' });
171
- * const order = await step(() => createOrder(user), { key: 'create-order' });
172
- * return order;
173
- * },
174
- * {
175
- * id: 'checkout-123',
176
- * store,
177
- * }
178
- * );
60
+ * // Restore
61
+ * const snapshot = await store.load('wf-123');
62
+ * const wf2 = createWorkflow(deps, { snapshot });
63
+ * await wf2(myWorkflowFn);
179
64
  * ```
180
65
  *
181
66
  * @example
182
67
  * ```typescript
183
- * // Using individual connection options
184
- * const store = await createPostgresPersistence({
185
- * host: 'localhost',
186
- * port: 5432,
187
- * database: 'myapp',
188
- * user: 'postgres',
189
- * password: 'password',
190
- * tableName: 'custom_workflow_state',
68
+ * // With options including cross-process locking
69
+ * const store = postgres({
70
+ * url: 'postgresql://localhost/mydb',
71
+ * table: 'my_workflow_snapshots',
72
+ * prefix: 'orders:',
73
+ * lock: { lockTableName: 'my_workflow_locks' },
191
74
  * });
192
75
  * ```
193
76
  */
194
- type PostgresStatePersistence = StatePersistence & {
195
- loadRaw(runId: string): Promise<SerializedState | undefined>;
196
- listPage(options?: ListPageOptions): Promise<ListPageResult>;
197
- deleteMany(ids: string[]): Promise<number>;
198
- clear(): Promise<void>;
199
- };
200
- type PostgresStatePersistenceWithLock = PostgresStatePersistence & WorkflowLock;
201
- declare function createPostgresPersistence(options?: PostgresPersistenceOptions): Promise<PostgresStatePersistence | PostgresStatePersistenceWithLock>;
77
+ declare function postgres(urlOrOptions: string | PostgresOptions): SnapshotStore & Partial<WorkflowLock>;
202
78
 
203
- export { PostgresKeyValueStore, type PostgresKeyValueStoreOptions, type PostgresLockOptions, type PostgresPersistenceOptions, type PostgresStatePersistence, type PostgresStatePersistenceWithLock, createPostgresPersistence };
79
+ export { type PostgresLockOptions, type PostgresOptions, postgres };
package/dist/index.js CHANGED
@@ -1,59 +1,11 @@
1
- import{Pool as L}from"pg";import{Pool as N}from"pg";var g=class{pool;tableName;initialized=!1;initPromise=null;constructor(e){e.existingPool?this.pool=e.existingPool:e.connectionString?this.pool=new N({connectionString:e.connectionString,...e.pool}):this.pool=new N({host:e.host??"localhost",port:e.port??5432,database:e.database,user:e.user,password:e.password,...e.pool}),this.tableName=e.tableName??"awaitly_workflow_state"}async ensureInitialized(){if(!this.initialized)return this.initPromise?this.initPromise:(this.initPromise=(async()=>{try{await this.createTable(),this.initialized=!0}catch(e){throw this.initPromise=null,e}})(),this.initPromise)}async createTable(){await this.pool.query(`
2
- CREATE TABLE IF NOT EXISTS ${this.tableName} (
3
- key TEXT PRIMARY KEY,
4
- value TEXT NOT NULL,
5
- expires_at TIMESTAMP,
6
- updated_at TIMESTAMPTZ
7
- )
8
- `),await this.pool.query(`
9
- ALTER TABLE ${this.tableName}
10
- ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ
11
- `),await this.pool.query(`
12
- CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expires_at
13
- ON ${this.tableName}(expires_at)
14
- WHERE expires_at IS NOT NULL
15
- `),await this.pool.query(`
16
- CREATE INDEX IF NOT EXISTS idx_${this.tableName}_updated_at
17
- ON ${this.tableName}(updated_at)
18
- WHERE updated_at IS NOT NULL
19
- `)}patternToLike(e){return e.replace(/%/g,"\\%").replace(/_/g,"\\_").replace(/\*/g,"%")}async get(e){await this.ensureInitialized();let t=`
20
- SELECT value
21
- FROM ${this.tableName}
22
- WHERE key = $1
23
- AND (expires_at IS NULL OR expires_at > NOW())
24
- `,s=await this.pool.query(t,[e]);return s.rows.length===0?null:s.rows[0].value}async set(e,t,s){await this.ensureInitialized();let n=s?.ttl?new Date(Date.now()+s.ttl*1e3):null,o=`
25
- INSERT INTO ${this.tableName} (key, value, expires_at, updated_at)
26
- VALUES ($1, $2, $3, NOW())
27
- ON CONFLICT (key)
28
- DO UPDATE SET
29
- value = EXCLUDED.value,
30
- expires_at = EXCLUDED.expires_at,
31
- updated_at = NOW()
32
- `;await this.pool.query(o,[e,t,n])}async delete(e){await this.ensureInitialized();let t=`DELETE FROM ${this.tableName} WHERE key = $1`;return((await this.pool.query(t,[e])).rowCount??0)>0}async exists(e){await this.ensureInitialized();let t=`
33
- SELECT 1
34
- FROM ${this.tableName}
35
- WHERE key = $1
36
- AND (expires_at IS NULL OR expires_at > NOW())
37
- LIMIT 1
38
- `;return(await this.pool.query(t,[e])).rows.length>0}async keys(e){await this.ensureInitialized();let t=this.patternToLike(e),s=`
39
- SELECT key
40
- FROM ${this.tableName}
41
- WHERE key LIKE $1
42
- AND (expires_at IS NULL OR expires_at > NOW())
43
- `;return(await this.pool.query(s,[t])).rows.map(o=>o.key)}async listKeys(e,t={}){await this.ensureInitialized();let s=Math.min(Math.max(0,t.limit??100),1e4),n=Math.max(0,t.offset??0),o=t.orderBy==="key"?"key":"updated_at",P=t.orderDir==="asc"?"ASC":"DESC",c=this.patternToLike(e),l=["key LIKE $1","(expires_at IS NULL OR expires_at > NOW())"],p=[c],r=2;t.updatedBefore!=null&&(l.push(`updated_at < $${r}`),p.push(t.updatedBefore),r++),t.updatedAfter!=null&&(l.push(`updated_at > $${r}`),p.push(t.updatedAfter),r++);let a=l.join(" AND "),i=o==="updated_at"?" NULLS LAST":"",u=`
44
- SELECT key
45
- FROM ${this.tableName}
46
- WHERE ${a}
47
- ORDER BY ${o} ${P}${i}
48
- LIMIT $${r} OFFSET $${r+1}
49
- `,E=[...p,s,n],w=(await this.pool.query(u,E)).rows.map(T=>T.key),y;if(t.includeTotal===!0||n>0){let T=await this.pool.query(`SELECT COUNT(*) AS count FROM ${this.tableName} WHERE ${a}`,p);y=parseInt(T.rows[0]?.count??"0",10)}return{keys:w,total:y}}async deleteMany(e){return e.length===0?0:(await this.ensureInitialized(),(await this.pool.query(`DELETE FROM ${this.tableName} WHERE key = ANY($1::text[])`,[e])).rowCount??0)}async clear(){await this.ensureInitialized(),await this.pool.query(`TRUNCATE TABLE ${this.tableName}`)}async close(){await this.pool.end()}};import{randomUUID as k}from"crypto";function h(d,e={}){let t=e.lockTableName??"awaitly_workflow_lock",s=`idx_${t.replace(/[^a-zA-Z0-9_]/g,"_")}_expires_at`;async function n(){await d.query(`
1
+ import{Pool as k}from"pg";import{randomUUID as f}from"crypto";function T(n,e={}){let t=e.lockTableName??"awaitly_workflow_lock",i=`idx_${t.replace(/[^a-zA-Z0-9_]/g,"_")}_expires_at`;async function E(){await n.query(`
50
2
  CREATE TABLE IF NOT EXISTS ${t} (
51
3
  workflow_id TEXT PRIMARY KEY,
52
4
  owner_token TEXT NOT NULL,
53
5
  expires_at TIMESTAMPTZ NOT NULL
54
6
  );
55
- CREATE INDEX IF NOT EXISTS ${s} ON ${t}(expires_at);
56
- `)}async function o(c,l){let p=l?.ttlMs??6e4,r=k(),a=new Date(Date.now()+p);await n();let i=await d.query(`
7
+ CREATE INDEX IF NOT EXISTS ${i} ON ${t}(expires_at);
8
+ `)}async function w(p,a){let l=a?.ttlMs??6e4,c=f(),r=new Date(Date.now()+l);await E();let s=await n.query(`
57
9
  INSERT INTO ${t} (workflow_id, owner_token, expires_at)
58
10
  VALUES ($1, $2, $3)
59
11
  ON CONFLICT (workflow_id) DO UPDATE SET
@@ -61,5 +13,18 @@ import{Pool as L}from"pg";import{Pool as N}from"pg";var g=class{pool;tableName;i
61
13
  expires_at = EXCLUDED.expires_at
62
14
  WHERE ${t}.expires_at < NOW()
63
15
  RETURNING owner_token
64
- `,[c,r,a]);return i.rowCount===1&&i.rows[0].owner_token===r?{ownerToken:r}:null}async function P(c,l){await d.query(`DELETE FROM ${t} WHERE workflow_id = $1 AND owner_token = $2`,[c,l])}return{tryAcquire:o,release:P,ensureLockTable:n}}import{createStatePersistence as S}from"awaitly/persistence";async function A(d={}){let{prefix:e,lock:t,...s}=d,n=r=>r.slice((e??"workflow:state:").length),o=e??"workflow:state:",P=r=>`${o}${r}`,c=(r,a)=>Object.assign(r,{async listPage(i={}){let{keys:u,total:E}=await a.listKeys(`${o}*`,i),m=u.map(n),w=Math.min(Math.max(0,i.limit??100),1e4),y=m.length===w?(i.offset??0)+m.length:void 0;return{ids:m,total:E,nextOffset:y}},async deleteMany(i){if(i.length===0)return 0;let u=i.map(P);return a.deleteMany(u)},async clear(){return a.clear()}});if(t!==void 0){let r=s.existingPool??new L(s.connectionString?{connectionString:s.connectionString,...s.pool}:{host:s.host??"localhost",port:s.port??5432,database:s.database,user:s.user,password:s.password,...s.pool}),a=new g({...s,existingPool:r}),i=S(a,e),u=h(r,{lockTableName:t.lockTableName});return Object.assign(c(i,a),{tryAcquire:u.tryAcquire.bind(u),release:u.release.bind(u)})}let l=new g(s),p=S(l,e);return c(p,l)}export{g as PostgresKeyValueStore,A as createPostgresPersistence};
16
+ `,[p,c,r]);return s.rowCount===1&&s.rows[0].owner_token===c?{ownerToken:c}:null}async function o(p,a){await n.query(`DELETE FROM ${t} WHERE workflow_id = $1 AND owner_token = $2`,[p,a])}return{tryAcquire:w,release:o,ensureLockTable:E}}var m=/^[a-zA-Z_][a-zA-Z0-9_]*$/;function N(n){let e=typeof n=="string"?{url:n}:n,t=e.table??"awaitly_snapshots";if(!m.test(t))throw new Error(`Invalid table name: ${t}. Must be alphanumeric with underscores.`);let i=e.prefix??"",E=e.autoCreateTable??!0,w=!e.pool,o=e.pool??new k({connectionString:e.url}),p=!1,a=async()=>{!E||p||(await o.query(`
17
+ CREATE TABLE IF NOT EXISTS ${t} (
18
+ id TEXT PRIMARY KEY,
19
+ snapshot JSONB NOT NULL,
20
+ updated_at TIMESTAMPTZ DEFAULT NOW()
21
+ )
22
+ `),await o.query(`
23
+ CREATE INDEX IF NOT EXISTS ${t}_updated_at_idx ON ${t} (updated_at DESC)
24
+ `),p=!0)},l=e.lock?T(o,e.lock):null,c={async save(r,s){await a();let u=i+r;await o.query(`INSERT INTO ${t} (id, snapshot, updated_at)
25
+ VALUES ($1, $2, NOW())
26
+ ON CONFLICT (id) DO UPDATE SET snapshot = $2, updated_at = NOW()`,[u,JSON.stringify(s)])},async load(r){await a();let s=i+r,u=await o.query(`SELECT snapshot FROM ${t} WHERE id = $1`,[s]);return u.rows.length===0?null:u.rows[0].snapshot},async delete(r){await a();let s=i+r;await o.query(`DELETE FROM ${t} WHERE id = $1`,[s])},async list(r){await a();let s=i+(r?.prefix??""),u=r?.limit??100;return(await o.query(`SELECT id, updated_at FROM ${t}
27
+ WHERE id LIKE $1
28
+ ORDER BY updated_at DESC
29
+ LIMIT $2`,[s+"%",u])).rows.map(d=>({id:d.id.slice(i.length),updatedAt:d.updated_at.toISOString()}))},async close(){w&&await o.end()}};return l&&(c.tryAcquire=l.tryAcquire.bind(l),c.release=l.release.bind(l)),c}export{N as postgres};
65
30
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/postgres-store.ts","../src/postgres-lock.ts"],"sourcesContent":["/**\n * awaitly-postgres\n *\n * PostgreSQL persistence adapter for awaitly workflows.\n * Provides ready-to-use StatePersistence backed by PostgreSQL.\n */\n\nimport { Pool as PgPool } from \"pg\";\nimport { PostgresKeyValueStore, type PostgresKeyValueStoreOptions } from \"./postgres-store\";\nimport { createPostgresLock } from \"./postgres-lock\";\nimport {\n createStatePersistence,\n type StatePersistence,\n type SerializedState,\n type ListPageOptions,\n type ListPageResult,\n} from \"awaitly/persistence\";\nimport type { WorkflowLock } from \"awaitly/durable\";\n\n/**\n * Options for cross-process locking (lease + owner token).\n * When set, the returned store implements WorkflowLock so only one process\n * runs a given workflow ID at a time (when durable.run allowConcurrent is false).\n */\nexport interface PostgresLockOptions {\n /**\n * Table name for workflow locks.\n * @default 'awaitly_workflow_lock'\n */\n lockTableName?: string;\n}\n\n/**\n * Options for creating PostgreSQL persistence.\n */\nexport interface PostgresPersistenceOptions extends PostgresKeyValueStoreOptions {\n /**\n * Key prefix for state entries.\n * @default 'workflow:state:'\n */\n prefix?: string;\n\n /**\n * When set, the store implements WorkflowLock for cross-process concurrency control.\n * Uses a lease (TTL) + owner token; release verifies the token.\n */\n lock?: PostgresLockOptions;\n}\n\n/**\n * Create a StatePersistence instance backed by PostgreSQL.\n *\n * The table is automatically created on first use.\n *\n * @param options - PostgreSQL connection and configuration options\n * @returns StatePersistence instance ready to use with durable.run()\n *\n * @example\n * ```typescript\n * import { createPostgresPersistence } from 'awaitly-postgres';\n * import { durable } from 'awaitly/durable';\n *\n * const store = await createPostgresPersistence({\n * connectionString: process.env.DATABASE_URL,\n * });\n *\n * const result = await durable.run(\n * { fetchUser, createOrder },\n * async (step, { fetchUser, createOrder }) => {\n * const user = await step(() => fetchUser('123'), { key: 'fetch-user' });\n * const order = await step(() => createOrder(user), { key: 'create-order' });\n * return order;\n * },\n * {\n * id: 'checkout-123',\n * store,\n * }\n * );\n * ```\n *\n * @example\n * ```typescript\n * // Using individual connection options\n * const store = await createPostgresPersistence({\n * host: 'localhost',\n * port: 5432,\n * database: 'myapp',\n * user: 'postgres',\n * password: 'password',\n * tableName: 'custom_workflow_state',\n * });\n * ```\n */\nexport type PostgresStatePersistence = StatePersistence & {\n loadRaw(runId: string): Promise<SerializedState | undefined>;\n listPage(options?: ListPageOptions): Promise<ListPageResult>;\n deleteMany(ids: string[]): Promise<number>;\n clear(): Promise<void>;\n};\n\nexport type PostgresStatePersistenceWithLock = PostgresStatePersistence & WorkflowLock;\n\nexport async function createPostgresPersistence(\n options: PostgresPersistenceOptions = {}\n): Promise<PostgresStatePersistence | PostgresStatePersistenceWithLock> {\n const { prefix, lock: lockOptions, ...storeOptions } = options;\n\n const stripPrefix = (key: string): string =>\n key.slice((prefix ?? \"workflow:state:\").length);\n\n const effectivePrefix = prefix ?? \"workflow:state:\";\n const prefixKey = (runId: string): string => `${effectivePrefix}${runId}`;\n\n const addListPageAndDeleteMany = (\n statePersistence: StatePersistence & { loadRaw(runId: string): Promise<SerializedState | undefined> },\n store: PostgresKeyValueStore\n ): PostgresStatePersistence => {\n return Object.assign(statePersistence, {\n async listPage(options: ListPageOptions = {}): Promise<ListPageResult> {\n const { keys, total } = await store.listKeys(`${effectivePrefix}*`, options);\n const ids = keys.map(stripPrefix);\n const limit = Math.min(Math.max(0, options.limit ?? 100), 10_000);\n const nextOffset =\n ids.length === limit ? (options.offset ?? 0) + ids.length : undefined;\n return { ids, total, nextOffset };\n },\n async deleteMany(ids: string[]): Promise<number> {\n if (ids.length === 0) return 0;\n const keys = ids.map(prefixKey);\n return store.deleteMany(keys);\n },\n async clear(): Promise<void> {\n return store.clear();\n },\n });\n };\n\n if (lockOptions !== undefined) {\n const pool =\n storeOptions.existingPool ??\n new PgPool(\n storeOptions.connectionString\n ? { connectionString: storeOptions.connectionString, ...storeOptions.pool }\n : {\n host: storeOptions.host ?? \"localhost\",\n port: storeOptions.port ?? 5432,\n database: storeOptions.database,\n user: storeOptions.user,\n password: storeOptions.password,\n ...storeOptions.pool,\n }\n );\n const store = new PostgresKeyValueStore({ ...storeOptions, existingPool: pool });\n const statePersistence = createStatePersistence(store, prefix) as PostgresStatePersistence;\n const lock = createPostgresLock(pool, {\n lockTableName: lockOptions.lockTableName,\n });\n return Object.assign(addListPageAndDeleteMany(statePersistence, store), {\n tryAcquire: lock.tryAcquire.bind(lock),\n release: lock.release.bind(lock),\n });\n }\n\n const store = new PostgresKeyValueStore(storeOptions);\n const base = createStatePersistence(store, prefix);\n return addListPageAndDeleteMany(base as StatePersistence & { loadRaw(runId: string): Promise<SerializedState | undefined> }, store);\n}\n\n/**\n * PostgreSQL KeyValueStore implementation.\n * Use this directly if you need more control over the store.\n *\n * @example\n * ```typescript\n * import { PostgresKeyValueStore } from 'awaitly-postgres';\n * import { createStatePersistence } from 'awaitly/persistence';\n *\n * const store = new PostgresKeyValueStore({\n * connectionString: process.env.DATABASE_URL,\n * });\n *\n * const persistence = createStatePersistence(store, 'custom:prefix:');\n * ```\n */\nexport { PostgresKeyValueStore, type PostgresKeyValueStoreOptions };\n","/**\n * awaitly-postgres\n *\n * PostgreSQL KeyValueStore implementation for awaitly persistence.\n */\n\nimport type { Pool, PoolConfig, QueryResult } from \"pg\";\nimport { Pool as PgPool } from \"pg\";\nimport type { KeyValueStore, ListPageOptions } from \"awaitly/persistence\";\n\n/**\n * Options for PostgreSQL KeyValueStore.\n */\nexport interface PostgresKeyValueStoreOptions {\n /**\n * PostgreSQL connection string.\n * If provided, other connection options are ignored.\n *\n * @example 'postgresql://user:password@localhost:5432/dbname'\n */\n connectionString?: string;\n\n /**\n * Database host.\n * @default 'localhost'\n */\n host?: string;\n\n /**\n * Database port.\n * @default 5432\n */\n port?: number;\n\n /**\n * Database name.\n */\n database?: string;\n\n /**\n * Database user.\n */\n user?: string;\n\n /**\n * Database password.\n */\n password?: string;\n\n /**\n * Table name for storing key-value pairs.\n * @default 'awaitly_workflow_state'\n */\n tableName?: string;\n\n /**\n * Additional pool configuration options.\n * Ignored if `existingPool` is provided.\n */\n pool?: PoolConfig;\n\n /**\n * Existing PostgreSQL pool to use.\n * If provided, connection options are ignored.\n */\n existingPool?: Pool;\n}\n\n/**\n * PostgreSQL implementation of KeyValueStore.\n *\n * Automatically creates the required table on first use.\n * Supports TTL via expires_at column.\n */\nexport class PostgresKeyValueStore implements KeyValueStore {\n private pool: Pool;\n private tableName: string;\n private initialized: boolean = false;\n private initPromise: Promise<void> | null = null;\n\n constructor(options: PostgresKeyValueStoreOptions) {\n if (options.existingPool) {\n // Use provided pool\n this.pool = options.existingPool;\n } else if (options.connectionString) {\n // Create pool from connection string\n this.pool = new PgPool({\n connectionString: options.connectionString,\n ...options.pool,\n });\n } else {\n // Create pool from individual options\n this.pool = new PgPool({\n host: options.host ?? \"localhost\",\n port: options.port ?? 5432,\n database: options.database,\n user: options.user,\n password: options.password,\n ...options.pool,\n });\n }\n\n this.tableName = options.tableName ?? \"awaitly_workflow_state\";\n }\n\n /**\n * Initialize the store by creating the table if it doesn't exist.\n * This is called automatically on first use.\n */\n private async ensureInitialized(): Promise<void> {\n if (this.initialized) {\n return;\n }\n\n if (this.initPromise) {\n return this.initPromise;\n }\n\n this.initPromise = (async () => {\n try {\n await this.createTable();\n this.initialized = true;\n } catch (error) {\n this.initPromise = null;\n throw error;\n }\n })();\n\n return this.initPromise;\n }\n\n /**\n * Create the table if it doesn't exist.\n * Adds updated_at column to existing tables for listKeys ordering.\n */\n private async createTable(): Promise<void> {\n await this.pool.query(`\n CREATE TABLE IF NOT EXISTS ${this.tableName} (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n expires_at TIMESTAMP,\n updated_at TIMESTAMPTZ\n )\n `);\n\n // Add updated_at to existing tables that don't have it (before creating index)\n await this.pool.query(`\n ALTER TABLE ${this.tableName} \n ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ\n `);\n\n await this.pool.query(`\n CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expires_at \n ON ${this.tableName}(expires_at) \n WHERE expires_at IS NOT NULL\n `);\n\n await this.pool.query(`\n CREATE INDEX IF NOT EXISTS idx_${this.tableName}_updated_at \n ON ${this.tableName}(updated_at) \n WHERE updated_at IS NOT NULL\n `);\n }\n\n /**\n * Convert glob pattern to SQL LIKE pattern.\n * Supports * wildcard (matches any characters).\n */\n private patternToLike(pattern: string): string {\n // Escape SQL LIKE special characters and convert * to %\n return pattern.replace(/%/g, \"\\\\%\").replace(/_/g, \"\\\\_\").replace(/\\*/g, \"%\");\n }\n\n async get(key: string): Promise<string | null> {\n await this.ensureInitialized();\n\n const query = `\n SELECT value \n FROM ${this.tableName} \n WHERE key = $1 \n AND (expires_at IS NULL OR expires_at > NOW())\n `;\n\n const result: QueryResult<{ value: string }> = await this.pool.query(query, [key]);\n\n if (result.rows.length === 0) {\n return null;\n }\n\n return result.rows[0].value;\n }\n\n async set(key: string, value: string, options?: { ttl?: number }): Promise<void> {\n await this.ensureInitialized();\n\n const expiresAt = options?.ttl\n ? new Date(Date.now() + options.ttl * 1000)\n : null;\n\n const query = `\n INSERT INTO ${this.tableName} (key, value, expires_at, updated_at)\n VALUES ($1, $2, $3, NOW())\n ON CONFLICT (key) \n DO UPDATE SET \n value = EXCLUDED.value,\n expires_at = EXCLUDED.expires_at,\n updated_at = NOW()\n `;\n\n await this.pool.query(query, [key, value, expiresAt]);\n }\n\n async delete(key: string): Promise<boolean> {\n await this.ensureInitialized();\n\n const query = `DELETE FROM ${this.tableName} WHERE key = $1`;\n const result = await this.pool.query(query, [key]);\n\n return (result.rowCount ?? 0) > 0;\n }\n\n async exists(key: string): Promise<boolean> {\n await this.ensureInitialized();\n\n const query = `\n SELECT 1 \n FROM ${this.tableName} \n WHERE key = $1 \n AND (expires_at IS NULL OR expires_at > NOW())\n LIMIT 1\n `;\n\n const result = await this.pool.query(query, [key]);\n return result.rows.length > 0;\n }\n\n async keys(pattern: string): Promise<string[]> {\n await this.ensureInitialized();\n\n // Convert glob pattern to SQL LIKE\n const likePattern = this.patternToLike(pattern);\n\n const query = `\n SELECT key \n FROM ${this.tableName} \n WHERE key LIKE $1 \n AND (expires_at IS NULL OR expires_at > NOW())\n `;\n\n const result: QueryResult<{ key: string }> = await this.pool.query(query, [likePattern]);\n\n return result.rows.map((row) => row.key);\n }\n\n /**\n * List keys with pagination, filtering, and ordering.\n */\n async listKeys(\n pattern: string,\n options: ListPageOptions = {}\n ): Promise<{ keys: string[]; total?: number }> {\n await this.ensureInitialized();\n\n const limit = Math.min(Math.max(0, options.limit ?? 100), 10_000);\n const offset = Math.max(0, options.offset ?? 0);\n const orderBy = options.orderBy === \"key\" ? \"key\" : \"updated_at\";\n const orderDir = options.orderDir === \"asc\" ? \"ASC\" : \"DESC\";\n const likePattern = this.patternToLike(pattern);\n\n const conditions: string[] = [\n \"key LIKE $1\",\n \"(expires_at IS NULL OR expires_at > NOW())\",\n ];\n const args: unknown[] = [likePattern];\n let paramIndex = 2;\n\n if (options.updatedBefore != null) {\n conditions.push(`updated_at < $${paramIndex}`);\n args.push(options.updatedBefore);\n paramIndex++;\n }\n if (options.updatedAfter != null) {\n conditions.push(`updated_at > $${paramIndex}`);\n args.push(options.updatedAfter);\n paramIndex++;\n }\n\n const whereClause = conditions.join(\" AND \");\n const orderNulls = orderBy === \"updated_at\" ? \" NULLS LAST\" : \"\";\n\n const listQuery = `\n SELECT key \n FROM ${this.tableName} \n WHERE ${whereClause}\n ORDER BY ${orderBy} ${orderDir}${orderNulls}\n LIMIT $${paramIndex} OFFSET $${paramIndex + 1}\n `;\n const listArgs = [...args, limit, offset];\n\n const result: QueryResult<{ key: string }> = await this.pool.query(listQuery, listArgs);\n const keys = result.rows.map((row) => row.key);\n\n let total: number | undefined;\n if (options.includeTotal === true || offset > 0) {\n const countResult: QueryResult<{ count: string }> = await this.pool.query(\n `SELECT COUNT(*) AS count FROM ${this.tableName} WHERE ${whereClause}`,\n args\n );\n total = parseInt(countResult.rows[0]?.count ?? \"0\", 10);\n }\n\n return { keys, total };\n }\n\n /**\n * Delete multiple keys in one round-trip.\n */\n async deleteMany(keys: string[]): Promise<number> {\n if (keys.length === 0) return 0;\n await this.ensureInitialized();\n const result = await this.pool.query(\n `DELETE FROM ${this.tableName} WHERE key = ANY($1::text[])`,\n [keys]\n );\n return result.rowCount ?? 0;\n }\n\n /**\n * Remove all entries from the table (clear all workflow state).\n */\n async clear(): Promise<void> {\n await this.ensureInitialized();\n await this.pool.query(`TRUNCATE TABLE ${this.tableName}`);\n }\n\n /**\n * Close the database connection pool.\n * Call this when done with the store.\n */\n async close(): Promise<void> {\n await this.pool.end();\n }\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","PgPool","PostgresKeyValueStore","options","error","pattern","key","query","result","value","expiresAt","likePattern","row","limit","offset","orderBy","orderDir","conditions","args","paramIndex","whereClause","orderNulls","listQuery","listArgs","keys","total","countResult","randomUUID","createPostgresLock","pool","options","lockTableName","safeIndexName","ensureLockTable","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","release","createStatePersistence","createPostgresPersistence","options","prefix","lockOptions","storeOptions","stripPrefix","key","effectivePrefix","prefixKey","runId","addListPageAndDeleteMany","statePersistence","store","keys","total","ids","limit","nextOffset","pool","PgPool","PostgresKeyValueStore","lock","createPostgresLock","base"]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "awaitly-postgres",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "description": "PostgreSQL persistence adapter for awaitly workflows",
6
6
  "main": "./dist/index.cjs",
@@ -38,20 +38,20 @@
38
38
  },
39
39
  "license": "MIT",
40
40
  "peerDependencies": {
41
- "awaitly": "^1.13.0"
41
+ "awaitly": "^1.14.0"
42
42
  },
43
43
  "dependencies": {
44
- "pg": "^8.17.2"
44
+ "pg": "^8.18.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@total-typescript/ts-reset": "^0.6.1",
48
48
  "@total-typescript/tsconfig": "^1.0.4",
49
- "@types/node": "^25.1.0",
49
+ "@types/node": "^25.2.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.13.0"
54
+ "awaitly": "^1.14.0"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public",