awaitly-mongo 20.0.0 → 22.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var b=Object.defineProperty;var P=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var L=Object.prototype.hasOwnProperty;var v=(e,t)=>{for(var n in t)b(e,n,{get:t[n],enumerable:!0})},C=(e,t,n,l)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of D(t))!L.call(e,s)&&s!==n&&b(e,s,{get:()=>t[s],enumerable:!(l=P(t,s))||l.enumerable});return e};var R=e=>C(b({},"__esModule",{value:!0}),e);var O={};v(O,{mongo:()=>I});module.exports=R(O);var M=require("mongodb"),d=require("awaitly/workflow");var x=require("crypto");function A(e,t={}){let n=t.lockCollectionName??"workflow_lock",l=e.collection(n);async function s(){(await e.listCollections({name:n}).toArray()).length===0&&await e.createCollection(n)}async function f(a,c){let y=c?.ttlMs??6e4,u=(0,x.randomUUID)(),m=new Date(Date.now()+y);await s();try{let i=await l.findOneAndUpdate({_id:a,$or:[{expiresAt:{$lt:new Date}},{expiresAt:{$exists:!1}}]},{$set:{ownerToken:u,expiresAt:m}},{upsert:!0,returnDocument:"after"});return i&&i.ownerToken===u?{ownerToken:u}:null}catch(i){if(i&&typeof i=="object"&&"code"in i&&i.code===11e3)return null;throw i}}async function S(a,c){await l.deleteOne({_id:a,ownerToken:c})}return{tryAcquire:f,release:S,ensureLockCollection:s}}function I(e){let t=typeof e=="string"?{url:e}:e,n=t.prefix??"",l=t.database,s=t.url.match(/mongodb(?:\+srv)?:\/\/[^/]+\/([^?]+)/);!l&&s&&s[1]&&(l=s[1]),l=l??"awaitly";let f=t.collection??"awaitly_snapshots",S=!t.client,a=t.client,c,y=!1,u=null,m=async()=>(c&&y||(a||(a=new M.MongoClient(t.url,{directConnection:!t.url.includes("mongodb+srv://"),...t.clientOptions})),await a.connect(),y=!0,c=a.db(l),await c.collection(f).createIndex({updatedAt:-1},{background:!0}).catch(()=>{}),t.lock&&!u&&(u=A(c,t.lock))),c),i={async save(o,r){let g=(await m()).collection(f),k=n+o,p=(0,d.isResumeState)(r)?(0,d.serializeResumeState)(r):r;await g.updateOne({_id:k},{$set:{snapshot:p,updatedAt:new Date}},{upsert:!0})},async load(o){let w=(await m()).collection(f),g=n+o,k=await w.findOne({_id:g});if(!k)return null;let p=k.snapshot;return(0,d.isSerializedResumeState)(p)?(0,d.deserializeResumeState)(p):((0,d.isWorkflowSnapshot)(p),p)},async loadResumeState(o){let r=await i.load(o);return r===null?null:(0,d.isResumeState)(r)?r:null},async delete(o){let w=(await m()).collection(f),g=n+o;await w.deleteOne({_id:g})},async list(o){let w=(await m()).collection(f),g=n+(o?.prefix??""),k=o?.limit??100,p=g.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return(await w.find({_id:{$regex:`^${p}`}}).sort({updatedAt:-1}).limit(k).toArray()).map(h=>({id:String(h._id).slice(n.length),updatedAt:h.updatedAt.toISOString()}))},async close(){S&&a&&(await a.close(),y=!1,c=void 0)},async tryAcquire(o,r){return await m(),u?u.tryAcquire(o,r):null},async release(o,r){if(await m(),!!u)return u.release(o,r)}};return t.lock||(delete i.tryAcquire,delete i.release),i}0&&(module.exports={mongo});
1
+ "use strict";var b=Object.defineProperty;var h=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var L=Object.prototype.hasOwnProperty;var v=(n,e)=>{for(var r in e)b(n,r,{get:e[r],enumerable:!0})},C=(n,e,r,l)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of D(e))!L.call(n,a)&&a!==r&&b(n,a,{get:()=>e[a],enumerable:!(l=h(e,a))||l.enumerable});return n};var O=n=>C(b({},"__esModule",{value:!0}),n);var _={};v(_,{mongo:()=>R});module.exports=O(_);var A=require("mongodb"),u=require("awaitly/workflow");var P=require("crypto");function x(n,e={}){let r=e.lockCollectionName??"workflow_lock",l=n.collection(r);async function a(){(await n.listCollections({name:r}).toArray()).length===0&&await n.createCollection(r)}async function w(i,d){let c=d?.ttlMs??6e4,s=(0,P.randomUUID)(),p=new Date(Date.now()+c);await a();try{let t=await l.findOneAndUpdate({_id:i,$or:[{expiresAt:{$lt:new Date}},{expiresAt:{$exists:!1}}]},{$set:{ownerToken:s,expiresAt:p}},{upsert:!0,returnDocument:"after"});return t&&t.ownerToken===s?{ownerToken:s}:null}catch(t){if(t&&typeof t=="object"&&"code"in t&&t.code===11e3)return null;throw t}}async function S(i,d){await l.deleteOne({_id:i,ownerToken:d})}async function f(i,d,c){let s=c?.ttlMs??6e4,p=new Date(Date.now()+s);return(await l.updateOne({_id:i,ownerToken:d},{$set:{expiresAt:p}})).modifiedCount>0}return{tryAcquire:w,release:S,renew:f,ensureLockCollection:a}}function R(n){let e=typeof n=="string"?{url:n}:n,r=e.prefix??"",l=e.database,a=e.url.match(/mongodb(?:\+srv)?:\/\/[^/]+\/([^?]+)/);!l&&a&&a[1]&&(l=a[1]),l=l??"awaitly";let w=e.collection??"awaitly_snapshots",S=!e.client,f=e.client,i,d=!1,c=null,s=async()=>(i&&d||(f||(f=new A.MongoClient(e.url,{directConnection:!e.url.includes("mongodb+srv://"),...e.clientOptions})),await f.connect(),d=!0,i=f.db(l),await i.collection(w).createIndex({updatedAt:-1},{background:!0}).catch(()=>{}),e.lock&&!c&&(c=x(i,e.lock))),i),p={async save(t,o){let k=(await s()).collection(w),y=r+t,m=(0,u.isResumeState)(o)?(0,u.serializeResumeState)(o):o;await k.updateOne({_id:y},{$set:{snapshot:m,updatedAt:new Date}},{upsert:!0})},async load(t){let g=(await s()).collection(w),k=r+t,y=await g.findOne({_id:k});if(!y)return null;let m=y.snapshot;return(0,u.isSerializedResumeState)(m)?(0,u.deserializeResumeState)(m):((0,u.isWorkflowSnapshot)(m),m)},async loadResumeState(t){let o=await p.load(t);return o===null?null:(0,u.isResumeState)(o)?o:null},async delete(t){let g=(await s()).collection(w),k=r+t;await g.deleteOne({_id:k})},async list(t){let g=(await s()).collection(w),k=r+(t?.prefix??""),y=t?.limit??100,m=k.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return(await g.find({_id:{$regex:`^${m}`}}).sort({updatedAt:-1}).limit(y).toArray()).map(M=>({id:String(M._id).slice(r.length),updatedAt:M.updatedAt.toISOString()}))},async close(){S&&f&&(await f.close(),d=!1,i=void 0)},async tryAcquire(t,o){return await s(),c?c.tryAcquire(t,o):null},async release(t,o){if(await s(),!!c)return c.release(t,o)},async renew(t,o,g){return await s(),c?c.renew(t,o,g):!1}};return e.lock||(delete p.tryAcquire,delete p.release,delete p.renew),p}0&&(module.exports={mongo});
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
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 * Supports both WorkflowSnapshot and ResumeState (serialized via serializeResumeState).\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 {\n type ResumeState,\n type StoreSaveInput,\n type StoreLoadResult,\n isWorkflowSnapshot,\n isResumeState,\n isSerializedResumeState,\n serializeResumeState,\n deserializeResumeState,\n} from \"awaitly/workflow\";\nimport { createMongoLock, type MongoLockOptions } from \"./mongo-lock\";\n\n/** Document shape for the snapshots collection (string _id). */\ninterface SnapshotDoc {\n _id: string;\n snapshot: WorkflowSnapshot | import(\"awaitly/workflow\").SerializedResumeState;\n updatedAt: Date;\n}\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\";\nexport type { StoreSaveInput, StoreLoadResult } from \"awaitly/workflow\";\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/** Mongo store with widened save/load for WorkflowSnapshot and ResumeState. Compatible with SnapshotStore for snapshot-only usage. */\nexport interface MongoStore extends Partial<WorkflowLock> {\n save(id: string, state: StoreSaveInput): Promise<void>;\n load(id: string): Promise<StoreLoadResult>;\n loadResumeState(id: string): Promise<ResumeState | null>;\n delete(id: string): Promise<void>;\n list(options?: { prefix?: string; limit?: number }): Promise<Array<{ id: string; updatedAt: string }>>;\n close(): Promise<void>;\n}\n\n// =============================================================================\n// mongo() - One-liner Snapshot Store Setup\n// =============================================================================\n\n/**\n * Create a snapshot store backed by MongoDB.\n * Save accepts WorkflowSnapshot or ResumeState; load returns whichever was stored.\n * Use loadResumeState(id) for type-safe restore, or toResumeState(await store.load(id)).\n *\n * @example\n * ```typescript\n * import { mongo } from 'awaitly-mongo';\n * import { createWorkflow } from 'awaitly/workflow';\n *\n * const store = mongo('mongodb://localhost:27017/mydb');\n * const workflow = createWorkflow(deps);\n *\n * // Run and persist resume state\n * const { result, resumeState } = await workflow.runWithState(fn);\n * await store.save('wf-123', resumeState);\n *\n * // Restore\n * const resumeState = await store.loadResumeState('wf-123');\n * if (resumeState) await workflow.run(fn, { resumeState });\n * ```\n *\n * @example\n * ```typescript\n * // With options including cross-process locking\n * const store = 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): MongoStore {\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<SnapshotDoc>(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: MongoStore = {\n async save(id: string, state: StoreSaveInput): Promise<void> {\n const db = await ensureConnected();\n const collection = db.collection<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n const toStore = isResumeState(state) ? serializeResumeState(state) : state;\n await collection.updateOne(\n { _id: fullId },\n {\n $set: {\n snapshot: toStore,\n updatedAt: new Date(),\n },\n },\n { upsert: true }\n );\n },\n\n async load(id: string): Promise<StoreLoadResult> {\n const db = await ensureConnected();\n const collection = db.collection<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n const doc = await collection.findOne({ _id: fullId });\n if (!doc) return null;\n const raw = doc.snapshot;\n if (isSerializedResumeState(raw)) return deserializeResumeState(raw);\n if (isWorkflowSnapshot(raw)) return raw;\n return raw as WorkflowSnapshot;\n },\n\n async loadResumeState(id: string): Promise<ResumeState | null> {\n const loaded = await store.load(id);\n if (loaded === null) return null;\n if (isResumeState(loaded)) return loaded;\n return null;\n },\n\n async delete(id: string): Promise<void> {\n const db = await ensureConnected();\n const collection = db.collection<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n await collection.deleteOne({ _id: fullId });\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<SnapshotDoc>(collectionName);\n const filterPrefix = prefix + (options?.prefix ?? \"\");\n const limit = options?.limit ?? 100;\n const escaped = filterPrefix.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n const cursor = collection\n .find({ _id: { $regex: `^${escaped}` } })\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.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_workflow","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","state","collection","fullId","toStore","doc","raw","loaded","options","filterPrefix","limit","escaped","ownerToken"]}
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 * Supports both WorkflowSnapshot and ResumeState (serialized via serializeResumeState).\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 {\n type ResumeState,\n type StoreSaveInput,\n type StoreLoadResult,\n isWorkflowSnapshot,\n isResumeState,\n isSerializedResumeState,\n serializeResumeState,\n deserializeResumeState,\n} from \"awaitly/workflow\";\nimport { createMongoLock, type MongoLockOptions } from \"./mongo-lock\";\n\n/** Document shape for the snapshots collection (string _id). */\ninterface SnapshotDoc {\n _id: string;\n snapshot: WorkflowSnapshot | import(\"awaitly/workflow\").SerializedResumeState;\n updatedAt: Date;\n}\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\";\nexport type { StoreSaveInput, StoreLoadResult } from \"awaitly/workflow\";\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/** Mongo store with widened save/load for WorkflowSnapshot and ResumeState. Compatible with SnapshotStore for snapshot-only usage. */\nexport interface MongoStore extends Partial<WorkflowLock> {\n save(id: string, state: StoreSaveInput): Promise<void>;\n load(id: string): Promise<StoreLoadResult>;\n loadResumeState(id: string): Promise<ResumeState | null>;\n delete(id: string): Promise<void>;\n list(options?: { prefix?: string; limit?: number }): Promise<Array<{ id: string; updatedAt: string }>>;\n close(): Promise<void>;\n}\n\n// =============================================================================\n// mongo() - One-liner Snapshot Store Setup\n// =============================================================================\n\n/**\n * Create a snapshot store backed by MongoDB.\n * Save accepts WorkflowSnapshot or ResumeState; load returns whichever was stored.\n * Use loadResumeState(id) for type-safe restore, or toResumeState(await store.load(id)).\n *\n * @example\n * ```typescript\n * import { mongo } from 'awaitly-mongo';\n * import { createWorkflow } from 'awaitly/workflow';\n *\n * const store = mongo('mongodb://localhost:27017/mydb');\n * const workflow = createWorkflow(deps);\n *\n * // Run and persist resume state\n * const { result, resumeState } = await workflow.runWithState(fn);\n * await store.save('wf-123', resumeState);\n *\n * // Restore\n * const resumeState = await store.loadResumeState('wf-123');\n * if (resumeState) await workflow.run(fn, { resumeState });\n * ```\n *\n * @example\n * ```typescript\n * // With options including cross-process locking\n * const store = 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): MongoStore {\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\"]; renew: NonNullable<WorkflowLock[\"renew\"]> } | 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<SnapshotDoc>(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: MongoStore = {\n async save(id: string, state: StoreSaveInput): Promise<void> {\n const db = await ensureConnected();\n const collection = db.collection<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n const toStore = isResumeState(state) ? serializeResumeState(state) : state;\n await collection.updateOne(\n { _id: fullId },\n {\n $set: {\n snapshot: toStore,\n updatedAt: new Date(),\n },\n },\n { upsert: true }\n );\n },\n\n async load(id: string): Promise<StoreLoadResult> {\n const db = await ensureConnected();\n const collection = db.collection<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n const doc = await collection.findOne({ _id: fullId });\n if (!doc) return null;\n const raw = doc.snapshot;\n if (isSerializedResumeState(raw)) return deserializeResumeState(raw);\n if (isWorkflowSnapshot(raw)) return raw;\n return raw as WorkflowSnapshot;\n },\n\n async loadResumeState(id: string): Promise<ResumeState | null> {\n const loaded = await store.load(id);\n if (loaded === null) return null;\n if (isResumeState(loaded)) return loaded;\n return null;\n },\n\n async delete(id: string): Promise<void> {\n const db = await ensureConnected();\n const collection = db.collection<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n await collection.deleteOne({ _id: fullId });\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<SnapshotDoc>(collectionName);\n const filterPrefix = prefix + (options?.prefix ?? \"\");\n const limit = options?.limit ?? 100;\n const escaped = filterPrefix.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n const cursor = collection\n .find({ _id: { $regex: `^${escaped}` } })\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.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 async renew(id: string, ownerToken: string, opts?: { ttlMs?: number }): Promise<boolean> {\n await ensureConnected();\n if (!lock) return false;\n return lock.renew(id, ownerToken, opts);\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 delete store.renew;\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 renew(id: string, ownerToken: string, opts?: { ttlMs?: number }): Promise<boolean>;\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 async function renew(\n id: string,\n ownerToken: string,\n opts?: { ttlMs?: number }\n ): Promise<boolean> {\n const ttlMs = opts?.ttlMs ?? 60_000;\n const expiresAt = new Date(Date.now() + ttlMs);\n\n const result = await collection.updateOne(\n { _id: id, ownerToken },\n { $set: { expiresAt } }\n );\n\n return result.modifiedCount > 0;\n }\n\n return { tryAcquire, release, renew, ensureLockCollection };\n}\n"],"mappings":"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","names":["index_exports","__export","mongo","__toCommonJS","import_mongodb","import_workflow","import_node_crypto","createMongoLock","db","options","lockCollectionName","collection","ensureLockCollection","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","error","release","renew","mongo","urlOrOptions","opts","prefix","databaseName","urlMatch","collectionName","ownClient","client","db","connected","lock","ensureConnected","MongoClientImpl","createMongoLock","store","id","state","collection","fullId","toStore","doc","raw","loaded","options","filterPrefix","limit","escaped","ownerToken"]}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import{MongoClient as A}from"mongodb";import{isWorkflowSnapshot as M,isResumeState as h,isSerializedResumeState as P,serializeResumeState as D,deserializeResumeState as L}from"awaitly/workflow";import{randomUUID as x}from"crypto";function b(u,e={}){let l=e.lockCollectionName??"workflow_lock",c=u.collection(l);async function f(){(await u.listCollections({name:l}).toArray()).length===0&&await u.createCollection(l)}async function p(r,i){let k=i?.ttlMs??6e4,s=x(),d=new Date(Date.now()+k);await f();try{let n=await c.findOneAndUpdate({_id:r,$or:[{expiresAt:{$lt:new Date}},{expiresAt:{$exists:!1}}]},{$set:{ownerToken:s,expiresAt:d}},{upsert:!0,returnDocument:"after"});return n&&n.ownerToken===s?{ownerToken:s}:null}catch(n){if(n&&typeof n=="object"&&"code"in n&&n.code===11e3)return null;throw n}}async function y(r,i){await c.deleteOne({_id:r,ownerToken:i})}return{tryAcquire:p,release:y,ensureLockCollection:f}}function $(u){let e=typeof u=="string"?{url:u}:u,l=e.prefix??"",c=e.database,f=e.url.match(/mongodb(?:\+srv)?:\/\/[^/]+\/([^?]+)/);!c&&f&&f[1]&&(c=f[1]),c=c??"awaitly";let p=e.collection??"awaitly_snapshots",y=!e.client,r=e.client,i,k=!1,s=null,d=async()=>(i&&k||(r||(r=new A(e.url,{directConnection:!e.url.includes("mongodb+srv://"),...e.clientOptions})),await r.connect(),k=!0,i=r.db(c),await i.collection(p).createIndex({updatedAt:-1},{background:!0}).catch(()=>{}),e.lock&&!s&&(s=b(i,e.lock))),i),n={async save(t,o){let m=(await d()).collection(p),w=l+t,a=h(o)?D(o):o;await m.updateOne({_id:w},{$set:{snapshot:a,updatedAt:new Date}},{upsert:!0})},async load(t){let g=(await d()).collection(p),m=l+t,w=await g.findOne({_id:m});if(!w)return null;let a=w.snapshot;return P(a)?L(a):(M(a),a)},async loadResumeState(t){let o=await n.load(t);return o===null?null:h(o)?o:null},async delete(t){let g=(await d()).collection(p),m=l+t;await g.deleteOne({_id:m})},async list(t){let g=(await d()).collection(p),m=l+(t?.prefix??""),w=t?.limit??100,a=m.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return(await g.find({_id:{$regex:`^${a}`}}).sort({updatedAt:-1}).limit(w).toArray()).map(S=>({id:String(S._id).slice(l.length),updatedAt:S.updatedAt.toISOString()}))},async close(){y&&r&&(await r.close(),k=!1,i=void 0)},async tryAcquire(t,o){return await d(),s?s.tryAcquire(t,o):null},async release(t,o){if(await d(),!!s)return s.release(t,o)}};return e.lock||(delete n.tryAcquire,delete n.release),n}export{$ as mongo};
1
+ import{MongoClient as x}from"mongodb";import{isWorkflowSnapshot as A,isResumeState as M,isSerializedResumeState as h,serializeResumeState as D,deserializeResumeState as L}from"awaitly/workflow";import{randomUUID as P}from"crypto";function b(m,o={}){let s=o.lockCollectionName??"workflow_lock",l=m.collection(s);async function w(){(await m.listCollections({name:s}).toArray()).length===0&&await m.createCollection(s)}async function f(n,a){let i=a?.ttlMs??6e4,r=P(),c=new Date(Date.now()+i);await w();try{let t=await l.findOneAndUpdate({_id:n,$or:[{expiresAt:{$lt:new Date}},{expiresAt:{$exists:!1}}]},{$set:{ownerToken:r,expiresAt:c}},{upsert:!0,returnDocument:"after"});return t&&t.ownerToken===r?{ownerToken:r}:null}catch(t){if(t&&typeof t=="object"&&"code"in t&&t.code===11e3)return null;throw t}}async function y(n,a){await l.deleteOne({_id:n,ownerToken:a})}async function d(n,a,i){let r=i?.ttlMs??6e4,c=new Date(Date.now()+r);return(await l.updateOne({_id:n,ownerToken:a},{$set:{expiresAt:c}})).modifiedCount>0}return{tryAcquire:f,release:y,renew:d,ensureLockCollection:w}}function $(m){let o=typeof m=="string"?{url:m}:m,s=o.prefix??"",l=o.database,w=o.url.match(/mongodb(?:\+srv)?:\/\/[^/]+\/([^?]+)/);!l&&w&&w[1]&&(l=w[1]),l=l??"awaitly";let f=o.collection??"awaitly_snapshots",y=!o.client,d=o.client,n,a=!1,i=null,r=async()=>(n&&a||(d||(d=new x(o.url,{directConnection:!o.url.includes("mongodb+srv://"),...o.clientOptions})),await d.connect(),a=!0,n=d.db(l),await n.collection(f).createIndex({updatedAt:-1},{background:!0}).catch(()=>{}),o.lock&&!i&&(i=b(n,o.lock))),n),c={async save(t,e){let g=(await r()).collection(f),k=s+t,u=M(e)?D(e):e;await g.updateOne({_id:k},{$set:{snapshot:u,updatedAt:new Date}},{upsert:!0})},async load(t){let p=(await r()).collection(f),g=s+t,k=await p.findOne({_id:g});if(!k)return null;let u=k.snapshot;return h(u)?L(u):(A(u),u)},async loadResumeState(t){let e=await c.load(t);return e===null?null:M(e)?e:null},async delete(t){let p=(await r()).collection(f),g=s+t;await p.deleteOne({_id:g})},async list(t){let p=(await r()).collection(f),g=s+(t?.prefix??""),k=t?.limit??100,u=g.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return(await p.find({_id:{$regex:`^${u}`}}).sort({updatedAt:-1}).limit(k).toArray()).map(S=>({id:String(S._id).slice(s.length),updatedAt:S.updatedAt.toISOString()}))},async close(){y&&d&&(await d.close(),a=!1,n=void 0)},async tryAcquire(t,e){return await r(),i?i.tryAcquire(t,e):null},async release(t,e){if(await r(),!!i)return i.release(t,e)},async renew(t,e,p){return await r(),i?i.renew(t,e,p):!1}};return o.lock||(delete c.tryAcquire,delete c.release,delete c.renew),c}export{$ 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-lock.ts"],"sourcesContent":["/**\n * awaitly-mongo\n *\n * MongoDB persistence adapter for awaitly workflows.\n * Provides ready-to-use SnapshotStore backed by MongoDB.\n * Supports both WorkflowSnapshot and ResumeState (serialized via serializeResumeState).\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 {\n type ResumeState,\n type StoreSaveInput,\n type StoreLoadResult,\n isWorkflowSnapshot,\n isResumeState,\n isSerializedResumeState,\n serializeResumeState,\n deserializeResumeState,\n} from \"awaitly/workflow\";\nimport { createMongoLock, type MongoLockOptions } from \"./mongo-lock\";\n\n/** Document shape for the snapshots collection (string _id). */\ninterface SnapshotDoc {\n _id: string;\n snapshot: WorkflowSnapshot | import(\"awaitly/workflow\").SerializedResumeState;\n updatedAt: Date;\n}\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\";\nexport type { StoreSaveInput, StoreLoadResult } from \"awaitly/workflow\";\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/** Mongo store with widened save/load for WorkflowSnapshot and ResumeState. Compatible with SnapshotStore for snapshot-only usage. */\nexport interface MongoStore extends Partial<WorkflowLock> {\n save(id: string, state: StoreSaveInput): Promise<void>;\n load(id: string): Promise<StoreLoadResult>;\n loadResumeState(id: string): Promise<ResumeState | null>;\n delete(id: string): Promise<void>;\n list(options?: { prefix?: string; limit?: number }): Promise<Array<{ id: string; updatedAt: string }>>;\n close(): Promise<void>;\n}\n\n// =============================================================================\n// mongo() - One-liner Snapshot Store Setup\n// =============================================================================\n\n/**\n * Create a snapshot store backed by MongoDB.\n * Save accepts WorkflowSnapshot or ResumeState; load returns whichever was stored.\n * Use loadResumeState(id) for type-safe restore, or toResumeState(await store.load(id)).\n *\n * @example\n * ```typescript\n * import { mongo } from 'awaitly-mongo';\n * import { createWorkflow } from 'awaitly/workflow';\n *\n * const store = mongo('mongodb://localhost:27017/mydb');\n * const workflow = createWorkflow(deps);\n *\n * // Run and persist resume state\n * const { result, resumeState } = await workflow.runWithState(fn);\n * await store.save('wf-123', resumeState);\n *\n * // Restore\n * const resumeState = await store.loadResumeState('wf-123');\n * if (resumeState) await workflow.run(fn, { resumeState });\n * ```\n *\n * @example\n * ```typescript\n * // With options including cross-process locking\n * const store = 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): MongoStore {\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<SnapshotDoc>(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: MongoStore = {\n async save(id: string, state: StoreSaveInput): Promise<void> {\n const db = await ensureConnected();\n const collection = db.collection<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n const toStore = isResumeState(state) ? serializeResumeState(state) : state;\n await collection.updateOne(\n { _id: fullId },\n {\n $set: {\n snapshot: toStore,\n updatedAt: new Date(),\n },\n },\n { upsert: true }\n );\n },\n\n async load(id: string): Promise<StoreLoadResult> {\n const db = await ensureConnected();\n const collection = db.collection<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n const doc = await collection.findOne({ _id: fullId });\n if (!doc) return null;\n const raw = doc.snapshot;\n if (isSerializedResumeState(raw)) return deserializeResumeState(raw);\n if (isWorkflowSnapshot(raw)) return raw;\n return raw as WorkflowSnapshot;\n },\n\n async loadResumeState(id: string): Promise<ResumeState | null> {\n const loaded = await store.load(id);\n if (loaded === null) return null;\n if (isResumeState(loaded)) return loaded;\n return null;\n },\n\n async delete(id: string): Promise<void> {\n const db = await ensureConnected();\n const collection = db.collection<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n await collection.deleteOne({ _id: fullId });\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<SnapshotDoc>(collectionName);\n const filterPrefix = prefix + (options?.prefix ?? \"\");\n const limit = options?.limit ?? 100;\n const escaped = filterPrefix.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n const cursor = collection\n .find({ _id: { $regex: `^${escaped}` } })\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.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","isWorkflowSnapshot","isResumeState","isSerializedResumeState","serializeResumeState","deserializeResumeState","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","state","collection","fullId","toStore","isResumeState","serializeResumeState","doc","raw","isSerializedResumeState","deserializeResumeState","isWorkflowSnapshot","loaded","options","filterPrefix","limit","escaped","ownerToken"]}
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 * Supports both WorkflowSnapshot and ResumeState (serialized via serializeResumeState).\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 {\n type ResumeState,\n type StoreSaveInput,\n type StoreLoadResult,\n isWorkflowSnapshot,\n isResumeState,\n isSerializedResumeState,\n serializeResumeState,\n deserializeResumeState,\n} from \"awaitly/workflow\";\nimport { createMongoLock, type MongoLockOptions } from \"./mongo-lock\";\n\n/** Document shape for the snapshots collection (string _id). */\ninterface SnapshotDoc {\n _id: string;\n snapshot: WorkflowSnapshot | import(\"awaitly/workflow\").SerializedResumeState;\n updatedAt: Date;\n}\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\";\nexport type { StoreSaveInput, StoreLoadResult } from \"awaitly/workflow\";\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/** Mongo store with widened save/load for WorkflowSnapshot and ResumeState. Compatible with SnapshotStore for snapshot-only usage. */\nexport interface MongoStore extends Partial<WorkflowLock> {\n save(id: string, state: StoreSaveInput): Promise<void>;\n load(id: string): Promise<StoreLoadResult>;\n loadResumeState(id: string): Promise<ResumeState | null>;\n delete(id: string): Promise<void>;\n list(options?: { prefix?: string; limit?: number }): Promise<Array<{ id: string; updatedAt: string }>>;\n close(): Promise<void>;\n}\n\n// =============================================================================\n// mongo() - One-liner Snapshot Store Setup\n// =============================================================================\n\n/**\n * Create a snapshot store backed by MongoDB.\n * Save accepts WorkflowSnapshot or ResumeState; load returns whichever was stored.\n * Use loadResumeState(id) for type-safe restore, or toResumeState(await store.load(id)).\n *\n * @example\n * ```typescript\n * import { mongo } from 'awaitly-mongo';\n * import { createWorkflow } from 'awaitly/workflow';\n *\n * const store = mongo('mongodb://localhost:27017/mydb');\n * const workflow = createWorkflow(deps);\n *\n * // Run and persist resume state\n * const { result, resumeState } = await workflow.runWithState(fn);\n * await store.save('wf-123', resumeState);\n *\n * // Restore\n * const resumeState = await store.loadResumeState('wf-123');\n * if (resumeState) await workflow.run(fn, { resumeState });\n * ```\n *\n * @example\n * ```typescript\n * // With options including cross-process locking\n * const store = 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): MongoStore {\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\"]; renew: NonNullable<WorkflowLock[\"renew\"]> } | 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<SnapshotDoc>(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: MongoStore = {\n async save(id: string, state: StoreSaveInput): Promise<void> {\n const db = await ensureConnected();\n const collection = db.collection<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n const toStore = isResumeState(state) ? serializeResumeState(state) : state;\n await collection.updateOne(\n { _id: fullId },\n {\n $set: {\n snapshot: toStore,\n updatedAt: new Date(),\n },\n },\n { upsert: true }\n );\n },\n\n async load(id: string): Promise<StoreLoadResult> {\n const db = await ensureConnected();\n const collection = db.collection<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n const doc = await collection.findOne({ _id: fullId });\n if (!doc) return null;\n const raw = doc.snapshot;\n if (isSerializedResumeState(raw)) return deserializeResumeState(raw);\n if (isWorkflowSnapshot(raw)) return raw;\n return raw as WorkflowSnapshot;\n },\n\n async loadResumeState(id: string): Promise<ResumeState | null> {\n const loaded = await store.load(id);\n if (loaded === null) return null;\n if (isResumeState(loaded)) return loaded;\n return null;\n },\n\n async delete(id: string): Promise<void> {\n const db = await ensureConnected();\n const collection = db.collection<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n await collection.deleteOne({ _id: fullId });\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<SnapshotDoc>(collectionName);\n const filterPrefix = prefix + (options?.prefix ?? \"\");\n const limit = options?.limit ?? 100;\n const escaped = filterPrefix.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n const cursor = collection\n .find({ _id: { $regex: `^${escaped}` } })\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.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 async renew(id: string, ownerToken: string, opts?: { ttlMs?: number }): Promise<boolean> {\n await ensureConnected();\n if (!lock) return false;\n return lock.renew(id, ownerToken, opts);\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 delete store.renew;\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 renew(id: string, ownerToken: string, opts?: { ttlMs?: number }): Promise<boolean>;\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 async function renew(\n id: string,\n ownerToken: string,\n opts?: { ttlMs?: number }\n ): Promise<boolean> {\n const ttlMs = opts?.ttlMs ?? 60_000;\n const expiresAt = new Date(Date.now() + ttlMs);\n\n const result = await collection.updateOne(\n { _id: id, ownerToken },\n { $set: { expiresAt } }\n );\n\n return result.modifiedCount > 0;\n }\n\n return { tryAcquire, release, renew, ensureLockCollection };\n}\n"],"mappings":"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","names":["MongoClientImpl","isWorkflowSnapshot","isResumeState","isSerializedResumeState","serializeResumeState","deserializeResumeState","randomUUID","createMongoLock","db","options","lockCollectionName","collection","ensureLockCollection","tryAcquire","id","opts","ttlMs","ownerToken","expiresAt","result","error","release","renew","mongo","urlOrOptions","opts","prefix","databaseName","urlMatch","collectionName","ownClient","client","db","connected","lock","ensureConnected","MongoClientImpl","createMongoLock","store","id","state","collection","fullId","toStore","isResumeState","serializeResumeState","doc","raw","isSerializedResumeState","deserializeResumeState","isWorkflowSnapshot","loaded","options","filterPrefix","limit","escaped","ownerToken"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "awaitly-mongo",
3
- "version": "20.0.0",
3
+ "version": "22.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.30.0"
41
+ "awaitly": "^1.32.0"
42
42
  },
43
43
  "dependencies": {
44
44
  "mongodb": "^7.1.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.3.3",
49
+ "@types/node": "^25.3.5",
50
50
  "tsup": "^8.5.1",
51
51
  "typescript": "^5.9.3",
52
52
  "vitest": "^4.0.18",
53
- "awaitly": "^1.30.0"
53
+ "awaitly": "^1.32.0"
54
54
  },
55
55
  "publishConfig": {
56
56
  "access": "public",