@chriscdn/promise-semaphore 1.0.4 → 1.0.9

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 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
@@ -33,7 +33,7 @@ The `maxConcurrent` parameter is optional, and defaults to `1` (making it an exc
33
33
  semaphore.acquire([key])
34
34
  ```
35
35
 
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.
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.
37
37
 
38
38
  ### Release a lock
39
39
 
@@ -41,7 +41,52 @@ This returns a `Promise`, which resolves once a lock has been acquired. The `ke
41
41
  semaphore.release([key])
42
42
  ```
43
43
 
44
- ## Example
44
+ The `release` call should be executed from a `finally` block (whether using promises or a try/catch block) to guarantee it gets called.
45
+
46
+ ### Check if a lock can be acquired
47
+
48
+ ```js
49
+ semaphore.canAcquire([key])
50
+ ```
51
+
52
+ This method is synchronous, and returns `true` if a lock can be immediately acquired, `false` otherwise.
53
+
54
+ ### request function
55
+
56
+ ```js
57
+ const results = await semaphore.request(fn [,key])
58
+ ```
59
+
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:
61
+
62
+ ```js
63
+ try {
64
+ await semaphore.acquire([key])
65
+ const results = await fn()
66
+ } finally {
67
+ semaphore.release([key])
68
+ }
69
+ ```
70
+
71
+ See the examples below.
72
+
73
+ ### requestIfAvailable function
74
+
75
+ ```js
76
+ const results = await semaphore.requestIfAvailable(fn [,key])
77
+ ```
78
+
79
+ This is functionally equivalent to:
80
+
81
+ ```js
82
+ const results = semaphore.canAcquire([key] ?
83
+ await semaphore.request(fn, [key]) :
84
+ null
85
+ ```
86
+
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.
88
+
89
+ ## Example 1
45
90
 
46
91
  ```js
47
92
  const Semaphore = require('@chriscdn/promise-semaphore')
@@ -50,23 +95,109 @@ const semaphore = new Semaphore()
50
95
  // using promises
51
96
  semaphore.acquire()
52
97
  .then(() => {
53
- // This block executes once a lock has been acquired. If already locked
54
- // then this block will wait and execute once all locks preceeding it have been
55
- // released.
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
+
56
104
  })
57
105
  .finally(() => {
58
- // release the lock permitting the next queued process to continue
106
+ // release the lock permitting the next queued block to continue
59
107
  semaphore.release()
60
108
  })
61
109
 
62
- // or, using async/await
63
- await semaphore.acquire()
64
110
 
111
+ // or, using async/await
65
112
  try {
66
- // do your stuff here
113
+ await semaphore.acquire()
114
+
115
+ // do your critical stuff here
116
+
67
117
  } finally {
68
118
  semaphore.release()
69
119
  }
120
+
121
+
122
+ // or, using the request function
123
+ semaphore.request(() => {
124
+
125
+ // do your critical stuff here
126
+
127
+ })
128
+
129
+ ```
130
+
131
+ ## Example 2
132
+
133
+ Say you have an asynchronous function to download a file and save it to disk
134
+
135
+ ```js
136
+ async function downloadAndSave(url) {
137
+
138
+ const filePath = urlToFilePath(url)
139
+
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
147
+ }
148
+ ```
149
+
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.
151
+
152
+ This can be resolved with a `Semaphore` instance using the `key` parameter:
153
+
154
+ ```js
155
+ const Semaphore = require('@chriscdn/promise-semaphore')
156
+ const semaphore = new Semaphore()
157
+
158
+ async function downloadAndSave(url) {
159
+
160
+ try {
161
+
162
+ await semaphore.acquire(url)
163
+
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
176
+
177
+ } finally {
178
+ semaphore.release(url)
179
+ }
180
+ }
181
+ ```
182
+
183
+ Alternatively, this can be accomplished with the `request` function:
184
+
185
+ ```js
186
+ async function downloadAndSave(url) {
187
+
188
+ return semaphore.request(() => {
189
+ const filePath = urlToFilePath(url)
190
+
191
+ if (await pathExists(filePath)) {
192
+ // the file is on disk, so no action is required
193
+ } else {
194
+ await downloadToFile(url, filePath)
195
+ }
196
+
197
+ return filePath
198
+ }, url)
199
+
200
+ }
70
201
  ```
71
202
 
72
203
  ## License
package/lib/index.cjs.js CHANGED
@@ -1,76 +1,103 @@
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
+ constructor(max) {
5
+ this.queue = [];
6
+ this.max = max;
7
+ this.count = 0;
8
+ }
9
+
10
+ get canAcquire() {
11
+ return this.count < this.max
12
+ }
13
+
14
+ acquire() {
15
+ if (this.canAcquire) {
16
+ this.count++;
17
+ return Promise.resolve()
18
+ } else {
19
+ return new Promise((resolve) => {
20
+ this.queue.push(resolve);
21
+ })
22
+ }
23
+ }
24
+
25
+ release() {
26
+ const resolveFunc = this.queue.shift();
27
+
28
+ if (resolveFunc) {
29
+ // Give the micro task queue a small break instead of calling resolveFunc() directly
30
+ setTimeout(resolveFunc, 0);
31
+ } else {
32
+ this.count--;
33
+ }
34
+ }
31
35
  }
32
36
 
33
37
  const defaultKey = '_default';
34
38
 
35
39
  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
- }
40
+ constructor(max = 1) {
41
+ this.semaphoreItems = {};
42
+ this.max = max;
43
+ }
44
+
45
+ _getSemaphoreInstance(key = defaultKey) {
46
+ if (!this.semaphoreItems[key]) {
47
+ this.semaphoreItems[key] = new SemaphoreItem(this.max);
48
+ }
49
+ return this.semaphoreItems[key]
50
+ }
51
+
52
+ _tidy(key = defaultKey) {
53
+ if (this._getSemaphoreInstance(key).count == 0) {
54
+ delete this.semaphoreItems[key];
55
+ }
56
+ }
57
+
58
+ canAcquire(key = defaultKey) {
59
+ return this._getSemaphoreInstance(key).canAcquire
60
+ }
61
+
62
+ acquire(key = defaultKey) {
63
+ return this._getSemaphoreInstance(key).acquire()
64
+ }
65
+
66
+ release(key = defaultKey) {
67
+ this._getSemaphoreInstance(key).release();
68
+ this._tidy(key);
69
+ }
70
+
71
+ count(key = defaultKey) {
72
+ if (this.semaphoreItems[key]) {
73
+ return this.semaphoreItems[key].count
74
+ } else {
75
+ return 0
76
+ }
77
+ }
78
+
79
+ hasTasks(key = defaultKey) {
80
+ return this.count(key) > 0
81
+ }
82
+
83
+ async request(fn, key = defaultKey) {
84
+ try {
85
+ await this.acquire(key);
86
+ return await fn()
87
+ } finally {
88
+ this.release(key);
89
+ }
90
+ }
91
+
92
+ async requestIfAvailable(fn, key = defaultKey) {
93
+ if (this.canAcquire(key)) {
94
+ return this.request(fn, key)
95
+ } else {
96
+ // Use canAcquire if you need to know if a function will be dismissed due
97
+ // to an existing lock.
98
+ return null
99
+ }
100
+ }
74
101
  }
75
102
 
76
103
  module.exports = Semaphore;
package/lib/index.es.js CHANGED
@@ -1,76 +1,103 @@
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
+ 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
+ }
29
33
  }
30
34
 
31
35
  const defaultKey = '_default';
32
36
 
33
37
  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
- }
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
+ }
72
99
  }
73
100
 
74
101
  var src = Semaphore;
75
102
 
76
- export default src;
103
+ export { src as default };
package/package.json CHANGED
@@ -1,22 +1,23 @@
1
1
  {
2
- "name": "@chriscdn/promise-semaphore",
3
- "version": "1.0.4",
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": "^13.0.0"
15
- },
16
- "keywords": [
17
- "semeaphore",
18
- "lock",
19
- "mutex",
20
- "async"
21
- ]
2
+ "name": "@chriscdn/promise-semaphore",
3
+ "version": "1.0.9",
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": "^21.0.2"
15
+ },
16
+ "keywords": [
17
+ "promise",
18
+ "semaphore",
19
+ "lock",
20
+ "mutex",
21
+ "async"
22
+ ]
22
23
  }
package/src/index.js CHANGED
@@ -1,74 +1,101 @@
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
+ 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
+ }
29
33
  }
30
34
 
31
35
  const defaultKey = '_default'
32
36
 
33
37
  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
- }
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
+ }
72
99
  }
73
100
 
74
- module.exports = Semaphore
101
+ module.exports = Semaphore