@chriscdn/promise-semaphore 1.0.9 → 2.0.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.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021 Christopher Meyer
3
+ Copyright (c) 2021-2023 Christopher Meyer
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -16,16 +16,20 @@ Using yarn:
16
16
  $ yarn add @chriscdn/promise-semaphore
17
17
  ```
18
18
 
19
+ ## Updating v1 to v2
20
+
21
+ Version 2 adds TypeScript and better inline documentation. The API remains the same, and doesn't introduce any breaking changes.
22
+
19
23
  ## API
20
24
 
21
25
  ### Create an instance
22
26
 
23
27
  ```js
24
- const Semaphore = require('@chriscdn/promise-semaphore')
28
+ import Semaphore from '@chriscdn/promise-semaphore'
25
29
  const semaphore = new Semaphore([maxConcurrent])
26
30
  ```
27
31
 
28
- The `maxConcurrent` parameter is optional, and defaults to `1` (making it an exclusive lock or *binary semaphore*). Use an integer value greater than one to limit how many times the code block can be simultaneously executing from separate iterations of the event loop.
32
+ The `maxConcurrent` parameter is optional, and defaults to `1` (making it an exclusive lock or _binary semaphore_). Use an integer value greater than one to limit how many times the code block can be simultaneously executing from separate iterations of the event loop.
29
33
 
30
34
  ### Acquire a lock
31
35
 
@@ -33,7 +37,7 @@ The `maxConcurrent` parameter is optional, and defaults to `1` (making it an exc
33
37
  semaphore.acquire([key])
34
38
  ```
35
39
 
36
- This returns a `Promise`, which resolves once a lock has been acquired. The `key` parameter is optional and permits the same `Semaphore` instance to be used in different contexts. See the second example.
40
+ This returns a `Promise`, which resolves once a lock has been acquired. The `key` parameter is optional and permits the same `Semaphore` instance to be used in different contexts. See the second example.
37
41
 
38
42
  ### Release a lock
39
43
 
@@ -57,14 +61,14 @@ This method is synchronous, and returns `true` if a lock can be immediately acqu
57
61
  const results = await semaphore.request(fn [,key])
58
62
  ```
59
63
 
60
- This function reduces boilerplate when using `acquire` and `release`. It returns a promise, which resolves once `fn` has completed. It is functionally equivalent to:
64
+ This function reduces boilerplate when using `acquire` and `release`. It returns a promise, which resolves once `fn` has completed. It is functionally equivalent to:
61
65
 
62
66
  ```js
63
67
  try {
64
- await semaphore.acquire([key])
65
- const results = await fn()
68
+ await semaphore.acquire([key])
69
+ const results = await fn()
66
70
  } finally {
67
- semaphore.release([key])
71
+ semaphore.release([key])
68
72
  }
69
73
  ```
70
74
 
@@ -84,99 +88,88 @@ const results = semaphore.canAcquire([key] ?
84
88
  null
85
89
  ```
86
90
 
87
- This is useful in situations where only one instance of a function block should run at a time, while discarding other attempts to execute the block. E.g., a button that is being repeatedly tapped or clicked by the user.
91
+ This is useful in situations where only one instance of a function block should run at a time, while discarding other attempts to execute the block. E.g., a button is repeatedly clicked.
88
92
 
89
93
  ## Example 1
90
94
 
91
95
  ```js
92
- const Semaphore = require('@chriscdn/promise-semaphore')
96
+ import Semaphore from '@chriscdn/promise-semaphore'
93
97
  const semaphore = new Semaphore()
94
98
 
95
99
  // using promises
96
- semaphore.acquire()
97
- .then(() => {
98
- // This block executes once a lock has been acquired. If already
99
- // locked then this block will wait and execute once all preceeding
100
- // locks have been released.
101
-
102
- // do your critical stuff here
103
-
104
- })
105
- .finally(() => {
106
- // release the lock permitting the next queued block to continue
107
- semaphore.release()
108
- })
109
-
100
+ semaphore
101
+ .acquire()
102
+ .then(() => {
103
+ // This block executes once a lock is acquired. If already locked,
104
+ // then wait and execute once all preceeding locks have been released.
105
+ //
106
+ // do your critical stuff here
107
+ })
108
+ .finally(() => {
109
+ // release the lock permitting the next queued block to continue
110
+ semaphore.release()
111
+ })
110
112
 
111
113
  // or, using async/await
112
114
  try {
113
- await semaphore.acquire()
114
-
115
- // do your critical stuff here
116
-
115
+ await semaphore.acquire()
116
+
117
+ // do your critical stuff here
117
118
  } finally {
118
- semaphore.release()
119
+ semaphore.release()
119
120
  }
120
121
 
121
-
122
122
  // or, using the request function
123
123
  semaphore.request(() => {
124
-
125
- // do your critical stuff here
126
-
124
+ // do your critical stuff here
127
125
  })
128
-
129
126
  ```
130
127
 
131
128
  ## Example 2
132
129
 
133
- Say you have an asynchronous function to download a file and save it to disk
130
+ Say you have an asynchronous function to download a file and save it to disk:
134
131
 
135
132
  ```js
136
133
  async function downloadAndSave(url) {
134
+ const filePath = urlToFilePath(url)
137
135
 
138
- const filePath = urlToFilePath(url)
136
+ if (await pathExists(filePath)) {
137
+ // the file is on disk, so no action is required
138
+ } else {
139
+ await downloadAndSaveToFilepath(url, filePath)
140
+ }
139
141
 
140
- if (await pathExists(filePath)) {
141
- // the file is on disk, so no action is required
142
- } else {
143
- await downloadToFile(url, filePath)
144
- }
145
-
146
- return filePath
142
+ return filePath
147
143
  }
148
144
  ```
149
145
 
150
- This works until a process calls `downloadAndSave()` in short succession with the same `url` parameter. This can cause multiple simultaneous downloads that attempt to write to the same file.
146
+ This works until a process calls `downloadAndSave()` multiple times in short succession with the same `url`. This can cause multiple simultaneous downloads that attempt to write to the same file at the same time.
151
147
 
152
148
  This can be resolved with a `Semaphore` instance using the `key` parameter:
153
149
 
154
150
  ```js
155
- const Semaphore = require('@chriscdn/promise-semaphore')
151
+ import Semaphore from '@chriscdn/promise-semaphore'
156
152
  const semaphore = new Semaphore()
157
153
 
158
154
  async function downloadAndSave(url) {
155
+ try {
156
+ await semaphore.acquire(url)
159
157
 
160
- try {
161
-
162
- await semaphore.acquire(url)
158
+ // This block continues once a lock on url is acquired. This
159
+ // permits multiple simulataneous downloads for different urls.
163
160
 
164
- // This block continues once a lock on url is acquired. This permits
165
- // multiple simulataneous downloads for each unique url.
166
-
167
- const filePath = urlToFilePath(url)
168
-
169
- if (await pathExists(filePath)) {
170
- // the file is on disk, so no action is required
171
- } else {
172
- await downloadToFile(url, filePath)
173
- }
174
-
175
- return filePath
161
+ const filePath = urlToFilePath(url)
176
162
 
177
- } finally {
178
- semaphore.release(url)
179
- }
163
+ if (await pathExists(filePath)) {
164
+ // the file is on disk, so no action is required
165
+ } else {
166
+ await downloadAndSaveToFilepath(url, filePath)
167
+ }
168
+
169
+ return filePath
170
+ } finally {
171
+ semaphore.release(url)
172
+ }
180
173
  }
181
174
  ```
182
175
 
@@ -187,14 +180,14 @@ async function downloadAndSave(url) {
187
180
 
188
181
  return semaphore.request(() => {
189
182
  const filePath = urlToFilePath(url)
190
-
183
+
191
184
  if (await pathExists(filePath)) {
192
185
  // the file is on disk, so no action is required
193
186
  } else {
194
- await downloadToFile(url, filePath)
187
+ await downloadAndSaveToFilepath(url, filePath)
195
188
  }
196
-
197
- return filePath
189
+
190
+ return filePath
198
191
  }, url)
199
192
 
200
193
  }
@@ -202,4 +195,4 @@ async function downloadAndSave(url) {
202
195
 
203
196
  ## License
204
197
 
205
- [MIT](LICENSE)
198
+ [MIT](LICENSE)
@@ -0,0 +1,143 @@
1
+ import Semaphore from '../src/index'
2
+
3
+ const pause = async (ms) => new Promise((resolve) => setTimeout(resolve, ms))
4
+
5
+ test('Acquire & Release Basic', async () => {
6
+ const semaphore = new Semaphore()
7
+ expect(semaphore.canAcquire()).toBe(true)
8
+ await semaphore.acquire()
9
+ expect(semaphore.canAcquire()).toBe(false)
10
+ expect(semaphore.hasTasks()).toBe(true)
11
+ expect(semaphore.count()).toBe(1)
12
+ semaphore.release()
13
+ expect(semaphore.canAcquire()).toBe(true)
14
+ expect(semaphore.count()).toBe(0)
15
+ })
16
+
17
+ test('Semaphore 1', async () => {
18
+ const semaphore = new Semaphore()
19
+
20
+ semaphore
21
+ .acquire()
22
+ .then(async () => await pause(1000))
23
+ .finally(() => semaphore.release())
24
+
25
+ expect(semaphore.canAcquire()).toBe(false)
26
+
27
+ await pause(600)
28
+
29
+ expect(semaphore.canAcquire()).toBe(false)
30
+
31
+ await pause(600)
32
+
33
+ expect(semaphore.canAcquire()).toBe(true)
34
+ })
35
+
36
+ test('Semaphore 2', async () => {
37
+ const semaphore = new Semaphore()
38
+
39
+ let tester = 0
40
+
41
+ semaphore
42
+ .acquire()
43
+ .then(() => pause(1000))
44
+ .then(() => (tester = 10))
45
+ .finally(() => semaphore.release())
46
+
47
+ // tests acquire waits for previous to complete
48
+ await semaphore
49
+ .acquire()
50
+ .then(() => expect(tester).toBe(10))
51
+ .finally(() => semaphore.release())
52
+ })
53
+
54
+ test('Semaphore 3', async () => {
55
+ const semaphore = new Semaphore(2)
56
+
57
+ let tester = 0
58
+
59
+ semaphore
60
+ .acquire()
61
+ .then(() => pause(1000))
62
+ .then(() => (tester = 20))
63
+ .finally(() => semaphore.release())
64
+
65
+ semaphore
66
+ .acquire()
67
+ .then(() => pause(500))
68
+ .then(() => (tester = 10))
69
+ .finally(() => semaphore.release())
70
+
71
+ expect(semaphore.count()).toBe(2)
72
+ expect(semaphore.hasTasks()).toBe(true)
73
+
74
+ // expect 10 since this next block will run before the first has completed
75
+ await semaphore
76
+ .acquire()
77
+ .then(() => expect(tester).toBe(10))
78
+ .finally(() => semaphore.release())
79
+
80
+ expect(semaphore.count()).toBe(1)
81
+ expect(semaphore.hasTasks()).toBe(true)
82
+ expect(semaphore.canAcquire()).toBe(true)
83
+ })
84
+
85
+ test('Request 1', async () => {
86
+ const semaphore = new Semaphore()
87
+
88
+ let tester = 0
89
+
90
+ semaphore.request(async () => {
91
+ await pause(1000)
92
+ tester = 20
93
+ })
94
+
95
+ semaphore.request(async () => {
96
+ await pause(500)
97
+ tester = 10
98
+ })
99
+
100
+ await semaphore.request(async () => {
101
+ expect(tester).toBe(10)
102
+ })
103
+ })
104
+
105
+ test('Request 2', async () => {
106
+ const semaphore = new Semaphore(2)
107
+
108
+ let tester = 0
109
+
110
+ semaphore.request(async () => {
111
+ await pause(1000)
112
+ tester = 20
113
+ })
114
+
115
+ semaphore.request(async () => {
116
+ await pause(500)
117
+ tester = 10
118
+ })
119
+
120
+ await semaphore.request(async () => {
121
+ expect(tester).toBe(10)
122
+ })
123
+ })
124
+
125
+ test('Request 3', async () => {
126
+ const semaphore = new Semaphore(3)
127
+
128
+ let tester = 0
129
+
130
+ semaphore.request(async () => {
131
+ await pause(1000)
132
+ tester = 20
133
+ })
134
+
135
+ semaphore.request(async () => {
136
+ await pause(500)
137
+ tester = 10
138
+ })
139
+
140
+ await semaphore.request(async () => {
141
+ expect(tester).toBe(0)
142
+ })
143
+ })
package/jest.config.js ADDED
@@ -0,0 +1,5 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'node',
5
+ };
package/lib/index.cjs.js CHANGED
@@ -1,101 +1,139 @@
1
1
  'use strict';
2
2
 
3
3
  class SemaphoreItem {
4
- constructor(max) {
4
+ queue;
5
+ maxConcurrent;
6
+ /**
7
+ * The number of locks.
8
+ */
9
+ count;
10
+ constructor(maxConcurrent) {
5
11
  this.queue = [];
6
- this.max = max;
12
+ this.maxConcurrent = maxConcurrent;
7
13
  this.count = 0;
8
14
  }
9
-
10
15
  get canAcquire() {
11
- return this.count < this.max
16
+ return this.count < this.maxConcurrent;
12
17
  }
13
-
14
18
  acquire() {
15
19
  if (this.canAcquire) {
16
20
  this.count++;
17
- return Promise.resolve()
21
+ return Promise.resolve();
18
22
  } else {
19
- return new Promise((resolve) => {
20
- this.queue.push(resolve);
21
- })
23
+ return new Promise((resolve) => this.queue.push(resolve));
22
24
  }
23
25
  }
24
-
25
26
  release() {
26
27
  const resolveFunc = this.queue.shift();
27
-
28
28
  if (resolveFunc) {
29
- // Give the micro task queue a small break instead of calling resolveFunc() directly
30
29
  setTimeout(resolveFunc, 0);
31
30
  } else {
32
31
  this.count--;
33
32
  }
34
33
  }
35
34
  }
36
-
37
- const defaultKey = '_default';
38
-
35
+ const defaultKey = "_default";
39
36
  class Semaphore {
40
- constructor(max = 1) {
41
- this.semaphoreItems = {};
42
- this.max = max;
37
+ semaphoreInstances;
38
+ maxConcurrent;
39
+ /**
40
+ *
41
+ * @param {number} [maxConcurrent] The maximum number of concurrent locks.
42
+ */
43
+ constructor(maxConcurrent = 1) {
44
+ this.semaphoreInstances = {};
45
+ this.maxConcurrent = maxConcurrent;
43
46
  }
44
-
45
- _getSemaphoreInstance(key = defaultKey) {
46
- if (!this.semaphoreItems[key]) {
47
- this.semaphoreItems[key] = new SemaphoreItem(this.max);
47
+ hasSemaphoreInstance(key = defaultKey) {
48
+ return Boolean(this.semaphoreInstances[key]);
49
+ }
50
+ getSemaphoreInstance(key = defaultKey) {
51
+ if (!this.hasSemaphoreInstance(key)) {
52
+ this.semaphoreInstances[key] = new SemaphoreItem(this.maxConcurrent);
48
53
  }
49
- return this.semaphoreItems[key]
54
+ return this.semaphoreInstances[key];
50
55
  }
51
-
52
- _tidy(key = defaultKey) {
53
- if (this._getSemaphoreInstance(key).count == 0) {
54
- delete this.semaphoreItems[key];
56
+ /**
57
+ *
58
+ * @param {string | number} [key]- Optional, the semaphore key.
59
+ */
60
+ tidy(key = defaultKey) {
61
+ if (this.hasSemaphoreInstance(key) && this.getSemaphoreInstance(key).count == 0) {
62
+ delete this.semaphoreInstances[key];
55
63
  }
56
64
  }
57
-
65
+ /**
66
+ * A synchronous function to determine whether a lock can be acquired.
67
+ *
68
+ * @param {string | number} [key]- Optional, the semaphore key.
69
+ * @returns {boolean} Returns true if the lock on `key` can be acquired, false
70
+ * otherwise.
71
+ */
58
72
  canAcquire(key = defaultKey) {
59
- return this._getSemaphoreInstance(key).canAcquire
73
+ return this.getSemaphoreInstance(key).canAcquire;
60
74
  }
61
-
75
+ /**
76
+ *
77
+ * @param {string | number} [key]- Optional, the semaphore key.
78
+ */
62
79
  acquire(key = defaultKey) {
63
- return this._getSemaphoreInstance(key).acquire()
80
+ return this.getSemaphoreInstance(key).acquire();
64
81
  }
65
-
82
+ /**
83
+ *
84
+ * @param {string | number} [key]- Optional, the semaphore key.
85
+ */
66
86
  release(key = defaultKey) {
67
- this._getSemaphoreInstance(key).release();
68
- this._tidy(key);
87
+ this.getSemaphoreInstance(key).release();
88
+ this.tidy(key);
69
89
  }
70
-
90
+ /**
91
+ * The number of active locks. Will always be less or equal to `max`.
92
+ *
93
+ * @param {string | number} [key]- Optional, the semaphore key.
94
+ */
71
95
  count(key = defaultKey) {
72
- if (this.semaphoreItems[key]) {
73
- return this.semaphoreItems[key].count
96
+ if (this.hasSemaphoreInstance(key)) {
97
+ return this.getSemaphoreInstance(key).count;
74
98
  } else {
75
- return 0
99
+ return 0;
76
100
  }
77
101
  }
78
-
102
+ /**
103
+ *
104
+ * @param {string | number} [key]- Optional, the semaphore key.
105
+ * @returns {boolean} True if the semaphore and key has locks, false otherwise.
106
+ */
79
107
  hasTasks(key = defaultKey) {
80
- return this.count(key) > 0
108
+ return this.count(key) > 0;
81
109
  }
82
-
110
+ /**
111
+ *
112
+ * @param {Function<T>} fn The function to execute.
113
+ * @param {string | number} [key]- Optional, the semaphore key.
114
+ * @returns {Promise<T>}
115
+ */
83
116
  async request(fn, key = defaultKey) {
84
117
  try {
85
118
  await this.acquire(key);
86
- return await fn()
119
+ return await fn();
87
120
  } finally {
88
121
  this.release(key);
89
122
  }
90
123
  }
91
-
124
+ /**
125
+ * Asynchronously executes `fn` if a lock can be immediately acquired.
126
+ * Otherwise, returns null.
127
+ *
128
+ * @param {Function<T>} fn The function to execute.
129
+ * @param {string | number} [key]- Optional, the semaphore key.
130
+ * @returns {Promise<T>}
131
+ */
92
132
  async requestIfAvailable(fn, key = defaultKey) {
93
133
  if (this.canAcquire(key)) {
94
- return this.request(fn, key)
134
+ return this.request(fn, key);
95
135
  } else {
96
- // Use canAcquire if you need to know if a function will be dismissed due
97
- // to an existing lock.
98
- return null
136
+ return null;
99
137
  }
100
138
  }
101
139
  }
package/lib/index.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ declare class Semaphore {
2
+ private semaphoreInstances;
3
+ private maxConcurrent;
4
+ /**
5
+ *
6
+ * @param {number} [maxConcurrent] The maximum number of concurrent locks.
7
+ */
8
+ constructor(maxConcurrent?: number);
9
+ private hasSemaphoreInstance;
10
+ private getSemaphoreInstance;
11
+ /**
12
+ *
13
+ * @param {string | number} [key]- Optional, the semaphore key.
14
+ */
15
+ private tidy;
16
+ /**
17
+ * A synchronous function to determine whether a lock can be acquired.
18
+ *
19
+ * @param {string | number} [key]- Optional, the semaphore key.
20
+ * @returns {boolean} Returns true if the lock on `key` can be acquired, false
21
+ * otherwise.
22
+ */
23
+ canAcquire(key?: string | number): boolean;
24
+ /**
25
+ *
26
+ * @param {string | number} [key]- Optional, the semaphore key.
27
+ */
28
+ acquire(key?: string | number): Promise<void>;
29
+ /**
30
+ *
31
+ * @param {string | number} [key]- Optional, the semaphore key.
32
+ */
33
+ release(key?: string | number): void;
34
+ /**
35
+ * The number of active locks. Will always be less or equal to `max`.
36
+ *
37
+ * @param {string | number} [key]- Optional, the semaphore key.
38
+ */
39
+ count(key?: string | number): number;
40
+ /**
41
+ *
42
+ * @param {string | number} [key]- Optional, the semaphore key.
43
+ * @returns {boolean} True if the semaphore and key has locks, false otherwise.
44
+ */
45
+ hasTasks(key?: string | number): boolean;
46
+ /**
47
+ *
48
+ * @param {Function<T>} fn The function to execute.
49
+ * @param {string | number} [key]- Optional, the semaphore key.
50
+ * @returns {Promise<T>}
51
+ */
52
+ request<T>(fn: Function, key?: string | number): Promise<T>;
53
+ /**
54
+ * Asynchronously executes `fn` if a lock can be immediately acquired.
55
+ * Otherwise, returns null.
56
+ *
57
+ * @param {Function<T>} fn The function to execute.
58
+ * @param {string | number} [key]- Optional, the semaphore key.
59
+ * @returns {Promise<T>}
60
+ */
61
+ requestIfAvailable<T>(fn: Function, key?: string | number): Promise<T | null>;
62
+ }
63
+
64
+ export { Semaphore as default };
package/lib/index.es.js CHANGED
@@ -1,103 +1,139 @@
1
1
  class SemaphoreItem {
2
- constructor(max) {
2
+ queue;
3
+ maxConcurrent;
4
+ /**
5
+ * The number of locks.
6
+ */
7
+ count;
8
+ constructor(maxConcurrent) {
3
9
  this.queue = [];
4
- this.max = max;
10
+ this.maxConcurrent = maxConcurrent;
5
11
  this.count = 0;
6
12
  }
7
-
8
13
  get canAcquire() {
9
- return this.count < this.max
14
+ return this.count < this.maxConcurrent;
10
15
  }
11
-
12
16
  acquire() {
13
17
  if (this.canAcquire) {
14
18
  this.count++;
15
- return Promise.resolve()
19
+ return Promise.resolve();
16
20
  } else {
17
- return new Promise((resolve) => {
18
- this.queue.push(resolve);
19
- })
21
+ return new Promise((resolve) => this.queue.push(resolve));
20
22
  }
21
23
  }
22
-
23
24
  release() {
24
25
  const resolveFunc = this.queue.shift();
25
-
26
26
  if (resolveFunc) {
27
- // Give the micro task queue a small break instead of calling resolveFunc() directly
28
27
  setTimeout(resolveFunc, 0);
29
28
  } else {
30
29
  this.count--;
31
30
  }
32
31
  }
33
32
  }
34
-
35
- const defaultKey = '_default';
36
-
33
+ const defaultKey = "_default";
37
34
  class Semaphore {
38
- constructor(max = 1) {
39
- this.semaphoreItems = {};
40
- this.max = max;
35
+ semaphoreInstances;
36
+ maxConcurrent;
37
+ /**
38
+ *
39
+ * @param {number} [maxConcurrent] The maximum number of concurrent locks.
40
+ */
41
+ constructor(maxConcurrent = 1) {
42
+ this.semaphoreInstances = {};
43
+ this.maxConcurrent = maxConcurrent;
41
44
  }
42
-
43
- _getSemaphoreInstance(key = defaultKey) {
44
- if (!this.semaphoreItems[key]) {
45
- this.semaphoreItems[key] = new SemaphoreItem(this.max);
45
+ hasSemaphoreInstance(key = defaultKey) {
46
+ return Boolean(this.semaphoreInstances[key]);
47
+ }
48
+ getSemaphoreInstance(key = defaultKey) {
49
+ if (!this.hasSemaphoreInstance(key)) {
50
+ this.semaphoreInstances[key] = new SemaphoreItem(this.maxConcurrent);
46
51
  }
47
- return this.semaphoreItems[key]
52
+ return this.semaphoreInstances[key];
48
53
  }
49
-
50
- _tidy(key = defaultKey) {
51
- if (this._getSemaphoreInstance(key).count == 0) {
52
- delete this.semaphoreItems[key];
54
+ /**
55
+ *
56
+ * @param {string | number} [key]- Optional, the semaphore key.
57
+ */
58
+ tidy(key = defaultKey) {
59
+ if (this.hasSemaphoreInstance(key) && this.getSemaphoreInstance(key).count == 0) {
60
+ delete this.semaphoreInstances[key];
53
61
  }
54
62
  }
55
-
63
+ /**
64
+ * A synchronous function to determine whether a lock can be acquired.
65
+ *
66
+ * @param {string | number} [key]- Optional, the semaphore key.
67
+ * @returns {boolean} Returns true if the lock on `key` can be acquired, false
68
+ * otherwise.
69
+ */
56
70
  canAcquire(key = defaultKey) {
57
- return this._getSemaphoreInstance(key).canAcquire
71
+ return this.getSemaphoreInstance(key).canAcquire;
58
72
  }
59
-
73
+ /**
74
+ *
75
+ * @param {string | number} [key]- Optional, the semaphore key.
76
+ */
60
77
  acquire(key = defaultKey) {
61
- return this._getSemaphoreInstance(key).acquire()
78
+ return this.getSemaphoreInstance(key).acquire();
62
79
  }
63
-
80
+ /**
81
+ *
82
+ * @param {string | number} [key]- Optional, the semaphore key.
83
+ */
64
84
  release(key = defaultKey) {
65
- this._getSemaphoreInstance(key).release();
66
- this._tidy(key);
85
+ this.getSemaphoreInstance(key).release();
86
+ this.tidy(key);
67
87
  }
68
-
88
+ /**
89
+ * The number of active locks. Will always be less or equal to `max`.
90
+ *
91
+ * @param {string | number} [key]- Optional, the semaphore key.
92
+ */
69
93
  count(key = defaultKey) {
70
- if (this.semaphoreItems[key]) {
71
- return this.semaphoreItems[key].count
94
+ if (this.hasSemaphoreInstance(key)) {
95
+ return this.getSemaphoreInstance(key).count;
72
96
  } else {
73
- return 0
97
+ return 0;
74
98
  }
75
99
  }
76
-
100
+ /**
101
+ *
102
+ * @param {string | number} [key]- Optional, the semaphore key.
103
+ * @returns {boolean} True if the semaphore and key has locks, false otherwise.
104
+ */
77
105
  hasTasks(key = defaultKey) {
78
- return this.count(key) > 0
106
+ return this.count(key) > 0;
79
107
  }
80
-
108
+ /**
109
+ *
110
+ * @param {Function<T>} fn The function to execute.
111
+ * @param {string | number} [key]- Optional, the semaphore key.
112
+ * @returns {Promise<T>}
113
+ */
81
114
  async request(fn, key = defaultKey) {
82
115
  try {
83
116
  await this.acquire(key);
84
- return await fn()
117
+ return await fn();
85
118
  } finally {
86
119
  this.release(key);
87
120
  }
88
121
  }
89
-
122
+ /**
123
+ * Asynchronously executes `fn` if a lock can be immediately acquired.
124
+ * Otherwise, returns null.
125
+ *
126
+ * @param {Function<T>} fn The function to execute.
127
+ * @param {string | number} [key]- Optional, the semaphore key.
128
+ * @returns {Promise<T>}
129
+ */
90
130
  async requestIfAvailable(fn, key = defaultKey) {
91
131
  if (this.canAcquire(key)) {
92
- return this.request(fn, key)
132
+ return this.request(fn, key);
93
133
  } else {
94
- // Use canAcquire if you need to know if a function will be dismissed due
95
- // to an existing lock.
96
- return null
134
+ return null;
97
135
  }
98
136
  }
99
137
  }
100
138
 
101
- var src = Semaphore;
102
-
103
- export { src as default };
139
+ export { Semaphore as default };
package/package.json CHANGED
@@ -1,23 +1,34 @@
1
1
  {
2
2
  "name": "@chriscdn/promise-semaphore",
3
- "version": "1.0.9",
3
+ "version": "2.0.1",
4
4
  "description": "Limit or throttle the simultaneous execution of asynchronous code in separate iterations of the event loop.",
5
5
  "main": "lib/index.cjs.js",
6
6
  "module": "lib/index.es.js",
7
+ "types": "lib/index.d.ts",
7
8
  "repository": "https://github.com/chriscdn/promise-semaphore",
8
9
  "author": "Christopher Meyer <chris@schwiiz.org>",
9
10
  "license": "MIT",
10
11
  "scripts": {
11
- "build": "rollup -c"
12
+ "build": "rollup -c",
13
+ "watch": "rollup -c --watch",
14
+ "test": "jest"
12
15
  },
13
16
  "devDependencies": {
14
- "@rollup/plugin-commonjs": "^21.0.2"
17
+ "@types/jest": "^29.4.0",
18
+ "esbuild": "^0.17.10",
19
+ "jest": "^29.4.3",
20
+ "rollup": "^3.17.3",
21
+ "rollup-plugin-dts": "^5.2.0",
22
+ "rollup-plugin-esbuild": "^5.0.0",
23
+ "ts-jest": "^29.0.5",
24
+ "typescript": "^4.9.5"
15
25
  },
16
26
  "keywords": [
17
27
  "promise",
18
28
  "semaphore",
19
29
  "lock",
20
30
  "mutex",
21
- "async"
31
+ "async",
32
+ "throttle"
22
33
  ]
23
34
  }
@@ -0,0 +1,40 @@
1
+ import pkg from './package.json' assert { type: 'json' }
2
+ import dts from 'rollup-plugin-dts'
3
+ import esbuild from 'rollup-plugin-esbuild'
4
+
5
+ const input = 'src/index.ts'
6
+
7
+ export default [
8
+ {
9
+ input,
10
+ plugins: [esbuild()],
11
+ output: [
12
+ {
13
+ file: pkg.main,
14
+ format: 'cjs',
15
+ // sourcemap: true,
16
+ // exports: 'default',
17
+ },
18
+ ],
19
+ },
20
+ {
21
+ input,
22
+ plugins: [esbuild()],
23
+ output: [
24
+ {
25
+ file: pkg.module,
26
+ format: 'es',
27
+ // sourcemap: true,
28
+ // exports: 'default',
29
+ },
30
+ ],
31
+ },
32
+ {
33
+ input,
34
+ plugins: [dts()],
35
+ output: {
36
+ file: 'lib/index.d.ts',
37
+ format: 'es',
38
+ },
39
+ },
40
+ ]
package/src/index.ts ADDED
@@ -0,0 +1,168 @@
1
+ class SemaphoreItem {
2
+ private queue: Array<Function>
3
+ private maxConcurrent: number
4
+ /**
5
+ * The number of locks.
6
+ */
7
+ public count: number
8
+
9
+ constructor(maxConcurrent: number) {
10
+ this.queue = []
11
+ this.maxConcurrent = maxConcurrent
12
+ this.count = 0
13
+ }
14
+
15
+ get canAcquire(): boolean {
16
+ return this.count < this.maxConcurrent
17
+ }
18
+
19
+ acquire(): Promise<void> {
20
+ if (this.canAcquire) {
21
+ this.count++
22
+ return Promise.resolve()
23
+ } else {
24
+ return new Promise((resolve) => this.queue.push(resolve))
25
+ }
26
+ }
27
+
28
+ release(): void {
29
+ const resolveFunc = this.queue.shift()
30
+
31
+ if (resolveFunc) {
32
+ // Give the micro task queue a small break instead of calling resolveFunc() directly
33
+ setTimeout(resolveFunc, 0)
34
+ // resolveFunc()
35
+ } else {
36
+ this.count--
37
+ }
38
+ }
39
+ }
40
+
41
+ const defaultKey = '_default'
42
+
43
+ class Semaphore {
44
+ private semaphoreInstances: Record<string | number, SemaphoreItem>
45
+ private maxConcurrent: number
46
+
47
+ /**
48
+ *
49
+ * @param {number} [maxConcurrent] The maximum number of concurrent locks.
50
+ */
51
+ constructor(maxConcurrent: number = 1) {
52
+ this.semaphoreInstances = {}
53
+ this.maxConcurrent = maxConcurrent
54
+ }
55
+
56
+ private hasSemaphoreInstance(key: string | number = defaultKey) {
57
+ return Boolean(this.semaphoreInstances[key])
58
+ }
59
+
60
+ private getSemaphoreInstance(key: string | number = defaultKey) {
61
+ if (!this.hasSemaphoreInstance(key)) {
62
+ this.semaphoreInstances[key] = new SemaphoreItem(this.maxConcurrent)
63
+ }
64
+ return this.semaphoreInstances[key]
65
+ }
66
+
67
+ /**
68
+ *
69
+ * @param {string | number} [key]- Optional, the semaphore key.
70
+ */
71
+ private tidy(key: string | number = defaultKey): void {
72
+ if (
73
+ this.hasSemaphoreInstance(key) &&
74
+ this.getSemaphoreInstance(key).count == 0
75
+ ) {
76
+ delete this.semaphoreInstances[key]
77
+ }
78
+ }
79
+
80
+ /**
81
+ * A synchronous function to determine whether a lock can be acquired.
82
+ *
83
+ * @param {string | number} [key]- Optional, the semaphore key.
84
+ * @returns {boolean} Returns true if the lock on `key` can be acquired, false
85
+ * otherwise.
86
+ */
87
+ canAcquire(key: string | number = defaultKey): boolean {
88
+ return this.getSemaphoreInstance(key).canAcquire
89
+ }
90
+
91
+ /**
92
+ *
93
+ * @param {string | number} [key]- Optional, the semaphore key.
94
+ */
95
+ acquire(key: string | number = defaultKey) {
96
+ return this.getSemaphoreInstance(key).acquire()
97
+ }
98
+
99
+ /**
100
+ *
101
+ * @param {string | number} [key]- Optional, the semaphore key.
102
+ */
103
+ release(key: string | number = defaultKey): void {
104
+ this.getSemaphoreInstance(key).release()
105
+ this.tidy(key)
106
+ }
107
+
108
+ /**
109
+ * The number of active locks. Will always be less or equal to `max`.
110
+ *
111
+ * @param {string | number} [key]- Optional, the semaphore key.
112
+ */
113
+ count(key: string | number = defaultKey): number {
114
+ if (this.hasSemaphoreInstance(key)) {
115
+ return this.getSemaphoreInstance(key).count
116
+ } else {
117
+ return 0
118
+ }
119
+ }
120
+
121
+ /**
122
+ *
123
+ * @param {string | number} [key]- Optional, the semaphore key.
124
+ * @returns {boolean} True if the semaphore and key has locks, false otherwise.
125
+ */
126
+ hasTasks(key: string | number = defaultKey): boolean {
127
+ return this.count(key) > 0
128
+ }
129
+
130
+ /**
131
+ *
132
+ * @param {Function<T>} fn The function to execute.
133
+ * @param {string | number} [key]- Optional, the semaphore key.
134
+ * @returns {Promise<T>}
135
+ */
136
+ async request<T>(
137
+ fn: Function,
138
+ key: string | number = defaultKey
139
+ ): Promise<T> {
140
+ try {
141
+ await this.acquire(key)
142
+ return await fn()
143
+ } finally {
144
+ this.release(key)
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Asynchronously executes `fn` if a lock can be immediately acquired.
150
+ * Otherwise, returns null.
151
+ *
152
+ * @param {Function<T>} fn The function to execute.
153
+ * @param {string | number} [key]- Optional, the semaphore key.
154
+ * @returns {Promise<T>}
155
+ */
156
+ async requestIfAvailable<T>(
157
+ fn: Function,
158
+ key: string | number = defaultKey
159
+ ): Promise<T | null> {
160
+ if (this.canAcquire(key)) {
161
+ return this.request(fn, key)
162
+ } else {
163
+ return null
164
+ }
165
+ }
166
+ }
167
+
168
+ export default Semaphore
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "esModuleInterop": true
5
+ }
6
+ }
package/rollup.config.js DELETED
@@ -1,19 +0,0 @@
1
- import pkg from './package.json'
2
- import commonjs from '@rollup/plugin-commonjs'
3
-
4
- export default [{
5
- input: 'src/index.js',
6
- output: [{
7
- file: pkg.main,
8
- format: 'cjs'
9
- }]
10
- }, {
11
- input: 'src/index.js',
12
- output: [{
13
- file: pkg.module,
14
- format: 'es'
15
- }],
16
- plugins: [
17
- commonjs()
18
- ]
19
- }]
package/src/index.js DELETED
@@ -1,101 +0,0 @@
1
- class SemaphoreItem {
2
- constructor(max) {
3
- this.queue = []
4
- this.max = max
5
- this.count = 0
6
- }
7
-
8
- get canAcquire() {
9
- return this.count < this.max
10
- }
11
-
12
- acquire() {
13
- if (this.canAcquire) {
14
- this.count++
15
- return Promise.resolve()
16
- } else {
17
- return new Promise((resolve) => {
18
- this.queue.push(resolve)
19
- })
20
- }
21
- }
22
-
23
- release() {
24
- const resolveFunc = this.queue.shift()
25
-
26
- if (resolveFunc) {
27
- // Give the micro task queue a small break instead of calling resolveFunc() directly
28
- setTimeout(resolveFunc, 0)
29
- } else {
30
- this.count--
31
- }
32
- }
33
- }
34
-
35
- const defaultKey = '_default'
36
-
37
- class Semaphore {
38
- constructor(max = 1) {
39
- this.semaphoreItems = {}
40
- this.max = max
41
- }
42
-
43
- _getSemaphoreInstance(key = defaultKey) {
44
- if (!this.semaphoreItems[key]) {
45
- this.semaphoreItems[key] = new SemaphoreItem(this.max)
46
- }
47
- return this.semaphoreItems[key]
48
- }
49
-
50
- _tidy(key = defaultKey) {
51
- if (this._getSemaphoreInstance(key).count == 0) {
52
- delete this.semaphoreItems[key]
53
- }
54
- }
55
-
56
- canAcquire(key = defaultKey) {
57
- return this._getSemaphoreInstance(key).canAcquire
58
- }
59
-
60
- acquire(key = defaultKey) {
61
- return this._getSemaphoreInstance(key).acquire()
62
- }
63
-
64
- release(key = defaultKey) {
65
- this._getSemaphoreInstance(key).release()
66
- this._tidy(key)
67
- }
68
-
69
- count(key = defaultKey) {
70
- if (this.semaphoreItems[key]) {
71
- return this.semaphoreItems[key].count
72
- } else {
73
- return 0
74
- }
75
- }
76
-
77
- hasTasks(key = defaultKey) {
78
- return this.count(key) > 0
79
- }
80
-
81
- async request(fn, key = defaultKey) {
82
- try {
83
- await this.acquire(key)
84
- return await fn()
85
- } finally {
86
- this.release(key)
87
- }
88
- }
89
-
90
- async requestIfAvailable(fn, key = defaultKey) {
91
- if (this.canAcquire(key)) {
92
- return this.request(fn, key)
93
- } else {
94
- // Use canAcquire if you need to know if a function will be dismissed due
95
- // to an existing lock.
96
- return null
97
- }
98
- }
99
- }
100
-
101
- module.exports = Semaphore