mongo-locks 3.0.1 → 3.1.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
@@ -5,6 +5,10 @@ Simple and bountiful locks to avoid doing the same operation multiple times
5
5
  The purpose of this package is simply to avoid doing the same thing twice at the same time
6
6
  and make your system a bit more user-proof.
7
7
 
8
+ ```bash
9
+ npm install mongo-locks
10
+ ```
11
+
8
12
  ```ts
9
13
  import { LockManager } from "mongo-locks";
10
14
  import { MongoClient } from "mongodb";
@@ -13,9 +17,9 @@ const client = new MongoClient("mongodb://localhost:27017");
13
17
  const lockManager = new LockManager(client.db().collection("mongo-locks"));
14
18
 
15
19
  async function doStuff() {
16
- await using lock = lockManager.lock("unique string key");
20
+ await using lock = await lockManager.lock("unique string key");
17
21
 
18
- if (!lock.locked) {
22
+ if (!lock) {
19
23
  console.log("Lock already taken");
20
24
  return;
21
25
  }
@@ -34,7 +38,7 @@ If you don't want to use the `using` syntax, you can do it this way:
34
38
  async function doStuff() {
35
39
  const lock = await lockManager.lock("unique string key");
36
40
 
37
- if (!lock.locked) {
41
+ if (!lock) {
38
42
  console.log("Lock already taken");
39
43
  return;
40
44
  }
@@ -22,11 +22,11 @@ const makeKey = (key) => {
22
22
  return Array.isArray(key) ? { _key_array: key } : key;
23
23
  };
24
24
  class Lock {
25
- constructor(_id, action, collection, locked) {
25
+ constructor(_id, action, collection) {
26
26
  this._id = _id;
27
27
  this.action = action;
28
28
  this.collection = collection;
29
- this.locked = locked;
29
+ this.locked = true;
30
30
  if (this.locked) {
31
31
  (() => __awaiter(this, void 0, void 0, function* () {
32
32
  while (this.locked) {
@@ -48,6 +48,7 @@ class Lock {
48
48
  if (!this.locked) {
49
49
  return false;
50
50
  }
51
+ this.locked = false;
51
52
  const result = yield this.collection.deleteOne({ _id: this._id });
52
53
  return result.deletedCount === 1;
53
54
  });
@@ -103,11 +104,11 @@ class LockManager {
103
104
  refreshedAt: new Date(),
104
105
  expiresAt: new Date(Date.now() + 60000),
105
106
  });
106
- return new Lock(id, lockId, this.collection, true);
107
+ return new Lock(id, lockId, this.collection);
107
108
  }
108
109
  catch (err) {
109
110
  if (err instanceof Error && "code" in err && err.code === 11000) {
110
- return new Lock(id, lockId, this.collection, false);
111
+ return null;
111
112
  }
112
113
  throw err;
113
114
  }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,113 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { ObjectId } from "mongodb";
11
+ export class MongoLocksError extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ }
15
+ }
16
+ const makeKey = (key) => {
17
+ // Arrays mess up the unique MongoDB indexes
18
+ return Array.isArray(key) ? { _key_array: key } : key;
19
+ };
20
+ class Lock {
21
+ constructor(_id, action, collection) {
22
+ this._id = _id;
23
+ this.action = action;
24
+ this.collection = collection;
25
+ this.locked = true;
26
+ if (this.locked) {
27
+ (() => __awaiter(this, void 0, void 0, function* () {
28
+ while (this.locked) {
29
+ yield new Promise((resolve) => setTimeout(resolve, 10000));
30
+ yield this.refresh().catch(() => {
31
+ console.error("Failed to refresh lock", action);
32
+ });
33
+ }
34
+ }))();
35
+ }
36
+ }
37
+ /**
38
+ * Instead of calling `this.free` directly you can use the `using` syntax
39
+ *
40
+ * @returns true if the lock was successfully released. False if the lock was not found
41
+ */
42
+ free() {
43
+ return __awaiter(this, void 0, void 0, function* () {
44
+ if (!this.locked) {
45
+ return false;
46
+ }
47
+ this.locked = false;
48
+ const result = yield this.collection.deleteOne({ _id: this._id });
49
+ return result.deletedCount === 1;
50
+ });
51
+ }
52
+ /**
53
+ * Refreshes the lock's TTL
54
+ *
55
+ * Automatically called every 10 seconds if the lock is still active, no
56
+ * need to call this manually
57
+ *
58
+ * @returns true if the lock was successfully refreshed. False if the lock was not found
59
+ */
60
+ refresh() {
61
+ return __awaiter(this, void 0, void 0, function* () {
62
+ if (!this.locked) {
63
+ return false;
64
+ }
65
+ const res = yield this.collection.updateOne({ _id: this._id }, { $set: { refreshedAt: new Date(), expiresAt: new Date(Date.now() + 60000) } });
66
+ this.locked = res.matchedCount === 1;
67
+ return this.locked;
68
+ });
69
+ }
70
+ [Symbol.dispose]() {
71
+ this.free();
72
+ }
73
+ [Symbol.asyncDispose]() {
74
+ return __awaiter(this, void 0, void 0, function* () {
75
+ return this.free();
76
+ });
77
+ }
78
+ }
79
+ export class LockManager {
80
+ constructor(collection) {
81
+ this.MongoLocksError = MongoLocksError;
82
+ this.collection = collection;
83
+ collection.createIndex({ action: 1 }, { unique: true });
84
+ collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
85
+ }
86
+ /**
87
+ * Creates a DB lock based on the key provided
88
+ *
89
+ * @returns a function that when called will release the lock
90
+ */
91
+ lock(key) {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ const lockId = makeKey(key);
94
+ const id = new ObjectId();
95
+ try {
96
+ yield this.collection.insertOne({
97
+ _id: id,
98
+ action: lockId,
99
+ createdAt: new Date(),
100
+ refreshedAt: new Date(),
101
+ expiresAt: new Date(Date.now() + 60000),
102
+ });
103
+ return new Lock(id, lockId, this.collection);
104
+ }
105
+ catch (err) {
106
+ if (err instanceof Error && "code" in err && err.code === 11000) {
107
+ return null;
108
+ }
109
+ throw err;
110
+ }
111
+ });
112
+ }
113
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "mongo-locks",
3
- "version": "3.0.1",
3
+ "type": "module",
4
+ "version": "3.1.0",
4
5
  "description": "Simple and bountiful locks to avoid doing the same operation multiple times",
5
- "main": "dist/index.js",
6
+ "main": "./dist/commonjs/index.js",
6
7
  "scripts": {
7
8
  "test": "echo \"Error: no test specified\" && exit 1",
8
- "prepublishOnly": "npm run clean && tsc",
9
- "clean": "rm -Rf dist"
9
+ "prepare": "tshy"
10
10
  },
11
11
  "repository": {
12
12
  "type": "git",
@@ -29,8 +29,9 @@
29
29
  "mongodb": ">=4"
30
30
  },
31
31
  "devDependencies": {
32
- "@types/node": "^20.12.7",
32
+ "@types/node": "^22.12.7",
33
33
  "mongodb": "^6.5.0",
34
+ "tshy": "^3.1.0",
34
35
  "typescript": "^5.4.5"
35
36
  },
36
37
  "files": [
@@ -38,5 +39,26 @@
38
39
  "index.d.ts",
39
40
  "index.ts",
40
41
  "tsconfig.json"
41
- ]
42
+ ],
43
+ "tshy": {
44
+ "exports": {
45
+ "./package.json": "./package.json",
46
+ ".": "./src/index.ts"
47
+ }
48
+ },
49
+ "exports": {
50
+ "./package.json": "./package.json",
51
+ ".": {
52
+ "import": {
53
+ "types": "./dist/esm/index.d.ts",
54
+ "default": "./dist/esm/index.js"
55
+ },
56
+ "require": {
57
+ "types": "./dist/commonjs/index.d.ts",
58
+ "default": "./dist/commonjs/index.js"
59
+ }
60
+ }
61
+ },
62
+ "types": "./dist/commonjs/index.d.ts",
63
+ "module": "./dist/esm/index.js"
42
64
  }
package/index.ts DELETED
@@ -1,127 +0,0 @@
1
- import { Collection, ObjectId } from "mongodb";
2
-
3
- export class MongoLocksError extends Error {
4
- constructor(message?: string) {
5
- super(message);
6
- }
7
- }
8
-
9
- const makeKey = (key: unknown) => {
10
- // Arrays mess up the unique MongoDB indexes
11
- return Array.isArray(key) ? { _key_array: key } : key;
12
- };
13
-
14
- interface LockModel {
15
- _id: ObjectId;
16
- action: unknown;
17
- createdAt: Date;
18
- refreshedAt: Date;
19
- expiresAt: Date;
20
- }
21
-
22
- class Lock {
23
- constructor(
24
- public _id: ObjectId,
25
- public action: unknown,
26
- public collection: Collection<LockModel>,
27
- public locked: boolean
28
- ) {
29
- if (this.locked) {
30
- (async () => {
31
- while (this.locked) {
32
- await new Promise((resolve) => setTimeout(resolve, 10_000));
33
- await this.refresh().catch(() => {
34
- console.error("Failed to refresh lock", action);
35
- });
36
- }
37
- })();
38
- }
39
- }
40
-
41
- /**
42
- * Instead of calling `this.free` directly you can use the `using` syntax
43
- *
44
- * @returns true if the lock was successfully released. False if the lock was not found
45
- */
46
- async free(): Promise<boolean> {
47
- if (!this.locked) {
48
- return false;
49
- }
50
-
51
- const result = await this.collection.deleteOne({ _id: this._id });
52
- return result.deletedCount === 1;
53
- }
54
-
55
- /**
56
- * Refreshes the lock's TTL
57
- *
58
- * Automatically called every 10 seconds if the lock is still active, no
59
- * need to call this manually
60
- *
61
- * @returns true if the lock was successfully refreshed. False if the lock was not found
62
- */
63
- async refresh(): Promise<boolean> {
64
- if (!this.locked) {
65
- return false;
66
- }
67
-
68
- const res = await this.collection.updateOne(
69
- { _id: this._id },
70
- { $set: { refreshedAt: new Date(), expiresAt: new Date(Date.now() + 60_000) } }
71
- );
72
-
73
- this.locked = res.matchedCount === 1;
74
-
75
- return this.locked;
76
- }
77
-
78
- [Symbol.dispose]() {
79
- this.free();
80
- }
81
-
82
- async [Symbol.asyncDispose]() {
83
- return this.free();
84
- }
85
- }
86
-
87
- export class LockManager {
88
- collection: Collection<LockModel>;
89
- MongoLocksError = MongoLocksError;
90
-
91
- constructor(collection: Collection) {
92
- this.collection = collection as unknown as Collection<LockModel>;
93
-
94
- collection.createIndex({ action: 1 }, { unique: true });
95
- collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
96
- }
97
-
98
- /**
99
- * Creates a DB lock based on the key provided
100
- *
101
- * @returns a function that when called will release the lock
102
- */
103
- async lock(key: unknown): Promise<Lock> {
104
- const lockId = makeKey(key);
105
- const id = new ObjectId();
106
-
107
- try {
108
- await this.collection.insertOne({
109
- _id: id,
110
- action: lockId,
111
- createdAt: new Date(),
112
- refreshedAt: new Date(),
113
- expiresAt: new Date(Date.now() + 60_000),
114
- });
115
-
116
- return new Lock(id, lockId, this.collection, true);
117
- } catch (err) {
118
- if (err instanceof Error && "code" in err && err.code === 11000) {
119
- return new Lock(id, lockId, this.collection, false);
120
- }
121
-
122
- throw err;
123
- }
124
- }
125
- }
126
-
127
- export type { LockModel, Lock };