awaitly-postgres 20.0.0 → 22.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 +7 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var T=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var _=Object.prototype.hasOwnProperty;var L=(r,t)=>{for(var e in t)T(r,e,{get:t[e],enumerable:!0})},N=(r,t,e,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let l of y(t))!_.call(r,l)&&l!==e&&T(r,l,{get:()=>t[l],enumerable:!(a=k(t,l))||a.enumerable});return r};var R=r=>N(T({},"__esModule",{value:!0}),r);var O={};L(O,{postgres:()=>I});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 l(){await r.query(`
|
|
2
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
7
|
CREATE INDEX IF NOT EXISTS ${a} ON ${e}(expires_at);
|
|
8
|
-
`)}async function m(
|
|
8
|
+
`)}async function m(d,n){let c=n?.ttlMs??6e4,s=(0,f.randomUUID)(),o=new Date(Date.now()+c);await l();let p=await r.query(`
|
|
9
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
|
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
expires_at = EXCLUDED.expires_at
|
|
14
14
|
WHERE ${e}.expires_at < NOW()
|
|
15
15
|
RETURNING owner_token
|
|
16
|
-
`,[
|
|
16
|
+
`,[d,s,o]);return p.rowCount===1&&p.rows[0].owner_token===s?{ownerToken:s}:null}async function u(d,n){await r.query(`DELETE FROM ${e} WHERE workflow_id = $1 AND owner_token = $2`,[d,n])}async function E(d,n,c){let s=c?.ttlMs??6e4,o=new Date(Date.now()+s);return((await r.query(`UPDATE ${e} SET expires_at = $1 WHERE workflow_id = $2 AND owner_token = $3`,[o,d,n])).rowCount??0)>0}return{tryAcquire:m,release:u,renew:E,ensureLockTable:l}}var A=/^[a-zA-Z_][a-zA-Z0-9_]*$/;function I(r){let t=typeof r=="string"?{url:r}:r,e=t.table??"awaitly_snapshots";if(!A.test(e))throw new Error(`Invalid table name: ${e}. Must be alphanumeric with underscores.`);let a=t.prefix??"",l=t.autoCreateTable??!0,m=!t.pool,u=t.pool??new P.Pool({connectionString:t.url}),E=!1,d=async()=>{!l||E||(await u.query(`
|
|
17
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
|
|
22
|
+
`),await u.query(`
|
|
23
23
|
CREATE INDEX IF NOT EXISTS ${e}_updated_at_idx ON ${e} (updated_at DESC)
|
|
24
|
-
`),
|
|
24
|
+
`),E=!0)},n=t.lock?g(u,t.lock):null,c={async save(s,o){await d();let p=a+s,w=(0,i.isResumeState)(o)?(0,i.serializeResumeState)(o):o;await u.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()`,[p,JSON.stringify(w)])},async load(s){await d();let o=a+s,p=await u.query(`SELECT snapshot FROM ${e} WHERE id = $1`,[o]);if(p.rows.length===0)return null;let w=p.rows[0].snapshot;return(0,i.isSerializedResumeState)(w)?(0,i.deserializeResumeState)(w):((0,i.isWorkflowSnapshot)(w),w)},async loadResumeState(s){let o=await c.load(s);return o===null?null:(0,i.isResumeState)(o)?o:null},async delete(s){await d();let o=a+s;await u.query(`DELETE FROM ${e} WHERE id = $1`,[o])},async list(s){await d();let o=a+(s?.prefix??""),p=s?.limit??100;return(await u.query(`SELECT id, updated_at FROM ${e}
|
|
27
27
|
WHERE id LIKE $1
|
|
28
28
|
ORDER BY updated_at DESC
|
|
29
|
-
LIMIT $2`,[o+"%",
|
|
29
|
+
LIMIT $2`,[o+"%",p])).rows.map(S=>({id:S.id.slice(a.length),updatedAt:S.updated_at.toISOString()}))},async close(){m&&await u.end()}};return n&&(c.tryAcquire=n.tryAcquire.bind(n),c.release=n.release.bind(n),c.renew=n.renew.bind(n)),c}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 * 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"]}
|
|
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 store.renew = lock.renew.bind(lock);\n }\n\n return store;\n}\n","/**\n * PostgreSQL workflow lock (lease) for cross-process concurrency control.\n * Uses a lease (TTL) + owner token; release verifies the token.\n */\n\nimport type { Pool } from \"pg\";\nimport { randomUUID } from \"node:crypto\";\n\nexport interface PostgresLockOptions {\n /**\n * Table name for workflow locks.\n * @default 'awaitly_workflow_lock'\n */\n lockTableName?: string;\n}\n\n/**\n * Create tryAcquire and release functions that use a PostgreSQL lock table.\n * Caller must pass the same pool used for state (so one connection pool).\n */\nexport function createPostgresLock(\n pool: Pool,\n options: PostgresLockOptions = {}\n): {\n tryAcquire(\n id: string,\n opts?: { ttlMs?: number }\n ): Promise<{ ownerToken: string } | null>;\n release(id: string, ownerToken: string): Promise<void>;\n renew(id: string, ownerToken: string, opts?: { ttlMs?: number }): Promise<boolean>;\n ensureLockTable(): Promise<void>;\n} {\n const lockTableName = options.lockTableName ?? \"awaitly_workflow_lock\";\n\n const safeIndexName = `idx_${lockTableName.replace(/[^a-zA-Z0-9_]/g, \"_\")}_expires_at`;\n\n 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 async function renew(\n id: string,\n ownerToken: string,\n opts?: { ttlMs?: number }\n ): Promise<boolean> {\n const ttlMs = opts?.ttlMs ?? 60_000;\n const expiresAt = new Date(Date.now() + ttlMs);\n\n const result = await pool.query(\n `UPDATE ${lockTableName} SET expires_at = $1 WHERE workflow_id = $2 AND owner_token = $3`,\n [expiresAt, id, ownerToken]\n );\n\n return (result.rowCount ?? 0) > 0;\n }\n\n return { tryAcquire, release, renew, ensureLockTable };\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,cAAAE,IAAA,eAAAC,EAAAH,GAQA,IAAAI,EAA+B,cAG/BC,EASO,4BCdP,IAAAC,EAA2B,kBAcpB,SAASC,EACdC,EACAC,EAA+B,CAAC,EAShC,CACA,IAAMC,EAAgBD,EAAQ,eAAiB,wBAEzCE,EAAgB,OAAOD,EAAc,QAAQ,iBAAkB,GAAG,CAAC,cAEzE,eAAeE,GAAiC,CAC9C,MAAMJ,EAAK,MAAM;AAAA,mCACcE,CAAa;AAAA;AAAA;AAAA;AAAA;AAAA,mCAKbC,CAAa,OAAOD,CAAa;AAAA,KAC/D,CACH,CAEA,eAAeG,EACbC,EACAC,EACwC,CACxC,IAAMC,EAAQD,GAAM,OAAS,IACvBE,KAAa,cAAW,EACxBC,EAAY,IAAI,KAAK,KAAK,IAAI,EAAIF,CAAK,EAE7C,MAAMJ,EAAgB,EAGtB,IAAMO,EAAS,MAAMX,EAAK,MACxB;AAAA,oBACcE,CAAa;AAAA;AAAA;AAAA;AAAA;AAAA,cAKnBA,CAAa;AAAA;AAAA,MAGrB,CAACI,EAAIG,EAAYC,CAAS,CAC5B,EAEA,OAAIC,EAAO,WAAa,GAAKA,EAAO,KAAK,CAAC,EAAE,cAAgBF,EACnD,CAAE,WAAAA,CAAW,EAEf,IACT,CAEA,eAAeG,EAAQN,EAAYG,EAAmC,CACpE,MAAMT,EAAK,MACT,eAAeE,CAAa,+CAC5B,CAACI,EAAIG,CAAU,CACjB,CACF,CAEA,eAAeI,EACbP,EACAG,EACAF,EACkB,CAClB,IAAMC,EAAQD,GAAM,OAAS,IACvBG,EAAY,IAAI,KAAK,KAAK,IAAI,EAAIF,CAAK,EAO7C,QALe,MAAMR,EAAK,MACxB,UAAUE,CAAa,mEACvB,CAACQ,EAAWJ,EAAIG,CAAU,CAC5B,GAEe,UAAY,GAAK,CAClC,CAEA,MAAO,CAAE,WAAAJ,EAAY,QAAAO,EAAS,MAAAC,EAAO,gBAAAT,CAAgB,CACvD,CD9CA,IAAMU,EAAkB,2BA6CjB,SAASC,EAASC,EAAuD,CAC9E,IAAMC,EAAO,OAAOD,GAAiB,SAAW,CAAE,IAAKA,CAAa,EAAIA,EAClEE,EAAYD,EAAK,OAAS,oBAEhC,GAAI,CAACH,EAAgB,KAAKI,CAAS,EACjC,MAAM,IAAI,MAAM,uBAAuBA,CAAS,0CAA0C,EAG5F,IAAMC,EAASF,EAAK,QAAU,GACxBG,EAAkBH,EAAK,iBAAmB,GAG1CI,EAAU,CAACJ,EAAK,KAChBK,EAAOL,EAAK,MAAQ,IAAI,EAAAM,KAAO,CAAE,iBAAkBN,EAAK,GAAI,CAAC,EAC/DO,EAAe,GAEbC,EAAc,SAA2B,CACzC,CAACL,GAAmBI,IACxB,MAAMF,EAAK,MAAM;AAAA,mCACcJ,CAAS;AAAA;AAAA;AAAA;AAAA;AAAA,KAKvC,EACD,MAAMI,EAAK,MAAM;AAAA,mCACcJ,CAAS,sBAAsBA,CAAS;AAAA,KACtE,EACDM,EAAe,GACjB,EAGME,EAAOT,EAAK,KAAOU,EAAmBL,EAAML,EAAK,IAAI,EAAI,KAEzDW,EAAuB,CAC3B,MAAM,KAAKC,EAAYC,EAAsC,CAC3D,MAAML,EAAY,EAClB,IAAMM,EAASZ,EAASU,EAClBG,KAAU,iBAAcF,CAAK,KAAI,wBAAqBA,CAAK,EAAIA,EACrE,MAAMR,EAAK,MACT,eAAeJ,CAAS;AAAA;AAAA,2EAGxB,CAACa,EAAQ,KAAK,UAAUC,CAAO,CAAC,CAClC,CACF,EAEA,MAAM,KAAKH,EAAsC,CAC/C,MAAMJ,EAAY,EAClB,IAAMM,EAASZ,EAASU,EAClBI,EAAS,MAAMX,EAAK,MACxB,wBAAwBJ,CAAS,iBACjC,CAACa,CAAM,CACT,EACA,GAAIE,EAAO,KAAK,SAAW,EAAG,OAAO,KACrC,IAAMC,EAAMD,EAAO,KAAK,CAAC,EAAE,SAC3B,SAAI,2BAAwBC,CAAG,KAAU,0BAAuBA,CAAG,MAC/D,sBAAmBA,CAAG,EAAUA,EAEtC,EAEA,MAAM,gBAAgBL,EAAyC,CAC7D,IAAMM,EAAS,MAAMP,EAAM,KAAKC,CAAE,EAClC,OAAIM,IAAW,KAAa,QACxB,iBAAcA,CAAM,EAAUA,EAC3B,IACT,EAEA,MAAM,OAAON,EAA2B,CACtC,MAAMJ,EAAY,EAClB,IAAMM,EAASZ,EAASU,EACxB,MAAMP,EAAK,MAAM,eAAeJ,CAAS,iBAAkB,CAACa,CAAM,CAAC,CACrE,EAEA,MAAM,KAAKK,EAAkG,CAC3G,MAAMX,EAAY,EAClB,IAAMY,EAAelB,GAAUiB,GAAS,QAAU,IAC5CE,EAAQF,GAAS,OAAS,IAUhC,OARe,MAAMd,EAAK,MACxB,8BAA8BJ,CAAS;AAAA;AAAA;AAAA,mBAIvC,CAACmB,EAAe,IAAKC,CAAK,CAC5B,GAEc,KAAK,IAAIC,IAAQ,CAC7B,GAAKA,EAAI,GAAc,MAAMpB,EAAO,MAAM,EAC1C,UAAYoB,EAAI,WAAoB,YAAY,CAClD,EAAE,CACJ,EAEA,MAAM,OAAuB,CAEvBlB,GACF,MAAMC,EAAK,IAAI,CAEnB,CACF,EAGA,OAAII,IACFE,EAAM,WAAaF,EAAK,WAAW,KAAKA,CAAI,EAC5CE,EAAM,QAAUF,EAAK,QAAQ,KAAKA,CAAI,EACtCE,EAAM,MAAQF,EAAK,MAAM,KAAKA,CAAI,GAG7BE,CACT","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","renew","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.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import{Pool as g}from"pg";import{isWorkflowSnapshot as P,isResumeState as
|
|
1
|
+
import{Pool as g}from"pg";import{isWorkflowSnapshot as P,isResumeState as S,isSerializedResumeState as k,serializeResumeState as y,deserializeResumeState as _}from"awaitly/workflow";import{randomUUID as f}from"crypto";function T(u,s={}){let e=s.lockTableName??"awaitly_workflow_lock",p=`idx_${e.replace(/[^a-zA-Z0-9_]/g,"_")}_expires_at`;async function c(){await u.query(`
|
|
2
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 E(
|
|
7
|
+
CREATE INDEX IF NOT EXISTS ${p} ON ${e}(expires_at);
|
|
8
|
+
`)}async function E(i,r){let l=r?.ttlMs??6e4,o=f(),t=new Date(Date.now()+l);await c();let a=await u.query(`
|
|
9
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
|
|
@@ -13,18 +13,18 @@ import{Pool as g}from"pg";import{isWorkflowSnapshot as P,isResumeState as T,isSe
|
|
|
13
13
|
expires_at = EXCLUDED.expires_at
|
|
14
14
|
WHERE ${e}.expires_at < NOW()
|
|
15
15
|
RETURNING owner_token
|
|
16
|
-
`,[
|
|
16
|
+
`,[i,o,t]);return a.rowCount===1&&a.rows[0].owner_token===o?{ownerToken:o}:null}async function n(i,r){await u.query(`DELETE FROM ${e} WHERE workflow_id = $1 AND owner_token = $2`,[i,r])}async function w(i,r,l){let o=l?.ttlMs??6e4,t=new Date(Date.now()+o);return((await u.query(`UPDATE ${e} SET expires_at = $1 WHERE workflow_id = $2 AND owner_token = $3`,[t,i,r])).rowCount??0)>0}return{tryAcquire:E,release:n,renew:w,ensureLockTable:c}}var L=/^[a-zA-Z_][a-zA-Z0-9_]*$/;function $(u){let s=typeof u=="string"?{url:u}:u,e=s.table??"awaitly_snapshots";if(!L.test(e))throw new Error(`Invalid table name: ${e}. Must be alphanumeric with underscores.`);let p=s.prefix??"",c=s.autoCreateTable??!0,E=!s.pool,n=s.pool??new g({connectionString:s.url}),w=!1,i=async()=>{!c||w||(await n.query(`
|
|
17
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
|
|
22
|
+
`),await n.query(`
|
|
23
23
|
CREATE INDEX IF NOT EXISTS ${e}_updated_at_idx ON ${e} (updated_at DESC)
|
|
24
|
-
`),
|
|
24
|
+
`),w=!0)},r=s.lock?T(n,s.lock):null,l={async save(o,t){await i();let a=p+o,d=S(t)?y(t):t;await n.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()`,[a,JSON.stringify(d)])},async load(o){await i();let t=p+o,a=await n.query(`SELECT snapshot FROM ${e} WHERE id = $1`,[t]);if(a.rows.length===0)return null;let d=a.rows[0].snapshot;return k(d)?_(d):(P(d),d)},async loadResumeState(o){let t=await l.load(o);return t===null?null:S(t)?t:null},async delete(o){await i();let t=p+o;await n.query(`DELETE FROM ${e} WHERE id = $1`,[t])},async list(o){await i();let t=p+(o?.prefix??""),a=o?.limit??100;return(await n.query(`SELECT id, updated_at FROM ${e}
|
|
27
27
|
WHERE id LIKE $1
|
|
28
28
|
ORDER BY updated_at DESC
|
|
29
|
-
LIMIT $2`,[t+"%",
|
|
29
|
+
LIMIT $2`,[t+"%",a])).rows.map(m=>({id:m.id.slice(p.length),updatedAt:m.updated_at.toISOString()}))},async close(){E&&await n.end()}};return r&&(l.tryAcquire=r.tryAcquire.bind(r),l.release=r.release.bind(r),l.renew=r.renew.bind(r)),l}export{$ 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 * 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"]}
|
|
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 store.renew = lock.renew.bind(lock);\n }\n\n return store;\n}\n","/**\n * PostgreSQL workflow lock (lease) for cross-process concurrency control.\n * Uses a lease (TTL) + owner token; release verifies the token.\n */\n\nimport type { Pool } from \"pg\";\nimport { randomUUID } from \"node:crypto\";\n\nexport interface PostgresLockOptions {\n /**\n * Table name for workflow locks.\n * @default 'awaitly_workflow_lock'\n */\n lockTableName?: string;\n}\n\n/**\n * Create tryAcquire and release functions that use a PostgreSQL lock table.\n * Caller must pass the same pool used for state (so one connection pool).\n */\nexport function createPostgresLock(\n pool: Pool,\n options: PostgresLockOptions = {}\n): {\n tryAcquire(\n id: string,\n opts?: { ttlMs?: number }\n ): Promise<{ ownerToken: string } | null>;\n release(id: string, ownerToken: string): Promise<void>;\n renew(id: string, ownerToken: string, opts?: { ttlMs?: number }): Promise<boolean>;\n ensureLockTable(): Promise<void>;\n} {\n const lockTableName = options.lockTableName ?? \"awaitly_workflow_lock\";\n\n const safeIndexName = `idx_${lockTableName.replace(/[^a-zA-Z0-9_]/g, \"_\")}_expires_at`;\n\n 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 async function renew(\n id: string,\n ownerToken: string,\n opts?: { ttlMs?: number }\n ): Promise<boolean> {\n const ttlMs = opts?.ttlMs ?? 60_000;\n const expiresAt = new Date(Date.now() + ttlMs);\n\n const result = await pool.query(\n `UPDATE ${lockTableName} SET expires_at = $1 WHERE workflow_id = $2 AND owner_token = $3`,\n [expiresAt, id, ownerToken]\n );\n\n return (result.rowCount ?? 0) > 0;\n }\n\n return { tryAcquire, release, renew, ensureLockTable };\n}\n"],"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","renew","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": "22.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.
|
|
41
|
+
"awaitly": "^1.32.0"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"pg": "^8.
|
|
44
|
+
"pg": "^8.20.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.3.
|
|
49
|
+
"@types/node": "^25.3.5",
|
|
50
50
|
"@types/pg": "^8.18.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.32.0"
|
|
55
55
|
},
|
|
56
56
|
"publishConfig": {
|
|
57
57
|
"access": "public",
|