awaitly-mongo 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -116,23 +116,6 @@ A TTL index is automatically created on the `expiresAt` field, which MongoDB use
116
116
 
117
117
  The collection is created automatically on first use. You can customize the collection name via the `collection` option.
118
118
 
119
- ## Advanced Usage
120
-
121
- ### Direct KeyValueStore Access
122
-
123
- If you need more control, you can use the `MongoKeyValueStore` class directly:
124
-
125
- ```typescript
126
- import { MongoKeyValueStore } from 'awaitly-mongo';
127
- import { createStatePersistence } from 'awaitly/persistence';
128
-
129
- const store = new MongoKeyValueStore({
130
- connectionString: process.env.MONGODB_URI,
131
- });
132
-
133
- const persistence = createStatePersistence(store, 'custom:prefix:');
134
- ```
135
-
136
119
  ## Features
137
120
 
138
121
  - ✅ Automatic collection creation
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var f=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var A=Object.prototype.hasOwnProperty;var C=(l,e)=>{for(var t in e)f(l,t,{get:e[t],enumerable:!0})},D=(l,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of M(e))!A.call(l,n)&&n!==t&&f(l,n,{get:()=>e[n],enumerable:!(i=k(e,n))||i.enumerable});return l};var S=l=>D(f({},"__esModule",{value:!0}),l);var O={};C(O,{MongoKeyValueStore:()=>m,createMongoPersistence:()=>$});module.exports=S(O);var b=require("mongodb");var w=require("mongodb"),m=class{client=null;db;collection;collectionName;initialized=!1;initPromise=null;shouldCloseClient=!1;constructor(e){if(e.existingDb)this.db=e.existingDb,this.collectionName=e.collection??"workflow_state",this.collection=this.db.collection(this.collectionName);else if(e.existingClient){this.client=e.existingClient;let t=e.database??"awaitly";this.db=this.client.db(t),this.collectionName=e.collection??"workflow_state",this.collection=this.db.collection(this.collectionName)}else{let t=e.connectionString??"mongodb://localhost:27017",i=e.database,n=t.match(/mongodb:\/\/[^/]+\/([^?]+)/);n&&n[1]&&(i=i||n[1],t=t.replace(/\/[^/?]+(\?|$)/,"/$1")),this.client=new w.MongoClient(t,{directConnection:!0,...e.clientOptions}),this.shouldCloseClient=!0,i=i??"awaitly",this.db=this.client.db(i),this.collectionName=e.collection??"workflow_state",this.collection=this.db.collection(this.collectionName)}}async ensureInitialized(){if(!this.initialized)return this.initPromise?this.initPromise:(this.initPromise=(async()=>{try{if(this.client&&this.shouldCloseClient)try{await this.client.db("admin").command({ping:1})}catch{await this.client.connect()}await this.createCollection(),this.initialized=!0}catch(e){throw this.initPromise=null,e}})(),this.initPromise)}async createCollection(){(await this.db.listCollections({name:this.collectionName}).toArray()).length===0&&await this.db.createCollection(this.collectionName);try{(await this.collection.indexes()).some(n=>n.key&&"expiresAt"in n.key&&n.expireAfterSeconds!==void 0)||await this.collection.createIndex({expiresAt:1},{expireAfterSeconds:0,name:"expiresAt_ttl"})}catch(t){if(t?.code!==85)throw t}}patternToRegex(e){let t=e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(`^${t}$`)}async get(e){await this.ensureInitialized();let t=await this.collection.findOne({_id:e,$or:[{expiresAt:{$exists:!1}},{expiresAt:null},{expiresAt:{$gt:new Date}}]});return t?t.value:null}async set(e,t,i){await this.ensureInitialized();let n=i?.ttl?new Date(Date.now()+i.ttl*1e3):void 0,c={$set:{value:t,updatedAt:new Date,...n!==void 0?{expiresAt:n}:{}}};n===void 0&&(c.$unset={expiresAt:""}),await this.collection.updateOne({_id:e},c,{upsert:!0})}async delete(e){return await this.ensureInitialized(),(await this.collection.deleteOne({_id:e})).deletedCount>0}async exists(e){return await this.ensureInitialized(),await this.collection.countDocuments({_id:e,$or:[{expiresAt:{$exists:!1}},{expiresAt:null},{expiresAt:{$gt:new Date}}]})>0}async keys(e){await this.ensureInitialized();let t=this.patternToRegex(e);return(await this.collection.find({_id:t,$or:[{expiresAt:{$exists:!1}},{expiresAt:null},{expiresAt:{$gt:new Date}}]}).project({_id:1}).toArray()).map(n=>n._id)}async listKeys(e,t={}){await this.ensureInitialized();let i=Math.min(Math.max(0,t.limit??100),1e4),n=Math.max(0,t.offset??0),c=t.orderDir==="asc"?1:-1,r={_id:this.patternToRegex(e),$or:[{expiresAt:{$exists:!1}},{expiresAt:null},{expiresAt:{$gt:new Date}}]};t.updatedBefore!=null&&t.updatedAfter!=null?r.updatedAt={$lt:t.updatedBefore,$gt:t.updatedAfter}:t.updatedBefore!=null?r.updatedAt={$lt:t.updatedBefore}:t.updatedAfter!=null&&(r.updatedAt={$gt:t.updatedAfter});let d=t.orderBy==="key"?"_id":"updatedAt",g=(await this.collection.find(r).project({_id:1}).sort({[d]:c}).skip(n).limit(i).toArray()).map(p=>p._id),s;return(t.includeTotal===!0||n>0)&&(s=await this.collection.countDocuments(r)),{keys:g,total:s}}async deleteMany(e){return e.length===0?0:(await this.ensureInitialized(),(await this.collection.deleteMany({_id:{$in:e}})).deletedCount??0)}async clear(){await this.ensureInitialized(),await this.collection.deleteMany({})}async close(){this.client&&this.shouldCloseClient&&(await this.client.close(),this.client=null)}};var y=require("crypto");function x(l,e={}){let t=e.lockCollectionName??"workflow_lock",i=l.collection(t);async function n(){(await l.listCollections({name:t}).toArray()).length===0&&await l.createCollection(t)}async function c(r,d){let a=d?.ttlMs??6e4,u=(0,y.randomUUID)(),g=new Date(Date.now()+a);await n();try{let s=await i.findOneAndUpdate({_id:r,$or:[{expiresAt:{$lt:new Date}},{expiresAt:{$exists:!1}}]},{$set:{ownerToken:u,expiresAt:g}},{upsert:!0,returnDocument:"after"});return s&&s.ownerToken===u?{ownerToken:u}:null}catch(s){if(s&&typeof s=="object"&&"code"in s&&s.code===11e3)return null;throw s}}async function o(r,d){await i.deleteOne({_id:r,ownerToken:d})}return{tryAcquire:c,release:o,ensureLockCollection:n}}var h=require("awaitly/persistence");function P(l,e,t){let i=t??"workflow:state:",n=o=>o.slice(i.length),c=o=>`${i}${o}`;return Object.assign(l,{async listPage(o={}){let{keys:r,total:d}=await e.listKeys(`${i}*`,o),a=r.map(n),u=Math.min(Math.max(0,o.limit??100),1e4),g=a.length===u?(o.offset??0)+a.length:void 0;return{ids:a,total:d,nextOffset:g}},async deleteMany(o){if(o.length===0)return 0;let r=o.map(c);return e.deleteMany(r)},async clear(){return e.clear()}})}async function $(l={}){let{prefix:e,lock:t,...i}=l;if(t!==void 0){let o;if(i.existingDb)o=i.existingDb;else if(i.existingClient)o=i.existingClient.db(i.database??"awaitly");else{let u=i.connectionString??"mongodb://localhost:27017",g=new b.MongoClient(u,{directConnection:!0,...i.clientOptions});await g.connect();let s=i.database,p=u.match(/mongodb:\/\/[^/]+\/([^?]+)/);p&&p[1]&&(s=s??p[1]),s=s??"awaitly",o=g.db(s),i.existingClient=g,i.database=s}let r=new m(i),d=(0,h.createStatePersistence)(r,e),a=x(o,{lockCollectionName:t.lockCollectionName});return Object.assign(P(d,r,e),{tryAcquire:a.tryAcquire.bind(a),release:a.release.bind(a)})}let n=new m(i),c=(0,h.createStatePersistence)(n,e);return P(c,n,e)}0&&(module.exports={MongoKeyValueStore,createMongoPersistence});
1
+ "use strict";var y=Object.defineProperty;var h=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var L=Object.prototype.hasOwnProperty;var P=(t,o)=>{for(var e in o)y(t,e,{get:o[e],enumerable:!0})},S=(t,o,e,l)=>{if(o&&typeof o=="object"||typeof o=="function")for(let r of C(o))!L.call(t,r)&&r!==e&&y(t,r,{get:()=>o[r],enumerable:!(l=h(o,r))||l.enumerable});return t};var D=t=>S(y({},"__esModule",{value:!0}),t);var v={};P(v,{mongo:()=>O});module.exports=D(v);var x=require("mongodb");var A=require("crypto");function M(t,o={}){let e=o.lockCollectionName??"workflow_lock",l=t.collection(e);async function r(){(await t.listCollections({name:e}).toArray()).length===0&&await t.createCollection(e)}async function p(c,s){let w=s?.ttlMs??6e4,a=(0,A.randomUUID)(),d=new Date(Date.now()+w);await r();try{let i=await l.findOneAndUpdate({_id:c,$or:[{expiresAt:{$lt:new Date}},{expiresAt:{$exists:!1}}]},{$set:{ownerToken:a,expiresAt:d}},{upsert:!0,returnDocument:"after"});return i&&i.ownerToken===a?{ownerToken:a}:null}catch(i){if(i&&typeof i=="object"&&"code"in i&&i.code===11e3)return null;throw i}}async function k(c,s){await l.deleteOne({_id:c,ownerToken:s})}return{tryAcquire:p,release:k,ensureLockCollection:r}}function O(t){let o=typeof t=="string"?{url:t}:t,e=o.prefix??"",l=o.database,r=o.url.match(/mongodb(?:\+srv)?:\/\/[^/]+\/([^?]+)/);!l&&r&&r[1]&&(l=r[1]),l=l??"awaitly";let p=o.collection??"awaitly_snapshots",k=!o.client,c=o.client,s,w=!1,a=null,d=async()=>(s&&w||(c||(c=new x.MongoClient(o.url,{directConnection:!o.url.includes("mongodb+srv://"),...o.clientOptions})),await c.connect(),w=!0,s=c.db(l),await s.collection(p).createIndex({updatedAt:-1},{background:!0}).catch(()=>{}),o.lock&&!a&&(a=M(s,o.lock))),s),i={async save(n,u){let f=(await d()).collection(p),g=e+n;await f.updateOne({_id:g},{$set:{snapshot:u,updatedAt:new Date}},{upsert:!0})},async load(n){let m=(await d()).collection(p),f=e+n,g=await m.findOne({_id:f});return g?g.snapshot:null},async delete(n){let m=(await d()).collection(p),f=e+n;await m.deleteOne({_id:f})},async list(n){let m=(await d()).collection(p),f=e+(n?.prefix??""),g=n?.limit??100;return(await m.find({_id:{$regex:`^${f.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}`}}).sort({updatedAt:-1}).limit(g).toArray()).map(b=>({id:String(b._id).slice(e.length),updatedAt:b.updatedAt.toISOString()}))},async close(){k&&c&&(await c.close(),w=!1,s=void 0)},async tryAcquire(n,u){return await d(),a?a.tryAcquire(n,u):null},async release(n,u){if(await d(),!!a)return a.release(n,u)}};return o.lock||(delete i.tryAcquire,delete i.release),i}0&&(module.exports={mongo});
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/mongo-store.ts","../src/mongo-lock.ts"],"sourcesContent":["/**\n * awaitly-mongo\n *\n * MongoDB persistence adapter for awaitly workflows.\n * Provides ready-to-use StatePersistence backed by MongoDB.\n */\n\nimport type { Db } from \"mongodb\";\nimport { MongoClient as MongoClientImpl } from \"mongodb\";\nimport { MongoKeyValueStore, type MongoKeyValueStoreOptions } from \"./mongo-store\";\nimport { createMongoLock } from \"./mongo-lock\";\nimport {\n createStatePersistence,\n type StatePersistence,\n type SerializedState,\n type ListPageOptions,\n type ListPageResult,\n} from \"awaitly/persistence\";\nimport type { WorkflowLock } from \"awaitly/durable\";\n\n/**\n * Options for cross-process locking (lease + owner token).\n * When set, the returned store implements WorkflowLock so only one process\n * runs a given workflow ID at a time (when durable.run allowConcurrent is false).\n */\nexport interface MongoLockOptions {\n /**\n * Collection name for workflow locks.\n * @default 'workflow_lock'\n */\n lockCollectionName?: string;\n}\n\n/**\n * Options for creating MongoDB persistence.\n */\nexport interface MongoPersistenceOptions extends MongoKeyValueStoreOptions {\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?: MongoLockOptions;\n}\n\n/**\n * Create a StatePersistence instance backed by MongoDB.\n *\n * The collection is automatically created on first use with a TTL index.\n *\n * @param options - MongoDB connection and configuration options\n * @returns StatePersistence instance ready to use with durable.run()\n *\n * @example\n * ```typescript\n * import { createMongoPersistence } from 'awaitly-mongo';\n * import { durable } from 'awaitly/durable';\n *\n * const store = await createMongoPersistence({\n * connectionString: process.env.MONGODB_URI,\n * });\n *\n * const result = await durable.run(\n * { fetchUser, createOrder },\n * async (step, { fetchUser, createOrder }) => {\n * const user = await step(() => fetchUser('123'), { key: 'fetch-user' });\n * const order = await step(() => createOrder(user), { key: 'create-order' });\n * return order;\n * },\n * {\n * id: 'checkout-123',\n * store,\n * }\n * );\n * ```\n *\n * @example\n * ```typescript\n * // Using individual connection options\n * const store = await createMongoPersistence({\n * connectionString: 'mongodb://localhost:27017',\n * database: 'myapp',\n * collection: 'custom_workflow_state',\n * });\n * ```\n */\nexport type MongoStatePersistence = 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 MongoStatePersistenceWithLock = MongoStatePersistence & WorkflowLock;\n\nfunction addListPageAndDeleteMany(\n statePersistence: StatePersistence & {\n loadRaw(runId: string): Promise<SerializedState | undefined>;\n },\n store: MongoKeyValueStore,\n prefix: string | undefined\n): MongoStatePersistence {\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 return Object.assign(statePersistence, {\n async listPage(options: ListPageOptions = {}): Promise<ListPageResult> {\n const { keys, total } = await store.listKeys(`${effectivePrefix}*`, options);\n const ids = keys.map(stripPrefix);\n const limit = Math.min(Math.max(0, options.limit ?? 100), 10_000);\n const nextOffset =\n ids.length === limit ? (options.offset ?? 0) + ids.length : undefined;\n return { ids, total, nextOffset };\n },\n async deleteMany(ids: string[]): Promise<number> {\n if (ids.length === 0) return 0;\n const keys = ids.map(prefixKey);\n return store.deleteMany(keys);\n },\n async clear(): Promise<void> {\n return store.clear();\n },\n });\n}\n\nexport async function createMongoPersistence(\n options: MongoPersistenceOptions = {}\n): Promise<MongoStatePersistence | MongoStatePersistenceWithLock> {\n const { prefix, lock: lockOptions, ...storeOptions } = options;\n\n if (lockOptions !== undefined) {\n let db: Db;\n if (storeOptions.existingDb) {\n db = storeOptions.existingDb;\n } else if (storeOptions.existingClient) {\n db = storeOptions.existingClient.db(storeOptions.database ?? \"awaitly\");\n } else {\n const connectionString =\n storeOptions.connectionString ?? \"mongodb://localhost:27017\";\n const client = new MongoClientImpl(connectionString, {\n directConnection: true,\n ...storeOptions.clientOptions,\n });\n await client.connect();\n let databaseName = storeOptions.database;\n const urlMatch = connectionString.match(/mongodb:\\/\\/[^/]+\\/([^?]+)/);\n if (urlMatch && urlMatch[1]) {\n databaseName = databaseName ?? urlMatch[1];\n }\n databaseName = databaseName ?? \"awaitly\";\n db = client.db(databaseName);\n storeOptions.existingClient = client;\n storeOptions.database = databaseName;\n }\n const store = new MongoKeyValueStore(storeOptions);\n const statePersistence = createStatePersistence(store, prefix) as MongoStatePersistence;\n const lock = createMongoLock(db, {\n lockCollectionName: lockOptions.lockCollectionName,\n });\n return Object.assign(addListPageAndDeleteMany(statePersistence, store, prefix), {\n tryAcquire: lock.tryAcquire.bind(lock),\n release: lock.release.bind(lock),\n });\n }\n\n const store = new MongoKeyValueStore(storeOptions);\n const base = createStatePersistence(store, prefix);\n return addListPageAndDeleteMany(\n base as StatePersistence & {\n loadRaw(runId: string): Promise<SerializedState | undefined>;\n },\n store,\n prefix\n );\n}\n\n/**\n * MongoDB KeyValueStore implementation.\n * Use this directly if you need more control over the store.\n *\n * @example\n * ```typescript\n * import { MongoKeyValueStore } from 'awaitly-mongo';\n * import { createStatePersistence } from 'awaitly/persistence';\n *\n * const store = new MongoKeyValueStore({\n * connectionString: process.env.MONGODB_URI,\n * });\n *\n * const persistence = createStatePersistence(store, 'custom:prefix:');\n * ```\n */\nexport { MongoKeyValueStore, type MongoKeyValueStoreOptions };\n","/**\n * awaitly-mongo\n *\n * MongoDB KeyValueStore implementation for awaitly persistence.\n */\n\nimport type {\n MongoClient,\n MongoClientOptions,\n Db,\n Collection,\n WithId,\n Document,\n} from \"mongodb\";\nimport { MongoClient as MongoClientImpl } from \"mongodb\";\nimport type { KeyValueStore, ListPageOptions } from \"awaitly/persistence\";\n\n/**\n * Options for MongoDB KeyValueStore.\n */\nexport interface MongoKeyValueStoreOptions {\n /**\n * MongoDB connection string.\n *\n * @example 'mongodb://localhost:27017'\n * @example 'mongodb://user:password@localhost:27017/dbname'\n */\n connectionString?: string;\n\n /**\n * Database name.\n * @default 'awaitly'\n */\n database?: string;\n\n /**\n * Collection name for storing key-value pairs.\n * @default 'workflow_state'\n */\n collection?: string;\n\n /**\n * Additional MongoDB client options.\n */\n clientOptions?: MongoClientOptions;\n\n /**\n * Existing MongoDB client to use.\n * If provided, connection options are ignored.\n */\n existingClient?: MongoClient;\n\n /**\n * Existing database instance to use.\n * If provided, connection and database options are ignored.\n */\n existingDb?: Db;\n}\n\n/**\n * Document schema for stored values.\n */\ninterface KeyValueDocument extends Document {\n _id: string;\n value: string;\n expiresAt?: Date | null;\n updatedAt?: Date | null;\n}\n\n/**\n * MongoDB implementation of KeyValueStore.\n *\n * Automatically creates the required collection with TTL index on first use.\n * Supports TTL via expiresAt field with automatic expiration.\n */\nexport class MongoKeyValueStore implements KeyValueStore {\n private client: MongoClient | null = null;\n private db: Db;\n private collection: Collection<KeyValueDocument>;\n private collectionName: string;\n private initialized: boolean = false;\n private initPromise: Promise<void> | null = null;\n private shouldCloseClient: boolean = false;\n\n constructor(options: MongoKeyValueStoreOptions) {\n if (options.existingDb) {\n // Use provided database\n this.db = options.existingDb;\n this.collectionName = options.collection ?? \"workflow_state\";\n this.collection = this.db.collection<KeyValueDocument>(this.collectionName);\n } else if (options.existingClient) {\n // Use provided client\n this.client = options.existingClient;\n const databaseName = options.database ?? \"awaitly\";\n this.db = this.client.db(databaseName);\n this.collectionName = options.collection ?? \"workflow_state\";\n this.collection = this.db.collection<KeyValueDocument>(this.collectionName);\n } else {\n // Create new client\n let connectionString = options.connectionString ?? \"mongodb://localhost:27017\";\n \n // Extract database name from connection string if present\n let databaseName = options.database;\n const urlMatch = connectionString.match(/mongodb:\\/\\/[^/]+\\/([^?]+)/);\n if (urlMatch && urlMatch[1]) {\n databaseName = databaseName || urlMatch[1];\n // Remove database from connection string to avoid conflicts\n connectionString = connectionString.replace(/\\/[^/?]+(\\?|$)/, '/$1');\n }\n \n this.client = new MongoClientImpl(connectionString, {\n directConnection: true, // Use direct connection for single-node instances\n ...options.clientOptions,\n });\n this.shouldCloseClient = true;\n databaseName = databaseName ?? \"awaitly\";\n this.db = this.client.db(databaseName);\n this.collectionName = options.collection ?? \"workflow_state\";\n this.collection = this.db.collection<KeyValueDocument>(this.collectionName);\n }\n }\n\n /**\n * Initialize the store by connecting to MongoDB and creating the collection with TTL index.\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 // Connect client if we created it\n if (this.client && this.shouldCloseClient) {\n // Connect if not already connected\n try {\n await this.client.db(\"admin\").command({ ping: 1 });\n } catch {\n // Not connected, connect now\n await this.client.connect();\n }\n }\n\n await this.createCollection();\n this.initialized = true;\n } catch (error) {\n this.initPromise = null;\n throw error;\n }\n })();\n\n return this.initPromise;\n }\n\n /**\n * Create the collection and TTL index if they don't exist.\n */\n private async createCollection(): Promise<void> {\n // Create collection if it doesn't exist\n const collections = await this.db.listCollections({ name: this.collectionName }).toArray();\n if (collections.length === 0) {\n await this.db.createCollection(this.collectionName);\n }\n\n // Create TTL index on expiresAt field\n try {\n const indexes = await this.collection.indexes();\n const hasTtlIndex = indexes.some(\n (index) => index.key && \"expiresAt\" in index.key && index.expireAfterSeconds !== undefined\n );\n\n if (!hasTtlIndex) {\n await this.collection.createIndex(\n { expiresAt: 1 },\n {\n expireAfterSeconds: 0, // Delete immediately when expiresAt is reached\n name: \"expiresAt_ttl\",\n }\n );\n }\n } catch (error) {\n // Index might already exist from a previous run, ignore duplicate key errors\n if ((error as { code?: number })?.code !== 85) {\n throw error;\n }\n }\n }\n\n /**\n * Convert glob pattern to MongoDB regex pattern.\n * Supports * wildcard (matches any characters).\n */\n private patternToRegex(pattern: string): RegExp {\n // Escape regex special characters and convert * to .*\n const escaped = pattern\n .replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \".*\");\n return new RegExp(`^${escaped}$`);\n }\n\n async get(key: string): Promise<string | null> {\n await this.ensureInitialized();\n\n const doc = await this.collection.findOne({\n _id: key,\n $or: [{ expiresAt: { $exists: false } }, { expiresAt: null }, { expiresAt: { $gt: new Date() } }],\n });\n\n if (!doc) {\n return null;\n }\n\n return doc.value;\n }\n\n async set(key: string, value: string, options?: { ttl?: number }): Promise<void> {\n await this.ensureInitialized();\n\n const expiresAt = options?.ttl ? new Date(Date.now() + options.ttl * 1000) : undefined;\n\n const update: Record<string, unknown> = {\n $set: {\n value,\n updatedAt: new Date(),\n ...(expiresAt !== undefined ? { expiresAt } : {}),\n },\n };\n if (expiresAt === undefined) {\n update.$unset = { expiresAt: \"\" };\n }\n\n await this.collection.updateOne({ _id: key }, update, { upsert: true });\n }\n\n async delete(key: string): Promise<boolean> {\n await this.ensureInitialized();\n\n const result = await this.collection.deleteOne({ _id: key });\n return result.deletedCount > 0;\n }\n\n async exists(key: string): Promise<boolean> {\n await this.ensureInitialized();\n\n const count = await this.collection.countDocuments({\n _id: key,\n $or: [{ expiresAt: { $exists: false } }, { expiresAt: null }, { expiresAt: { $gt: new Date() } }],\n });\n\n return count > 0;\n }\n\n async keys(pattern: string): Promise<string[]> {\n await this.ensureInitialized();\n\n const regex = this.patternToRegex(pattern);\n\n const docs = await this.collection\n .find({\n _id: regex,\n $or: [{ expiresAt: { $exists: false } }, { expiresAt: null }, { expiresAt: { $gt: new Date() } }],\n })\n .project({ _id: 1 })\n .toArray();\n\n return docs.map((doc) => doc._id);\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 orderDir = options.orderDir === \"asc\" ? 1 : -1;\n const regex = this.patternToRegex(pattern);\n\n const baseFilter: Record<string, unknown> = {\n _id: regex,\n $or: [{ expiresAt: { $exists: false } }, { expiresAt: null }, { expiresAt: { $gt: new Date() } }],\n };\n if (options.updatedBefore != null && options.updatedAfter != null) {\n (baseFilter as Record<string, unknown>).updatedAt = {\n $lt: options.updatedBefore,\n $gt: options.updatedAfter,\n };\n } else if (options.updatedBefore != null) {\n (baseFilter as Record<string, unknown>).updatedAt = { $lt: options.updatedBefore };\n } else if (options.updatedAfter != null) {\n (baseFilter as Record<string, unknown>).updatedAt = { $gt: options.updatedAfter };\n }\n\n const sortKey = options.orderBy === \"key\" ? \"_id\" : \"updatedAt\";\n const cursor = this.collection\n .find(baseFilter)\n .project({ _id: 1 })\n .sort({ [sortKey]: orderDir })\n .skip(offset)\n .limit(limit);\n\n const docs = await cursor.toArray();\n const keys = docs.map((doc) => doc._id);\n\n let total: number | undefined;\n if (options.includeTotal === true || offset > 0) {\n total = await this.collection.countDocuments(baseFilter);\n }\n\n return { keys, total };\n }\n\n /**\n * Delete multiple keys in one round-trip.\n */\n async deleteMany(keys: string[]): Promise<number> {\n if (keys.length === 0) return 0;\n await this.ensureInitialized();\n const result = await this.collection.deleteMany({ _id: { $in: keys } });\n return result.deletedCount ?? 0;\n }\n\n /**\n * Remove all entries from the collection (clear all workflow state).\n */\n async clear(): Promise<void> {\n await this.ensureInitialized();\n await this.collection.deleteMany({});\n }\n\n /**\n * Close the MongoDB client connection.\n * Only closes if this store created the client.\n */\n async close(): Promise<void> {\n if (this.client && this.shouldCloseClient) {\n await this.client.close();\n this.client = null;\n }\n }\n}\n","/**\n * MongoDB workflow lock (lease) for cross-process concurrency control.\n * Uses a lease (TTL) + owner token; release verifies the token.\n */\n\nimport type { Db, Collection } from \"mongodb\";\nimport { randomUUID } from \"node:crypto\";\n\nexport interface MongoLockOptions {\n /**\n * Collection name for workflow locks.\n * @default 'workflow_lock'\n */\n lockCollectionName?: string;\n}\n\ninterface LockDocument {\n _id: string;\n ownerToken: string;\n expiresAt: Date;\n}\n\n/**\n * Create tryAcquire and release functions that use a MongoDB lock collection.\n * Caller must pass the same Db used for state (so one connection).\n */\nexport function createMongoLock(\n db: Db,\n options: MongoLockOptions = {}\n): {\n tryAcquire(\n id: string,\n opts?: { ttlMs?: number }\n ): Promise<{ ownerToken: string } | null>;\n release(id: string, ownerToken: string): Promise<void>;\n ensureLockCollection(): Promise<void>;\n} {\n const lockCollectionName = options.lockCollectionName ?? \"workflow_lock\";\n const collection = db.collection<LockDocument>(lockCollectionName);\n\n async function ensureLockCollection(): Promise<void> {\n const collections = await db.listCollections({ name: lockCollectionName }).toArray();\n if (collections.length === 0) {\n await db.createCollection(lockCollectionName);\n }\n }\n\n async function tryAcquire(\n id: string,\n opts?: { ttlMs?: number }\n ): Promise<{ ownerToken: string } | null> {\n const ttlMs = opts?.ttlMs ?? 60_000;\n const ownerToken = randomUUID();\n const expiresAt = new Date(Date.now() + ttlMs);\n\n await ensureLockCollection();\n\n // Atomic: insert or update only when no doc or doc is expired.\n // When lock exists and is unexpired, filter won't match and upsert\n // throws duplicate key error (E11000) - catch it and return null.\n try {\n const result = await collection.findOneAndUpdate(\n {\n _id: id,\n $or: [\n { expiresAt: { $lt: new Date() } },\n { expiresAt: { $exists: false } },\n ],\n },\n { $set: { ownerToken, expiresAt } },\n { upsert: true, returnDocument: \"after\" }\n );\n\n if (result && result.ownerToken === ownerToken) {\n return { ownerToken };\n }\n return null;\n } catch (error: unknown) {\n // Duplicate key error means lock exists and is not expired\n if (\n error &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === 11000\n ) {\n return null;\n }\n throw error;\n }\n }\n\n async function release(id: string, ownerToken: string): Promise<void> {\n await collection.deleteOne({ _id: id, ownerToken });\n }\n\n return { tryAcquire, release, ensureLockCollection };\n}\n"],"mappings":"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","names":["index_exports","__export","MongoKeyValueStore","createMongoPersistence","__toCommonJS","import_mongodb","import_mongodb","MongoKeyValueStore","options","databaseName","connectionString","urlMatch","MongoClientImpl","error","index","pattern","escaped","key","doc","value","expiresAt","update","regex","limit","offset","orderDir","baseFilter","sortKey","keys","total","import_node_crypto","createMongoLock","db","options","lockCollectionName","collection","ensureLockCollection","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","error","release","import_persistence","addListPageAndDeleteMany","statePersistence","store","prefix","effectivePrefix","stripPrefix","key","prefixKey","runId","options","keys","total","ids","limit","nextOffset","createMongoPersistence","lockOptions","storeOptions","db","connectionString","client","MongoClientImpl","databaseName","urlMatch","MongoKeyValueStore","lock","createMongoLock","base"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/mongo-lock.ts"],"sourcesContent":["/**\n * awaitly-mongo\n *\n * MongoDB persistence adapter for awaitly workflows.\n * Provides ready-to-use SnapshotStore backed by MongoDB.\n */\n\nimport type { Db, MongoClientOptions } from \"mongodb\";\nimport { MongoClient as MongoClientImpl } from \"mongodb\";\nimport type { WorkflowSnapshot, SnapshotStore } from \"awaitly/persistence\";\nimport type { WorkflowLock } from \"awaitly/durable\";\nimport { createMongoLock, type MongoLockOptions } from \"./mongo-lock\";\n\n// Re-export types for convenience\nexport type { SnapshotStore, WorkflowSnapshot } from \"awaitly/persistence\";\nexport type { WorkflowLock } from \"awaitly/durable\";\nexport type { MongoLockOptions } from \"./mongo-lock\";\n\n// =============================================================================\n// MongoOptions\n// =============================================================================\n\n/**\n * Options for the mongo() shorthand function.\n */\nexport interface MongoOptions {\n /** MongoDB connection URL. */\n url: string;\n /** Database name. @default 'awaitly' */\n database?: string;\n /** Collection name for snapshots. @default 'awaitly_snapshots' */\n collection?: string;\n /** Key prefix for IDs. @default '' */\n prefix?: string;\n /** Bring your own client. */\n client?: MongoClientImpl;\n /** MongoDB client options. */\n clientOptions?: MongoClientOptions;\n /** Cross-process lock options. When set, the store implements WorkflowLock. */\n lock?: MongoLockOptions;\n}\n\n// =============================================================================\n// mongo() - One-liner Snapshot Store Setup\n// =============================================================================\n\n/**\n * Create a snapshot store backed by MongoDB.\n * This is the simplified one-liner API for workflow persistence.\n *\n * @example\n * ```typescript\n * import { mongo } from 'awaitly-mongo';\n *\n * // One-liner setup\n * const store = mongo('mongodb://localhost:27017/mydb');\n *\n * // Execute + persist\n * const wf = createWorkflow(deps);\n * await wf(myWorkflowFn);\n * await store.save('wf-123', wf.getSnapshot());\n *\n * // Restore\n * const snapshot = await store.load('wf-123');\n * const wf2 = createWorkflow(deps, { snapshot });\n * await wf2(myWorkflowFn);\n * ```\n *\n * @example\n * ```typescript\n * // With options including cross-process locking\n * const store = mongo({\n * url: 'mongodb://localhost:27017',\n * database: 'myapp',\n * collection: 'my_workflow_snapshots',\n * prefix: 'orders:',\n * lock: { lockCollectionName: 'my_workflow_locks' },\n * });\n * ```\n */\nexport function mongo(urlOrOptions: string | MongoOptions): SnapshotStore & Partial<WorkflowLock> {\n const opts = typeof urlOrOptions === \"string\" ? { url: urlOrOptions } : urlOrOptions;\n const prefix = opts.prefix ?? \"\";\n\n // Parse database from URL if provided\n let databaseName = opts.database;\n const urlMatch = opts.url.match(/mongodb(?:\\+srv)?:\\/\\/[^/]+\\/([^?]+)/);\n if (!databaseName && urlMatch && urlMatch[1]) {\n databaseName = urlMatch[1];\n }\n databaseName = databaseName ?? \"awaitly\";\n\n const collectionName = opts.collection ?? \"awaitly_snapshots\";\n\n // Create or use existing client\n const ownClient = !opts.client;\n let client: MongoClientImpl | undefined = opts.client;\n let db: Db | undefined;\n let connected = false;\n let lock: { tryAcquire: WorkflowLock[\"tryAcquire\"]; release: WorkflowLock[\"release\"] } | null = null;\n\n const ensureConnected = async (): Promise<Db> => {\n if (db && connected) return db;\n\n if (!client) {\n client = new MongoClientImpl(opts.url, {\n directConnection: !opts.url.includes(\"mongodb+srv://\"),\n ...opts.clientOptions,\n });\n }\n\n await client.connect();\n connected = true;\n db = client.db(databaseName);\n\n // Create index on updatedAt for list queries\n const collection = db.collection(collectionName);\n await collection.createIndex({ updatedAt: -1 }, { background: true }).catch(() => {\n // Index may already exist, ignore error\n });\n\n // Create lock if requested\n if (opts.lock && !lock) {\n lock = createMongoLock(db, opts.lock);\n }\n\n return db;\n };\n\n const store: SnapshotStore & Partial<WorkflowLock> = {\n async save(id: string, snapshot: WorkflowSnapshot): Promise<void> {\n const db = await ensureConnected();\n const collection = db.collection(collectionName);\n const fullId = prefix + id;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n await collection.updateOne(\n { _id: fullId } as any,\n {\n $set: {\n snapshot,\n updatedAt: new Date(),\n },\n },\n { upsert: true }\n );\n },\n\n async load(id: string): Promise<WorkflowSnapshot | null> {\n const db = await ensureConnected();\n const collection = db.collection(collectionName);\n const fullId = prefix + id;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const doc = await collection.findOne({ _id: fullId } as any);\n if (!doc) return null;\n return doc.snapshot as WorkflowSnapshot;\n },\n\n async delete(id: string): Promise<void> {\n const db = await ensureConnected();\n const collection = db.collection(collectionName);\n const fullId = prefix + id;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n await collection.deleteOne({ _id: fullId } as any);\n },\n\n async list(options?: { prefix?: string; limit?: number }): Promise<Array<{ id: string; updatedAt: string }>> {\n const db = await ensureConnected();\n const collection = db.collection(collectionName);\n const filterPrefix = prefix + (options?.prefix ?? \"\");\n const limit = options?.limit ?? 100;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const cursor = collection\n .find({ _id: { $regex: `^${filterPrefix.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}` } } as any)\n .sort({ updatedAt: -1 })\n .limit(limit);\n\n const docs = await cursor.toArray();\n return docs.map(doc => ({\n id: String(doc._id).slice(prefix.length),\n updatedAt: (doc.updatedAt as Date).toISOString(),\n }));\n },\n\n async close(): Promise<void> {\n // Only close client if we created it\n if (ownClient && client) {\n await client.close();\n connected = false;\n db = undefined;\n }\n },\n\n // Lock methods are added dynamically below after first connection\n async tryAcquire(id: string, options?: { ttlMs?: number }): Promise<{ ownerToken: string } | null> {\n await ensureConnected();\n if (!lock) return null;\n return lock.tryAcquire(id, options);\n },\n\n async release(id: string, ownerToken: string): Promise<void> {\n await ensureConnected();\n if (!lock) return;\n return lock.release(id, ownerToken);\n },\n };\n\n // Only include lock methods if lock is configured\n if (!opts.lock) {\n delete store.tryAcquire;\n delete store.release;\n }\n\n return store;\n}\n","/**\n * MongoDB workflow lock (lease) for cross-process concurrency control.\n * Uses a lease (TTL) + owner token; release verifies the token.\n */\n\nimport type { Db, Collection } from \"mongodb\";\nimport { randomUUID } from \"node:crypto\";\n\nexport interface MongoLockOptions {\n /**\n * Collection name for workflow locks.\n * @default 'workflow_lock'\n */\n lockCollectionName?: string;\n}\n\ninterface LockDocument {\n _id: string;\n ownerToken: string;\n expiresAt: Date;\n}\n\n/**\n * Create tryAcquire and release functions that use a MongoDB lock collection.\n * Caller must pass the same Db used for state (so one connection).\n */\nexport function createMongoLock(\n db: Db,\n options: MongoLockOptions = {}\n): {\n tryAcquire(\n id: string,\n opts?: { ttlMs?: number }\n ): Promise<{ ownerToken: string } | null>;\n release(id: string, ownerToken: string): Promise<void>;\n ensureLockCollection(): Promise<void>;\n} {\n const lockCollectionName = options.lockCollectionName ?? \"workflow_lock\";\n const collection = db.collection<LockDocument>(lockCollectionName);\n\n async function ensureLockCollection(): Promise<void> {\n const collections = await db.listCollections({ name: lockCollectionName }).toArray();\n if (collections.length === 0) {\n await db.createCollection(lockCollectionName);\n }\n }\n\n async function tryAcquire(\n id: string,\n opts?: { ttlMs?: number }\n ): Promise<{ ownerToken: string } | null> {\n const ttlMs = opts?.ttlMs ?? 60_000;\n const ownerToken = randomUUID();\n const expiresAt = new Date(Date.now() + ttlMs);\n\n await ensureLockCollection();\n\n // Atomic: insert or update only when no doc or doc is expired.\n // When lock exists and is unexpired, filter won't match and upsert\n // throws duplicate key error (E11000) - catch it and return null.\n try {\n const result = await collection.findOneAndUpdate(\n {\n _id: id,\n $or: [\n { expiresAt: { $lt: new Date() } },\n { expiresAt: { $exists: false } },\n ],\n },\n { $set: { ownerToken, expiresAt } },\n { upsert: true, returnDocument: \"after\" }\n );\n\n if (result && result.ownerToken === ownerToken) {\n return { ownerToken };\n }\n return null;\n } catch (error: unknown) {\n // Duplicate key error means lock exists and is not expired\n if (\n error &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === 11000\n ) {\n return null;\n }\n throw error;\n }\n }\n\n async function release(id: string, ownerToken: string): Promise<void> {\n await collection.deleteOne({ _id: id, ownerToken });\n }\n\n return { tryAcquire, release, ensureLockCollection };\n}\n"],"mappings":"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","names":["index_exports","__export","mongo","__toCommonJS","import_mongodb","import_node_crypto","createMongoLock","db","options","lockCollectionName","collection","ensureLockCollection","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","error","release","mongo","urlOrOptions","opts","prefix","databaseName","urlMatch","collectionName","ownClient","client","db","connected","lock","ensureConnected","MongoClientImpl","createMongoLock","store","id","snapshot","collection","fullId","doc","options","filterPrefix","limit","ownerToken"]}
package/dist/index.d.cts CHANGED
@@ -1,189 +1,82 @@
1
- import { MongoClientOptions, MongoClient, Db } from 'mongodb';
2
- import { KeyValueStore, ListPageOptions, StatePersistence, SerializedState, ListPageResult } from 'awaitly/persistence';
1
+ import { MongoClient, MongoClientOptions } from 'mongodb';
2
+ import { SnapshotStore } from 'awaitly/persistence';
3
+ export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
3
4
  import { WorkflowLock } from 'awaitly/durable';
5
+ export { WorkflowLock } from 'awaitly/durable';
4
6
 
5
7
  /**
6
- * awaitly-mongo
7
- *
8
- * MongoDB KeyValueStore implementation for awaitly persistence.
8
+ * MongoDB 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 MongoDB KeyValueStore.
13
- */
14
- interface MongoKeyValueStoreOptions {
15
- /**
16
- * MongoDB connection string.
17
- *
18
- * @example 'mongodb://localhost:27017'
19
- * @example 'mongodb://user:password@localhost:27017/dbname'
20
- */
21
- connectionString?: string;
22
- /**
23
- * Database name.
24
- * @default 'awaitly'
25
- */
26
- database?: string;
27
- /**
28
- * Collection name for storing key-value pairs.
29
- * @default 'workflow_state'
30
- */
31
- collection?: string;
32
- /**
33
- * Additional MongoDB client options.
34
- */
35
- clientOptions?: MongoClientOptions;
36
- /**
37
- * Existing MongoDB client to use.
38
- * If provided, connection options are ignored.
39
- */
40
- existingClient?: MongoClient;
41
- /**
42
- * Existing database instance to use.
43
- * If provided, connection and database options are ignored.
44
- */
45
- existingDb?: Db;
46
- }
47
- /**
48
- * MongoDB implementation of KeyValueStore.
49
- *
50
- * Automatically creates the required collection with TTL index on first use.
51
- * Supports TTL via expiresAt field with automatic expiration.
52
- */
53
- declare class MongoKeyValueStore implements KeyValueStore {
54
- private client;
55
- private db;
56
- private collection;
57
- private collectionName;
58
- private initialized;
59
- private initPromise;
60
- private shouldCloseClient;
61
- constructor(options: MongoKeyValueStoreOptions);
62
- /**
63
- * Initialize the store by connecting to MongoDB and creating the collection with TTL index.
64
- * This is called automatically on first use.
65
- */
66
- private ensureInitialized;
67
- /**
68
- * Create the collection and TTL index if they don't exist.
69
- */
70
- private createCollection;
71
- /**
72
- * Convert glob pattern to MongoDB regex pattern.
73
- * Supports * wildcard (matches any characters).
74
- */
75
- private patternToRegex;
76
- get(key: string): Promise<string | null>;
77
- set(key: string, value: string, options?: {
78
- ttl?: number;
79
- }): Promise<void>;
80
- delete(key: string): Promise<boolean>;
81
- exists(key: string): Promise<boolean>;
82
- keys(pattern: string): Promise<string[]>;
83
- /**
84
- * List keys with pagination, filtering, and ordering.
85
- */
86
- listKeys(pattern: string, options?: ListPageOptions): Promise<{
87
- keys: string[];
88
- total?: number;
89
- }>;
90
- /**
91
- * Delete multiple keys in one round-trip.
92
- */
93
- deleteMany(keys: string[]): Promise<number>;
94
- /**
95
- * Remove all entries from the collection (clear all workflow state).
96
- */
97
- clear(): Promise<void>;
12
+ interface MongoLockOptions {
98
13
  /**
99
- * Close the MongoDB client connection.
100
- * Only closes if this store created the client.
14
+ * Collection name for workflow locks.
15
+ * @default 'workflow_lock'
101
16
  */
102
- close(): Promise<void>;
17
+ lockCollectionName?: string;
103
18
  }
104
19
 
105
20
  /**
106
21
  * awaitly-mongo
107
22
  *
108
23
  * MongoDB persistence adapter for awaitly workflows.
109
- * Provides ready-to-use StatePersistence backed by MongoDB.
24
+ * Provides ready-to-use SnapshotStore backed by MongoDB.
110
25
  */
111
26
 
112
27
  /**
113
- * Options for cross-process locking (lease + owner token).
114
- * When set, the returned store implements WorkflowLock so only one process
115
- * runs a given workflow ID at a time (when durable.run allowConcurrent is false).
28
+ * Options for the mongo() shorthand function.
116
29
  */
117
- interface MongoLockOptions {
118
- /**
119
- * Collection name for workflow locks.
120
- * @default 'workflow_lock'
121
- */
122
- lockCollectionName?: string;
123
- }
124
- /**
125
- * Options for creating MongoDB persistence.
126
- */
127
- interface MongoPersistenceOptions extends MongoKeyValueStoreOptions {
128
- /**
129
- * Key prefix for state entries.
130
- * @default 'workflow:state:'
131
- */
30
+ interface MongoOptions {
31
+ /** MongoDB connection URL. */
32
+ url: string;
33
+ /** Database name. @default 'awaitly' */
34
+ database?: string;
35
+ /** Collection name for snapshots. @default 'awaitly_snapshots' */
36
+ collection?: string;
37
+ /** Key prefix for IDs. @default '' */
132
38
  prefix?: string;
133
- /**
134
- * When set, the store implements WorkflowLock for cross-process concurrency control.
135
- * Uses a lease (TTL) + owner token; release verifies the token.
136
- */
39
+ /** Bring your own client. */
40
+ client?: MongoClient;
41
+ /** MongoDB client options. */
42
+ clientOptions?: MongoClientOptions;
43
+ /** Cross-process lock options. When set, the store implements WorkflowLock. */
137
44
  lock?: MongoLockOptions;
138
45
  }
139
46
  /**
140
- * Create a StatePersistence instance backed by MongoDB.
141
- *
142
- * The collection is automatically created on first use with a TTL index.
143
- *
144
- * @param options - MongoDB connection and configuration options
145
- * @returns StatePersistence instance ready to use with durable.run()
47
+ * Create a snapshot store backed by MongoDB.
48
+ * This is the simplified one-liner API for workflow persistence.
146
49
  *
147
50
  * @example
148
51
  * ```typescript
149
- * import { createMongoPersistence } from 'awaitly-mongo';
150
- * import { durable } from 'awaitly/durable';
52
+ * import { mongo } from 'awaitly-mongo';
151
53
  *
152
- * const store = await createMongoPersistence({
153
- * connectionString: process.env.MONGODB_URI,
154
- * });
54
+ * // One-liner setup
55
+ * const store = mongo('mongodb://localhost:27017/mydb');
56
+ *
57
+ * // Execute + persist
58
+ * const wf = createWorkflow(deps);
59
+ * await wf(myWorkflowFn);
60
+ * await store.save('wf-123', wf.getSnapshot());
155
61
  *
156
- * const result = await durable.run(
157
- * { fetchUser, createOrder },
158
- * async (step, { fetchUser, createOrder }) => {
159
- * const user = await step(() => fetchUser('123'), { key: 'fetch-user' });
160
- * const order = await step(() => createOrder(user), { key: 'create-order' });
161
- * return order;
162
- * },
163
- * {
164
- * id: 'checkout-123',
165
- * store,
166
- * }
167
- * );
62
+ * // Restore
63
+ * const snapshot = await store.load('wf-123');
64
+ * const wf2 = createWorkflow(deps, { snapshot });
65
+ * await wf2(myWorkflowFn);
168
66
  * ```
169
67
  *
170
68
  * @example
171
69
  * ```typescript
172
- * // Using individual connection options
173
- * const store = await createMongoPersistence({
174
- * connectionString: 'mongodb://localhost:27017',
70
+ * // With options including cross-process locking
71
+ * const store = mongo({
72
+ * url: 'mongodb://localhost:27017',
175
73
  * database: 'myapp',
176
- * collection: 'custom_workflow_state',
74
+ * collection: 'my_workflow_snapshots',
75
+ * prefix: 'orders:',
76
+ * lock: { lockCollectionName: 'my_workflow_locks' },
177
77
  * });
178
78
  * ```
179
79
  */
180
- type MongoStatePersistence = StatePersistence & {
181
- loadRaw(runId: string): Promise<SerializedState | undefined>;
182
- listPage(options?: ListPageOptions): Promise<ListPageResult>;
183
- deleteMany(ids: string[]): Promise<number>;
184
- clear(): Promise<void>;
185
- };
186
- type MongoStatePersistenceWithLock = MongoStatePersistence & WorkflowLock;
187
- declare function createMongoPersistence(options?: MongoPersistenceOptions): Promise<MongoStatePersistence | MongoStatePersistenceWithLock>;
80
+ declare function mongo(urlOrOptions: string | MongoOptions): SnapshotStore & Partial<WorkflowLock>;
188
81
 
189
- export { MongoKeyValueStore, type MongoKeyValueStoreOptions, type MongoLockOptions, type MongoPersistenceOptions, type MongoStatePersistence, type MongoStatePersistenceWithLock, createMongoPersistence };
82
+ export { type MongoLockOptions, type MongoOptions, mongo };
package/dist/index.d.ts CHANGED
@@ -1,189 +1,82 @@
1
- import { MongoClientOptions, MongoClient, Db } from 'mongodb';
2
- import { KeyValueStore, ListPageOptions, StatePersistence, SerializedState, ListPageResult } from 'awaitly/persistence';
1
+ import { MongoClient, MongoClientOptions } from 'mongodb';
2
+ import { SnapshotStore } from 'awaitly/persistence';
3
+ export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
3
4
  import { WorkflowLock } from 'awaitly/durable';
5
+ export { WorkflowLock } from 'awaitly/durable';
4
6
 
5
7
  /**
6
- * awaitly-mongo
7
- *
8
- * MongoDB KeyValueStore implementation for awaitly persistence.
8
+ * MongoDB 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 MongoDB KeyValueStore.
13
- */
14
- interface MongoKeyValueStoreOptions {
15
- /**
16
- * MongoDB connection string.
17
- *
18
- * @example 'mongodb://localhost:27017'
19
- * @example 'mongodb://user:password@localhost:27017/dbname'
20
- */
21
- connectionString?: string;
22
- /**
23
- * Database name.
24
- * @default 'awaitly'
25
- */
26
- database?: string;
27
- /**
28
- * Collection name for storing key-value pairs.
29
- * @default 'workflow_state'
30
- */
31
- collection?: string;
32
- /**
33
- * Additional MongoDB client options.
34
- */
35
- clientOptions?: MongoClientOptions;
36
- /**
37
- * Existing MongoDB client to use.
38
- * If provided, connection options are ignored.
39
- */
40
- existingClient?: MongoClient;
41
- /**
42
- * Existing database instance to use.
43
- * If provided, connection and database options are ignored.
44
- */
45
- existingDb?: Db;
46
- }
47
- /**
48
- * MongoDB implementation of KeyValueStore.
49
- *
50
- * Automatically creates the required collection with TTL index on first use.
51
- * Supports TTL via expiresAt field with automatic expiration.
52
- */
53
- declare class MongoKeyValueStore implements KeyValueStore {
54
- private client;
55
- private db;
56
- private collection;
57
- private collectionName;
58
- private initialized;
59
- private initPromise;
60
- private shouldCloseClient;
61
- constructor(options: MongoKeyValueStoreOptions);
62
- /**
63
- * Initialize the store by connecting to MongoDB and creating the collection with TTL index.
64
- * This is called automatically on first use.
65
- */
66
- private ensureInitialized;
67
- /**
68
- * Create the collection and TTL index if they don't exist.
69
- */
70
- private createCollection;
71
- /**
72
- * Convert glob pattern to MongoDB regex pattern.
73
- * Supports * wildcard (matches any characters).
74
- */
75
- private patternToRegex;
76
- get(key: string): Promise<string | null>;
77
- set(key: string, value: string, options?: {
78
- ttl?: number;
79
- }): Promise<void>;
80
- delete(key: string): Promise<boolean>;
81
- exists(key: string): Promise<boolean>;
82
- keys(pattern: string): Promise<string[]>;
83
- /**
84
- * List keys with pagination, filtering, and ordering.
85
- */
86
- listKeys(pattern: string, options?: ListPageOptions): Promise<{
87
- keys: string[];
88
- total?: number;
89
- }>;
90
- /**
91
- * Delete multiple keys in one round-trip.
92
- */
93
- deleteMany(keys: string[]): Promise<number>;
94
- /**
95
- * Remove all entries from the collection (clear all workflow state).
96
- */
97
- clear(): Promise<void>;
12
+ interface MongoLockOptions {
98
13
  /**
99
- * Close the MongoDB client connection.
100
- * Only closes if this store created the client.
14
+ * Collection name for workflow locks.
15
+ * @default 'workflow_lock'
101
16
  */
102
- close(): Promise<void>;
17
+ lockCollectionName?: string;
103
18
  }
104
19
 
105
20
  /**
106
21
  * awaitly-mongo
107
22
  *
108
23
  * MongoDB persistence adapter for awaitly workflows.
109
- * Provides ready-to-use StatePersistence backed by MongoDB.
24
+ * Provides ready-to-use SnapshotStore backed by MongoDB.
110
25
  */
111
26
 
112
27
  /**
113
- * Options for cross-process locking (lease + owner token).
114
- * When set, the returned store implements WorkflowLock so only one process
115
- * runs a given workflow ID at a time (when durable.run allowConcurrent is false).
28
+ * Options for the mongo() shorthand function.
116
29
  */
117
- interface MongoLockOptions {
118
- /**
119
- * Collection name for workflow locks.
120
- * @default 'workflow_lock'
121
- */
122
- lockCollectionName?: string;
123
- }
124
- /**
125
- * Options for creating MongoDB persistence.
126
- */
127
- interface MongoPersistenceOptions extends MongoKeyValueStoreOptions {
128
- /**
129
- * Key prefix for state entries.
130
- * @default 'workflow:state:'
131
- */
30
+ interface MongoOptions {
31
+ /** MongoDB connection URL. */
32
+ url: string;
33
+ /** Database name. @default 'awaitly' */
34
+ database?: string;
35
+ /** Collection name for snapshots. @default 'awaitly_snapshots' */
36
+ collection?: string;
37
+ /** Key prefix for IDs. @default '' */
132
38
  prefix?: string;
133
- /**
134
- * When set, the store implements WorkflowLock for cross-process concurrency control.
135
- * Uses a lease (TTL) + owner token; release verifies the token.
136
- */
39
+ /** Bring your own client. */
40
+ client?: MongoClient;
41
+ /** MongoDB client options. */
42
+ clientOptions?: MongoClientOptions;
43
+ /** Cross-process lock options. When set, the store implements WorkflowLock. */
137
44
  lock?: MongoLockOptions;
138
45
  }
139
46
  /**
140
- * Create a StatePersistence instance backed by MongoDB.
141
- *
142
- * The collection is automatically created on first use with a TTL index.
143
- *
144
- * @param options - MongoDB connection and configuration options
145
- * @returns StatePersistence instance ready to use with durable.run()
47
+ * Create a snapshot store backed by MongoDB.
48
+ * This is the simplified one-liner API for workflow persistence.
146
49
  *
147
50
  * @example
148
51
  * ```typescript
149
- * import { createMongoPersistence } from 'awaitly-mongo';
150
- * import { durable } from 'awaitly/durable';
52
+ * import { mongo } from 'awaitly-mongo';
151
53
  *
152
- * const store = await createMongoPersistence({
153
- * connectionString: process.env.MONGODB_URI,
154
- * });
54
+ * // One-liner setup
55
+ * const store = mongo('mongodb://localhost:27017/mydb');
56
+ *
57
+ * // Execute + persist
58
+ * const wf = createWorkflow(deps);
59
+ * await wf(myWorkflowFn);
60
+ * await store.save('wf-123', wf.getSnapshot());
155
61
  *
156
- * const result = await durable.run(
157
- * { fetchUser, createOrder },
158
- * async (step, { fetchUser, createOrder }) => {
159
- * const user = await step(() => fetchUser('123'), { key: 'fetch-user' });
160
- * const order = await step(() => createOrder(user), { key: 'create-order' });
161
- * return order;
162
- * },
163
- * {
164
- * id: 'checkout-123',
165
- * store,
166
- * }
167
- * );
62
+ * // Restore
63
+ * const snapshot = await store.load('wf-123');
64
+ * const wf2 = createWorkflow(deps, { snapshot });
65
+ * await wf2(myWorkflowFn);
168
66
  * ```
169
67
  *
170
68
  * @example
171
69
  * ```typescript
172
- * // Using individual connection options
173
- * const store = await createMongoPersistence({
174
- * connectionString: 'mongodb://localhost:27017',
70
+ * // With options including cross-process locking
71
+ * const store = mongo({
72
+ * url: 'mongodb://localhost:27017',
175
73
  * database: 'myapp',
176
- * collection: 'custom_workflow_state',
74
+ * collection: 'my_workflow_snapshots',
75
+ * prefix: 'orders:',
76
+ * lock: { lockCollectionName: 'my_workflow_locks' },
177
77
  * });
178
78
  * ```
179
79
  */
180
- type MongoStatePersistence = StatePersistence & {
181
- loadRaw(runId: string): Promise<SerializedState | undefined>;
182
- listPage(options?: ListPageOptions): Promise<ListPageResult>;
183
- deleteMany(ids: string[]): Promise<number>;
184
- clear(): Promise<void>;
185
- };
186
- type MongoStatePersistenceWithLock = MongoStatePersistence & WorkflowLock;
187
- declare function createMongoPersistence(options?: MongoPersistenceOptions): Promise<MongoStatePersistence | MongoStatePersistenceWithLock>;
80
+ declare function mongo(urlOrOptions: string | MongoOptions): SnapshotStore & Partial<WorkflowLock>;
188
81
 
189
- export { MongoKeyValueStore, type MongoKeyValueStoreOptions, type MongoLockOptions, type MongoPersistenceOptions, type MongoStatePersistence, type MongoStatePersistenceWithLock, createMongoPersistence };
82
+ export { type MongoLockOptions, type MongoOptions, mongo };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import{MongoClient as P}from"mongodb";import{MongoClient as y}from"mongodb";var p=class{client=null;db;collection;collectionName;initialized=!1;initPromise=null;shouldCloseClient=!1;constructor(t){if(t.existingDb)this.db=t.existingDb,this.collectionName=t.collection??"workflow_state",this.collection=this.db.collection(this.collectionName);else if(t.existingClient){this.client=t.existingClient;let e=t.database??"awaitly";this.db=this.client.db(e),this.collectionName=t.collection??"workflow_state",this.collection=this.db.collection(this.collectionName)}else{let e=t.connectionString??"mongodb://localhost:27017",i=t.database,n=e.match(/mongodb:\/\/[^/]+\/([^?]+)/);n&&n[1]&&(i=i||n[1],e=e.replace(/\/[^/?]+(\?|$)/,"/$1")),this.client=new y(e,{directConnection:!0,...t.clientOptions}),this.shouldCloseClient=!0,i=i??"awaitly",this.db=this.client.db(i),this.collectionName=t.collection??"workflow_state",this.collection=this.db.collection(this.collectionName)}}async ensureInitialized(){if(!this.initialized)return this.initPromise?this.initPromise:(this.initPromise=(async()=>{try{if(this.client&&this.shouldCloseClient)try{await this.client.db("admin").command({ping:1})}catch{await this.client.connect()}await this.createCollection(),this.initialized=!0}catch(t){throw this.initPromise=null,t}})(),this.initPromise)}async createCollection(){(await this.db.listCollections({name:this.collectionName}).toArray()).length===0&&await this.db.createCollection(this.collectionName);try{(await this.collection.indexes()).some(n=>n.key&&"expiresAt"in n.key&&n.expireAfterSeconds!==void 0)||await this.collection.createIndex({expiresAt:1},{expireAfterSeconds:0,name:"expiresAt_ttl"})}catch(e){if(e?.code!==85)throw e}}patternToRegex(t){let e=t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(`^${e}$`)}async get(t){await this.ensureInitialized();let e=await this.collection.findOne({_id:t,$or:[{expiresAt:{$exists:!1}},{expiresAt:null},{expiresAt:{$gt:new Date}}]});return e?e.value:null}async set(t,e,i){await this.ensureInitialized();let n=i?.ttl?new Date(Date.now()+i.ttl*1e3):void 0,a={$set:{value:e,updatedAt:new Date,...n!==void 0?{expiresAt:n}:{}}};n===void 0&&(a.$unset={expiresAt:""}),await this.collection.updateOne({_id:t},a,{upsert:!0})}async delete(t){return await this.ensureInitialized(),(await this.collection.deleteOne({_id:t})).deletedCount>0}async exists(t){return await this.ensureInitialized(),await this.collection.countDocuments({_id:t,$or:[{expiresAt:{$exists:!1}},{expiresAt:null},{expiresAt:{$gt:new Date}}]})>0}async keys(t){await this.ensureInitialized();let e=this.patternToRegex(t);return(await this.collection.find({_id:e,$or:[{expiresAt:{$exists:!1}},{expiresAt:null},{expiresAt:{$gt:new Date}}]}).project({_id:1}).toArray()).map(n=>n._id)}async listKeys(t,e={}){await this.ensureInitialized();let i=Math.min(Math.max(0,e.limit??100),1e4),n=Math.max(0,e.offset??0),a=e.orderDir==="asc"?1:-1,r={_id:this.patternToRegex(t),$or:[{expiresAt:{$exists:!1}},{expiresAt:null},{expiresAt:{$gt:new Date}}]};e.updatedBefore!=null&&e.updatedAfter!=null?r.updatedAt={$lt:e.updatedBefore,$gt:e.updatedAfter}:e.updatedBefore!=null?r.updatedAt={$lt:e.updatedBefore}:e.updatedAfter!=null&&(r.updatedAt={$gt:e.updatedAfter});let c=e.orderBy==="key"?"_id":"updatedAt",u=(await this.collection.find(r).project({_id:1}).sort({[c]:a}).skip(n).limit(i).toArray()).map(m=>m._id),s;return(e.includeTotal===!0||n>0)&&(s=await this.collection.countDocuments(r)),{keys:u,total:s}}async deleteMany(t){return t.length===0?0:(await this.ensureInitialized(),(await this.collection.deleteMany({_id:{$in:t}})).deletedCount??0)}async clear(){await this.ensureInitialized(),await this.collection.deleteMany({})}async close(){this.client&&this.shouldCloseClient&&(await this.client.close(),this.client=null)}};import{randomUUID as x}from"crypto";function f(g,t={}){let e=t.lockCollectionName??"workflow_lock",i=g.collection(e);async function n(){(await g.listCollections({name:e}).toArray()).length===0&&await g.createCollection(e)}async function a(r,c){let l=c?.ttlMs??6e4,d=x(),u=new Date(Date.now()+l);await n();try{let s=await i.findOneAndUpdate({_id:r,$or:[{expiresAt:{$lt:new Date}},{expiresAt:{$exists:!1}}]},{$set:{ownerToken:d,expiresAt:u}},{upsert:!0,returnDocument:"after"});return s&&s.ownerToken===d?{ownerToken:d}:null}catch(s){if(s&&typeof s=="object"&&"code"in s&&s.code===11e3)return null;throw s}}async function o(r,c){await i.deleteOne({_id:r,ownerToken:c})}return{tryAcquire:a,release:o,ensureLockCollection:n}}import{createStatePersistence as h}from"awaitly/persistence";function w(g,t,e){let i=e??"workflow:state:",n=o=>o.slice(i.length),a=o=>`${i}${o}`;return Object.assign(g,{async listPage(o={}){let{keys:r,total:c}=await t.listKeys(`${i}*`,o),l=r.map(n),d=Math.min(Math.max(0,o.limit??100),1e4),u=l.length===d?(o.offset??0)+l.length:void 0;return{ids:l,total:c,nextOffset:u}},async deleteMany(o){if(o.length===0)return 0;let r=o.map(a);return t.deleteMany(r)},async clear(){return t.clear()}})}async function O(g={}){let{prefix:t,lock:e,...i}=g;if(e!==void 0){let o;if(i.existingDb)o=i.existingDb;else if(i.existingClient)o=i.existingClient.db(i.database??"awaitly");else{let d=i.connectionString??"mongodb://localhost:27017",u=new P(d,{directConnection:!0,...i.clientOptions});await u.connect();let s=i.database,m=d.match(/mongodb:\/\/[^/]+\/([^?]+)/);m&&m[1]&&(s=s??m[1]),s=s??"awaitly",o=u.db(s),i.existingClient=u,i.database=s}let r=new p(i),c=h(r,t),l=f(o,{lockCollectionName:e.lockCollectionName});return Object.assign(w(c,r,t),{tryAcquire:l.tryAcquire.bind(l),release:l.release.bind(l)})}let n=new p(i),a=h(n,t);return w(a,n,t)}export{p as MongoKeyValueStore,O as createMongoPersistence};
1
+ import{MongoClient as M}from"mongodb";import{randomUUID as A}from"crypto";function b(d,o={}){let l=o.lockCollectionName??"workflow_lock",c=d.collection(l);async function f(){(await d.listCollections({name:l}).toArray()).length===0&&await d.createCollection(l)}async function u(n,r){let w=r?.ttlMs??6e4,i=A(),s=new Date(Date.now()+w);await f();try{let e=await c.findOneAndUpdate({_id:n,$or:[{expiresAt:{$lt:new Date}},{expiresAt:{$exists:!1}}]},{$set:{ownerToken:i,expiresAt:s}},{upsert:!0,returnDocument:"after"});return e&&e.ownerToken===i?{ownerToken:i}:null}catch(e){if(e&&typeof e=="object"&&"code"in e&&e.code===11e3)return null;throw e}}async function k(n,r){await c.deleteOne({_id:n,ownerToken:r})}return{tryAcquire:u,release:k,ensureLockCollection:f}}function D(d){let o=typeof d=="string"?{url:d}:d,l=o.prefix??"",c=o.database,f=o.url.match(/mongodb(?:\+srv)?:\/\/[^/]+\/([^?]+)/);!c&&f&&f[1]&&(c=f[1]),c=c??"awaitly";let u=o.collection??"awaitly_snapshots",k=!o.client,n=o.client,r,w=!1,i=null,s=async()=>(r&&w||(n||(n=new M(o.url,{directConnection:!o.url.includes("mongodb+srv://"),...o.clientOptions})),await n.connect(),w=!0,r=n.db(c),await r.collection(u).createIndex({updatedAt:-1},{background:!0}).catch(()=>{}),o.lock&&!i&&(i=b(r,o.lock))),r),e={async save(t,a){let p=(await s()).collection(u),g=l+t;await p.updateOne({_id:g},{$set:{snapshot:a,updatedAt:new Date}},{upsert:!0})},async load(t){let m=(await s()).collection(u),p=l+t,g=await m.findOne({_id:p});return g?g.snapshot:null},async delete(t){let m=(await s()).collection(u),p=l+t;await m.deleteOne({_id:p})},async list(t){let m=(await s()).collection(u),p=l+(t?.prefix??""),g=t?.limit??100;return(await m.find({_id:{$regex:`^${p.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}`}}).sort({updatedAt:-1}).limit(g).toArray()).map(y=>({id:String(y._id).slice(l.length),updatedAt:y.updatedAt.toISOString()}))},async close(){k&&n&&(await n.close(),w=!1,r=void 0)},async tryAcquire(t,a){return await s(),i?i.tryAcquire(t,a):null},async release(t,a){if(await s(),!!i)return i.release(t,a)}};return o.lock||(delete e.tryAcquire,delete e.release),e}export{D as mongo};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/mongo-store.ts","../src/mongo-lock.ts"],"sourcesContent":["/**\n * awaitly-mongo\n *\n * MongoDB persistence adapter for awaitly workflows.\n * Provides ready-to-use StatePersistence backed by MongoDB.\n */\n\nimport type { Db } from \"mongodb\";\nimport { MongoClient as MongoClientImpl } from \"mongodb\";\nimport { MongoKeyValueStore, type MongoKeyValueStoreOptions } from \"./mongo-store\";\nimport { createMongoLock } from \"./mongo-lock\";\nimport {\n createStatePersistence,\n type StatePersistence,\n type SerializedState,\n type ListPageOptions,\n type ListPageResult,\n} from \"awaitly/persistence\";\nimport type { WorkflowLock } from \"awaitly/durable\";\n\n/**\n * Options for cross-process locking (lease + owner token).\n * When set, the returned store implements WorkflowLock so only one process\n * runs a given workflow ID at a time (when durable.run allowConcurrent is false).\n */\nexport interface MongoLockOptions {\n /**\n * Collection name for workflow locks.\n * @default 'workflow_lock'\n */\n lockCollectionName?: string;\n}\n\n/**\n * Options for creating MongoDB persistence.\n */\nexport interface MongoPersistenceOptions extends MongoKeyValueStoreOptions {\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?: MongoLockOptions;\n}\n\n/**\n * Create a StatePersistence instance backed by MongoDB.\n *\n * The collection is automatically created on first use with a TTL index.\n *\n * @param options - MongoDB connection and configuration options\n * @returns StatePersistence instance ready to use with durable.run()\n *\n * @example\n * ```typescript\n * import { createMongoPersistence } from 'awaitly-mongo';\n * import { durable } from 'awaitly/durable';\n *\n * const store = await createMongoPersistence({\n * connectionString: process.env.MONGODB_URI,\n * });\n *\n * const result = await durable.run(\n * { fetchUser, createOrder },\n * async (step, { fetchUser, createOrder }) => {\n * const user = await step(() => fetchUser('123'), { key: 'fetch-user' });\n * const order = await step(() => createOrder(user), { key: 'create-order' });\n * return order;\n * },\n * {\n * id: 'checkout-123',\n * store,\n * }\n * );\n * ```\n *\n * @example\n * ```typescript\n * // Using individual connection options\n * const store = await createMongoPersistence({\n * connectionString: 'mongodb://localhost:27017',\n * database: 'myapp',\n * collection: 'custom_workflow_state',\n * });\n * ```\n */\nexport type MongoStatePersistence = 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 MongoStatePersistenceWithLock = MongoStatePersistence & WorkflowLock;\n\nfunction addListPageAndDeleteMany(\n statePersistence: StatePersistence & {\n loadRaw(runId: string): Promise<SerializedState | undefined>;\n },\n store: MongoKeyValueStore,\n prefix: string | undefined\n): MongoStatePersistence {\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 return Object.assign(statePersistence, {\n async listPage(options: ListPageOptions = {}): Promise<ListPageResult> {\n const { keys, total } = await store.listKeys(`${effectivePrefix}*`, options);\n const ids = keys.map(stripPrefix);\n const limit = Math.min(Math.max(0, options.limit ?? 100), 10_000);\n const nextOffset =\n ids.length === limit ? (options.offset ?? 0) + ids.length : undefined;\n return { ids, total, nextOffset };\n },\n async deleteMany(ids: string[]): Promise<number> {\n if (ids.length === 0) return 0;\n const keys = ids.map(prefixKey);\n return store.deleteMany(keys);\n },\n async clear(): Promise<void> {\n return store.clear();\n },\n });\n}\n\nexport async function createMongoPersistence(\n options: MongoPersistenceOptions = {}\n): Promise<MongoStatePersistence | MongoStatePersistenceWithLock> {\n const { prefix, lock: lockOptions, ...storeOptions } = options;\n\n if (lockOptions !== undefined) {\n let db: Db;\n if (storeOptions.existingDb) {\n db = storeOptions.existingDb;\n } else if (storeOptions.existingClient) {\n db = storeOptions.existingClient.db(storeOptions.database ?? \"awaitly\");\n } else {\n const connectionString =\n storeOptions.connectionString ?? \"mongodb://localhost:27017\";\n const client = new MongoClientImpl(connectionString, {\n directConnection: true,\n ...storeOptions.clientOptions,\n });\n await client.connect();\n let databaseName = storeOptions.database;\n const urlMatch = connectionString.match(/mongodb:\\/\\/[^/]+\\/([^?]+)/);\n if (urlMatch && urlMatch[1]) {\n databaseName = databaseName ?? urlMatch[1];\n }\n databaseName = databaseName ?? \"awaitly\";\n db = client.db(databaseName);\n storeOptions.existingClient = client;\n storeOptions.database = databaseName;\n }\n const store = new MongoKeyValueStore(storeOptions);\n const statePersistence = createStatePersistence(store, prefix) as MongoStatePersistence;\n const lock = createMongoLock(db, {\n lockCollectionName: lockOptions.lockCollectionName,\n });\n return Object.assign(addListPageAndDeleteMany(statePersistence, store, prefix), {\n tryAcquire: lock.tryAcquire.bind(lock),\n release: lock.release.bind(lock),\n });\n }\n\n const store = new MongoKeyValueStore(storeOptions);\n const base = createStatePersistence(store, prefix);\n return addListPageAndDeleteMany(\n base as StatePersistence & {\n loadRaw(runId: string): Promise<SerializedState | undefined>;\n },\n store,\n prefix\n );\n}\n\n/**\n * MongoDB KeyValueStore implementation.\n * Use this directly if you need more control over the store.\n *\n * @example\n * ```typescript\n * import { MongoKeyValueStore } from 'awaitly-mongo';\n * import { createStatePersistence } from 'awaitly/persistence';\n *\n * const store = new MongoKeyValueStore({\n * connectionString: process.env.MONGODB_URI,\n * });\n *\n * const persistence = createStatePersistence(store, 'custom:prefix:');\n * ```\n */\nexport { MongoKeyValueStore, type MongoKeyValueStoreOptions };\n","/**\n * awaitly-mongo\n *\n * MongoDB KeyValueStore implementation for awaitly persistence.\n */\n\nimport type {\n MongoClient,\n MongoClientOptions,\n Db,\n Collection,\n WithId,\n Document,\n} from \"mongodb\";\nimport { MongoClient as MongoClientImpl } from \"mongodb\";\nimport type { KeyValueStore, ListPageOptions } from \"awaitly/persistence\";\n\n/**\n * Options for MongoDB KeyValueStore.\n */\nexport interface MongoKeyValueStoreOptions {\n /**\n * MongoDB connection string.\n *\n * @example 'mongodb://localhost:27017'\n * @example 'mongodb://user:password@localhost:27017/dbname'\n */\n connectionString?: string;\n\n /**\n * Database name.\n * @default 'awaitly'\n */\n database?: string;\n\n /**\n * Collection name for storing key-value pairs.\n * @default 'workflow_state'\n */\n collection?: string;\n\n /**\n * Additional MongoDB client options.\n */\n clientOptions?: MongoClientOptions;\n\n /**\n * Existing MongoDB client to use.\n * If provided, connection options are ignored.\n */\n existingClient?: MongoClient;\n\n /**\n * Existing database instance to use.\n * If provided, connection and database options are ignored.\n */\n existingDb?: Db;\n}\n\n/**\n * Document schema for stored values.\n */\ninterface KeyValueDocument extends Document {\n _id: string;\n value: string;\n expiresAt?: Date | null;\n updatedAt?: Date | null;\n}\n\n/**\n * MongoDB implementation of KeyValueStore.\n *\n * Automatically creates the required collection with TTL index on first use.\n * Supports TTL via expiresAt field with automatic expiration.\n */\nexport class MongoKeyValueStore implements KeyValueStore {\n private client: MongoClient | null = null;\n private db: Db;\n private collection: Collection<KeyValueDocument>;\n private collectionName: string;\n private initialized: boolean = false;\n private initPromise: Promise<void> | null = null;\n private shouldCloseClient: boolean = false;\n\n constructor(options: MongoKeyValueStoreOptions) {\n if (options.existingDb) {\n // Use provided database\n this.db = options.existingDb;\n this.collectionName = options.collection ?? \"workflow_state\";\n this.collection = this.db.collection<KeyValueDocument>(this.collectionName);\n } else if (options.existingClient) {\n // Use provided client\n this.client = options.existingClient;\n const databaseName = options.database ?? \"awaitly\";\n this.db = this.client.db(databaseName);\n this.collectionName = options.collection ?? \"workflow_state\";\n this.collection = this.db.collection<KeyValueDocument>(this.collectionName);\n } else {\n // Create new client\n let connectionString = options.connectionString ?? \"mongodb://localhost:27017\";\n \n // Extract database name from connection string if present\n let databaseName = options.database;\n const urlMatch = connectionString.match(/mongodb:\\/\\/[^/]+\\/([^?]+)/);\n if (urlMatch && urlMatch[1]) {\n databaseName = databaseName || urlMatch[1];\n // Remove database from connection string to avoid conflicts\n connectionString = connectionString.replace(/\\/[^/?]+(\\?|$)/, '/$1');\n }\n \n this.client = new MongoClientImpl(connectionString, {\n directConnection: true, // Use direct connection for single-node instances\n ...options.clientOptions,\n });\n this.shouldCloseClient = true;\n databaseName = databaseName ?? \"awaitly\";\n this.db = this.client.db(databaseName);\n this.collectionName = options.collection ?? \"workflow_state\";\n this.collection = this.db.collection<KeyValueDocument>(this.collectionName);\n }\n }\n\n /**\n * Initialize the store by connecting to MongoDB and creating the collection with TTL index.\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 // Connect client if we created it\n if (this.client && this.shouldCloseClient) {\n // Connect if not already connected\n try {\n await this.client.db(\"admin\").command({ ping: 1 });\n } catch {\n // Not connected, connect now\n await this.client.connect();\n }\n }\n\n await this.createCollection();\n this.initialized = true;\n } catch (error) {\n this.initPromise = null;\n throw error;\n }\n })();\n\n return this.initPromise;\n }\n\n /**\n * Create the collection and TTL index if they don't exist.\n */\n private async createCollection(): Promise<void> {\n // Create collection if it doesn't exist\n const collections = await this.db.listCollections({ name: this.collectionName }).toArray();\n if (collections.length === 0) {\n await this.db.createCollection(this.collectionName);\n }\n\n // Create TTL index on expiresAt field\n try {\n const indexes = await this.collection.indexes();\n const hasTtlIndex = indexes.some(\n (index) => index.key && \"expiresAt\" in index.key && index.expireAfterSeconds !== undefined\n );\n\n if (!hasTtlIndex) {\n await this.collection.createIndex(\n { expiresAt: 1 },\n {\n expireAfterSeconds: 0, // Delete immediately when expiresAt is reached\n name: \"expiresAt_ttl\",\n }\n );\n }\n } catch (error) {\n // Index might already exist from a previous run, ignore duplicate key errors\n if ((error as { code?: number })?.code !== 85) {\n throw error;\n }\n }\n }\n\n /**\n * Convert glob pattern to MongoDB regex pattern.\n * Supports * wildcard (matches any characters).\n */\n private patternToRegex(pattern: string): RegExp {\n // Escape regex special characters and convert * to .*\n const escaped = pattern\n .replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \".*\");\n return new RegExp(`^${escaped}$`);\n }\n\n async get(key: string): Promise<string | null> {\n await this.ensureInitialized();\n\n const doc = await this.collection.findOne({\n _id: key,\n $or: [{ expiresAt: { $exists: false } }, { expiresAt: null }, { expiresAt: { $gt: new Date() } }],\n });\n\n if (!doc) {\n return null;\n }\n\n return doc.value;\n }\n\n async set(key: string, value: string, options?: { ttl?: number }): Promise<void> {\n await this.ensureInitialized();\n\n const expiresAt = options?.ttl ? new Date(Date.now() + options.ttl * 1000) : undefined;\n\n const update: Record<string, unknown> = {\n $set: {\n value,\n updatedAt: new Date(),\n ...(expiresAt !== undefined ? { expiresAt } : {}),\n },\n };\n if (expiresAt === undefined) {\n update.$unset = { expiresAt: \"\" };\n }\n\n await this.collection.updateOne({ _id: key }, update, { upsert: true });\n }\n\n async delete(key: string): Promise<boolean> {\n await this.ensureInitialized();\n\n const result = await this.collection.deleteOne({ _id: key });\n return result.deletedCount > 0;\n }\n\n async exists(key: string): Promise<boolean> {\n await this.ensureInitialized();\n\n const count = await this.collection.countDocuments({\n _id: key,\n $or: [{ expiresAt: { $exists: false } }, { expiresAt: null }, { expiresAt: { $gt: new Date() } }],\n });\n\n return count > 0;\n }\n\n async keys(pattern: string): Promise<string[]> {\n await this.ensureInitialized();\n\n const regex = this.patternToRegex(pattern);\n\n const docs = await this.collection\n .find({\n _id: regex,\n $or: [{ expiresAt: { $exists: false } }, { expiresAt: null }, { expiresAt: { $gt: new Date() } }],\n })\n .project({ _id: 1 })\n .toArray();\n\n return docs.map((doc) => doc._id);\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 orderDir = options.orderDir === \"asc\" ? 1 : -1;\n const regex = this.patternToRegex(pattern);\n\n const baseFilter: Record<string, unknown> = {\n _id: regex,\n $or: [{ expiresAt: { $exists: false } }, { expiresAt: null }, { expiresAt: { $gt: new Date() } }],\n };\n if (options.updatedBefore != null && options.updatedAfter != null) {\n (baseFilter as Record<string, unknown>).updatedAt = {\n $lt: options.updatedBefore,\n $gt: options.updatedAfter,\n };\n } else if (options.updatedBefore != null) {\n (baseFilter as Record<string, unknown>).updatedAt = { $lt: options.updatedBefore };\n } else if (options.updatedAfter != null) {\n (baseFilter as Record<string, unknown>).updatedAt = { $gt: options.updatedAfter };\n }\n\n const sortKey = options.orderBy === \"key\" ? \"_id\" : \"updatedAt\";\n const cursor = this.collection\n .find(baseFilter)\n .project({ _id: 1 })\n .sort({ [sortKey]: orderDir })\n .skip(offset)\n .limit(limit);\n\n const docs = await cursor.toArray();\n const keys = docs.map((doc) => doc._id);\n\n let total: number | undefined;\n if (options.includeTotal === true || offset > 0) {\n total = await this.collection.countDocuments(baseFilter);\n }\n\n return { keys, total };\n }\n\n /**\n * Delete multiple keys in one round-trip.\n */\n async deleteMany(keys: string[]): Promise<number> {\n if (keys.length === 0) return 0;\n await this.ensureInitialized();\n const result = await this.collection.deleteMany({ _id: { $in: keys } });\n return result.deletedCount ?? 0;\n }\n\n /**\n * Remove all entries from the collection (clear all workflow state).\n */\n async clear(): Promise<void> {\n await this.ensureInitialized();\n await this.collection.deleteMany({});\n }\n\n /**\n * Close the MongoDB client connection.\n * Only closes if this store created the client.\n */\n async close(): Promise<void> {\n if (this.client && this.shouldCloseClient) {\n await this.client.close();\n this.client = null;\n }\n }\n}\n","/**\n * MongoDB workflow lock (lease) for cross-process concurrency control.\n * Uses a lease (TTL) + owner token; release verifies the token.\n */\n\nimport type { Db, Collection } from \"mongodb\";\nimport { randomUUID } from \"node:crypto\";\n\nexport interface MongoLockOptions {\n /**\n * Collection name for workflow locks.\n * @default 'workflow_lock'\n */\n lockCollectionName?: string;\n}\n\ninterface LockDocument {\n _id: string;\n ownerToken: string;\n expiresAt: Date;\n}\n\n/**\n * Create tryAcquire and release functions that use a MongoDB lock collection.\n * Caller must pass the same Db used for state (so one connection).\n */\nexport function createMongoLock(\n db: Db,\n options: MongoLockOptions = {}\n): {\n tryAcquire(\n id: string,\n opts?: { ttlMs?: number }\n ): Promise<{ ownerToken: string } | null>;\n release(id: string, ownerToken: string): Promise<void>;\n ensureLockCollection(): Promise<void>;\n} {\n const lockCollectionName = options.lockCollectionName ?? \"workflow_lock\";\n const collection = db.collection<LockDocument>(lockCollectionName);\n\n async function ensureLockCollection(): Promise<void> {\n const collections = await db.listCollections({ name: lockCollectionName }).toArray();\n if (collections.length === 0) {\n await db.createCollection(lockCollectionName);\n }\n }\n\n async function tryAcquire(\n id: string,\n opts?: { ttlMs?: number }\n ): Promise<{ ownerToken: string } | null> {\n const ttlMs = opts?.ttlMs ?? 60_000;\n const ownerToken = randomUUID();\n const expiresAt = new Date(Date.now() + ttlMs);\n\n await ensureLockCollection();\n\n // Atomic: insert or update only when no doc or doc is expired.\n // When lock exists and is unexpired, filter won't match and upsert\n // throws duplicate key error (E11000) - catch it and return null.\n try {\n const result = await collection.findOneAndUpdate(\n {\n _id: id,\n $or: [\n { expiresAt: { $lt: new Date() } },\n { expiresAt: { $exists: false } },\n ],\n },\n { $set: { ownerToken, expiresAt } },\n { upsert: true, returnDocument: \"after\" }\n );\n\n if (result && result.ownerToken === ownerToken) {\n return { ownerToken };\n }\n return null;\n } catch (error: unknown) {\n // Duplicate key error means lock exists and is not expired\n if (\n error &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === 11000\n ) {\n return null;\n }\n throw error;\n }\n }\n\n async function release(id: string, ownerToken: string): Promise<void> {\n await collection.deleteOne({ _id: id, ownerToken });\n }\n\n return { tryAcquire, release, ensureLockCollection };\n}\n"],"mappings":"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","names":["MongoClientImpl","MongoClientImpl","MongoKeyValueStore","options","databaseName","connectionString","urlMatch","error","index","pattern","escaped","key","doc","value","expiresAt","update","regex","limit","offset","orderDir","baseFilter","sortKey","keys","total","randomUUID","createMongoLock","db","options","lockCollectionName","collection","ensureLockCollection","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","error","release","createStatePersistence","addListPageAndDeleteMany","statePersistence","store","prefix","effectivePrefix","stripPrefix","key","prefixKey","runId","options","keys","total","ids","limit","nextOffset","createMongoPersistence","lockOptions","storeOptions","db","connectionString","client","MongoClientImpl","databaseName","urlMatch","MongoKeyValueStore","lock","createMongoLock","base"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/mongo-lock.ts"],"sourcesContent":["/**\n * awaitly-mongo\n *\n * MongoDB persistence adapter for awaitly workflows.\n * Provides ready-to-use SnapshotStore backed by MongoDB.\n */\n\nimport type { Db, MongoClientOptions } from \"mongodb\";\nimport { MongoClient as MongoClientImpl } from \"mongodb\";\nimport type { WorkflowSnapshot, SnapshotStore } from \"awaitly/persistence\";\nimport type { WorkflowLock } from \"awaitly/durable\";\nimport { createMongoLock, type MongoLockOptions } from \"./mongo-lock\";\n\n// Re-export types for convenience\nexport type { SnapshotStore, WorkflowSnapshot } from \"awaitly/persistence\";\nexport type { WorkflowLock } from \"awaitly/durable\";\nexport type { MongoLockOptions } from \"./mongo-lock\";\n\n// =============================================================================\n// MongoOptions\n// =============================================================================\n\n/**\n * Options for the mongo() shorthand function.\n */\nexport interface MongoOptions {\n /** MongoDB connection URL. */\n url: string;\n /** Database name. @default 'awaitly' */\n database?: string;\n /** Collection name for snapshots. @default 'awaitly_snapshots' */\n collection?: string;\n /** Key prefix for IDs. @default '' */\n prefix?: string;\n /** Bring your own client. */\n client?: MongoClientImpl;\n /** MongoDB client options. */\n clientOptions?: MongoClientOptions;\n /** Cross-process lock options. When set, the store implements WorkflowLock. */\n lock?: MongoLockOptions;\n}\n\n// =============================================================================\n// mongo() - One-liner Snapshot Store Setup\n// =============================================================================\n\n/**\n * Create a snapshot store backed by MongoDB.\n * This is the simplified one-liner API for workflow persistence.\n *\n * @example\n * ```typescript\n * import { mongo } from 'awaitly-mongo';\n *\n * // One-liner setup\n * const store = mongo('mongodb://localhost:27017/mydb');\n *\n * // Execute + persist\n * const wf = createWorkflow(deps);\n * await wf(myWorkflowFn);\n * await store.save('wf-123', wf.getSnapshot());\n *\n * // Restore\n * const snapshot = await store.load('wf-123');\n * const wf2 = createWorkflow(deps, { snapshot });\n * await wf2(myWorkflowFn);\n * ```\n *\n * @example\n * ```typescript\n * // With options including cross-process locking\n * const store = mongo({\n * url: 'mongodb://localhost:27017',\n * database: 'myapp',\n * collection: 'my_workflow_snapshots',\n * prefix: 'orders:',\n * lock: { lockCollectionName: 'my_workflow_locks' },\n * });\n * ```\n */\nexport function mongo(urlOrOptions: string | MongoOptions): SnapshotStore & Partial<WorkflowLock> {\n const opts = typeof urlOrOptions === \"string\" ? { url: urlOrOptions } : urlOrOptions;\n const prefix = opts.prefix ?? \"\";\n\n // Parse database from URL if provided\n let databaseName = opts.database;\n const urlMatch = opts.url.match(/mongodb(?:\\+srv)?:\\/\\/[^/]+\\/([^?]+)/);\n if (!databaseName && urlMatch && urlMatch[1]) {\n databaseName = urlMatch[1];\n }\n databaseName = databaseName ?? \"awaitly\";\n\n const collectionName = opts.collection ?? \"awaitly_snapshots\";\n\n // Create or use existing client\n const ownClient = !opts.client;\n let client: MongoClientImpl | undefined = opts.client;\n let db: Db | undefined;\n let connected = false;\n let lock: { tryAcquire: WorkflowLock[\"tryAcquire\"]; release: WorkflowLock[\"release\"] } | null = null;\n\n const ensureConnected = async (): Promise<Db> => {\n if (db && connected) return db;\n\n if (!client) {\n client = new MongoClientImpl(opts.url, {\n directConnection: !opts.url.includes(\"mongodb+srv://\"),\n ...opts.clientOptions,\n });\n }\n\n await client.connect();\n connected = true;\n db = client.db(databaseName);\n\n // Create index on updatedAt for list queries\n const collection = db.collection(collectionName);\n await collection.createIndex({ updatedAt: -1 }, { background: true }).catch(() => {\n // Index may already exist, ignore error\n });\n\n // Create lock if requested\n if (opts.lock && !lock) {\n lock = createMongoLock(db, opts.lock);\n }\n\n return db;\n };\n\n const store: SnapshotStore & Partial<WorkflowLock> = {\n async save(id: string, snapshot: WorkflowSnapshot): Promise<void> {\n const db = await ensureConnected();\n const collection = db.collection(collectionName);\n const fullId = prefix + id;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n await collection.updateOne(\n { _id: fullId } as any,\n {\n $set: {\n snapshot,\n updatedAt: new Date(),\n },\n },\n { upsert: true }\n );\n },\n\n async load(id: string): Promise<WorkflowSnapshot | null> {\n const db = await ensureConnected();\n const collection = db.collection(collectionName);\n const fullId = prefix + id;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const doc = await collection.findOne({ _id: fullId } as any);\n if (!doc) return null;\n return doc.snapshot as WorkflowSnapshot;\n },\n\n async delete(id: string): Promise<void> {\n const db = await ensureConnected();\n const collection = db.collection(collectionName);\n const fullId = prefix + id;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n await collection.deleteOne({ _id: fullId } as any);\n },\n\n async list(options?: { prefix?: string; limit?: number }): Promise<Array<{ id: string; updatedAt: string }>> {\n const db = await ensureConnected();\n const collection = db.collection(collectionName);\n const filterPrefix = prefix + (options?.prefix ?? \"\");\n const limit = options?.limit ?? 100;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const cursor = collection\n .find({ _id: { $regex: `^${filterPrefix.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}` } } as any)\n .sort({ updatedAt: -1 })\n .limit(limit);\n\n const docs = await cursor.toArray();\n return docs.map(doc => ({\n id: String(doc._id).slice(prefix.length),\n updatedAt: (doc.updatedAt as Date).toISOString(),\n }));\n },\n\n async close(): Promise<void> {\n // Only close client if we created it\n if (ownClient && client) {\n await client.close();\n connected = false;\n db = undefined;\n }\n },\n\n // Lock methods are added dynamically below after first connection\n async tryAcquire(id: string, options?: { ttlMs?: number }): Promise<{ ownerToken: string } | null> {\n await ensureConnected();\n if (!lock) return null;\n return lock.tryAcquire(id, options);\n },\n\n async release(id: string, ownerToken: string): Promise<void> {\n await ensureConnected();\n if (!lock) return;\n return lock.release(id, ownerToken);\n },\n };\n\n // Only include lock methods if lock is configured\n if (!opts.lock) {\n delete store.tryAcquire;\n delete store.release;\n }\n\n return store;\n}\n","/**\n * MongoDB workflow lock (lease) for cross-process concurrency control.\n * Uses a lease (TTL) + owner token; release verifies the token.\n */\n\nimport type { Db, Collection } from \"mongodb\";\nimport { randomUUID } from \"node:crypto\";\n\nexport interface MongoLockOptions {\n /**\n * Collection name for workflow locks.\n * @default 'workflow_lock'\n */\n lockCollectionName?: string;\n}\n\ninterface LockDocument {\n _id: string;\n ownerToken: string;\n expiresAt: Date;\n}\n\n/**\n * Create tryAcquire and release functions that use a MongoDB lock collection.\n * Caller must pass the same Db used for state (so one connection).\n */\nexport function createMongoLock(\n db: Db,\n options: MongoLockOptions = {}\n): {\n tryAcquire(\n id: string,\n opts?: { ttlMs?: number }\n ): Promise<{ ownerToken: string } | null>;\n release(id: string, ownerToken: string): Promise<void>;\n ensureLockCollection(): Promise<void>;\n} {\n const lockCollectionName = options.lockCollectionName ?? \"workflow_lock\";\n const collection = db.collection<LockDocument>(lockCollectionName);\n\n async function ensureLockCollection(): Promise<void> {\n const collections = await db.listCollections({ name: lockCollectionName }).toArray();\n if (collections.length === 0) {\n await db.createCollection(lockCollectionName);\n }\n }\n\n async function tryAcquire(\n id: string,\n opts?: { ttlMs?: number }\n ): Promise<{ ownerToken: string } | null> {\n const ttlMs = opts?.ttlMs ?? 60_000;\n const ownerToken = randomUUID();\n const expiresAt = new Date(Date.now() + ttlMs);\n\n await ensureLockCollection();\n\n // Atomic: insert or update only when no doc or doc is expired.\n // When lock exists and is unexpired, filter won't match and upsert\n // throws duplicate key error (E11000) - catch it and return null.\n try {\n const result = await collection.findOneAndUpdate(\n {\n _id: id,\n $or: [\n { expiresAt: { $lt: new Date() } },\n { expiresAt: { $exists: false } },\n ],\n },\n { $set: { ownerToken, expiresAt } },\n { upsert: true, returnDocument: \"after\" }\n );\n\n if (result && result.ownerToken === ownerToken) {\n return { ownerToken };\n }\n return null;\n } catch (error: unknown) {\n // Duplicate key error means lock exists and is not expired\n if (\n error &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === 11000\n ) {\n return null;\n }\n throw error;\n }\n }\n\n async function release(id: string, ownerToken: string): Promise<void> {\n await collection.deleteOne({ _id: id, ownerToken });\n }\n\n return { tryAcquire, release, ensureLockCollection };\n}\n"],"mappings":"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","names":["MongoClientImpl","randomUUID","createMongoLock","db","options","lockCollectionName","collection","ensureLockCollection","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","error","release","mongo","urlOrOptions","opts","prefix","databaseName","urlMatch","collectionName","ownClient","client","db","connected","lock","ensureConnected","MongoClientImpl","createMongoLock","store","id","snapshot","collection","fullId","doc","options","filterPrefix","limit","ownerToken"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "awaitly-mongo",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "description": "MongoDB persistence adapter for awaitly workflows",
6
6
  "main": "./dist/index.cjs",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "license": "MIT",
40
40
  "peerDependencies": {
41
- "awaitly": "^1.13.0"
41
+ "awaitly": "^1.14.0"
42
42
  },
43
43
  "dependencies": {
44
44
  "mongodb": "^7.0.0"
@@ -46,11 +46,11 @@
46
46
  "devDependencies": {
47
47
  "@total-typescript/ts-reset": "^0.6.1",
48
48
  "@total-typescript/tsconfig": "^1.0.4",
49
- "@types/node": "^25.1.0",
49
+ "@types/node": "^25.2.0",
50
50
  "tsup": "^8.5.1",
51
51
  "typescript": "^5.9.3",
52
52
  "vitest": "^4.0.18",
53
- "awaitly": "^1.13.0"
53
+ "awaitly": "^1.14.0"
54
54
  },
55
55
  "publishConfig": {
56
56
  "access": "public",