@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 +1 -1
- package/README.md +60 -67
- package/__tests__/semaphore.test.ts +143 -0
- package/jest.config.js +5 -0
- package/lib/index.cjs.js +85 -47
- package/lib/index.d.ts +64 -0
- package/lib/index.es.js +86 -50
- package/package.json +15 -4
- package/rollup.config.mjs +40 -0
- package/src/index.ts +168 -0
- package/tsconfig.json +6 -0
- package/rollup.config.js +0 -19
- package/src/index.js +0 -101
package/LICENSE
CHANGED
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
|
-
|
|
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
|
|
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.
|
|
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`.
|
|
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
|
-
|
|
65
|
-
|
|
68
|
+
await semaphore.acquire([key])
|
|
69
|
+
const results = await fn()
|
|
66
70
|
} finally {
|
|
67
|
-
|
|
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.
|
|
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
|
-
|
|
96
|
+
import Semaphore from '@chriscdn/promise-semaphore'
|
|
93
97
|
const semaphore = new Semaphore()
|
|
94
98
|
|
|
95
99
|
// using promises
|
|
96
|
-
semaphore
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
await semaphore.acquire()
|
|
116
|
+
|
|
117
|
+
// do your critical stuff here
|
|
117
118
|
} finally {
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
|
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
package/lib/index.cjs.js
CHANGED
|
@@ -1,101 +1,139 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
class SemaphoreItem {
|
|
4
|
-
|
|
4
|
+
queue;
|
|
5
|
+
maxConcurrent;
|
|
6
|
+
/**
|
|
7
|
+
* The number of locks.
|
|
8
|
+
*/
|
|
9
|
+
count;
|
|
10
|
+
constructor(maxConcurrent) {
|
|
5
11
|
this.queue = [];
|
|
6
|
-
this.
|
|
12
|
+
this.maxConcurrent = maxConcurrent;
|
|
7
13
|
this.count = 0;
|
|
8
14
|
}
|
|
9
|
-
|
|
10
15
|
get canAcquire() {
|
|
11
|
-
return this.count < this.
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
54
|
+
return this.semaphoreInstances[key];
|
|
50
55
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
68
|
-
this.
|
|
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.
|
|
73
|
-
return this.
|
|
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
|
-
|
|
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
|
-
|
|
2
|
+
queue;
|
|
3
|
+
maxConcurrent;
|
|
4
|
+
/**
|
|
5
|
+
* The number of locks.
|
|
6
|
+
*/
|
|
7
|
+
count;
|
|
8
|
+
constructor(maxConcurrent) {
|
|
3
9
|
this.queue = [];
|
|
4
|
-
this.
|
|
10
|
+
this.maxConcurrent = maxConcurrent;
|
|
5
11
|
this.count = 0;
|
|
6
12
|
}
|
|
7
|
-
|
|
8
13
|
get canAcquire() {
|
|
9
|
-
return this.count < this.
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
52
|
+
return this.semaphoreInstances[key];
|
|
48
53
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
66
|
-
this.
|
|
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.
|
|
71
|
-
return this.
|
|
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
|
-
|
|
95
|
-
// to an existing lock.
|
|
96
|
-
return null
|
|
134
|
+
return null;
|
|
97
135
|
}
|
|
98
136
|
}
|
|
99
137
|
}
|
|
100
138
|
|
|
101
|
-
|
|
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": "
|
|
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
|
-
"@
|
|
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
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
|