awaitly-libsql 2.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
@@ -31,8 +31,8 @@ const store = await createLibSqlPersistence({
31
31
  const result = await durable.run(
32
32
  { fetchUser, createOrder },
33
33
  async (step, { fetchUser, createOrder }) => {
34
- const user = await step(() => fetchUser("123"), { key: "fetch-user" });
35
- const order = await step(() => createOrder(user), { key: "create-order" });
34
+ const user = await step("fetchUser", () => fetchUser("123"), { key: "fetch-user" });
35
+ const order = await step("createOrder", () => createOrder(user), { key: "create-order" });
36
36
  return order;
37
37
  },
38
38
  {
@@ -98,23 +98,6 @@ ON awaitly_workflow_state(expires_at);
98
98
 
99
99
  The `expires_at` column stores ISO 8601 timestamps and is used for TTL support.
100
100
 
101
- ## Using the KeyValueStore Directly
102
-
103
- If you want lower-level control, you can use `LibSqlKeyValueStore` directly
104
- and wire it into `createStatePersistence`:
105
-
106
- ```ts
107
- import { LibSqlKeyValueStore } from "awaitly-libsql";
108
- import { createStatePersistence } from "awaitly/persistence";
109
-
110
- const store = new LibSqlKeyValueStore({
111
- url: "file:./awaitly.db",
112
- tableName: "awaitly_workflow_state",
113
- });
114
-
115
- const persistence = createStatePersistence(store, "custom:prefix:");
116
- ```
117
-
118
101
  ## Requirements
119
102
 
120
103
  - Node.js >= 22
package/dist/index.cjs CHANGED
@@ -1,60 +1,32 @@
1
- "use strict";var y=Object.defineProperty;var N=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var _=Object.prototype.hasOwnProperty;var R=(n,e)=>{for(var t in e)y(n,t,{get:e[t],enumerable:!0})},q=(n,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of I(e))!_.call(n,s)&&s!==t&&y(n,s,{get:()=>e[s],enumerable:!(i=N(e,s))||i.enumerable});return n};var A=n=>q(y({},"__esModule",{value:!0}),n);var D={};R(D,{LibSqlKeyValueStore:()=>S,createLibSqlPersistence:()=>$});module.exports=A(D);var O=require("@libsql/client");var k=require("@libsql/client"),S=class{client;tableName;initialized=!1;initPromise=null;constructor(e={}){if(e.client)this.client=e.client;else{let i=e.url??"file:./awaitly.db";this.client=(0,k.createClient)({url:i,authToken:e.authToken})}let t=e.tableName??"awaitly_workflow_state";if(!/^[A-Za-z0-9_]+$/.test(t))throw new Error(`Invalid table name '${t}'. Only alphanumeric and underscore characters are allowed.`);this.tableName=t}async ensureInitialized(){if(!this.initialized)return this.initPromise?this.initPromise:(this.initPromise=(async()=>{try{await this.createTable(),this.initialized=!0}finally{this.initialized||(this.initPromise=null)}})(),this.initPromise)}async createTable(){await this.client.execute({sql:`
2
- CREATE TABLE IF NOT EXISTS ${this.tableName} (
3
- key TEXT PRIMARY KEY,
4
- value TEXT NOT NULL,
5
- expires_at TEXT,
6
- updated_at TEXT
7
- );
8
- `,args:[]}),await this.client.execute({sql:`
9
- CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expires_at
10
- ON ${this.tableName}(expires_at);
11
- `,args:[]}),await this.client.execute({sql:`
12
- CREATE INDEX IF NOT EXISTS idx_${this.tableName}_updated_at
13
- ON ${this.tableName}(updated_at);
14
- `,args:[]});try{await this.client.execute({sql:`ALTER TABLE ${this.tableName} ADD COLUMN updated_at TEXT`,args:[]})}catch{}}patternToLike(e){return e.replace(/%/g,"\\%").replace(/_/g,"\\_").replace(/\*/g,"%")}async get(e){await this.ensureInitialized();let t=new Date().toISOString(),i=await this.client.execute({sql:`
15
- SELECT value, expires_at
16
- FROM ${this.tableName}
17
- WHERE key = ?
18
- `,args:[e]});if(i.rows.length===0)return null;let s=i.rows[0],o=s.expires_at;return o&&o<=t?null:s.value??null}async set(e,t,i){await this.ensureInitialized();let s=i?.ttl&&i.ttl>0?new Date(Date.now()+i.ttl*1e3).toISOString():null,o=new Date().toISOString();await this.client.execute({sql:`
19
- INSERT INTO ${this.tableName} (key, value, expires_at, updated_at)
20
- VALUES (?, ?, ?, ?)
21
- ON CONFLICT(key) DO UPDATE SET
22
- value = excluded.value,
23
- expires_at = excluded.expires_at,
24
- updated_at = excluded.updated_at
25
- `,args:[e,t,s,o]})}async delete(e){await this.ensureInitialized();let i=(await this.client.execute({sql:`DELETE FROM ${this.tableName} WHERE key = ?`,args:[e]})).rowsAffected;return typeof i=="number"?i>0:await this.get(e)===null}async exists(e){await this.ensureInitialized();let t=new Date().toISOString();return(await this.client.execute({sql:`
26
- SELECT 1
27
- FROM ${this.tableName}
28
- WHERE key = ?
29
- AND (expires_at IS NULL OR expires_at > ?)
30
- LIMIT 1
31
- `,args:[e,t]})).rows.length>0}async keys(e){await this.ensureInitialized();let t=this.patternToLike(e),i=new Date().toISOString();return(await this.client.execute({sql:`
32
- SELECT key
33
- FROM ${this.tableName}
34
- WHERE key LIKE ? ESCAPE '\\'
35
- AND (expires_at IS NULL OR expires_at > ?)
36
- `,args:[t,i]})).rows.map(o=>o.key)}async listKeys(e,t={}){await this.ensureInitialized();let i=Math.min(Math.max(0,t.limit??100),1e4),s=Math.max(0,t.offset??0),o=t.orderBy==="key"?"key":"updated_at",w=t.orderDir==="asc"?"ASC":"DESC",u=this.patternToLike(e),d=new Date().toISOString(),m=["key LIKE ? ESCAPE '\\'","(expires_at IS NULL OR expires_at > ?)"],r=[u,d],l=3;t.updatedBefore!=null&&(m.push("updated_at < ?"),r.push(t.updatedBefore.toISOString()),l++),t.updatedAfter!=null&&(m.push("updated_at > ?"),r.push(t.updatedAfter.toISOString()),l++);let a=m.join(" AND "),c=o==="updated_at"?" NULLS LAST":"",p=[...r,i,s],g=`
37
- SELECT key
38
- FROM ${this.tableName}
39
- WHERE ${a}
40
- ORDER BY ${o} ${w}${c}
41
- LIMIT ? OFFSET ?
42
- `,E=(await this.client.execute({sql:g,args:p})).rows.map(b=>b.key),h;if(t.includeTotal===!0||s>0){let f=(await this.client.execute({sql:`SELECT COUNT(*) AS count FROM ${this.tableName} WHERE ${a}`,args:r})).rows[0];h=typeof f?.count=="number"?f.count:parseInt(String(f?.count??"0"),10)}return{keys:E,total:h}}async deleteMany(e){if(e.length===0)return 0;await this.ensureInitialized();let t=e.map(()=>"?").join(", ");return(await this.client.execute({sql:`DELETE FROM ${this.tableName} WHERE key IN (${t})`,args:e})).rowsAffected??0}async clear(){await this.ensureInitialized(),await this.client.execute({sql:`DELETE FROM ${this.tableName}`,args:[]})}async close(){}};var x=require("crypto");function P(n,e={}){let t=e.lockTableName??"awaitly_workflow_lock",i=t.replace(/[^a-zA-Z0-9_]/g,"_");async function s(){await n.execute({sql:`
43
- CREATE TABLE IF NOT EXISTS ${t} (
1
+ "use strict";var w=Object.defineProperty;var S=Object.getOwnPropertyDescriptor;var L=Object.getOwnPropertyNames;var g=Object.prototype.hasOwnProperty;var _=(o,t)=>{for(var e in t)w(o,e,{get:t[e],enumerable:!0})},x=(o,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of L(t))!g.call(o,n)&&n!==e&&w(o,n,{get:()=>t[n],enumerable:!(i=S(t,n))||i.enumerable});return o};var N=o=>x(w({},"__esModule",{value:!0}),o);var y={};_(y,{libsql:()=>b});module.exports=N(y);var m=require("@libsql/client");var f=require("crypto");function k(o,t={}){let e=t.lockTableName??"awaitly_workflow_lock",i=e.replace(/[^a-zA-Z0-9_]/g,"_");async function n(){await o.execute({sql:`
2
+ CREATE TABLE IF NOT EXISTS ${e} (
44
3
  workflow_id TEXT PRIMARY KEY,
45
4
  owner_token TEXT NOT NULL,
46
5
  expires_at TEXT NOT NULL
47
6
  )
48
- `,args:[]}),await n.execute({sql:`
7
+ `,args:[]}),await o.execute({sql:`
49
8
  CREATE INDEX IF NOT EXISTS idx_${i}_expires_at
50
- ON ${t}(expires_at)
51
- `,args:[]})}async function o(u,d){let m=d?.ttlMs??6e4,r=(0,x.randomUUID)(),l=new Date(Date.now()+m).toISOString();await s();let a=await n.execute({sql:`
52
- INSERT INTO ${t} (workflow_id, owner_token, expires_at)
9
+ ON ${e}(expires_at)
10
+ `,args:[]})}async function s(l,p){let u=p?.ttlMs??6e4,r=(0,f.randomUUID)(),a=new Date(Date.now()+u).toISOString();await n();let c=await o.execute({sql:`
11
+ INSERT INTO ${e} (workflow_id, owner_token, expires_at)
53
12
  VALUES (?, ?, ?)
54
13
  ON CONFLICT(workflow_id) DO UPDATE SET
55
14
  owner_token = excluded.owner_token,
56
15
  expires_at = excluded.expires_at
57
- WHERE ${t}.expires_at < datetime('now')
16
+ WHERE ${e}.expires_at < datetime('now')
58
17
  RETURNING owner_token
59
- `,args:[u,r,l]}),c=a.rows[0];return a.rows.length===1&&c?.owner_token===r?{ownerToken:r}:null}async function w(u,d){await n.execute({sql:`DELETE FROM ${t} WHERE workflow_id = ? AND owner_token = ?`,args:[u,d]})}return{tryAcquire:o,release:w,ensureLockTable:s}}var L=require("awaitly/persistence");async function $(n={}){let{prefix:e,lock:t,...i}=n,s=e??"workflow:state:",o=r=>r.slice(s.length),w=r=>`${s}${r}`,u=(r,l)=>Object.assign(r,{async listPage(a={}){let{keys:c,total:p}=await l.listKeys(`${s}*`,a),g=c.map(o),T=Math.min(Math.max(0,a.limit??100),1e4),E=g.length===T?(a.offset??0)+g.length:void 0;return{ids:g,total:p,nextOffset:E}},async deleteMany(a){if(a.length===0)return 0;let c=a.map(w);return l.deleteMany(c)},async clear(){return l.clear()}});if(t!==void 0){let r=i.client??(0,O.createClient)({url:i.url??"file:./awaitly.db",authToken:i.authToken}),l=new S({...i,client:r}),a=(0,L.createStatePersistence)(l,e),c=u(a,l),p=P(r,t);return Object.assign(c,{tryAcquire:p.tryAcquire.bind(p),release:p.release.bind(p)})}let d=new S(i),m=(0,L.createStatePersistence)(d,e);return u(m,d)}0&&(module.exports={LibSqlKeyValueStore,createLibSqlPersistence});
18
+ `,args:[l,r,a]}),T=c.rows[0];return c.rows.length===1&&T?.owner_token===r?{ownerToken:r}:null}async function d(l,p){await o.execute({sql:`DELETE FROM ${e} WHERE workflow_id = ? AND owner_token = ?`,args:[l,p]})}return{tryAcquire:s,release:d,ensureLockTable:n}}function b(o){let t=typeof o=="string"?{url:o}:o,e=t.table??"awaitly_snapshots",i=t.prefix??"";if(!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(e))throw new Error(`Invalid table name: ${e}. Must be alphanumeric with underscores.`);let n=!t.client,s=t.client??(0,m.createClient)({url:t.url,authToken:t.authToken}),d=!1,l=t.lock?k(s,t.lock):null,p=async()=>{d||(await s.execute(`
19
+ CREATE TABLE IF NOT EXISTS ${e} (
20
+ id TEXT PRIMARY KEY,
21
+ snapshot TEXT NOT NULL,
22
+ updated_at TEXT DEFAULT (datetime('now'))
23
+ )
24
+ `),await s.execute(`
25
+ CREATE INDEX IF NOT EXISTS ${e}_updated_at_idx ON ${e} (updated_at DESC)
26
+ `),d=!0)},u={async save(r,a){await p();let c=i+r;await s.execute({sql:`INSERT INTO ${e} (id, snapshot, updated_at)
27
+ VALUES (?, ?, datetime('now'))
28
+ ON CONFLICT(id) DO UPDATE SET snapshot = ?, updated_at = datetime('now')`,args:[c,JSON.stringify(a),JSON.stringify(a)]})},async load(r){await p();let a=i+r,c=await s.execute({sql:`SELECT snapshot FROM ${e} WHERE id = ?`,args:[a]});return c.rows.length===0?null:JSON.parse(c.rows[0].snapshot)},async delete(r){await p();let a=i+r;await s.execute({sql:`DELETE FROM ${e} WHERE id = ?`,args:[a]})},async list(r){await p();let a=i+(r?.prefix??""),c=r?.limit??100;return(await s.execute({sql:`SELECT id, updated_at FROM ${e}
29
+ WHERE id LIKE ?
30
+ ORDER BY updated_at DESC
31
+ LIMIT ?`,args:[a+"%",c]})).rows.map(E=>({id:E.id.slice(i.length),updatedAt:E.updated_at}))},async close(){n&&s.close()}};return l&&(u.tryAcquire=l.tryAcquire.bind(l),u.release=l.release.bind(l)),u}0&&(module.exports={libsql});
60
32
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/libsql-store.ts","../src/libsql-lock.ts"],"sourcesContent":["/**\n * awaitly-libsql\n *\n * libSQL / SQLite persistence adapter for awaitly workflows.\n * Provides ready-to-use StatePersistence backed by libSQL.\n */\n\nimport { createClient } from \"@libsql/client\";\nimport { LibSqlKeyValueStore, type LibSqlKeyValueStoreOptions } from \"./libsql-store\";\nimport { createLibSqlLock, type LibSqlLockOptions } from \"./libsql-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 type { LibSqlLockOptions } from \"./libsql-lock\";\n\n/**\n * Options for creating libSQL persistence.\n */\nexport interface LibSqlPersistenceOptions extends LibSqlKeyValueStoreOptions {\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?: LibSqlLockOptions;\n}\n\n/**\n * Create a StatePersistence instance backed by libSQL / SQLite.\n *\n * The table is automatically created on first use.\n *\n * @param options - libSQL connection and configuration options\n * @returns StatePersistence instance ready to use with durable.run()\n *\n * @example\n * ```typescript\n * import { createLibSqlPersistence } from \"awaitly-libsql\";\n * import { durable } from \"awaitly/durable\";\n *\n * const store = await createLibSqlPersistence({\n * url: \"file:./awaitly.db\",\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 remote Turso (libSQL) instance\n * const store = await createLibSqlPersistence({\n * url: process.env.LIBSQL_URL!,\n * authToken: process.env.LIBSQL_AUTH_TOKEN,\n * tableName: \"awaitly_workflow_state\",\n * });\n * ```\n */\nexport type LibSqlStatePersistence = 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 LibSqlStatePersistenceWithLock = LibSqlStatePersistence & WorkflowLock;\n\nexport async function createLibSqlPersistence(\n options: LibSqlPersistenceOptions = {}\n): Promise<LibSqlStatePersistence | LibSqlStatePersistenceWithLock> {\n const { prefix, lock: lockOptions, ...storeOptions } = options;\n\n const effectivePrefix = prefix ?? \"workflow:state:\";\n const stripPrefix = (key: string): string => key.slice(effectivePrefix.length);\n const prefixKey = (runId: string): string => `${effectivePrefix}${runId}`;\n\n const addExtensions = (\n base: StatePersistence & { loadRaw(runId: string): Promise<SerializedState | undefined> },\n store: LibSqlKeyValueStore\n ): LibSqlStatePersistence =>\n Object.assign(base, {\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 if (lockOptions !== undefined) {\n const client =\n storeOptions.client ??\n createClient({\n url: storeOptions.url ?? \"file:./awaitly.db\",\n authToken: storeOptions.authToken,\n });\n const store = new LibSqlKeyValueStore({ ...storeOptions, client });\n const base = createStatePersistence(store, prefix) as LibSqlStatePersistence;\n const persistence = addExtensions(\n base as StatePersistence & { loadRaw(runId: string): Promise<SerializedState | undefined> },\n store\n );\n const lock = createLibSqlLock(client, lockOptions);\n return Object.assign(persistence, {\n tryAcquire: lock.tryAcquire.bind(lock),\n release: lock.release.bind(lock),\n });\n }\n\n const store = new LibSqlKeyValueStore(storeOptions);\n const base = createStatePersistence(store, prefix);\n return addExtensions(\n base as StatePersistence & { loadRaw(runId: string): Promise<SerializedState | undefined> },\n store\n );\n}\n\n/**\n * libSQL KeyValueStore implementation.\n * Use this directly if you need more control over the store.\n *\n * @example\n * ```typescript\n * import { LibSqlKeyValueStore } from \"awaitly-libsql\";\n * import { createStatePersistence } from \"awaitly/persistence\";\n *\n * const store = new LibSqlKeyValueStore({\n * url: \"file:./awaitly.db\",\n * });\n *\n * const persistence = createStatePersistence(store, \"custom:prefix:\");\n * ```\n */\nexport { LibSqlKeyValueStore, type LibSqlKeyValueStoreOptions };\n\n","/**\n * awaitly-libsql\n *\n * libSQL / SQLite KeyValueStore implementation for awaitly persistence.\n */\n\nimport { createClient, type Client } from \"@libsql/client\";\nimport type { KeyValueStore, ListPageOptions } from \"awaitly/persistence\";\n\n/**\n * Options for libSQL / SQLite KeyValueStore.\n */\nexport interface LibSqlKeyValueStoreOptions {\n /**\n * libSQL database URL.\n *\n * Examples:\n * - \"file:./awaitly.db\" (local file)\n * - \":memory:\" (in-memory, for tests)\n * - \"libsql://your-db.turso.io\" (remote)\n *\n * @default \"file:./awaitly.db\"\n */\n url?: string;\n\n /**\n * Authentication token for remote libSQL databases (e.g. Turso).\n */\n authToken?: string;\n\n /**\n * Table name for storing key-value pairs.\n * @default \"awaitly_workflow_state\"\n */\n tableName?: string;\n\n /**\n * Existing libSQL client to use.\n * If provided, url/authToken options are ignored.\n */\n client?: Client;\n}\n\n/**\n * libSQL / SQLite implementation of KeyValueStore.\n *\n * Automatically creates the required table on first use.\n * Supports TTL via ISO 8601 `expires_at` column.\n */\nexport class LibSqlKeyValueStore implements KeyValueStore {\n private client: Client;\n private tableName: string;\n private initialized = false;\n private initPromise: Promise<void> | null = null;\n\n constructor(options: LibSqlKeyValueStoreOptions = {}) {\n if (options.client) {\n this.client = options.client;\n } else {\n const url = options.url ?? \"file:./awaitly.db\";\n this.client = createClient({\n url,\n authToken: options.authToken,\n });\n }\n\n const tableName = options.tableName ?? \"awaitly_workflow_state\";\n if (!/^[A-Za-z0-9_]+$/.test(tableName)) {\n throw new Error(\n `Invalid table name '${tableName}'. Only alphanumeric and underscore characters are allowed.`\n );\n }\n this.tableName = tableName;\n }\n\n /**\n * Initialize the store by creating the table and index if they don'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 } finally {\n if (!this.initialized) {\n this.initPromise = null;\n }\n }\n })();\n\n return this.initPromise;\n }\n\n /**\n * Create the table and index if they don't exist.\n */\n private async createTable(): Promise<void> {\n // SQLite / libSQL: execute schema changes as separate statements\n await this.client.execute({\n sql: `\n CREATE TABLE IF NOT EXISTS ${this.tableName} (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n expires_at TEXT,\n updated_at TEXT\n );\n `,\n args: [],\n });\n\n await this.client.execute({\n sql: `\n CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expires_at\n ON ${this.tableName}(expires_at);\n `,\n args: [],\n });\n\n await this.client.execute({\n sql: `\n CREATE INDEX IF NOT EXISTS idx_${this.tableName}_updated_at\n ON ${this.tableName}(updated_at);\n `,\n args: [],\n });\n\n // Add updated_at to existing tables (ignore if column already exists)\n try {\n await this.client.execute({\n sql: `ALTER TABLE ${this.tableName} ADD COLUMN updated_at TEXT`,\n args: [],\n });\n } catch {\n // Column may already exist\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 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 nowIso = new Date().toISOString();\n const result = await this.client.execute({\n sql: `\n SELECT value, expires_at\n FROM ${this.tableName}\n WHERE key = ?\n `,\n args: [key],\n });\n\n if (result.rows.length === 0) {\n return null;\n }\n\n const row = result.rows[0] as Record<string, unknown>;\n const expiresAt = row[\"expires_at\"] as string | null | undefined;\n\n if (expiresAt && expiresAt <= nowIso) {\n // Expired - behave as if key doesn't exist\n return null;\n }\n\n return (row[\"value\"] as string) ?? null;\n }\n\n async set(key: string, value: string, options?: { ttl?: number }): Promise<void> {\n await this.ensureInitialized();\n\n const expiresAt =\n options?.ttl && options.ttl > 0\n ? new Date(Date.now() + options.ttl * 1000).toISOString()\n : null;\n const updatedAt = new Date().toISOString();\n\n await this.client.execute({\n sql: `\n INSERT INTO ${this.tableName} (key, value, expires_at, updated_at)\n VALUES (?, ?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET\n value = excluded.value,\n expires_at = excluded.expires_at,\n updated_at = excluded.updated_at\n `,\n args: [key, value, expiresAt, updatedAt],\n });\n }\n\n async delete(key: string): Promise<boolean> {\n await this.ensureInitialized();\n\n const result = await this.client.execute({\n sql: `DELETE FROM ${this.tableName} WHERE key = ?`,\n args: [key],\n });\n\n // libSQL .rowsAffected is available on hrana responses; fall back to > 0 check\n const affected: number | undefined = result.rowsAffected;\n if (typeof affected === \"number\") {\n return affected > 0;\n }\n\n // If rowsAffected is not available, perform an existence check as a fallback\n const after = await this.get(key);\n return after === null;\n }\n\n async exists(key: string): Promise<boolean> {\n await this.ensureInitialized();\n\n const nowIso = new Date().toISOString();\n const result = await this.client.execute({\n sql: `\n SELECT 1\n FROM ${this.tableName}\n WHERE key = ?\n AND (expires_at IS NULL OR expires_at > ?)\n LIMIT 1\n `,\n args: [key, nowIso],\n });\n\n return result.rows.length > 0;\n }\n\n async keys(pattern: string): Promise<string[]> {\n await this.ensureInitialized();\n\n const likePattern = this.patternToLike(pattern);\n const nowIso = new Date().toISOString();\n\n const result = await this.client.execute({\n sql: `\n SELECT key\n FROM ${this.tableName}\n WHERE key LIKE ? ESCAPE '\\\\'\n AND (expires_at IS NULL OR expires_at > ?)\n `,\n args: [likePattern, nowIso],\n });\n\n return result.rows.map((row) => (row as Record<string, unknown>)[\"key\"] as string);\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 const nowIso = new Date().toISOString();\n\n const conditions: string[] = [\n \"key LIKE ? ESCAPE '\\\\'\",\n \"(expires_at IS NULL OR expires_at > ?)\",\n ];\n const args: (string | number)[] = [likePattern, nowIso];\n let paramIndex = 3;\n\n if (options.updatedBefore != null) {\n conditions.push(`updated_at < ?`);\n args.push(options.updatedBefore.toISOString());\n paramIndex++;\n }\n if (options.updatedAfter != null) {\n conditions.push(`updated_at > ?`);\n args.push(options.updatedAfter.toISOString());\n paramIndex++;\n }\n\n const whereClause = conditions.join(\" AND \");\n const orderNulls = orderBy === \"updated_at\" ? \" NULLS LAST\" : \"\";\n\n const listArgs = [...args, limit, offset];\n const listQuery = `\n SELECT key\n FROM ${this.tableName}\n WHERE ${whereClause}\n ORDER BY ${orderBy} ${orderDir}${orderNulls}\n LIMIT ? OFFSET ?\n `;\n\n const result = await this.client.execute({\n sql: listQuery,\n args: listArgs,\n });\n const keys = result.rows.map((row) => (row as Record<string, unknown>)[\"key\"] as string);\n\n let total: number | undefined;\n if (options.includeTotal === true || offset > 0) {\n const countResult = await this.client.execute({\n sql: `SELECT COUNT(*) AS count FROM ${this.tableName} WHERE ${whereClause}`,\n args,\n });\n const countRow = countResult.rows[0] as Record<string, unknown>;\n total = typeof countRow?.count === \"number\" ? countRow.count : parseInt(String(countRow?.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 placeholders = keys.map(() => \"?\").join(\", \");\n const result = await this.client.execute({\n sql: `DELETE FROM ${this.tableName} WHERE key IN (${placeholders})`,\n args: keys,\n });\n return result.rowsAffected ?? 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.client.execute({\n sql: `DELETE FROM ${this.tableName}`,\n args: [],\n });\n }\n\n /**\n * Close the underlying client if it supports close().\n */\n async close(): Promise<void> {\n // libSQL client doesn't expose a close API in all runtimes; no-op for now.\n // If a future version adds client.close(), it can be wired here.\n }\n}\n\n","/**\n * libSQL workflow lock (lease) for cross-process concurrency control.\n * Uses a lease (TTL) + owner token; release verifies the token.\n */\n\nimport type { Client } from \"@libsql/client\";\nimport { randomUUID } from \"node:crypto\";\n\nexport interface LibSqlLockOptions {\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 libSQL lock table.\n * Caller must pass the same client used for state (so one connection).\n */\nexport function createLibSqlLock(\n client: Client,\n options: LibSqlLockOptions = {}\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 safeTableName = lockTableName.replace(/[^a-zA-Z0-9_]/g, \"_\");\n\n async function ensureLockTable(): Promise<void> {\n await client.execute({\n sql: `\n CREATE TABLE IF NOT EXISTS ${lockTableName} (\n workflow_id TEXT PRIMARY KEY,\n owner_token TEXT NOT NULL,\n expires_at TEXT NOT NULL\n )\n `,\n args: [],\n });\n await client.execute({\n sql: `\n CREATE INDEX IF NOT EXISTS idx_${safeTableName}_expires_at\n ON ${lockTableName}(expires_at)\n `,\n args: [],\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).toISOString();\n\n await ensureLockTable();\n\n // Insert new row or update only if current row is expired (or missing).\n // SQLite 3.35+ / libSQL support RETURNING.\n const result = await client.execute({\n sql: `\n INSERT INTO ${lockTableName} (workflow_id, owner_token, expires_at)\n VALUES (?, ?, ?)\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 < datetime('now')\n RETURNING owner_token\n `,\n args: [id, ownerToken, expiresAt],\n });\n\n const row = result.rows[0] as Record<string, unknown> | undefined;\n if (result.rows.length === 1 && row?.owner_token === ownerToken) {\n return { ownerToken };\n }\n return null;\n }\n\n async function release(id: string, ownerToken: string): Promise<void> {\n await client.execute({\n sql: `DELETE FROM ${lockTableName} WHERE workflow_id = ? AND owner_token = ?`,\n args: [id, ownerToken],\n });\n }\n\n return { tryAcquire, release, ensureLockTable };\n}\n"],"mappings":"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","names":["index_exports","__export","LibSqlKeyValueStore","createLibSqlPersistence","__toCommonJS","import_client","import_client","LibSqlKeyValueStore","options","url","tableName","pattern","key","nowIso","result","row","expiresAt","value","updatedAt","affected","likePattern","limit","offset","orderBy","orderDir","conditions","args","paramIndex","whereClause","orderNulls","listArgs","listQuery","keys","total","countRow","placeholders","import_node_crypto","createLibSqlLock","client","options","lockTableName","safeTableName","ensureLockTable","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","row","release","import_persistence","createLibSqlPersistence","options","prefix","lockOptions","storeOptions","effectivePrefix","stripPrefix","key","prefixKey","runId","addExtensions","base","store","keys","total","ids","limit","nextOffset","client","LibSqlKeyValueStore","persistence","lock","createLibSqlLock"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/libsql-lock.ts"],"sourcesContent":["/**\n * awaitly-libsql\n *\n * libSQL / SQLite persistence adapter for awaitly workflows.\n * Provides ready-to-use SnapshotStore backed by libSQL.\n */\n\nimport { createClient, type Client } from \"@libsql/client\";\nimport type { WorkflowSnapshot, SnapshotStore } from \"awaitly/persistence\";\nimport type { WorkflowLock } from \"awaitly/durable\";\nimport { createLibSqlLock, type LibSqlLockOptions } from \"./libsql-lock\";\n\n// Re-export types for convenience\nexport type { SnapshotStore, WorkflowSnapshot } from \"awaitly/persistence\";\nexport type { WorkflowLock } from \"awaitly/durable\";\nexport type { LibSqlLockOptions } from \"./libsql-lock\";\n\n// =============================================================================\n// LibSqlOptions\n// =============================================================================\n\n/**\n * Options for the libsql() shorthand function.\n */\nexport interface LibSqlOptions {\n /** libSQL connection URL (file: for local, https: for Turso). */\n url: string;\n /** Auth token for remote Turso instances. */\n authToken?: 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 client. */\n client?: Client;\n /** Cross-process lock options. When set, the store implements WorkflowLock. */\n lock?: LibSqlLockOptions;\n}\n\n// =============================================================================\n// libsql() - One-liner Snapshot Store Setup\n// =============================================================================\n\n/**\n * Create a snapshot store backed by libSQL / SQLite.\n * This is the simplified one-liner API for workflow persistence.\n *\n * @example\n * ```typescript\n * import { libsql } from 'awaitly-libsql';\n *\n * // One-liner setup (local SQLite)\n * const store = libsql('file:./workflow.db');\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 remote Turso and cross-process locking\n * const store = libsql({\n * url: process.env.TURSO_URL!,\n * authToken: process.env.TURSO_AUTH_TOKEN,\n * table: 'my_workflow_snapshots',\n * prefix: 'orders:',\n * lock: { lockTableName: 'my_workflow_locks' },\n * });\n * ```\n */\nexport function libsql(urlOrOptions: string | LibSqlOptions): SnapshotStore & Partial<WorkflowLock> {\n const opts = typeof urlOrOptions === \"string\" ? { url: urlOrOptions } : urlOrOptions;\n const tableName = opts.table ?? \"awaitly_snapshots\";\n const prefix = opts.prefix ?? \"\";\n\n // Validate table name for SQL injection prevention\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {\n throw new Error(`Invalid table name: ${tableName}. Must be alphanumeric with underscores.`);\n }\n\n // Create or use existing client\n const ownClient = !opts.client;\n const client = opts.client ?? createClient({\n url: opts.url,\n authToken: opts.authToken,\n });\n\n let tableCreated = false;\n\n // Create lock if requested\n const lock = opts.lock ? createLibSqlLock(client, opts.lock) : null;\n\n const ensureTable = async (): Promise<void> => {\n if (tableCreated) return;\n await client.execute(`\n CREATE TABLE IF NOT EXISTS ${tableName} (\n id TEXT PRIMARY KEY,\n snapshot TEXT NOT NULL,\n updated_at TEXT DEFAULT (datetime('now'))\n )\n `);\n await client.execute(`\n CREATE INDEX IF NOT EXISTS ${tableName}_updated_at_idx ON ${tableName} (updated_at DESC)\n `);\n tableCreated = true;\n };\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 client.execute({\n sql: `INSERT INTO ${tableName} (id, snapshot, updated_at)\n VALUES (?, ?, datetime('now'))\n ON CONFLICT(id) DO UPDATE SET snapshot = ?, updated_at = datetime('now')`,\n args: [fullId, JSON.stringify(snapshot), 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 client.execute({\n sql: `SELECT snapshot FROM ${tableName} WHERE id = ?`,\n args: [fullId],\n });\n if (result.rows.length === 0) return null;\n return JSON.parse(result.rows[0].snapshot as string) as WorkflowSnapshot;\n },\n\n async delete(id: string): Promise<void> {\n await ensureTable();\n const fullId = prefix + id;\n await client.execute({\n sql: `DELETE FROM ${tableName} WHERE id = ?`,\n args: [fullId],\n });\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 client.execute({\n sql: `SELECT id, updated_at FROM ${tableName}\n WHERE id LIKE ?\n ORDER BY updated_at DESC\n LIMIT ?`,\n args: [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 string,\n }));\n },\n\n async close(): Promise<void> {\n // Only close client if we created it\n if (ownClient) {\n client.close();\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 * libSQL workflow lock (lease) for cross-process concurrency control.\n * Uses a lease (TTL) + owner token; release verifies the token.\n */\n\nimport type { Client } from \"@libsql/client\";\nimport { randomUUID } from \"node:crypto\";\n\nexport interface LibSqlLockOptions {\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 libSQL lock table.\n * Caller must pass the same client used for state (so one connection).\n */\nexport function createLibSqlLock(\n client: Client,\n options: LibSqlLockOptions = {}\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 safeTableName = lockTableName.replace(/[^a-zA-Z0-9_]/g, \"_\");\n\n async function ensureLockTable(): Promise<void> {\n await client.execute({\n sql: `\n CREATE TABLE IF NOT EXISTS ${lockTableName} (\n workflow_id TEXT PRIMARY KEY,\n owner_token TEXT NOT NULL,\n expires_at TEXT NOT NULL\n )\n `,\n args: [],\n });\n await client.execute({\n sql: `\n CREATE INDEX IF NOT EXISTS idx_${safeTableName}_expires_at\n ON ${lockTableName}(expires_at)\n `,\n args: [],\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).toISOString();\n\n await ensureLockTable();\n\n // Insert new row or update only if current row is expired (or missing).\n // SQLite 3.35+ / libSQL support RETURNING.\n const result = await client.execute({\n sql: `\n INSERT INTO ${lockTableName} (workflow_id, owner_token, expires_at)\n VALUES (?, ?, ?)\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 < datetime('now')\n RETURNING owner_token\n `,\n args: [id, ownerToken, expiresAt],\n });\n\n const row = result.rows[0] as Record<string, unknown> | undefined;\n if (result.rows.length === 1 && row?.owner_token === ownerToken) {\n return { ownerToken };\n }\n return null;\n }\n\n async function release(id: string, ownerToken: string): Promise<void> {\n await client.execute({\n sql: `DELETE FROM ${lockTableName} WHERE workflow_id = ? AND owner_token = ?`,\n args: [id, ownerToken],\n });\n }\n\n return { tryAcquire, release, ensureLockTable };\n}\n"],"mappings":"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","names":["index_exports","__export","libsql","__toCommonJS","import_client","import_node_crypto","createLibSqlLock","client","options","lockTableName","safeTableName","ensureLockTable","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","row","release","libsql","urlOrOptions","opts","tableName","prefix","ownClient","client","tableCreated","lock","createLibSqlLock","ensureTable","store","id","snapshot","fullId","result","options","filterPrefix","limit","row"]}
package/dist/index.d.cts CHANGED
@@ -1,96 +1,8 @@
1
1
  import { Client } from '@libsql/client';
2
- import { KeyValueStore, ListPageOptions, StatePersistence, SerializedState, ListPageResult } from 'awaitly/persistence';
2
+ import { SnapshotStore } from 'awaitly/persistence';
3
+ export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
3
4
  import { WorkflowLock } from 'awaitly/durable';
4
-
5
- /**
6
- * awaitly-libsql
7
- *
8
- * libSQL / SQLite KeyValueStore implementation for awaitly persistence.
9
- */
10
-
11
- /**
12
- * Options for libSQL / SQLite KeyValueStore.
13
- */
14
- interface LibSqlKeyValueStoreOptions {
15
- /**
16
- * libSQL database URL.
17
- *
18
- * Examples:
19
- * - "file:./awaitly.db" (local file)
20
- * - ":memory:" (in-memory, for tests)
21
- * - "libsql://your-db.turso.io" (remote)
22
- *
23
- * @default "file:./awaitly.db"
24
- */
25
- url?: string;
26
- /**
27
- * Authentication token for remote libSQL databases (e.g. Turso).
28
- */
29
- authToken?: string;
30
- /**
31
- * Table name for storing key-value pairs.
32
- * @default "awaitly_workflow_state"
33
- */
34
- tableName?: string;
35
- /**
36
- * Existing libSQL client to use.
37
- * If provided, url/authToken options are ignored.
38
- */
39
- client?: Client;
40
- }
41
- /**
42
- * libSQL / SQLite implementation of KeyValueStore.
43
- *
44
- * Automatically creates the required table on first use.
45
- * Supports TTL via ISO 8601 `expires_at` column.
46
- */
47
- declare class LibSqlKeyValueStore implements KeyValueStore {
48
- private client;
49
- private tableName;
50
- private initialized;
51
- private initPromise;
52
- constructor(options?: LibSqlKeyValueStoreOptions);
53
- /**
54
- * Initialize the store by creating the table and index if they don't exist.
55
- * This is called automatically on first use.
56
- */
57
- private ensureInitialized;
58
- /**
59
- * Create the table and index if they don't exist.
60
- */
61
- private createTable;
62
- /**
63
- * Convert glob pattern to SQL LIKE pattern.
64
- * Supports * wildcard (matches any characters).
65
- */
66
- private patternToLike;
67
- get(key: string): Promise<string | null>;
68
- set(key: string, value: string, options?: {
69
- ttl?: number;
70
- }): Promise<void>;
71
- delete(key: string): Promise<boolean>;
72
- exists(key: string): Promise<boolean>;
73
- keys(pattern: string): Promise<string[]>;
74
- /**
75
- * List keys with pagination, filtering, and ordering.
76
- */
77
- listKeys(pattern: string, options?: ListPageOptions): Promise<{
78
- keys: string[];
79
- total?: number;
80
- }>;
81
- /**
82
- * Delete multiple keys in one round-trip.
83
- */
84
- deleteMany(keys: string[]): Promise<number>;
85
- /**
86
- * Remove all entries from the table (clear all workflow state).
87
- */
88
- clear(): Promise<void>;
89
- /**
90
- * Close the underlying client if it supports close().
91
- */
92
- close(): Promise<void>;
93
- }
5
+ export { WorkflowLock } from 'awaitly/durable';
94
6
 
95
7
  /**
96
8
  * libSQL workflow lock (lease) for cross-process concurrency control.
@@ -109,72 +21,60 @@ interface LibSqlLockOptions {
109
21
  * awaitly-libsql
110
22
  *
111
23
  * libSQL / SQLite persistence adapter for awaitly workflows.
112
- * Provides ready-to-use StatePersistence backed by libSQL.
24
+ * Provides ready-to-use SnapshotStore backed by libSQL.
113
25
  */
114
26
 
115
27
  /**
116
- * Options for creating libSQL persistence.
28
+ * Options for the libsql() shorthand function.
117
29
  */
118
- interface LibSqlPersistenceOptions extends LibSqlKeyValueStoreOptions {
119
- /**
120
- * Key prefix for state entries.
121
- * @default "workflow:state:"
122
- */
30
+ interface LibSqlOptions {
31
+ /** libSQL connection URL (file: for local, https: for Turso). */
32
+ url: string;
33
+ /** Auth token for remote Turso instances. */
34
+ authToken?: string;
35
+ /** Table name for snapshots. @default 'awaitly_snapshots' */
36
+ table?: string;
37
+ /** Key prefix for IDs. @default '' */
123
38
  prefix?: string;
124
- /**
125
- * When set, the store implements WorkflowLock for cross-process concurrency control.
126
- * Uses a lease (TTL) + owner token; release verifies the token.
127
- */
39
+ /** Bring your own client. */
40
+ client?: Client;
41
+ /** Cross-process lock options. When set, the store implements WorkflowLock. */
128
42
  lock?: LibSqlLockOptions;
129
43
  }
130
44
  /**
131
- * Create a StatePersistence instance backed by libSQL / SQLite.
132
- *
133
- * The table is automatically created on first use.
134
- *
135
- * @param options - libSQL connection and configuration options
136
- * @returns StatePersistence instance ready to use with durable.run()
45
+ * Create a snapshot store backed by libSQL / SQLite.
46
+ * This is the simplified one-liner API for workflow persistence.
137
47
  *
138
48
  * @example
139
49
  * ```typescript
140
- * import { createLibSqlPersistence } from "awaitly-libsql";
141
- * import { durable } from "awaitly/durable";
50
+ * import { libsql } from 'awaitly-libsql';
142
51
  *
143
- * const store = await createLibSqlPersistence({
144
- * url: "file:./awaitly.db",
145
- * });
52
+ * // One-liner setup (local SQLite)
53
+ * const store = libsql('file:./workflow.db');
54
+ *
55
+ * // Execute + persist
56
+ * const wf = createWorkflow(deps);
57
+ * await wf(myWorkflowFn);
58
+ * await store.save('wf-123', wf.getSnapshot());
146
59
  *
147
- * const result = await durable.run(
148
- * { fetchUser, createOrder },
149
- * async (step, { fetchUser, createOrder }) => {
150
- * const user = await step(() => fetchUser("123"), { key: "fetch-user" });
151
- * const order = await step(() => createOrder(user), { key: "create-order" });
152
- * return order;
153
- * },
154
- * {
155
- * id: "checkout-123",
156
- * store,
157
- * }
158
- * );
60
+ * // Restore
61
+ * const snapshot = await store.load('wf-123');
62
+ * const wf2 = createWorkflow(deps, { snapshot });
63
+ * await wf2(myWorkflowFn);
159
64
  * ```
160
65
  *
161
66
  * @example
162
67
  * ```typescript
163
- * // Using remote Turso (libSQL) instance
164
- * const store = await createLibSqlPersistence({
165
- * url: process.env.LIBSQL_URL!,
166
- * authToken: process.env.LIBSQL_AUTH_TOKEN,
167
- * tableName: "awaitly_workflow_state",
68
+ * // With remote Turso and cross-process locking
69
+ * const store = libsql({
70
+ * url: process.env.TURSO_URL!,
71
+ * authToken: process.env.TURSO_AUTH_TOKEN,
72
+ * table: 'my_workflow_snapshots',
73
+ * prefix: 'orders:',
74
+ * lock: { lockTableName: 'my_workflow_locks' },
168
75
  * });
169
76
  * ```
170
77
  */
171
- type LibSqlStatePersistence = StatePersistence & {
172
- loadRaw(runId: string): Promise<SerializedState | undefined>;
173
- listPage(options?: ListPageOptions): Promise<ListPageResult>;
174
- deleteMany(ids: string[]): Promise<number>;
175
- clear(): Promise<void>;
176
- };
177
- type LibSqlStatePersistenceWithLock = LibSqlStatePersistence & WorkflowLock;
178
- declare function createLibSqlPersistence(options?: LibSqlPersistenceOptions): Promise<LibSqlStatePersistence | LibSqlStatePersistenceWithLock>;
78
+ declare function libsql(urlOrOptions: string | LibSqlOptions): SnapshotStore & Partial<WorkflowLock>;
179
79
 
180
- export { LibSqlKeyValueStore, type LibSqlKeyValueStoreOptions, type LibSqlLockOptions, type LibSqlPersistenceOptions, type LibSqlStatePersistence, type LibSqlStatePersistenceWithLock, createLibSqlPersistence };
80
+ export { type LibSqlLockOptions, type LibSqlOptions, libsql };
package/dist/index.d.ts CHANGED
@@ -1,96 +1,8 @@
1
1
  import { Client } from '@libsql/client';
2
- import { KeyValueStore, ListPageOptions, StatePersistence, SerializedState, ListPageResult } from 'awaitly/persistence';
2
+ import { SnapshotStore } from 'awaitly/persistence';
3
+ export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
3
4
  import { WorkflowLock } from 'awaitly/durable';
4
-
5
- /**
6
- * awaitly-libsql
7
- *
8
- * libSQL / SQLite KeyValueStore implementation for awaitly persistence.
9
- */
10
-
11
- /**
12
- * Options for libSQL / SQLite KeyValueStore.
13
- */
14
- interface LibSqlKeyValueStoreOptions {
15
- /**
16
- * libSQL database URL.
17
- *
18
- * Examples:
19
- * - "file:./awaitly.db" (local file)
20
- * - ":memory:" (in-memory, for tests)
21
- * - "libsql://your-db.turso.io" (remote)
22
- *
23
- * @default "file:./awaitly.db"
24
- */
25
- url?: string;
26
- /**
27
- * Authentication token for remote libSQL databases (e.g. Turso).
28
- */
29
- authToken?: string;
30
- /**
31
- * Table name for storing key-value pairs.
32
- * @default "awaitly_workflow_state"
33
- */
34
- tableName?: string;
35
- /**
36
- * Existing libSQL client to use.
37
- * If provided, url/authToken options are ignored.
38
- */
39
- client?: Client;
40
- }
41
- /**
42
- * libSQL / SQLite implementation of KeyValueStore.
43
- *
44
- * Automatically creates the required table on first use.
45
- * Supports TTL via ISO 8601 `expires_at` column.
46
- */
47
- declare class LibSqlKeyValueStore implements KeyValueStore {
48
- private client;
49
- private tableName;
50
- private initialized;
51
- private initPromise;
52
- constructor(options?: LibSqlKeyValueStoreOptions);
53
- /**
54
- * Initialize the store by creating the table and index if they don't exist.
55
- * This is called automatically on first use.
56
- */
57
- private ensureInitialized;
58
- /**
59
- * Create the table and index if they don't exist.
60
- */
61
- private createTable;
62
- /**
63
- * Convert glob pattern to SQL LIKE pattern.
64
- * Supports * wildcard (matches any characters).
65
- */
66
- private patternToLike;
67
- get(key: string): Promise<string | null>;
68
- set(key: string, value: string, options?: {
69
- ttl?: number;
70
- }): Promise<void>;
71
- delete(key: string): Promise<boolean>;
72
- exists(key: string): Promise<boolean>;
73
- keys(pattern: string): Promise<string[]>;
74
- /**
75
- * List keys with pagination, filtering, and ordering.
76
- */
77
- listKeys(pattern: string, options?: ListPageOptions): Promise<{
78
- keys: string[];
79
- total?: number;
80
- }>;
81
- /**
82
- * Delete multiple keys in one round-trip.
83
- */
84
- deleteMany(keys: string[]): Promise<number>;
85
- /**
86
- * Remove all entries from the table (clear all workflow state).
87
- */
88
- clear(): Promise<void>;
89
- /**
90
- * Close the underlying client if it supports close().
91
- */
92
- close(): Promise<void>;
93
- }
5
+ export { WorkflowLock } from 'awaitly/durable';
94
6
 
95
7
  /**
96
8
  * libSQL workflow lock (lease) for cross-process concurrency control.
@@ -109,72 +21,60 @@ interface LibSqlLockOptions {
109
21
  * awaitly-libsql
110
22
  *
111
23
  * libSQL / SQLite persistence adapter for awaitly workflows.
112
- * Provides ready-to-use StatePersistence backed by libSQL.
24
+ * Provides ready-to-use SnapshotStore backed by libSQL.
113
25
  */
114
26
 
115
27
  /**
116
- * Options for creating libSQL persistence.
28
+ * Options for the libsql() shorthand function.
117
29
  */
118
- interface LibSqlPersistenceOptions extends LibSqlKeyValueStoreOptions {
119
- /**
120
- * Key prefix for state entries.
121
- * @default "workflow:state:"
122
- */
30
+ interface LibSqlOptions {
31
+ /** libSQL connection URL (file: for local, https: for Turso). */
32
+ url: string;
33
+ /** Auth token for remote Turso instances. */
34
+ authToken?: string;
35
+ /** Table name for snapshots. @default 'awaitly_snapshots' */
36
+ table?: string;
37
+ /** Key prefix for IDs. @default '' */
123
38
  prefix?: string;
124
- /**
125
- * When set, the store implements WorkflowLock for cross-process concurrency control.
126
- * Uses a lease (TTL) + owner token; release verifies the token.
127
- */
39
+ /** Bring your own client. */
40
+ client?: Client;
41
+ /** Cross-process lock options. When set, the store implements WorkflowLock. */
128
42
  lock?: LibSqlLockOptions;
129
43
  }
130
44
  /**
131
- * Create a StatePersistence instance backed by libSQL / SQLite.
132
- *
133
- * The table is automatically created on first use.
134
- *
135
- * @param options - libSQL connection and configuration options
136
- * @returns StatePersistence instance ready to use with durable.run()
45
+ * Create a snapshot store backed by libSQL / SQLite.
46
+ * This is the simplified one-liner API for workflow persistence.
137
47
  *
138
48
  * @example
139
49
  * ```typescript
140
- * import { createLibSqlPersistence } from "awaitly-libsql";
141
- * import { durable } from "awaitly/durable";
50
+ * import { libsql } from 'awaitly-libsql';
142
51
  *
143
- * const store = await createLibSqlPersistence({
144
- * url: "file:./awaitly.db",
145
- * });
52
+ * // One-liner setup (local SQLite)
53
+ * const store = libsql('file:./workflow.db');
54
+ *
55
+ * // Execute + persist
56
+ * const wf = createWorkflow(deps);
57
+ * await wf(myWorkflowFn);
58
+ * await store.save('wf-123', wf.getSnapshot());
146
59
  *
147
- * const result = await durable.run(
148
- * { fetchUser, createOrder },
149
- * async (step, { fetchUser, createOrder }) => {
150
- * const user = await step(() => fetchUser("123"), { key: "fetch-user" });
151
- * const order = await step(() => createOrder(user), { key: "create-order" });
152
- * return order;
153
- * },
154
- * {
155
- * id: "checkout-123",
156
- * store,
157
- * }
158
- * );
60
+ * // Restore
61
+ * const snapshot = await store.load('wf-123');
62
+ * const wf2 = createWorkflow(deps, { snapshot });
63
+ * await wf2(myWorkflowFn);
159
64
  * ```
160
65
  *
161
66
  * @example
162
67
  * ```typescript
163
- * // Using remote Turso (libSQL) instance
164
- * const store = await createLibSqlPersistence({
165
- * url: process.env.LIBSQL_URL!,
166
- * authToken: process.env.LIBSQL_AUTH_TOKEN,
167
- * tableName: "awaitly_workflow_state",
68
+ * // With remote Turso and cross-process locking
69
+ * const store = libsql({
70
+ * url: process.env.TURSO_URL!,
71
+ * authToken: process.env.TURSO_AUTH_TOKEN,
72
+ * table: 'my_workflow_snapshots',
73
+ * prefix: 'orders:',
74
+ * lock: { lockTableName: 'my_workflow_locks' },
168
75
  * });
169
76
  * ```
170
77
  */
171
- type LibSqlStatePersistence = StatePersistence & {
172
- loadRaw(runId: string): Promise<SerializedState | undefined>;
173
- listPage(options?: ListPageOptions): Promise<ListPageResult>;
174
- deleteMany(ids: string[]): Promise<number>;
175
- clear(): Promise<void>;
176
- };
177
- type LibSqlStatePersistenceWithLock = LibSqlStatePersistence & WorkflowLock;
178
- declare function createLibSqlPersistence(options?: LibSqlPersistenceOptions): Promise<LibSqlStatePersistence | LibSqlStatePersistenceWithLock>;
78
+ declare function libsql(urlOrOptions: string | LibSqlOptions): SnapshotStore & Partial<WorkflowLock>;
179
79
 
180
- export { LibSqlKeyValueStore, type LibSqlKeyValueStoreOptions, type LibSqlLockOptions, type LibSqlPersistenceOptions, type LibSqlStatePersistence, type LibSqlStatePersistenceWithLock, createLibSqlPersistence };
80
+ export { type LibSqlLockOptions, type LibSqlOptions, libsql };
package/dist/index.js CHANGED
@@ -1,54 +1,13 @@
1
- import{createClient as P}from"@libsql/client";import{createClient as k}from"@libsql/client";var g=class{client;tableName;initialized=!1;initPromise=null;constructor(t={}){if(t.client)this.client=t.client;else{let i=t.url??"file:./awaitly.db";this.client=k({url:i,authToken:t.authToken})}let e=t.tableName??"awaitly_workflow_state";if(!/^[A-Za-z0-9_]+$/.test(e))throw new Error(`Invalid table name '${e}'. Only alphanumeric and underscore characters are allowed.`);this.tableName=e}async ensureInitialized(){if(!this.initialized)return this.initPromise?this.initPromise:(this.initPromise=(async()=>{try{await this.createTable(),this.initialized=!0}finally{this.initialized||(this.initPromise=null)}})(),this.initPromise)}async createTable(){await this.client.execute({sql:`
2
- CREATE TABLE IF NOT EXISTS ${this.tableName} (
3
- key TEXT PRIMARY KEY,
4
- value TEXT NOT NULL,
5
- expires_at TEXT,
6
- updated_at TEXT
7
- );
8
- `,args:[]}),await this.client.execute({sql:`
9
- CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expires_at
10
- ON ${this.tableName}(expires_at);
11
- `,args:[]}),await this.client.execute({sql:`
12
- CREATE INDEX IF NOT EXISTS idx_${this.tableName}_updated_at
13
- ON ${this.tableName}(updated_at);
14
- `,args:[]});try{await this.client.execute({sql:`ALTER TABLE ${this.tableName} ADD COLUMN updated_at TEXT`,args:[]})}catch{}}patternToLike(t){return t.replace(/%/g,"\\%").replace(/_/g,"\\_").replace(/\*/g,"%")}async get(t){await this.ensureInitialized();let e=new Date().toISOString(),i=await this.client.execute({sql:`
15
- SELECT value, expires_at
16
- FROM ${this.tableName}
17
- WHERE key = ?
18
- `,args:[t]});if(i.rows.length===0)return null;let r=i.rows[0],a=r.expires_at;return a&&a<=e?null:r.value??null}async set(t,e,i){await this.ensureInitialized();let r=i?.ttl&&i.ttl>0?new Date(Date.now()+i.ttl*1e3).toISOString():null,a=new Date().toISOString();await this.client.execute({sql:`
19
- INSERT INTO ${this.tableName} (key, value, expires_at, updated_at)
20
- VALUES (?, ?, ?, ?)
21
- ON CONFLICT(key) DO UPDATE SET
22
- value = excluded.value,
23
- expires_at = excluded.expires_at,
24
- updated_at = excluded.updated_at
25
- `,args:[t,e,r,a]})}async delete(t){await this.ensureInitialized();let i=(await this.client.execute({sql:`DELETE FROM ${this.tableName} WHERE key = ?`,args:[t]})).rowsAffected;return typeof i=="number"?i>0:await this.get(t)===null}async exists(t){await this.ensureInitialized();let e=new Date().toISOString();return(await this.client.execute({sql:`
26
- SELECT 1
27
- FROM ${this.tableName}
28
- WHERE key = ?
29
- AND (expires_at IS NULL OR expires_at > ?)
30
- LIMIT 1
31
- `,args:[t,e]})).rows.length>0}async keys(t){await this.ensureInitialized();let e=this.patternToLike(t),i=new Date().toISOString();return(await this.client.execute({sql:`
32
- SELECT key
33
- FROM ${this.tableName}
34
- WHERE key LIKE ? ESCAPE '\\'
35
- AND (expires_at IS NULL OR expires_at > ?)
36
- `,args:[e,i]})).rows.map(a=>a.key)}async listKeys(t,e={}){await this.ensureInitialized();let i=Math.min(Math.max(0,e.limit??100),1e4),r=Math.max(0,e.offset??0),a=e.orderBy==="key"?"key":"updated_at",S=e.orderDir==="asc"?"ASC":"DESC",c=this.patternToLike(t),u=new Date().toISOString(),m=["key LIKE ? ESCAPE '\\'","(expires_at IS NULL OR expires_at > ?)"],s=[c,u],o=3;e.updatedBefore!=null&&(m.push("updated_at < ?"),s.push(e.updatedBefore.toISOString()),o++),e.updatedAfter!=null&&(m.push("updated_at > ?"),s.push(e.updatedAfter.toISOString()),o++);let n=m.join(" AND "),l=a==="updated_at"?" NULLS LAST":"",d=[...s,i,r],w=`
37
- SELECT key
38
- FROM ${this.tableName}
39
- WHERE ${n}
40
- ORDER BY ${a} ${S}${l}
41
- LIMIT ? OFFSET ?
42
- `,E=(await this.client.execute({sql:w,args:d})).rows.map(T=>T.key),L;if(e.includeTotal===!0||r>0){let f=(await this.client.execute({sql:`SELECT COUNT(*) AS count FROM ${this.tableName} WHERE ${n}`,args:s})).rows[0];L=typeof f?.count=="number"?f.count:parseInt(String(f?.count??"0"),10)}return{keys:E,total:L}}async deleteMany(t){if(t.length===0)return 0;await this.ensureInitialized();let e=t.map(()=>"?").join(", ");return(await this.client.execute({sql:`DELETE FROM ${this.tableName} WHERE key IN (${e})`,args:t})).rowsAffected??0}async clear(){await this.ensureInitialized(),await this.client.execute({sql:`DELETE FROM ${this.tableName}`,args:[]})}async close(){}};import{randomUUID as x}from"crypto";function h(p,t={}){let e=t.lockTableName??"awaitly_workflow_lock",i=e.replace(/[^a-zA-Z0-9_]/g,"_");async function r(){await p.execute({sql:`
1
+ import{createClient as k}from"@libsql/client";import{randomUUID as f}from"crypto";function E(a,o={}){let e=o.lockTableName??"awaitly_workflow_lock",c=e.replace(/[^a-zA-Z0-9_]/g,"_");async function u(){await a.execute({sql:`
43
2
  CREATE TABLE IF NOT EXISTS ${e} (
44
3
  workflow_id TEXT PRIMARY KEY,
45
4
  owner_token TEXT NOT NULL,
46
5
  expires_at TEXT NOT NULL
47
6
  )
48
- `,args:[]}),await p.execute({sql:`
49
- CREATE INDEX IF NOT EXISTS idx_${i}_expires_at
7
+ `,args:[]}),await a.execute({sql:`
8
+ CREATE INDEX IF NOT EXISTS idx_${c}_expires_at
50
9
  ON ${e}(expires_at)
51
- `,args:[]})}async function a(c,u){let m=u?.ttlMs??6e4,s=x(),o=new Date(Date.now()+m).toISOString();await r();let n=await p.execute({sql:`
10
+ `,args:[]})}async function r(n,l){let p=l?.ttlMs??6e4,t=f(),i=new Date(Date.now()+p).toISOString();await u();let s=await a.execute({sql:`
52
11
  INSERT INTO ${e} (workflow_id, owner_token, expires_at)
53
12
  VALUES (?, ?, ?)
54
13
  ON CONFLICT(workflow_id) DO UPDATE SET
@@ -56,5 +15,18 @@ import{createClient as P}from"@libsql/client";import{createClient as k}from"@lib
56
15
  expires_at = excluded.expires_at
57
16
  WHERE ${e}.expires_at < datetime('now')
58
17
  RETURNING owner_token
59
- `,args:[c,s,o]}),l=n.rows[0];return n.rows.length===1&&l?.owner_token===s?{ownerToken:s}:null}async function S(c,u){await p.execute({sql:`DELETE FROM ${e} WHERE workflow_id = ? AND owner_token = ?`,args:[c,u]})}return{tryAcquire:a,release:S,ensureLockTable:r}}import{createStatePersistence as b}from"awaitly/persistence";async function D(p={}){let{prefix:t,lock:e,...i}=p,r=t??"workflow:state:",a=s=>s.slice(r.length),S=s=>`${r}${s}`,c=(s,o)=>Object.assign(s,{async listPage(n={}){let{keys:l,total:d}=await o.listKeys(`${r}*`,n),w=l.map(a),y=Math.min(Math.max(0,n.limit??100),1e4),E=w.length===y?(n.offset??0)+w.length:void 0;return{ids:w,total:d,nextOffset:E}},async deleteMany(n){if(n.length===0)return 0;let l=n.map(S);return o.deleteMany(l)},async clear(){return o.clear()}});if(e!==void 0){let s=i.client??P({url:i.url??"file:./awaitly.db",authToken:i.authToken}),o=new g({...i,client:s}),n=b(o,t),l=c(n,o),d=h(s,e);return Object.assign(l,{tryAcquire:d.tryAcquire.bind(d),release:d.release.bind(d)})}let u=new g(i),m=b(u,t);return c(m,u)}export{g as LibSqlKeyValueStore,D as createLibSqlPersistence};
18
+ `,args:[n,t,i]}),w=s.rows[0];return s.rows.length===1&&w?.owner_token===t?{ownerToken:t}:null}async function d(n,l){await a.execute({sql:`DELETE FROM ${e} WHERE workflow_id = ? AND owner_token = ?`,args:[n,l]})}return{tryAcquire:r,release:d,ensureLockTable:u}}function _(a){let o=typeof a=="string"?{url:a}:a,e=o.table??"awaitly_snapshots",c=o.prefix??"";if(!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(e))throw new Error(`Invalid table name: ${e}. Must be alphanumeric with underscores.`);let u=!o.client,r=o.client??k({url:o.url,authToken:o.authToken}),d=!1,n=o.lock?E(r,o.lock):null,l=async()=>{d||(await r.execute(`
19
+ CREATE TABLE IF NOT EXISTS ${e} (
20
+ id TEXT PRIMARY KEY,
21
+ snapshot TEXT NOT NULL,
22
+ updated_at TEXT DEFAULT (datetime('now'))
23
+ )
24
+ `),await r.execute(`
25
+ CREATE INDEX IF NOT EXISTS ${e}_updated_at_idx ON ${e} (updated_at DESC)
26
+ `),d=!0)},p={async save(t,i){await l();let s=c+t;await r.execute({sql:`INSERT INTO ${e} (id, snapshot, updated_at)
27
+ VALUES (?, ?, datetime('now'))
28
+ ON CONFLICT(id) DO UPDATE SET snapshot = ?, updated_at = datetime('now')`,args:[s,JSON.stringify(i),JSON.stringify(i)]})},async load(t){await l();let i=c+t,s=await r.execute({sql:`SELECT snapshot FROM ${e} WHERE id = ?`,args:[i]});return s.rows.length===0?null:JSON.parse(s.rows[0].snapshot)},async delete(t){await l();let i=c+t;await r.execute({sql:`DELETE FROM ${e} WHERE id = ?`,args:[i]})},async list(t){await l();let i=c+(t?.prefix??""),s=t?.limit??100;return(await r.execute({sql:`SELECT id, updated_at FROM ${e}
29
+ WHERE id LIKE ?
30
+ ORDER BY updated_at DESC
31
+ LIMIT ?`,args:[i+"%",s]})).rows.map(T=>({id:T.id.slice(c.length),updatedAt:T.updated_at}))},async close(){u&&r.close()}};return n&&(p.tryAcquire=n.tryAcquire.bind(n),p.release=n.release.bind(n)),p}export{_ as libsql};
60
32
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/libsql-store.ts","../src/libsql-lock.ts"],"sourcesContent":["/**\n * awaitly-libsql\n *\n * libSQL / SQLite persistence adapter for awaitly workflows.\n * Provides ready-to-use StatePersistence backed by libSQL.\n */\n\nimport { createClient } from \"@libsql/client\";\nimport { LibSqlKeyValueStore, type LibSqlKeyValueStoreOptions } from \"./libsql-store\";\nimport { createLibSqlLock, type LibSqlLockOptions } from \"./libsql-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 type { LibSqlLockOptions } from \"./libsql-lock\";\n\n/**\n * Options for creating libSQL persistence.\n */\nexport interface LibSqlPersistenceOptions extends LibSqlKeyValueStoreOptions {\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?: LibSqlLockOptions;\n}\n\n/**\n * Create a StatePersistence instance backed by libSQL / SQLite.\n *\n * The table is automatically created on first use.\n *\n * @param options - libSQL connection and configuration options\n * @returns StatePersistence instance ready to use with durable.run()\n *\n * @example\n * ```typescript\n * import { createLibSqlPersistence } from \"awaitly-libsql\";\n * import { durable } from \"awaitly/durable\";\n *\n * const store = await createLibSqlPersistence({\n * url: \"file:./awaitly.db\",\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 remote Turso (libSQL) instance\n * const store = await createLibSqlPersistence({\n * url: process.env.LIBSQL_URL!,\n * authToken: process.env.LIBSQL_AUTH_TOKEN,\n * tableName: \"awaitly_workflow_state\",\n * });\n * ```\n */\nexport type LibSqlStatePersistence = 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 LibSqlStatePersistenceWithLock = LibSqlStatePersistence & WorkflowLock;\n\nexport async function createLibSqlPersistence(\n options: LibSqlPersistenceOptions = {}\n): Promise<LibSqlStatePersistence | LibSqlStatePersistenceWithLock> {\n const { prefix, lock: lockOptions, ...storeOptions } = options;\n\n const effectivePrefix = prefix ?? \"workflow:state:\";\n const stripPrefix = (key: string): string => key.slice(effectivePrefix.length);\n const prefixKey = (runId: string): string => `${effectivePrefix}${runId}`;\n\n const addExtensions = (\n base: StatePersistence & { loadRaw(runId: string): Promise<SerializedState | undefined> },\n store: LibSqlKeyValueStore\n ): LibSqlStatePersistence =>\n Object.assign(base, {\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 if (lockOptions !== undefined) {\n const client =\n storeOptions.client ??\n createClient({\n url: storeOptions.url ?? \"file:./awaitly.db\",\n authToken: storeOptions.authToken,\n });\n const store = new LibSqlKeyValueStore({ ...storeOptions, client });\n const base = createStatePersistence(store, prefix) as LibSqlStatePersistence;\n const persistence = addExtensions(\n base as StatePersistence & { loadRaw(runId: string): Promise<SerializedState | undefined> },\n store\n );\n const lock = createLibSqlLock(client, lockOptions);\n return Object.assign(persistence, {\n tryAcquire: lock.tryAcquire.bind(lock),\n release: lock.release.bind(lock),\n });\n }\n\n const store = new LibSqlKeyValueStore(storeOptions);\n const base = createStatePersistence(store, prefix);\n return addExtensions(\n base as StatePersistence & { loadRaw(runId: string): Promise<SerializedState | undefined> },\n store\n );\n}\n\n/**\n * libSQL KeyValueStore implementation.\n * Use this directly if you need more control over the store.\n *\n * @example\n * ```typescript\n * import { LibSqlKeyValueStore } from \"awaitly-libsql\";\n * import { createStatePersistence } from \"awaitly/persistence\";\n *\n * const store = new LibSqlKeyValueStore({\n * url: \"file:./awaitly.db\",\n * });\n *\n * const persistence = createStatePersistence(store, \"custom:prefix:\");\n * ```\n */\nexport { LibSqlKeyValueStore, type LibSqlKeyValueStoreOptions };\n\n","/**\n * awaitly-libsql\n *\n * libSQL / SQLite KeyValueStore implementation for awaitly persistence.\n */\n\nimport { createClient, type Client } from \"@libsql/client\";\nimport type { KeyValueStore, ListPageOptions } from \"awaitly/persistence\";\n\n/**\n * Options for libSQL / SQLite KeyValueStore.\n */\nexport interface LibSqlKeyValueStoreOptions {\n /**\n * libSQL database URL.\n *\n * Examples:\n * - \"file:./awaitly.db\" (local file)\n * - \":memory:\" (in-memory, for tests)\n * - \"libsql://your-db.turso.io\" (remote)\n *\n * @default \"file:./awaitly.db\"\n */\n url?: string;\n\n /**\n * Authentication token for remote libSQL databases (e.g. Turso).\n */\n authToken?: string;\n\n /**\n * Table name for storing key-value pairs.\n * @default \"awaitly_workflow_state\"\n */\n tableName?: string;\n\n /**\n * Existing libSQL client to use.\n * If provided, url/authToken options are ignored.\n */\n client?: Client;\n}\n\n/**\n * libSQL / SQLite implementation of KeyValueStore.\n *\n * Automatically creates the required table on first use.\n * Supports TTL via ISO 8601 `expires_at` column.\n */\nexport class LibSqlKeyValueStore implements KeyValueStore {\n private client: Client;\n private tableName: string;\n private initialized = false;\n private initPromise: Promise<void> | null = null;\n\n constructor(options: LibSqlKeyValueStoreOptions = {}) {\n if (options.client) {\n this.client = options.client;\n } else {\n const url = options.url ?? \"file:./awaitly.db\";\n this.client = createClient({\n url,\n authToken: options.authToken,\n });\n }\n\n const tableName = options.tableName ?? \"awaitly_workflow_state\";\n if (!/^[A-Za-z0-9_]+$/.test(tableName)) {\n throw new Error(\n `Invalid table name '${tableName}'. Only alphanumeric and underscore characters are allowed.`\n );\n }\n this.tableName = tableName;\n }\n\n /**\n * Initialize the store by creating the table and index if they don'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 } finally {\n if (!this.initialized) {\n this.initPromise = null;\n }\n }\n })();\n\n return this.initPromise;\n }\n\n /**\n * Create the table and index if they don't exist.\n */\n private async createTable(): Promise<void> {\n // SQLite / libSQL: execute schema changes as separate statements\n await this.client.execute({\n sql: `\n CREATE TABLE IF NOT EXISTS ${this.tableName} (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n expires_at TEXT,\n updated_at TEXT\n );\n `,\n args: [],\n });\n\n await this.client.execute({\n sql: `\n CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expires_at\n ON ${this.tableName}(expires_at);\n `,\n args: [],\n });\n\n await this.client.execute({\n sql: `\n CREATE INDEX IF NOT EXISTS idx_${this.tableName}_updated_at\n ON ${this.tableName}(updated_at);\n `,\n args: [],\n });\n\n // Add updated_at to existing tables (ignore if column already exists)\n try {\n await this.client.execute({\n sql: `ALTER TABLE ${this.tableName} ADD COLUMN updated_at TEXT`,\n args: [],\n });\n } catch {\n // Column may already exist\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 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 nowIso = new Date().toISOString();\n const result = await this.client.execute({\n sql: `\n SELECT value, expires_at\n FROM ${this.tableName}\n WHERE key = ?\n `,\n args: [key],\n });\n\n if (result.rows.length === 0) {\n return null;\n }\n\n const row = result.rows[0] as Record<string, unknown>;\n const expiresAt = row[\"expires_at\"] as string | null | undefined;\n\n if (expiresAt && expiresAt <= nowIso) {\n // Expired - behave as if key doesn't exist\n return null;\n }\n\n return (row[\"value\"] as string) ?? null;\n }\n\n async set(key: string, value: string, options?: { ttl?: number }): Promise<void> {\n await this.ensureInitialized();\n\n const expiresAt =\n options?.ttl && options.ttl > 0\n ? new Date(Date.now() + options.ttl * 1000).toISOString()\n : null;\n const updatedAt = new Date().toISOString();\n\n await this.client.execute({\n sql: `\n INSERT INTO ${this.tableName} (key, value, expires_at, updated_at)\n VALUES (?, ?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET\n value = excluded.value,\n expires_at = excluded.expires_at,\n updated_at = excluded.updated_at\n `,\n args: [key, value, expiresAt, updatedAt],\n });\n }\n\n async delete(key: string): Promise<boolean> {\n await this.ensureInitialized();\n\n const result = await this.client.execute({\n sql: `DELETE FROM ${this.tableName} WHERE key = ?`,\n args: [key],\n });\n\n // libSQL .rowsAffected is available on hrana responses; fall back to > 0 check\n const affected: number | undefined = result.rowsAffected;\n if (typeof affected === \"number\") {\n return affected > 0;\n }\n\n // If rowsAffected is not available, perform an existence check as a fallback\n const after = await this.get(key);\n return after === null;\n }\n\n async exists(key: string): Promise<boolean> {\n await this.ensureInitialized();\n\n const nowIso = new Date().toISOString();\n const result = await this.client.execute({\n sql: `\n SELECT 1\n FROM ${this.tableName}\n WHERE key = ?\n AND (expires_at IS NULL OR expires_at > ?)\n LIMIT 1\n `,\n args: [key, nowIso],\n });\n\n return result.rows.length > 0;\n }\n\n async keys(pattern: string): Promise<string[]> {\n await this.ensureInitialized();\n\n const likePattern = this.patternToLike(pattern);\n const nowIso = new Date().toISOString();\n\n const result = await this.client.execute({\n sql: `\n SELECT key\n FROM ${this.tableName}\n WHERE key LIKE ? ESCAPE '\\\\'\n AND (expires_at IS NULL OR expires_at > ?)\n `,\n args: [likePattern, nowIso],\n });\n\n return result.rows.map((row) => (row as Record<string, unknown>)[\"key\"] as string);\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 const nowIso = new Date().toISOString();\n\n const conditions: string[] = [\n \"key LIKE ? ESCAPE '\\\\'\",\n \"(expires_at IS NULL OR expires_at > ?)\",\n ];\n const args: (string | number)[] = [likePattern, nowIso];\n let paramIndex = 3;\n\n if (options.updatedBefore != null) {\n conditions.push(`updated_at < ?`);\n args.push(options.updatedBefore.toISOString());\n paramIndex++;\n }\n if (options.updatedAfter != null) {\n conditions.push(`updated_at > ?`);\n args.push(options.updatedAfter.toISOString());\n paramIndex++;\n }\n\n const whereClause = conditions.join(\" AND \");\n const orderNulls = orderBy === \"updated_at\" ? \" NULLS LAST\" : \"\";\n\n const listArgs = [...args, limit, offset];\n const listQuery = `\n SELECT key\n FROM ${this.tableName}\n WHERE ${whereClause}\n ORDER BY ${orderBy} ${orderDir}${orderNulls}\n LIMIT ? OFFSET ?\n `;\n\n const result = await this.client.execute({\n sql: listQuery,\n args: listArgs,\n });\n const keys = result.rows.map((row) => (row as Record<string, unknown>)[\"key\"] as string);\n\n let total: number | undefined;\n if (options.includeTotal === true || offset > 0) {\n const countResult = await this.client.execute({\n sql: `SELECT COUNT(*) AS count FROM ${this.tableName} WHERE ${whereClause}`,\n args,\n });\n const countRow = countResult.rows[0] as Record<string, unknown>;\n total = typeof countRow?.count === \"number\" ? countRow.count : parseInt(String(countRow?.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 placeholders = keys.map(() => \"?\").join(\", \");\n const result = await this.client.execute({\n sql: `DELETE FROM ${this.tableName} WHERE key IN (${placeholders})`,\n args: keys,\n });\n return result.rowsAffected ?? 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.client.execute({\n sql: `DELETE FROM ${this.tableName}`,\n args: [],\n });\n }\n\n /**\n * Close the underlying client if it supports close().\n */\n async close(): Promise<void> {\n // libSQL client doesn't expose a close API in all runtimes; no-op for now.\n // If a future version adds client.close(), it can be wired here.\n }\n}\n\n","/**\n * libSQL workflow lock (lease) for cross-process concurrency control.\n * Uses a lease (TTL) + owner token; release verifies the token.\n */\n\nimport type { Client } from \"@libsql/client\";\nimport { randomUUID } from \"node:crypto\";\n\nexport interface LibSqlLockOptions {\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 libSQL lock table.\n * Caller must pass the same client used for state (so one connection).\n */\nexport function createLibSqlLock(\n client: Client,\n options: LibSqlLockOptions = {}\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 safeTableName = lockTableName.replace(/[^a-zA-Z0-9_]/g, \"_\");\n\n async function ensureLockTable(): Promise<void> {\n await client.execute({\n sql: `\n CREATE TABLE IF NOT EXISTS ${lockTableName} (\n workflow_id TEXT PRIMARY KEY,\n owner_token TEXT NOT NULL,\n expires_at TEXT NOT NULL\n )\n `,\n args: [],\n });\n await client.execute({\n sql: `\n CREATE INDEX IF NOT EXISTS idx_${safeTableName}_expires_at\n ON ${lockTableName}(expires_at)\n `,\n args: [],\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).toISOString();\n\n await ensureLockTable();\n\n // Insert new row or update only if current row is expired (or missing).\n // SQLite 3.35+ / libSQL support RETURNING.\n const result = await client.execute({\n sql: `\n INSERT INTO ${lockTableName} (workflow_id, owner_token, expires_at)\n VALUES (?, ?, ?)\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 < datetime('now')\n RETURNING owner_token\n `,\n args: [id, ownerToken, expiresAt],\n });\n\n const row = result.rows[0] as Record<string, unknown> | undefined;\n if (result.rows.length === 1 && row?.owner_token === ownerToken) {\n return { ownerToken };\n }\n return null;\n }\n\n async function release(id: string, ownerToken: string): Promise<void> {\n await client.execute({\n sql: `DELETE FROM ${lockTableName} WHERE workflow_id = ? AND owner_token = ?`,\n args: [id, ownerToken],\n });\n }\n\n return { tryAcquire, release, ensureLockTable };\n}\n"],"mappings":"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","names":["createClient","createClient","LibSqlKeyValueStore","options","url","tableName","pattern","key","nowIso","result","row","expiresAt","value","updatedAt","affected","likePattern","limit","offset","orderBy","orderDir","conditions","args","paramIndex","whereClause","orderNulls","listArgs","listQuery","keys","total","countRow","placeholders","randomUUID","createLibSqlLock","client","options","lockTableName","safeTableName","ensureLockTable","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","row","release","createStatePersistence","createLibSqlPersistence","options","prefix","lockOptions","storeOptions","effectivePrefix","stripPrefix","key","prefixKey","runId","addExtensions","base","store","keys","total","ids","limit","nextOffset","client","createClient","LibSqlKeyValueStore","persistence","lock","createLibSqlLock"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/libsql-lock.ts"],"sourcesContent":["/**\n * awaitly-libsql\n *\n * libSQL / SQLite persistence adapter for awaitly workflows.\n * Provides ready-to-use SnapshotStore backed by libSQL.\n */\n\nimport { createClient, type Client } from \"@libsql/client\";\nimport type { WorkflowSnapshot, SnapshotStore } from \"awaitly/persistence\";\nimport type { WorkflowLock } from \"awaitly/durable\";\nimport { createLibSqlLock, type LibSqlLockOptions } from \"./libsql-lock\";\n\n// Re-export types for convenience\nexport type { SnapshotStore, WorkflowSnapshot } from \"awaitly/persistence\";\nexport type { WorkflowLock } from \"awaitly/durable\";\nexport type { LibSqlLockOptions } from \"./libsql-lock\";\n\n// =============================================================================\n// LibSqlOptions\n// =============================================================================\n\n/**\n * Options for the libsql() shorthand function.\n */\nexport interface LibSqlOptions {\n /** libSQL connection URL (file: for local, https: for Turso). */\n url: string;\n /** Auth token for remote Turso instances. */\n authToken?: 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 client. */\n client?: Client;\n /** Cross-process lock options. When set, the store implements WorkflowLock. */\n lock?: LibSqlLockOptions;\n}\n\n// =============================================================================\n// libsql() - One-liner Snapshot Store Setup\n// =============================================================================\n\n/**\n * Create a snapshot store backed by libSQL / SQLite.\n * This is the simplified one-liner API for workflow persistence.\n *\n * @example\n * ```typescript\n * import { libsql } from 'awaitly-libsql';\n *\n * // One-liner setup (local SQLite)\n * const store = libsql('file:./workflow.db');\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 remote Turso and cross-process locking\n * const store = libsql({\n * url: process.env.TURSO_URL!,\n * authToken: process.env.TURSO_AUTH_TOKEN,\n * table: 'my_workflow_snapshots',\n * prefix: 'orders:',\n * lock: { lockTableName: 'my_workflow_locks' },\n * });\n * ```\n */\nexport function libsql(urlOrOptions: string | LibSqlOptions): SnapshotStore & Partial<WorkflowLock> {\n const opts = typeof urlOrOptions === \"string\" ? { url: urlOrOptions } : urlOrOptions;\n const tableName = opts.table ?? \"awaitly_snapshots\";\n const prefix = opts.prefix ?? \"\";\n\n // Validate table name for SQL injection prevention\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {\n throw new Error(`Invalid table name: ${tableName}. Must be alphanumeric with underscores.`);\n }\n\n // Create or use existing client\n const ownClient = !opts.client;\n const client = opts.client ?? createClient({\n url: opts.url,\n authToken: opts.authToken,\n });\n\n let tableCreated = false;\n\n // Create lock if requested\n const lock = opts.lock ? createLibSqlLock(client, opts.lock) : null;\n\n const ensureTable = async (): Promise<void> => {\n if (tableCreated) return;\n await client.execute(`\n CREATE TABLE IF NOT EXISTS ${tableName} (\n id TEXT PRIMARY KEY,\n snapshot TEXT NOT NULL,\n updated_at TEXT DEFAULT (datetime('now'))\n )\n `);\n await client.execute(`\n CREATE INDEX IF NOT EXISTS ${tableName}_updated_at_idx ON ${tableName} (updated_at DESC)\n `);\n tableCreated = true;\n };\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 client.execute({\n sql: `INSERT INTO ${tableName} (id, snapshot, updated_at)\n VALUES (?, ?, datetime('now'))\n ON CONFLICT(id) DO UPDATE SET snapshot = ?, updated_at = datetime('now')`,\n args: [fullId, JSON.stringify(snapshot), 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 client.execute({\n sql: `SELECT snapshot FROM ${tableName} WHERE id = ?`,\n args: [fullId],\n });\n if (result.rows.length === 0) return null;\n return JSON.parse(result.rows[0].snapshot as string) as WorkflowSnapshot;\n },\n\n async delete(id: string): Promise<void> {\n await ensureTable();\n const fullId = prefix + id;\n await client.execute({\n sql: `DELETE FROM ${tableName} WHERE id = ?`,\n args: [fullId],\n });\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 client.execute({\n sql: `SELECT id, updated_at FROM ${tableName}\n WHERE id LIKE ?\n ORDER BY updated_at DESC\n LIMIT ?`,\n args: [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 string,\n }));\n },\n\n async close(): Promise<void> {\n // Only close client if we created it\n if (ownClient) {\n client.close();\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 * libSQL workflow lock (lease) for cross-process concurrency control.\n * Uses a lease (TTL) + owner token; release verifies the token.\n */\n\nimport type { Client } from \"@libsql/client\";\nimport { randomUUID } from \"node:crypto\";\n\nexport interface LibSqlLockOptions {\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 libSQL lock table.\n * Caller must pass the same client used for state (so one connection).\n */\nexport function createLibSqlLock(\n client: Client,\n options: LibSqlLockOptions = {}\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 safeTableName = lockTableName.replace(/[^a-zA-Z0-9_]/g, \"_\");\n\n async function ensureLockTable(): Promise<void> {\n await client.execute({\n sql: `\n CREATE TABLE IF NOT EXISTS ${lockTableName} (\n workflow_id TEXT PRIMARY KEY,\n owner_token TEXT NOT NULL,\n expires_at TEXT NOT NULL\n )\n `,\n args: [],\n });\n await client.execute({\n sql: `\n CREATE INDEX IF NOT EXISTS idx_${safeTableName}_expires_at\n ON ${lockTableName}(expires_at)\n `,\n args: [],\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).toISOString();\n\n await ensureLockTable();\n\n // Insert new row or update only if current row is expired (or missing).\n // SQLite 3.35+ / libSQL support RETURNING.\n const result = await client.execute({\n sql: `\n INSERT INTO ${lockTableName} (workflow_id, owner_token, expires_at)\n VALUES (?, ?, ?)\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 < datetime('now')\n RETURNING owner_token\n `,\n args: [id, ownerToken, expiresAt],\n });\n\n const row = result.rows[0] as Record<string, unknown> | undefined;\n if (result.rows.length === 1 && row?.owner_token === ownerToken) {\n return { ownerToken };\n }\n return null;\n }\n\n async function release(id: string, ownerToken: string): Promise<void> {\n await client.execute({\n sql: `DELETE FROM ${lockTableName} WHERE workflow_id = ? AND owner_token = ?`,\n args: [id, ownerToken],\n });\n }\n\n return { tryAcquire, release, ensureLockTable };\n}\n"],"mappings":"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","names":["createClient","randomUUID","createLibSqlLock","client","options","lockTableName","safeTableName","ensureLockTable","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","row","release","libsql","urlOrOptions","opts","tableName","prefix","ownClient","client","createClient","tableCreated","lock","createLibSqlLock","ensureTable","store","id","snapshot","fullId","result","options","filterPrefix","limit","row"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "awaitly-libsql",
3
- "version": "2.0.0",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "description": "libSQL / SQLite persistence adapter for awaitly workflows",
6
6
  "main": "./dist/index.cjs",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "license": "MIT",
41
41
  "peerDependencies": {
42
- "awaitly": "^1.13.0"
42
+ "awaitly": "^1.15.0"
43
43
  },
44
44
  "dependencies": {
45
45
  "@libsql/client": "^0.17.0"
@@ -47,11 +47,11 @@
47
47
  "devDependencies": {
48
48
  "@total-typescript/ts-reset": "^0.6.1",
49
49
  "@total-typescript/tsconfig": "^1.0.4",
50
- "@types/node": "^25.1.0",
50
+ "@types/node": "^25.2.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.15.0"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public",