awaitly-libsql 1.0.0 → 3.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
@@ -52,6 +52,19 @@ const store = await createLibSqlPersistence({
52
52
  });
53
53
  ```
54
54
 
55
+ ## Cross-Process Locking
56
+
57
+ To ensure only one process runs a given workflow ID at a time (when `durable.run` is used without `allowConcurrent: true`), pass the `lock` option. The store will implement `WorkflowLock` (lease + owner token):
58
+
59
+ ```ts
60
+ const store = await createLibSqlPersistence({
61
+ url: "file:./awaitly.db",
62
+ lock: { lockTableName: "awaitly_workflow_lock" }, // optional; default table name
63
+ });
64
+
65
+ // durable.run(..., { id, store }) will tryAcquire before running and release in finally
66
+ ```
67
+
55
68
  ## Tenant-Aware Keying (Recommended)
56
69
 
57
70
  To make it easier to avoid cross-tenant leaks in multi-tenant setups, use a
@@ -85,23 +98,6 @@ ON awaitly_workflow_state(expires_at);
85
98
 
86
99
  The `expires_at` column stores ISO 8601 timestamps and is used for TTL support.
87
100
 
88
- ## Using the KeyValueStore Directly
89
-
90
- If you want lower-level control, you can use `LibSqlKeyValueStore` directly
91
- and wire it into `createStatePersistence`:
92
-
93
- ```ts
94
- import { LibSqlKeyValueStore } from "awaitly-libsql";
95
- import { createStatePersistence } from "awaitly/persistence";
96
-
97
- const store = new LibSqlKeyValueStore({
98
- url: "file:./awaitly.db",
99
- tableName: "awaitly_workflow_state",
100
- });
101
-
102
- const persistence = createStatePersistence(store, "custom:prefix:");
103
- ```
104
-
105
101
  ## Requirements
106
102
 
107
103
  - Node.js >= 22
package/dist/index.cjs CHANGED
@@ -1,32 +1,32 @@
1
- "use strict";var l=Object.defineProperty;var u=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var m=Object.prototype.hasOwnProperty;var h=(r,e)=>{for(var t in e)l(r,t,{get:e[t],enumerable:!0})},w=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of p(e))!m.call(r,s)&&s!==t&&l(r,s,{get:()=>e[s],enumerable:!(i=u(e,s))||i.enumerable});return r};var S=r=>w(l({},"__esModule",{value:!0}),r);var g={};h(g,{LibSqlKeyValueStore:()=>a,createLibSqlPersistence:()=>d});module.exports=S(g);var o=require("@libsql/client"),a=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,o.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
- );
7
- `,args:[]}),await this.client.execute({sql:`
8
- CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expires_at
9
- ON ${this.tableName}(expires_at);
10
- `,args:[]})}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:`
11
- SELECT value, expires_at
12
- FROM ${this.tableName}
13
- WHERE key = ?
14
- `,args:[e]});if(i.rows.length===0)return null;let s=i.rows[0],n=s.expires_at;return n&&n<=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;await this.client.execute({sql:`
15
- INSERT INTO ${this.tableName} (key, value, expires_at)
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} (
3
+ workflow_id TEXT PRIMARY KEY,
4
+ owner_token TEXT NOT NULL,
5
+ expires_at TEXT NOT NULL
6
+ )
7
+ `,args:[]}),await o.execute({sql:`
8
+ CREATE INDEX IF NOT EXISTS idx_${i}_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)
16
12
  VALUES (?, ?, ?)
17
- ON CONFLICT(key) DO UPDATE SET
18
- value = excluded.value,
13
+ ON CONFLICT(workflow_id) DO UPDATE SET
14
+ owner_token = excluded.owner_token,
19
15
  expires_at = excluded.expires_at
20
- `,args:[e,t,s]})}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:`
21
- SELECT 1
22
- FROM ${this.tableName}
23
- WHERE key = ?
24
- AND (expires_at IS NULL OR expires_at > ?)
25
- LIMIT 1
26
- `,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:`
27
- SELECT key
28
- FROM ${this.tableName}
29
- WHERE key LIKE ? ESCAPE '\\'
30
- AND (expires_at IS NULL OR expires_at > ?)
31
- `,args:[t,i]})).rows.map(n=>n.key)}async close(){}};var c=require("awaitly/persistence");async function d(r={}){let{prefix:e,...t}=r,i=new a(t);return(0,c.createStatePersistence)(i,e)}0&&(module.exports={LibSqlKeyValueStore,createLibSqlPersistence});
16
+ WHERE ${e}.expires_at < datetime('now')
17
+ RETURNING owner_token
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});
32
32
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/libsql-store.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 { LibSqlKeyValueStore, type LibSqlKeyValueStoreOptions } from \"./libsql-store\";\nimport { createStatePersistence, type StatePersistence } from \"awaitly/persistence\";\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/**\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 async function createLibSqlPersistence(\n options: LibSqlPersistenceOptions = {}\n): Promise<\n StatePersistence & {\n loadRaw(runId: string): Promise<import(\"awaitly/persistence\").SerializedState | undefined>;\n }\n> {\n const { prefix, ...storeOptions } = options;\n\n const store = new LibSqlKeyValueStore(storeOptions);\n return createStatePersistence(store, prefix);\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 } 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 );\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\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\n await this.client.execute({\n sql: `\n INSERT INTO ${this.tableName} (key, value, expires_at)\n VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET\n value = excluded.value,\n expires_at = excluded.expires_at\n `,\n args: [key, value, expiresAt],\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 * 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"],"mappings":"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","names":["index_exports","__export","LibSqlKeyValueStore","createLibSqlPersistence","__toCommonJS","import_client","LibSqlKeyValueStore","options","url","tableName","pattern","key","nowIso","result","row","expiresAt","value","affected","likePattern","import_persistence","createLibSqlPersistence","options","prefix","storeOptions","store","LibSqlKeyValueStore"]}
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,135 +1,80 @@
1
- import * as awaitly_persistence from 'awaitly/persistence';
2
- import { KeyValueStore, StatePersistence } from 'awaitly/persistence';
3
1
  import { Client } from '@libsql/client';
2
+ import { SnapshotStore } from 'awaitly/persistence';
3
+ export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
4
+ import { WorkflowLock } from 'awaitly/durable';
5
+ export { WorkflowLock } from 'awaitly/durable';
4
6
 
5
7
  /**
6
- * awaitly-libsql
7
- *
8
- * libSQL / SQLite KeyValueStore implementation for awaitly persistence.
8
+ * libSQL workflow lock (lease) for cross-process concurrency control.
9
+ * Uses a lease (TTL) + owner token; release verifies the token.
9
10
  */
10
11
 
11
- /**
12
- * Options for 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;
12
+ interface LibSqlLockOptions {
35
13
  /**
36
- * Existing libSQL client to use.
37
- * If provided, url/authToken options are ignored.
14
+ * Table name for workflow locks.
15
+ * @default 'awaitly_workflow_lock'
38
16
  */
39
- client?: Client;
17
+ lockTableName?: string;
40
18
  }
19
+
41
20
  /**
42
- * libSQL / SQLite implementation of KeyValueStore.
21
+ * awaitly-libsql
43
22
  *
44
- * Automatically creates the required table on first use.
45
- * Supports TTL via ISO 8601 `expires_at` column.
23
+ * libSQL / SQLite persistence adapter for awaitly workflows.
24
+ * Provides ready-to-use SnapshotStore backed by libSQL.
46
25
  */
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
- * Close the underlying client if it supports close().
76
- */
77
- close(): Promise<void>;
78
- }
79
26
 
80
27
  /**
81
- * Options for creating libSQL persistence.
28
+ * Options for the libsql() shorthand function.
82
29
  */
83
- interface LibSqlPersistenceOptions extends LibSqlKeyValueStoreOptions {
84
- /**
85
- * Key prefix for state entries.
86
- * @default "workflow:state:"
87
- */
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 '' */
88
38
  prefix?: string;
39
+ /** Bring your own client. */
40
+ client?: Client;
41
+ /** Cross-process lock options. When set, the store implements WorkflowLock. */
42
+ lock?: LibSqlLockOptions;
89
43
  }
90
44
  /**
91
- * Create a StatePersistence instance backed by libSQL / SQLite.
92
- *
93
- * The table is automatically created on first use.
94
- *
95
- * @param options - libSQL connection and configuration options
96
- * @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.
97
47
  *
98
48
  * @example
99
49
  * ```typescript
100
- * import { createLibSqlPersistence } from "awaitly-libsql";
101
- * import { durable } from "awaitly/durable";
50
+ * import { libsql } from 'awaitly-libsql';
102
51
  *
103
- * const store = await createLibSqlPersistence({
104
- * url: "file:./awaitly.db",
105
- * });
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());
106
59
  *
107
- * const result = await durable.run(
108
- * { fetchUser, createOrder },
109
- * async (step, { fetchUser, createOrder }) => {
110
- * const user = await step(() => fetchUser("123"), { key: "fetch-user" });
111
- * const order = await step(() => createOrder(user), { key: "create-order" });
112
- * return order;
113
- * },
114
- * {
115
- * id: "checkout-123",
116
- * store,
117
- * }
118
- * );
60
+ * // Restore
61
+ * const snapshot = await store.load('wf-123');
62
+ * const wf2 = createWorkflow(deps, { snapshot });
63
+ * await wf2(myWorkflowFn);
119
64
  * ```
120
65
  *
121
66
  * @example
122
67
  * ```typescript
123
- * // Using remote Turso (libSQL) instance
124
- * const store = await createLibSqlPersistence({
125
- * url: process.env.LIBSQL_URL!,
126
- * authToken: process.env.LIBSQL_AUTH_TOKEN,
127
- * 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' },
128
75
  * });
129
76
  * ```
130
77
  */
131
- declare function createLibSqlPersistence(options?: LibSqlPersistenceOptions): Promise<StatePersistence & {
132
- loadRaw(runId: string): Promise<awaitly_persistence.SerializedState | undefined>;
133
- }>;
78
+ declare function libsql(urlOrOptions: string | LibSqlOptions): SnapshotStore & Partial<WorkflowLock>;
134
79
 
135
- export { LibSqlKeyValueStore, type LibSqlKeyValueStoreOptions, type LibSqlPersistenceOptions, createLibSqlPersistence };
80
+ export { type LibSqlLockOptions, type LibSqlOptions, libsql };
package/dist/index.d.ts CHANGED
@@ -1,135 +1,80 @@
1
- import * as awaitly_persistence from 'awaitly/persistence';
2
- import { KeyValueStore, StatePersistence } from 'awaitly/persistence';
3
1
  import { Client } from '@libsql/client';
2
+ import { SnapshotStore } from 'awaitly/persistence';
3
+ export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
4
+ import { WorkflowLock } from 'awaitly/durable';
5
+ export { WorkflowLock } from 'awaitly/durable';
4
6
 
5
7
  /**
6
- * awaitly-libsql
7
- *
8
- * libSQL / SQLite KeyValueStore implementation for awaitly persistence.
8
+ * libSQL workflow lock (lease) for cross-process concurrency control.
9
+ * Uses a lease (TTL) + owner token; release verifies the token.
9
10
  */
10
11
 
11
- /**
12
- * Options for 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;
12
+ interface LibSqlLockOptions {
35
13
  /**
36
- * Existing libSQL client to use.
37
- * If provided, url/authToken options are ignored.
14
+ * Table name for workflow locks.
15
+ * @default 'awaitly_workflow_lock'
38
16
  */
39
- client?: Client;
17
+ lockTableName?: string;
40
18
  }
19
+
41
20
  /**
42
- * libSQL / SQLite implementation of KeyValueStore.
21
+ * awaitly-libsql
43
22
  *
44
- * Automatically creates the required table on first use.
45
- * Supports TTL via ISO 8601 `expires_at` column.
23
+ * libSQL / SQLite persistence adapter for awaitly workflows.
24
+ * Provides ready-to-use SnapshotStore backed by libSQL.
46
25
  */
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
- * Close the underlying client if it supports close().
76
- */
77
- close(): Promise<void>;
78
- }
79
26
 
80
27
  /**
81
- * Options for creating libSQL persistence.
28
+ * Options for the libsql() shorthand function.
82
29
  */
83
- interface LibSqlPersistenceOptions extends LibSqlKeyValueStoreOptions {
84
- /**
85
- * Key prefix for state entries.
86
- * @default "workflow:state:"
87
- */
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 '' */
88
38
  prefix?: string;
39
+ /** Bring your own client. */
40
+ client?: Client;
41
+ /** Cross-process lock options. When set, the store implements WorkflowLock. */
42
+ lock?: LibSqlLockOptions;
89
43
  }
90
44
  /**
91
- * Create a StatePersistence instance backed by libSQL / SQLite.
92
- *
93
- * The table is automatically created on first use.
94
- *
95
- * @param options - libSQL connection and configuration options
96
- * @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.
97
47
  *
98
48
  * @example
99
49
  * ```typescript
100
- * import { createLibSqlPersistence } from "awaitly-libsql";
101
- * import { durable } from "awaitly/durable";
50
+ * import { libsql } from 'awaitly-libsql';
102
51
  *
103
- * const store = await createLibSqlPersistence({
104
- * url: "file:./awaitly.db",
105
- * });
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());
106
59
  *
107
- * const result = await durable.run(
108
- * { fetchUser, createOrder },
109
- * async (step, { fetchUser, createOrder }) => {
110
- * const user = await step(() => fetchUser("123"), { key: "fetch-user" });
111
- * const order = await step(() => createOrder(user), { key: "create-order" });
112
- * return order;
113
- * },
114
- * {
115
- * id: "checkout-123",
116
- * store,
117
- * }
118
- * );
60
+ * // Restore
61
+ * const snapshot = await store.load('wf-123');
62
+ * const wf2 = createWorkflow(deps, { snapshot });
63
+ * await wf2(myWorkflowFn);
119
64
  * ```
120
65
  *
121
66
  * @example
122
67
  * ```typescript
123
- * // Using remote Turso (libSQL) instance
124
- * const store = await createLibSqlPersistence({
125
- * url: process.env.LIBSQL_URL!,
126
- * authToken: process.env.LIBSQL_AUTH_TOKEN,
127
- * 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' },
128
75
  * });
129
76
  * ```
130
77
  */
131
- declare function createLibSqlPersistence(options?: LibSqlPersistenceOptions): Promise<StatePersistence & {
132
- loadRaw(runId: string): Promise<awaitly_persistence.SerializedState | undefined>;
133
- }>;
78
+ declare function libsql(urlOrOptions: string | LibSqlOptions): SnapshotStore & Partial<WorkflowLock>;
134
79
 
135
- export { LibSqlKeyValueStore, type LibSqlKeyValueStoreOptions, type LibSqlPersistenceOptions, createLibSqlPersistence };
80
+ export { type LibSqlLockOptions, type LibSqlOptions, libsql };
package/dist/index.js CHANGED
@@ -1,32 +1,32 @@
1
- import{createClient as l}from"@libsql/client";var a=class{client;tableName;initialized=!1;initPromise=null;constructor(e={}){if(e.client)this.client=e.client;else{let t=e.url??"file:./awaitly.db";this.client=l({url:t,authToken:e.authToken})}let i=e.tableName??"awaitly_workflow_state";if(!/^[A-Za-z0-9_]+$/.test(i))throw new Error(`Invalid table name '${i}'. Only alphanumeric and underscore characters are allowed.`);this.tableName=i}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
- );
7
- `,args:[]}),await this.client.execute({sql:`
8
- CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expires_at
9
- ON ${this.tableName}(expires_at);
10
- `,args:[]})}patternToLike(e){return e.replace(/%/g,"\\%").replace(/_/g,"\\_").replace(/\*/g,"%")}async get(e){await this.ensureInitialized();let i=new Date().toISOString(),t=await this.client.execute({sql:`
11
- SELECT value, expires_at
12
- FROM ${this.tableName}
13
- WHERE key = ?
14
- `,args:[e]});if(t.rows.length===0)return null;let s=t.rows[0],r=s.expires_at;return r&&r<=i?null:s.value??null}async set(e,i,t){await this.ensureInitialized();let s=t?.ttl&&t.ttl>0?new Date(Date.now()+t.ttl*1e3).toISOString():null;await this.client.execute({sql:`
15
- INSERT INTO ${this.tableName} (key, value, expires_at)
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:`
2
+ CREATE TABLE IF NOT EXISTS ${e} (
3
+ workflow_id TEXT PRIMARY KEY,
4
+ owner_token TEXT NOT NULL,
5
+ expires_at TEXT NOT NULL
6
+ )
7
+ `,args:[]}),await a.execute({sql:`
8
+ CREATE INDEX IF NOT EXISTS idx_${c}_expires_at
9
+ ON ${e}(expires_at)
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:`
11
+ INSERT INTO ${e} (workflow_id, owner_token, expires_at)
16
12
  VALUES (?, ?, ?)
17
- ON CONFLICT(key) DO UPDATE SET
18
- value = excluded.value,
13
+ ON CONFLICT(workflow_id) DO UPDATE SET
14
+ owner_token = excluded.owner_token,
19
15
  expires_at = excluded.expires_at
20
- `,args:[e,i,s]})}async delete(e){await this.ensureInitialized();let t=(await this.client.execute({sql:`DELETE FROM ${this.tableName} WHERE key = ?`,args:[e]})).rowsAffected;return typeof t=="number"?t>0:await this.get(e)===null}async exists(e){await this.ensureInitialized();let i=new Date().toISOString();return(await this.client.execute({sql:`
21
- SELECT 1
22
- FROM ${this.tableName}
23
- WHERE key = ?
24
- AND (expires_at IS NULL OR expires_at > ?)
25
- LIMIT 1
26
- `,args:[e,i]})).rows.length>0}async keys(e){await this.ensureInitialized();let i=this.patternToLike(e),t=new Date().toISOString();return(await this.client.execute({sql:`
27
- SELECT key
28
- FROM ${this.tableName}
29
- WHERE key LIKE ? ESCAPE '\\'
30
- AND (expires_at IS NULL OR expires_at > ?)
31
- `,args:[i,t]})).rows.map(r=>r.key)}async close(){}};import{createStatePersistence as o}from"awaitly/persistence";async function h(n={}){let{prefix:e,...i}=n,t=new a(i);return o(t,e)}export{a as LibSqlKeyValueStore,h as createLibSqlPersistence};
16
+ WHERE ${e}.expires_at < datetime('now')
17
+ RETURNING owner_token
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};
32
32
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/libsql-store.ts","../src/index.ts"],"sourcesContent":["/**\n * awaitly-libsql\n *\n * libSQL / SQLite KeyValueStore implementation for awaitly persistence.\n */\n\nimport { createClient, type Client } from \"@libsql/client\";\nimport type { KeyValueStore } 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 );\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\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\n await this.client.execute({\n sql: `\n INSERT INTO ${this.tableName} (key, value, expires_at)\n VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET\n value = excluded.value,\n expires_at = excluded.expires_at\n `,\n args: [key, value, expiresAt],\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 * 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 * awaitly-libsql\n *\n * libSQL / SQLite persistence adapter for awaitly workflows.\n * Provides ready-to-use StatePersistence backed by libSQL.\n */\n\nimport { LibSqlKeyValueStore, type LibSqlKeyValueStoreOptions } from \"./libsql-store\";\nimport { createStatePersistence, type StatePersistence } from \"awaitly/persistence\";\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/**\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 async function createLibSqlPersistence(\n options: LibSqlPersistenceOptions = {}\n): Promise<\n StatePersistence & {\n loadRaw(runId: string): Promise<import(\"awaitly/persistence\").SerializedState | undefined>;\n }\n> {\n const { prefix, ...storeOptions } = options;\n\n const store = new LibSqlKeyValueStore(storeOptions);\n return createStatePersistence(store, prefix);\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"],"mappings":"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","names":["createClient","LibSqlKeyValueStore","options","url","tableName","pattern","key","nowIso","result","row","expiresAt","value","affected","likePattern","createStatePersistence","createLibSqlPersistence","options","prefix","storeOptions","store","LibSqlKeyValueStore"]}
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": "1.0.0",
3
+ "version": "3.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.12.0"
42
+ "awaitly": "^1.14.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.12.0"
54
+ "awaitly": "^1.14.0"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public",