mongo-locks 3.1.0 → 3.1.1

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.
Files changed (2) hide show
  1. package/package.json +2 -3
  2. package/src/index.ts +125 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongo-locks",
3
3
  "type": "module",
4
- "version": "3.1.0",
4
+ "version": "3.1.1",
5
5
  "description": "Simple and bountiful locks to avoid doing the same operation multiple times",
6
6
  "main": "./dist/commonjs/index.js",
7
7
  "scripts": {
@@ -36,8 +36,7 @@
36
36
  },
37
37
  "files": [
38
38
  "dist",
39
- "index.d.ts",
40
- "index.ts",
39
+ "src",
41
40
  "tsconfig.json"
42
41
  ],
43
42
  "tshy": {
package/src/index.ts ADDED
@@ -0,0 +1,125 @@
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(public _id: ObjectId, public action: unknown, public collection: Collection<LockModel>) {
24
+ if (this.locked) {
25
+ (async () => {
26
+ while (this.locked) {
27
+ await new Promise((resolve) => setTimeout(resolve, 10_000));
28
+ await this.refresh().catch(() => {
29
+ console.error("Failed to refresh lock", action);
30
+ });
31
+ }
32
+ })();
33
+ }
34
+ }
35
+
36
+ locked = true;
37
+
38
+ /**
39
+ * Instead of calling `this.free` directly you can use the `using` syntax
40
+ *
41
+ * @returns true if the lock was successfully released. False if the lock was not found
42
+ */
43
+ async free(): Promise<boolean> {
44
+ if (!this.locked) {
45
+ return false;
46
+ }
47
+ this.locked = false;
48
+
49
+ const result = await this.collection.deleteOne({ _id: this._id });
50
+ return result.deletedCount === 1;
51
+ }
52
+
53
+ /**
54
+ * Refreshes the lock's TTL
55
+ *
56
+ * Automatically called every 10 seconds if the lock is still active, no
57
+ * need to call this manually
58
+ *
59
+ * @returns true if the lock was successfully refreshed. False if the lock was not found
60
+ */
61
+ async refresh(): Promise<boolean> {
62
+ if (!this.locked) {
63
+ return false;
64
+ }
65
+
66
+ const res = await this.collection.updateOne(
67
+ { _id: this._id },
68
+ { $set: { refreshedAt: new Date(), expiresAt: new Date(Date.now() + 60_000) } }
69
+ );
70
+
71
+ this.locked = res.matchedCount === 1;
72
+
73
+ return this.locked;
74
+ }
75
+
76
+ [Symbol.dispose]() {
77
+ this.free();
78
+ }
79
+
80
+ async [Symbol.asyncDispose]() {
81
+ return this.free();
82
+ }
83
+ }
84
+
85
+ export class LockManager {
86
+ collection: Collection<LockModel>;
87
+ MongoLocksError = MongoLocksError;
88
+
89
+ constructor(collection: Collection) {
90
+ this.collection = collection as unknown as Collection<LockModel>;
91
+
92
+ collection.createIndex({ action: 1 }, { unique: true });
93
+ collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
94
+ }
95
+
96
+ /**
97
+ * Creates a DB lock based on the key provided
98
+ *
99
+ * @returns a function that when called will release the lock
100
+ */
101
+ async lock(key: unknown): Promise<Lock | null> {
102
+ const lockId = makeKey(key);
103
+ const id = new ObjectId();
104
+
105
+ try {
106
+ await this.collection.insertOne({
107
+ _id: id,
108
+ action: lockId,
109
+ createdAt: new Date(),
110
+ refreshedAt: new Date(),
111
+ expiresAt: new Date(Date.now() + 60_000),
112
+ });
113
+
114
+ return new Lock(id, lockId, this.collection);
115
+ } catch (err) {
116
+ if (err instanceof Error && "code" in err && err.code === 11000) {
117
+ return null;
118
+ }
119
+
120
+ throw err;
121
+ }
122
+ }
123
+ }
124
+
125
+ export type { LockModel, Lock };