@chriscdn/promise-semaphore 1.0.6 → 2.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/.prettierrc ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true
4
+ }
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020 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
 
@@ -43,82 +47,152 @@ semaphore.release([key])
43
47
 
44
48
  The `release` call should be executed from a `finally` block (whether using promises or a try/catch block) to guarantee it gets called.
45
49
 
50
+ ### Check if a lock can be acquired
51
+
52
+ ```js
53
+ semaphore.canAcquire([key])
54
+ ```
55
+
56
+ This method is synchronous, and returns `true` if a lock can be immediately acquired, `false` otherwise.
57
+
58
+ ### request function
59
+
60
+ ```js
61
+ const results = await semaphore.request(fn [,key])
62
+ ```
63
+
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:
65
+
66
+ ```js
67
+ try {
68
+ await semaphore.acquire([key])
69
+ const results = await fn()
70
+ } finally {
71
+ semaphore.release([key])
72
+ }
73
+ ```
74
+
75
+ See the examples below.
76
+
77
+ ### requestIfAvailable function
78
+
79
+ ```js
80
+ const results = await semaphore.requestIfAvailable(fn [,key])
81
+ ```
82
+
83
+ This is functionally equivalent to:
84
+
85
+ ```js
86
+ const results = semaphore.canAcquire([key] ?
87
+ await semaphore.request(fn, [key]) :
88
+ null
89
+ ```
90
+
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.
92
+
46
93
  ## Example 1
47
94
 
48
95
  ```js
49
- const Semaphore = require('@chriscdn/promise-semaphore')
96
+ import Semaphore from '@chriscdn/promise-semaphore'
50
97
  const semaphore = new Semaphore()
51
98
 
52
99
  // using promises
53
- semaphore.acquire()
54
- .then(() => {
55
- // This block executes once a lock has been acquired. If already locked
56
- // then this block will wait and execute once all locks preceeding it have been
57
- // released.
58
- })
59
- .finally(() => {
60
- // release the lock permitting the next queued process to continue
61
- semaphore.release()
62
- })
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
+ })
63
112
 
64
113
  // or, using async/await
65
- await semaphore.acquire()
66
-
67
114
  try {
68
- // do your stuff here
115
+ await semaphore.acquire()
116
+
117
+ // do your critical stuff here
69
118
  } finally {
70
- semaphore.release()
119
+ semaphore.release()
71
120
  }
121
+
122
+ // or, using the request function
123
+ semaphore.request(() => {
124
+ // do your critical stuff here
125
+ })
72
126
  ```
73
127
 
74
128
  ## Example 2
75
129
 
76
- Say you have an asynchronous function to download a file and cache it to disk:
130
+ Say you have an asynchronous function to download a file and save it to disk:
77
131
 
78
132
  ```js
79
- async function downloadAndCache(url) {
80
-
81
- // cacheFilePath could be based on a hash of the url
82
- const cacheFilePath = getCacheFilePath(url)
133
+ async function downloadAndSave(url) {
134
+ const filePath = urlToFilePath(url)
83
135
 
84
- if (!await pathExists(cacheFilePath)) {
85
- await downloadToFile(url, cacheFilePath)
86
- }
136
+ if (await pathExists(filePath)) {
137
+ // the file is on disk, so no action is required
138
+ } else {
139
+ await downloadAndSaveToFilepath(url, filePath)
140
+ }
87
141
 
88
- return cacheFilePath
142
+ return filePath
89
143
  }
90
144
  ```
91
145
 
92
- This works until a process calls `downloadAndCache()` in short succession with the same `url` parameter. This can cause multiple simultaneous downloads that attempt to write to the same cached 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.
93
147
 
94
148
  This can be resolved with a `Semaphore` instance using the `key` parameter:
95
149
 
96
150
  ```js
97
- const Semaphore = require('@chriscdn/promise-semaphore')
151
+ import Semaphore from '@chriscdn/promise-semaphore'
98
152
  const semaphore = new Semaphore()
99
153
 
100
- async function downloadAndCache(url) {
154
+ async function downloadAndSave(url) {
155
+ try {
156
+ await semaphore.acquire(url)
157
+
158
+ // This block continues once a lock on url is acquired. This
159
+ // permits multiple simulataneous downloads for different urls.
101
160
 
102
- await semaphore.acquire(url)
161
+ const filePath = urlToFilePath(url)
103
162
 
104
- // This block continues once a lock on url is acquired. This permits
105
- // multiple simulataneous downloads for unique url values.
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
+ }
173
+ }
174
+ ```
175
+
176
+ Alternatively, this can be accomplished with the `request` function:
177
+
178
+ ```js
179
+ async function downloadAndSave(url) {
106
180
 
107
- try {
108
- const cacheFileName = getCacheFilePath(url)
181
+ return semaphore.request(() => {
182
+ const filePath = urlToFilePath(url)
109
183
 
110
- if (!await pathExists(cacheFilePath)) {
111
- await downloadToFile(url, cacheFilePath)
184
+ if (await pathExists(filePath)) {
185
+ // the file is on disk, so no action is required
186
+ } else {
187
+ await downloadAndSaveToFilepath(url, filePath)
112
188
  }
113
189
 
114
- return cacheFilePath
190
+ return filePath
191
+ }, url)
115
192
 
116
- } finally {
117
- semaphore.release(url)
118
- }
119
193
  }
120
194
  ```
121
195
 
122
196
  ## License
123
197
 
124
- [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,76 +1,141 @@
1
1
  'use strict';
2
2
 
3
3
  class SemaphoreItem {
4
- constructor(max) {
5
- this.queue = [];
6
- this.max = max;
7
- this.count = 0;
8
- }
9
-
10
- acquire() {
11
- if (this.count < this.max) {
12
- this.count++;
13
- return Promise.resolve()
14
- } else {
15
- return new Promise(resolve => {
16
- this.queue.push(resolve);
17
- })
18
- }
19
- }
20
-
21
- release() {
22
- let resolveFunc = this.queue.shift();
23
-
24
- if (resolveFunc) {
25
- // Give the micro task queue a small break instead of calling resoleFunc() directly
26
- setTimeout(resolveFunc, 0);
27
- } else {
28
- this.count--;
29
- }
30
- }
4
+ queue;
5
+ maxConcurrent;
6
+ /**
7
+ * The number of locks.
8
+ */
9
+ count;
10
+ constructor(maxConcurrent) {
11
+ this.queue = [];
12
+ this.maxConcurrent = maxConcurrent;
13
+ this.count = 0;
14
+ }
15
+ get canAcquire() {
16
+ return this.count < this.maxConcurrent;
17
+ }
18
+ acquire() {
19
+ if (this.canAcquire) {
20
+ this.count++;
21
+ return Promise.resolve();
22
+ } else {
23
+ return new Promise((resolve) => this.queue.push(resolve));
24
+ }
25
+ }
26
+ release() {
27
+ const resolveFunc = this.queue.shift();
28
+ if (resolveFunc) {
29
+ setTimeout(resolveFunc, 0);
30
+ } else {
31
+ this.count--;
32
+ }
33
+ }
31
34
  }
32
-
33
- const defaultKey = '_default';
34
-
35
+ const defaultKey = "_default";
35
36
  class Semaphore {
36
- constructor(max = 1) {
37
- this.semaphoreItems = {};
38
- this.max = max;
39
- }
40
-
41
- _getSemaphoreInstance(key = defaultKey) {
42
- if (!this.semaphoreItems[key]) {
43
- this.semaphoreItems[key] = new SemaphoreItem(this.max);
44
- }
45
- return this.semaphoreItems[key]
46
- }
47
-
48
- _tidy(key = defaultKey) {
49
- if (this._getSemaphoreInstance(key).count == 0) {
50
- delete this.semaphoreItems[key];
51
- }
52
- }
53
-
54
- acquire(key = defaultKey) {
55
- return this._getSemaphoreInstance(key).acquire()
56
- }
57
-
58
- release(key = defaultKey) {
59
- this._getSemaphoreInstance(key).release();
60
- this._tidy(key);
61
- }
62
-
63
- count(key = defaultKey) {
64
- if (this.semaphoreItems[key]) {
65
- return this.semaphoreItems[key].count
66
- } else {
67
- return 0
68
- }
69
- }
70
-
71
- hasTasks(key = defaultKey) {
72
- return this.count(key) > 0
73
- }
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;
46
+ }
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);
53
+ }
54
+ return this.semaphoreInstances[key];
55
+ }
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];
63
+ }
64
+ }
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
+ */
72
+ canAcquire(key = defaultKey) {
73
+ return this.getSemaphoreInstance(key).canAcquire;
74
+ }
75
+ /**
76
+ *
77
+ * @param {string | number} [key]- Optional, the semaphore key.
78
+ */
79
+ acquire(key = defaultKey) {
80
+ return this.getSemaphoreInstance(key).acquire();
81
+ }
82
+ /**
83
+ *
84
+ * @param {string | number} [key]- Optional, the semaphore key.
85
+ */
86
+ release(key = defaultKey) {
87
+ this.getSemaphoreInstance(key).release();
88
+ this.tidy(key);
89
+ }
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
+ */
95
+ count(key = defaultKey) {
96
+ if (this.hasSemaphoreInstance(key)) {
97
+ return this.getSemaphoreInstance(key).count;
98
+ } else {
99
+ return 0;
100
+ }
101
+ }
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
+ */
107
+ hasTasks(key = defaultKey) {
108
+ return this.count(key) > 0;
109
+ }
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
+ */
116
+ async request(fn, key = defaultKey) {
117
+ try {
118
+ await this.acquire(key);
119
+ return await fn();
120
+ } finally {
121
+ this.release(key);
122
+ }
123
+ }
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
+ */
132
+ async requestIfAvailable(fn, key = defaultKey) {
133
+ if (this.canAcquire(key)) {
134
+ return this.request(fn, key);
135
+ } else {
136
+ return null;
137
+ }
138
+ }
74
139
  }
75
140
 
76
141
  module.exports = Semaphore;
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,76 +1,139 @@
1
1
  class SemaphoreItem {
2
- constructor(max) {
3
- this.queue = [];
4
- this.max = max;
5
- this.count = 0;
6
- }
7
-
8
- acquire() {
9
- if (this.count < this.max) {
10
- this.count++;
11
- return Promise.resolve()
12
- } else {
13
- return new Promise(resolve => {
14
- this.queue.push(resolve);
15
- })
16
- }
17
- }
18
-
19
- release() {
20
- let resolveFunc = this.queue.shift();
21
-
22
- if (resolveFunc) {
23
- // Give the micro task queue a small break instead of calling resoleFunc() directly
24
- setTimeout(resolveFunc, 0);
25
- } else {
26
- this.count--;
27
- }
28
- }
2
+ queue;
3
+ maxConcurrent;
4
+ /**
5
+ * The number of locks.
6
+ */
7
+ count;
8
+ constructor(maxConcurrent) {
9
+ this.queue = [];
10
+ this.maxConcurrent = maxConcurrent;
11
+ this.count = 0;
12
+ }
13
+ get canAcquire() {
14
+ return this.count < this.maxConcurrent;
15
+ }
16
+ acquire() {
17
+ if (this.canAcquire) {
18
+ this.count++;
19
+ return Promise.resolve();
20
+ } else {
21
+ return new Promise((resolve) => this.queue.push(resolve));
22
+ }
23
+ }
24
+ release() {
25
+ const resolveFunc = this.queue.shift();
26
+ if (resolveFunc) {
27
+ setTimeout(resolveFunc, 0);
28
+ } else {
29
+ this.count--;
30
+ }
31
+ }
29
32
  }
30
-
31
- const defaultKey = '_default';
32
-
33
+ const defaultKey = "_default";
33
34
  class Semaphore {
34
- constructor(max = 1) {
35
- this.semaphoreItems = {};
36
- this.max = max;
37
- }
38
-
39
- _getSemaphoreInstance(key = defaultKey) {
40
- if (!this.semaphoreItems[key]) {
41
- this.semaphoreItems[key] = new SemaphoreItem(this.max);
42
- }
43
- return this.semaphoreItems[key]
44
- }
45
-
46
- _tidy(key = defaultKey) {
47
- if (this._getSemaphoreInstance(key).count == 0) {
48
- delete this.semaphoreItems[key];
49
- }
50
- }
51
-
52
- acquire(key = defaultKey) {
53
- return this._getSemaphoreInstance(key).acquire()
54
- }
55
-
56
- release(key = defaultKey) {
57
- this._getSemaphoreInstance(key).release();
58
- this._tidy(key);
59
- }
60
-
61
- count(key = defaultKey) {
62
- if (this.semaphoreItems[key]) {
63
- return this.semaphoreItems[key].count
64
- } else {
65
- return 0
66
- }
67
- }
68
-
69
- hasTasks(key = defaultKey) {
70
- return this.count(key) > 0
71
- }
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;
44
+ }
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);
51
+ }
52
+ return this.semaphoreInstances[key];
53
+ }
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];
61
+ }
62
+ }
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
+ */
70
+ canAcquire(key = defaultKey) {
71
+ return this.getSemaphoreInstance(key).canAcquire;
72
+ }
73
+ /**
74
+ *
75
+ * @param {string | number} [key]- Optional, the semaphore key.
76
+ */
77
+ acquire(key = defaultKey) {
78
+ return this.getSemaphoreInstance(key).acquire();
79
+ }
80
+ /**
81
+ *
82
+ * @param {string | number} [key]- Optional, the semaphore key.
83
+ */
84
+ release(key = defaultKey) {
85
+ this.getSemaphoreInstance(key).release();
86
+ this.tidy(key);
87
+ }
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
+ */
93
+ count(key = defaultKey) {
94
+ if (this.hasSemaphoreInstance(key)) {
95
+ return this.getSemaphoreInstance(key).count;
96
+ } else {
97
+ return 0;
98
+ }
99
+ }
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
+ */
105
+ hasTasks(key = defaultKey) {
106
+ return this.count(key) > 0;
107
+ }
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
+ */
114
+ async request(fn, key = defaultKey) {
115
+ try {
116
+ await this.acquire(key);
117
+ return await fn();
118
+ } finally {
119
+ this.release(key);
120
+ }
121
+ }
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
+ */
130
+ async requestIfAvailable(fn, key = defaultKey) {
131
+ if (this.canAcquire(key)) {
132
+ return this.request(fn, key);
133
+ } else {
134
+ return null;
135
+ }
136
+ }
72
137
  }
73
138
 
74
- var src = Semaphore;
75
-
76
- export default src;
139
+ export { Semaphore as default };
package/package.json CHANGED
@@ -1,23 +1,34 @@
1
1
  {
2
- "name": "@chriscdn/promise-semaphore",
3
- "version": "1.0.6",
4
- "description": "Limit or throttle the simultaneous execution of asynchronous code in separate iterations of the event loop.",
5
- "main": "lib/index.cjs.js",
6
- "module": "lib/index.es.js",
7
- "repository": "https://github.com/chriscdn/promise-semaphore",
8
- "author": "Christopher Meyer <chris@schwiiz.org>",
9
- "license": "MIT",
10
- "scripts": {
11
- "build": "rollup -c"
12
- },
13
- "devDependencies": {
14
- "@rollup/plugin-commonjs": "^15.1.0"
15
- },
16
- "keywords": [
17
- "promise",
18
- "semaphore",
19
- "lock",
20
- "mutex",
21
- "async"
22
- ]
2
+ "name": "@chriscdn/promise-semaphore",
3
+ "version": "2.0.0",
4
+ "description": "Limit or throttle the simultaneous execution of asynchronous code in separate iterations of the event loop.",
5
+ "main": "lib/index.cjs.js",
6
+ "module": "lib/index.es.js",
7
+ "types": "lib/index.d.ts",
8
+ "repository": "https://github.com/chriscdn/promise-semaphore",
9
+ "author": "Christopher Meyer <chris@schwiiz.org>",
10
+ "license": "MIT",
11
+ "scripts": {
12
+ "build": "rollup -c",
13
+ "watch": "rollup -c --watch",
14
+ "test": "jest"
15
+ },
16
+ "devDependencies": {
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"
25
+ },
26
+ "keywords": [
27
+ "promise",
28
+ "semaphore",
29
+ "lock",
30
+ "mutex",
31
+ "async",
32
+ "throttle"
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,74 +0,0 @@
1
- class SemaphoreItem {
2
- constructor(max) {
3
- this.queue = []
4
- this.max = max
5
- this.count = 0
6
- }
7
-
8
- acquire() {
9
- if (this.count < this.max) {
10
- this.count++
11
- return Promise.resolve()
12
- } else {
13
- return new Promise(resolve => {
14
- this.queue.push(resolve)
15
- })
16
- }
17
- }
18
-
19
- release() {
20
- let resolveFunc = this.queue.shift()
21
-
22
- if (resolveFunc) {
23
- // Give the micro task queue a small break instead of calling resoleFunc() directly
24
- setTimeout(resolveFunc, 0)
25
- } else {
26
- this.count--
27
- }
28
- }
29
- }
30
-
31
- const defaultKey = '_default'
32
-
33
- class Semaphore {
34
- constructor(max = 1) {
35
- this.semaphoreItems = {}
36
- this.max = max
37
- }
38
-
39
- _getSemaphoreInstance(key = defaultKey) {
40
- if (!this.semaphoreItems[key]) {
41
- this.semaphoreItems[key] = new SemaphoreItem(this.max)
42
- }
43
- return this.semaphoreItems[key]
44
- }
45
-
46
- _tidy(key = defaultKey) {
47
- if (this._getSemaphoreInstance(key).count == 0) {
48
- delete this.semaphoreItems[key]
49
- }
50
- }
51
-
52
- acquire(key = defaultKey) {
53
- return this._getSemaphoreInstance(key).acquire()
54
- }
55
-
56
- release(key = defaultKey) {
57
- this._getSemaphoreInstance(key).release()
58
- this._tidy(key)
59
- }
60
-
61
- count(key = defaultKey) {
62
- if (this.semaphoreItems[key]) {
63
- return this.semaphoreItems[key].count
64
- } else {
65
- return 0
66
- }
67
- }
68
-
69
- hasTasks(key = defaultKey) {
70
- return this.count(key) > 0
71
- }
72
- }
73
-
74
- module.exports = Semaphore