awaitly-mongo 24.0.0 → 26.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  MongoDB persistence adapter for [awaitly](https://github.com/jagreehal/awaitly) workflows.
4
4
 
5
+ Provides a ready-to-use snapshot store backed by MongoDB. The store accepts both workflow snapshots and resume state, so it plugs straight into `durable.run` and `createWorkflow`.
6
+
5
7
  ## Installation
6
8
 
7
9
  ```bash
@@ -15,18 +17,16 @@ yarn add awaitly-mongo mongodb
15
17
  ## Quick Start
16
18
 
17
19
  ```typescript
18
- import { createMongoPersistence } from 'awaitly-mongo';
19
- import { durable } from 'awaitly/durable';
20
+ import { mongo } from 'awaitly-mongo';
21
+ import { durable } from 'awaitly/workflow';
20
22
 
21
- const store = await createMongoPersistence({
22
- connectionString: process.env.MONGODB_URI,
23
- });
23
+ const store = mongo(process.env.MONGODB_URI!);
24
24
 
25
25
  const result = await durable.run(
26
26
  { fetchUser, createOrder },
27
27
  async ({ step, deps: { fetchUser, createOrder } }) => {
28
- const user = await step('fetchUser', () => fetchUser('123'), { key: 'fetch-user' });
29
- const order = await step('createOrder', () => createOrder(user), { key: 'create-order' });
28
+ const user = await step('fetch-user', () => fetchUser('123'));
29
+ const order = await step('create-order', () => createOrder(user));
30
30
  return order;
31
31
  },
32
32
  {
@@ -38,96 +38,118 @@ const result = await durable.run(
38
38
 
39
39
  ## Configuration
40
40
 
41
- ### Connection String
41
+ ### Connection URL
42
42
 
43
43
  ```typescript
44
- const store = await createMongoPersistence({
45
- connectionString: 'mongodb://localhost:27017',
46
- });
44
+ const store = mongo('mongodb://localhost:27017');
47
45
  ```
48
46
 
49
- ### With Database and Collection Names
47
+ ### Options
50
48
 
51
49
  ```typescript
52
- const store = await createMongoPersistence({
53
- connectionString: process.env.MONGODB_URI,
54
- database: 'myapp',
55
- collection: 'custom_workflow_state', // optional, default: 'workflow_state'
56
- prefix: 'myapp:workflow:', // optional, default: 'workflow:state:'
50
+ const store = mongo({
51
+ url: process.env.MONGODB_URI!,
52
+ database: 'myapp', // optional, default: 'awaitly'
53
+ collection: 'my_workflow_snapshots', // optional, default: 'awaitly_snapshots'
54
+ prefix: 'orders:', // optional ID prefix, default: ''
57
55
  });
58
56
  ```
59
57
 
60
- ### Using Existing Client
58
+ ### Using an Existing Client
61
59
 
62
60
  ```typescript
63
61
  import { MongoClient } from 'mongodb';
64
- import { createMongoPersistence } from 'awaitly-mongo';
62
+ import { mongo } from 'awaitly-mongo';
65
63
 
66
- const client = new MongoClient(process.env.MONGODB_URI);
67
- await client.connect();
64
+ const client = new MongoClient(process.env.MONGODB_URI!);
68
65
 
69
- const store = await createMongoPersistence({
70
- existingClient: client,
66
+ const store = mongo({
67
+ url: process.env.MONGODB_URI!,
68
+ client,
71
69
  database: 'myapp',
72
70
  });
73
71
  ```
74
72
 
75
- ### Using Existing Database
73
+ ### Client Options
76
74
 
77
75
  ```typescript
78
- import { MongoClient } from 'mongodb';
79
- import { createMongoPersistence } from 'awaitly-mongo';
76
+ const store = mongo({
77
+ url: process.env.MONGODB_URI!,
78
+ clientOptions: {
79
+ maxPoolSize: 10,
80
+ serverSelectionTimeoutMS: 5000,
81
+ },
82
+ });
83
+ ```
80
84
 
81
- const client = new MongoClient(process.env.MONGODB_URI);
82
- await client.connect();
83
- const db = client.db('myapp');
85
+ ### Cross-Process Locking
84
86
 
85
- const store = await createMongoPersistence({
86
- existingDb: db,
87
- collection: 'workflow_state',
87
+ To ensure only one process runs a given workflow ID at a time, pass the `lock` option. The store then implements `WorkflowLock`, and `durable.run` acquires the lock before running (unless `allowConcurrent: true`) and releases it when done:
88
+
89
+ ```typescript
90
+ const store = mongo({
91
+ url: process.env.MONGODB_URI!,
92
+ lock: { lockCollectionName: 'workflow_lock' }, // optional; default collection name
88
93
  });
89
94
  ```
90
95
 
91
- ### Client Options
96
+ ## Using with createWorkflow
97
+
98
+ The store also works directly with workflow resume state:
92
99
 
93
100
  ```typescript
94
- const store = await createMongoPersistence({
95
- connectionString: process.env.MONGODB_URI,
96
- clientOptions: {
97
- maxPoolSize: 10,
98
- serverSelectionTimeoutMS: 5000,
99
- },
100
- });
101
+ import { mongo } from 'awaitly-mongo';
102
+ import { createWorkflow } from 'awaitly/workflow';
103
+
104
+ const store = mongo('mongodb://localhost:27017/mydb');
105
+ const workflow = createWorkflow(deps);
106
+
107
+ // Run and persist resume state
108
+ const { result, resumeState } = await workflow.runWithState(fn);
109
+ await store.save('wf-123', resumeState);
110
+
111
+ // Restore later
112
+ const saved = await store.loadResumeState('wf-123');
113
+ if (saved) await workflow.run(fn, { resumeState: saved });
101
114
  ```
102
115
 
103
- ## Collection Schema
116
+ ## Store API
104
117
 
105
- The adapter automatically creates a collection with the following document structure:
118
+ ```typescript
119
+ store.save(id, state); // WorkflowSnapshot or ResumeState
120
+ store.load(id); // returns whichever was stored
121
+ store.loadResumeState(id); // ResumeState | null
122
+ store.delete(id);
123
+ store.list({ prefix, limit }); // [{ id, updatedAt }]
124
+ store.close();
125
+ ```
126
+
127
+ ## Document Shape
128
+
129
+ Snapshots are stored as documents in the configured collection:
106
130
 
107
131
  ```typescript
108
132
  {
109
- _id: string, // The key
110
- value: string, // The serialized state value
111
- expiresAt?: Date // Optional expiration date (for TTL)
133
+ _id: string, // the (prefixed) workflow ID
134
+ snapshot: object, // the workflow snapshot or serialized resume state
135
+ updatedAt: Date
112
136
  }
113
137
  ```
114
138
 
115
- A TTL index is automatically created on the `expiresAt` field, which MongoDB uses to automatically delete expired documents.
116
-
117
139
  The collection is created automatically on first use. You can customize the collection name via the `collection` option.
118
140
 
119
141
  ## Features
120
142
 
121
143
  - ✅ Automatic collection creation
122
- - ✅ TTL support (automatic expiration via MongoDB TTL index)
123
- - ✅ Connection reuse (can share existing client/database)
124
- - ✅ Pattern matching for key queries
144
+ - ✅ Stores workflow snapshots and resume state
145
+ - ✅ Optional cross-process locking (`WorkflowLock`)
146
+ - ✅ Connection reuse (bring your own `MongoClient`)
125
147
  - ✅ Zero configuration required
126
148
 
127
149
  ## Requirements
128
150
 
129
151
  - Node.js >= 22
130
- - MongoDB >= 4.2 (for TTL index support)
152
+ - MongoDB >= 4.2
131
153
  - `mongodb` package (peer dependency)
132
154
 
133
155
  ## License
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
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});
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 g(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:g,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 g=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(g).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(g),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 w=(await s()).collection(g),k=r+t,y=await w.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 w=(await s()).collection(g),k=r+t;await w.deleteOne({_id:k})},async list(t){let w=(await s()).collection(g),k=r+(t?.prefix??""),y=t?.limit??100,m=k.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return(await w.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,w){return await s(),c?c.renew(t,o,w):!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\"]; 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"]}
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/workflow\";\nimport type { WorkflowLock } from \"awaitly/workflow\";\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/workflow\";\nexport type { WorkflowLock } from \"awaitly/workflow\";\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.d.cts CHANGED
@@ -1,9 +1,6 @@
1
1
  import { MongoClient, MongoClientOptions } from 'mongodb';
2
- import { WorkflowLock } from 'awaitly/durable';
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';
2
+ import { WorkflowLock, StoreSaveInput, StoreLoadResult, ResumeState } from 'awaitly/workflow';
3
+ export { SnapshotStore, StoreLoadResult, StoreSaveInput, WorkflowLock, WorkflowSnapshot } from 'awaitly/workflow';
7
4
 
8
5
  /**
9
6
  * MongoDB workflow lock (lease) for cross-process concurrency control.
package/dist/index.d.ts CHANGED
@@ -1,9 +1,6 @@
1
1
  import { MongoClient, MongoClientOptions } from 'mongodb';
2
- import { WorkflowLock } from 'awaitly/durable';
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';
2
+ import { WorkflowLock, StoreSaveInput, StoreLoadResult, ResumeState } from 'awaitly/workflow';
3
+ export { SnapshotStore, StoreLoadResult, StoreSaveInput, WorkflowLock, WorkflowSnapshot } from 'awaitly/workflow';
7
4
 
8
5
  /**
9
6
  * MongoDB workflow lock (lease) for cross-process concurrency control.
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
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};
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 g(){(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 g();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:g}}function $(m){let o=typeof m=="string"?{url:m}:m,s=o.prefix??"",l=o.database,g=o.url.match(/mongodb(?:\+srv)?:\/\/[^/]+\/([^?]+)/);!l&&g&&g[1]&&(l=g[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 w=(await r()).collection(f),k=s+t,u=M(e)?D(e):e;await w.updateOne({_id:k},{$set:{snapshot:u,updatedAt:new Date}},{upsert:!0})},async load(t){let p=(await r()).collection(f),w=s+t,k=await p.findOne({_id:w});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),w=s+t;await p.deleteOne({_id:w})},async list(t){let p=(await r()).collection(f),w=s+(t?.prefix??""),k=t?.limit??100,u=w.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\"]; 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"]}
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/workflow\";\nimport type { WorkflowLock } from \"awaitly/workflow\";\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/workflow\";\nexport type { WorkflowLock } from \"awaitly/workflow\";\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": "24.0.0",
3
+ "version": "26.0.0",
4
4
  "type": "module",
5
5
  "description": "MongoDB persistence adapter for awaitly workflows",
6
6
  "main": "./dist/index.cjs",
@@ -38,19 +38,19 @@
38
38
  },
39
39
  "license": "MIT",
40
40
  "peerDependencies": {
41
- "awaitly": "^1.34.0"
41
+ "awaitly": "^2.0.0"
42
42
  },
43
43
  "dependencies": {
44
- "mongodb": "^7.2.0"
44
+ "mongodb": "7.5.0"
45
45
  },
46
46
  "devDependencies": {
47
- "@total-typescript/ts-reset": "^0.6.1",
48
- "@total-typescript/tsconfig": "^1.0.4",
49
- "@types/node": "^25.6.0",
50
- "tsup": "^8.5.1",
51
- "typescript": "^5.9.3",
52
- "vitest": "^4.1.7",
53
- "awaitly": "^1.34.0"
47
+ "@total-typescript/ts-reset": "0.6.1",
48
+ "@total-typescript/tsconfig": "1.0.4",
49
+ "@types/node": "25.9.3",
50
+ "tsup": "8.5.1",
51
+ "typescript": "5.9.3",
52
+ "vitest": "4.1.10",
53
+ "awaitly": "^2.0.0"
54
54
  },
55
55
  "publishConfig": {
56
56
  "access": "public",