@chriscdn/promise-semaphore 1.0.6 → 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
@@ -43,6 +43,49 @@ semaphore.release([key])
43
43
 
44
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
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
+
46
89
  ## Example 1
47
90
 
48
91
  ```js
@@ -52,44 +95,59 @@ const semaphore = new Semaphore()
52
95
  // using promises
53
96
  semaphore.acquire()
54
97
  .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.
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
+
58
104
  })
59
105
  .finally(() => {
60
- // release the lock permitting the next queued process to continue
106
+ // release the lock permitting the next queued block to continue
61
107
  semaphore.release()
62
108
  })
63
109
 
64
- // or, using async/await
65
- await semaphore.acquire()
66
110
 
111
+ // or, using async/await
67
112
  try {
68
- // do your stuff here
113
+ await semaphore.acquire()
114
+
115
+ // do your critical stuff here
116
+
69
117
  } finally {
70
118
  semaphore.release()
71
119
  }
120
+
121
+
122
+ // or, using the request function
123
+ semaphore.request(() => {
124
+
125
+ // do your critical stuff here
126
+
127
+ })
128
+
72
129
  ```
73
130
 
74
131
  ## Example 2
75
132
 
76
- Say you have an asynchronous function to download a file and cache it to disk:
133
+ Say you have an asynchronous function to download a file and save it to disk
77
134
 
78
135
  ```js
79
- async function downloadAndCache(url) {
136
+ async function downloadAndSave(url) {
80
137
 
81
- // cacheFilePath could be based on a hash of the url
82
- const cacheFilePath = getCacheFilePath(url)
138
+ const filePath = urlToFilePath(url)
83
139
 
84
- if (!await pathExists(cacheFilePath)) {
85
- await downloadToFile(url, cacheFilePath)
140
+ if (await pathExists(filePath)) {
141
+ // the file is on disk, so no action is required
142
+ } else {
143
+ await downloadToFile(url, filePath)
86
144
  }
87
145
 
88
- return cacheFilePath
146
+ return filePath
89
147
  }
90
148
  ```
91
149
 
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.
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.
93
151
 
94
152
  This can be resolved with a `Semaphore` instance using the `key` parameter:
95
153
 
@@ -97,21 +155,24 @@ This can be resolved with a `Semaphore` instance using the `key` parameter:
97
155
  const Semaphore = require('@chriscdn/promise-semaphore')
98
156
  const semaphore = new Semaphore()
99
157
 
100
- async function downloadAndCache(url) {
101
-
102
- await semaphore.acquire(url)
103
-
104
- // This block continues once a lock on url is acquired. This permits
105
- // multiple simulataneous downloads for unique url values.
158
+ async function downloadAndSave(url) {
106
159
 
107
160
  try {
108
- const cacheFileName = getCacheFilePath(url)
109
-
110
- if (!await pathExists(cacheFilePath)) {
111
- await downloadToFile(url, cacheFilePath)
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)
112
173
  }
113
-
114
- return cacheFilePath
174
+
175
+ return filePath
115
176
 
116
177
  } finally {
117
178
  semaphore.release(url)
@@ -119,6 +180,26 @@ async function downloadAndCache(url) {
119
180
  }
120
181
  ```
121
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
+ }
201
+ ```
202
+
122
203
  ## License
123
204
 
124
205
  [MIT](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,23 +1,23 @@
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": "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
+ ]
23
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