awaitly-mongo 16.0.0 → 18.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 y=Object.defineProperty;var M=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var L=(t,o)=>{for(var e in o)y(t,e,{get:o[e],enumerable:!0})},P=(t,o,e,c)=>{if(o&&typeof o=="object"||typeof o=="function")for(let r of x(o))!C.call(t,r)&&r!==e&&y(t,r,{get:()=>o[r],enumerable:!(c=M(o,r))||c.enumerable});return t};var O=t=>P(y({},"__esModule",{value:!0}),t);var v={};L(v,{mongo:()=>_});module.exports=O(v);var S=require("mongodb");var h=require("crypto");function A(t,o={}){let e=o.lockCollectionName??"workflow_lock",c=t.collection(e);async function r(){(await t.listCollections({name:e}).toArray()).length===0&&await t.createCollection(e)}async function u(s,l){let w=l?.ttlMs??6e4,a=(0,h.randomUUID)(),d=new Date(Date.now()+w);await r();try{let i=await c.findOneAndUpdate({_id:s,$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(s,l){await c.deleteOne({_id:s,ownerToken:l})}return{tryAcquire:u,release:k,ensureLockCollection:r}}function _(t){let o=typeof t=="string"?{url:t}:t,e=o.prefix??"",c=o.database,r=o.url.match(/mongodb(?:\+srv)?:\/\/[^/]+\/([^?]+)/);!c&&r&&r[1]&&(c=r[1]),c=c??"awaitly";let u=o.collection??"awaitly_snapshots",k=!o.client,s=o.client,l,w=!1,a=null,d=async()=>(l&&w||(s||(s=new S.MongoClient(o.url,{directConnection:!o.url.includes("mongodb+srv://"),...o.clientOptions})),await s.connect(),w=!0,l=s.db(c),await l.collection(u).createIndex({updatedAt:-1},{background:!0}).catch(()=>{}),o.lock&&!a&&(a=A(l,o.lock))),l),i={async save(n,p){let f=(await d()).collection(u),g=e+n;await f.updateOne({_id:g},{$set:{snapshot:p,updatedAt:new Date}},{upsert:!0})},async load(n){let m=(await d()).collection(u),f=e+n,g=await m.findOne({_id:f});return g?g.snapshot:null},async delete(n){let m=(await d()).collection(u),f=e+n;await m.deleteOne({_id:f})},async list(n){let m=(await d()).collection(u),f=e+(n?.prefix??""),g=n?.limit??100,D=f.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return(await m.find({_id:{$regex:`^${D}`}}).sort({updatedAt:-1}).limit(g).toArray()).map(b=>({id:String(b._id).slice(e.length),updatedAt:b.updatedAt.toISOString()}))},async close(){k&&s&&(await s.close(),w=!1,l=void 0)},async tryAcquire(n,p){return await d(),a?a.tryAcquire(n,p):null},async release(n,p){if(await d(),!!a)return a.release(n,p)}};return o.lock||(delete i.tryAcquire,delete i.release),i}0&&(module.exports={mongo});
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});
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 */\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/** Document shape for the snapshots collection (string _id). */\ninterface SnapshotDoc {\n _id: string;\n snapshot: WorkflowSnapshot;\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\";\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<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: SnapshotStore & Partial<WorkflowLock> = {\n async save(id: string, snapshot: WorkflowSnapshot): Promise<void> {\n const db = await ensureConnected();\n const collection = db.collection<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n await collection.updateOne(\n { _id: fullId },\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<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n const doc = await collection.findOne({ _id: fullId });\n if (!doc) return null;\n return doc.snapshot;\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_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","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\"] } | 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"]}
package/dist/index.d.cts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { MongoClient, MongoClientOptions } from 'mongodb';
2
- import { SnapshotStore } from 'awaitly/persistence';
3
- export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
4
2
  import { WorkflowLock } from 'awaitly/durable';
5
3
  export { WorkflowLock } from 'awaitly/durable';
4
+ import { StoreSaveInput, StoreLoadResult, ResumeState } from 'awaitly/workflow';
5
+ export { StoreLoadResult, StoreSaveInput } from 'awaitly/workflow';
6
+ export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
6
7
 
7
8
  /**
8
9
  * MongoDB workflow lock (lease) for cross-process concurrency control.
@@ -22,6 +23,7 @@ interface MongoLockOptions {
22
23
  *
23
24
  * MongoDB persistence adapter for awaitly workflows.
24
25
  * Provides ready-to-use SnapshotStore backed by MongoDB.
26
+ * Supports both WorkflowSnapshot and ResumeState (serialized via serializeResumeState).
25
27
  */
26
28
 
27
29
  /**
@@ -43,26 +45,41 @@ interface MongoOptions {
43
45
  /** Cross-process lock options. When set, the store implements WorkflowLock. */
44
46
  lock?: MongoLockOptions;
45
47
  }
48
+ /** Mongo store with widened save/load for WorkflowSnapshot and ResumeState. Compatible with SnapshotStore for snapshot-only usage. */
49
+ interface MongoStore extends Partial<WorkflowLock> {
50
+ save(id: string, state: StoreSaveInput): Promise<void>;
51
+ load(id: string): Promise<StoreLoadResult>;
52
+ loadResumeState(id: string): Promise<ResumeState | null>;
53
+ delete(id: string): Promise<void>;
54
+ list(options?: {
55
+ prefix?: string;
56
+ limit?: number;
57
+ }): Promise<Array<{
58
+ id: string;
59
+ updatedAt: string;
60
+ }>>;
61
+ close(): Promise<void>;
62
+ }
46
63
  /**
47
64
  * Create a snapshot store backed by MongoDB.
48
- * This is the simplified one-liner API for workflow persistence.
65
+ * Save accepts WorkflowSnapshot or ResumeState; load returns whichever was stored.
66
+ * Use loadResumeState(id) for type-safe restore, or toResumeState(await store.load(id)).
49
67
  *
50
68
  * @example
51
69
  * ```typescript
52
70
  * import { mongo } from 'awaitly-mongo';
71
+ * import { createWorkflow } from 'awaitly/workflow';
53
72
  *
54
- * // One-liner setup
55
73
  * const store = mongo('mongodb://localhost:27017/mydb');
74
+ * const workflow = createWorkflow(deps);
56
75
  *
57
- * // Execute + persist
58
- * const wf = createWorkflow(deps);
59
- * await wf(myWorkflowFn);
60
- * await store.save('wf-123', wf.getSnapshot());
76
+ * // Run and persist resume state
77
+ * const { result, resumeState } = await workflow.runWithState(fn);
78
+ * await store.save('wf-123', resumeState);
61
79
  *
62
80
  * // Restore
63
- * const snapshot = await store.load('wf-123');
64
- * const wf2 = createWorkflow(deps, { snapshot });
65
- * await wf2(myWorkflowFn);
81
+ * const resumeState = await store.loadResumeState('wf-123');
82
+ * if (resumeState) await workflow.run(fn, { resumeState });
66
83
  * ```
67
84
  *
68
85
  * @example
@@ -77,6 +94,6 @@ interface MongoOptions {
77
94
  * });
78
95
  * ```
79
96
  */
80
- declare function mongo(urlOrOptions: string | MongoOptions): SnapshotStore & Partial<WorkflowLock>;
97
+ declare function mongo(urlOrOptions: string | MongoOptions): MongoStore;
81
98
 
82
- export { type MongoLockOptions, type MongoOptions, mongo };
99
+ export { type MongoLockOptions, type MongoOptions, type MongoStore, mongo };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { MongoClient, MongoClientOptions } from 'mongodb';
2
- import { SnapshotStore } from 'awaitly/persistence';
3
- export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
4
2
  import { WorkflowLock } from 'awaitly/durable';
5
3
  export { WorkflowLock } from 'awaitly/durable';
4
+ import { StoreSaveInput, StoreLoadResult, ResumeState } from 'awaitly/workflow';
5
+ export { StoreLoadResult, StoreSaveInput } from 'awaitly/workflow';
6
+ export { SnapshotStore, WorkflowSnapshot } from 'awaitly/persistence';
6
7
 
7
8
  /**
8
9
  * MongoDB workflow lock (lease) for cross-process concurrency control.
@@ -22,6 +23,7 @@ interface MongoLockOptions {
22
23
  *
23
24
  * MongoDB persistence adapter for awaitly workflows.
24
25
  * Provides ready-to-use SnapshotStore backed by MongoDB.
26
+ * Supports both WorkflowSnapshot and ResumeState (serialized via serializeResumeState).
25
27
  */
26
28
 
27
29
  /**
@@ -43,26 +45,41 @@ interface MongoOptions {
43
45
  /** Cross-process lock options. When set, the store implements WorkflowLock. */
44
46
  lock?: MongoLockOptions;
45
47
  }
48
+ /** Mongo store with widened save/load for WorkflowSnapshot and ResumeState. Compatible with SnapshotStore for snapshot-only usage. */
49
+ interface MongoStore extends Partial<WorkflowLock> {
50
+ save(id: string, state: StoreSaveInput): Promise<void>;
51
+ load(id: string): Promise<StoreLoadResult>;
52
+ loadResumeState(id: string): Promise<ResumeState | null>;
53
+ delete(id: string): Promise<void>;
54
+ list(options?: {
55
+ prefix?: string;
56
+ limit?: number;
57
+ }): Promise<Array<{
58
+ id: string;
59
+ updatedAt: string;
60
+ }>>;
61
+ close(): Promise<void>;
62
+ }
46
63
  /**
47
64
  * Create a snapshot store backed by MongoDB.
48
- * This is the simplified one-liner API for workflow persistence.
65
+ * Save accepts WorkflowSnapshot or ResumeState; load returns whichever was stored.
66
+ * Use loadResumeState(id) for type-safe restore, or toResumeState(await store.load(id)).
49
67
  *
50
68
  * @example
51
69
  * ```typescript
52
70
  * import { mongo } from 'awaitly-mongo';
71
+ * import { createWorkflow } from 'awaitly/workflow';
53
72
  *
54
- * // One-liner setup
55
73
  * const store = mongo('mongodb://localhost:27017/mydb');
74
+ * const workflow = createWorkflow(deps);
56
75
  *
57
- * // Execute + persist
58
- * const wf = createWorkflow(deps);
59
- * await wf(myWorkflowFn);
60
- * await store.save('wf-123', wf.getSnapshot());
76
+ * // Run and persist resume state
77
+ * const { result, resumeState } = await workflow.runWithState(fn);
78
+ * await store.save('wf-123', resumeState);
61
79
  *
62
80
  * // Restore
63
- * const snapshot = await store.load('wf-123');
64
- * const wf2 = createWorkflow(deps, { snapshot });
65
- * await wf2(myWorkflowFn);
81
+ * const resumeState = await store.loadResumeState('wf-123');
82
+ * if (resumeState) await workflow.run(fn, { resumeState });
66
83
  * ```
67
84
  *
68
85
  * @example
@@ -77,6 +94,6 @@ interface MongoOptions {
77
94
  * });
78
95
  * ```
79
96
  */
80
- declare function mongo(urlOrOptions: string | MongoOptions): SnapshotStore & Partial<WorkflowLock>;
97
+ declare function mongo(urlOrOptions: string | MongoOptions): MongoStore;
81
98
 
82
- export { type MongoLockOptions, type MongoOptions, mongo };
99
+ export { type MongoLockOptions, type MongoOptions, type MongoStore, mongo };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import{MongoClient as S}from"mongodb";import{randomUUID as A}from"crypto";function b(d,o={}){let c=o.lockCollectionName??"workflow_lock",s=d.collection(c);async function f(){(await d.listCollections({name:c}).toArray()).length===0&&await d.createCollection(c)}async function p(n,r){let w=r?.ttlMs??6e4,i=A(),l=new Date(Date.now()+w);await f();try{let e=await s.findOneAndUpdate({_id:n,$or:[{expiresAt:{$lt:new Date}},{expiresAt:{$exists:!1}}]},{$set:{ownerToken:i,expiresAt:l}},{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 s.deleteOne({_id:n,ownerToken:r})}return{tryAcquire:p,release:k,ensureLockCollection:f}}function O(d){let o=typeof d=="string"?{url:d}:d,c=o.prefix??"",s=o.database,f=o.url.match(/mongodb(?:\+srv)?:\/\/[^/]+\/([^?]+)/);!s&&f&&f[1]&&(s=f[1]),s=s??"awaitly";let p=o.collection??"awaitly_snapshots",k=!o.client,n=o.client,r,w=!1,i=null,l=async()=>(r&&w||(n||(n=new S(o.url,{directConnection:!o.url.includes("mongodb+srv://"),...o.clientOptions})),await n.connect(),w=!0,r=n.db(s),await r.collection(p).createIndex({updatedAt:-1},{background:!0}).catch(()=>{}),o.lock&&!i&&(i=b(r,o.lock))),r),e={async save(t,a){let u=(await l()).collection(p),g=c+t;await u.updateOne({_id:g},{$set:{snapshot:a,updatedAt:new Date}},{upsert:!0})},async load(t){let m=(await l()).collection(p),u=c+t,g=await m.findOne({_id:u});return g?g.snapshot:null},async delete(t){let m=(await l()).collection(p),u=c+t;await m.deleteOne({_id:u})},async list(t){let m=(await l()).collection(p),u=c+(t?.prefix??""),g=t?.limit??100,h=u.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return(await m.find({_id:{$regex:`^${h}`}}).sort({updatedAt:-1}).limit(g).toArray()).map(y=>({id:String(y._id).slice(c.length),updatedAt:y.updatedAt.toISOString()}))},async close(){k&&n&&(await n.close(),w=!1,r=void 0)},async tryAcquire(t,a){return await l(),i?i.tryAcquire(t,a):null},async release(t,a){if(await l(),!!i)return i.release(t,a)}};return o.lock||(delete e.tryAcquire,delete e.release),e}export{O as mongo};
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};
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 */\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/** Document shape for the snapshots collection (string _id). */\ninterface SnapshotDoc {\n _id: string;\n snapshot: WorkflowSnapshot;\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\";\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<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: SnapshotStore & Partial<WorkflowLock> = {\n async save(id: string, snapshot: WorkflowSnapshot): Promise<void> {\n const db = await ensureConnected();\n const collection = db.collection<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n await collection.updateOne(\n { _id: fullId },\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<SnapshotDoc>(collectionName);\n const fullId = prefix + id;\n const doc = await collection.findOne({ _id: fullId });\n if (!doc) return null;\n return doc.snapshot;\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","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","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\"] } | 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "awaitly-mongo",
3
- "version": "16.0.0",
3
+ "version": "18.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.26.0"
41
+ "awaitly": "^1.28.0"
42
42
  },
43
43
  "dependencies": {
44
44
  "mongodb": "^7.1.0"
@@ -50,7 +50,7 @@
50
50
  "tsup": "^8.5.1",
51
51
  "typescript": "^5.9.3",
52
52
  "vitest": "^4.0.18",
53
- "awaitly": "^1.26.0"
53
+ "awaitly": "^1.28.0"
54
54
  },
55
55
  "publishConfig": {
56
56
  "access": "public",