superlock 1.4.1 → 1.4.2
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/README.md +151 -4
- package/package.json +1 -1
- package/src/fair.js +27 -10
- package/src/index.d.ts +17 -4
- package/src/index.js +27 -2
package/README.md
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
**superlock** aims to be:
|
|
10
10
|
|
|
11
11
|
- **Simple**: Designed for usage with `async` and `await`
|
|
12
|
-
- **Powerful**: Mutex
|
|
12
|
+
- **Powerful**: Mutex, semaphore, and fair queuing
|
|
13
13
|
- **Secure**: Auto lock release toa void dead locks
|
|
14
|
-
- **Lightweight**: No dependencies
|
|
14
|
+
- **Lightweight**: No dependencies
|
|
15
15
|
- **Well-tested**: 100% code coverage
|
|
16
16
|
|
|
17
17
|
## Install
|
|
@@ -46,7 +46,43 @@ console.log(executions)
|
|
|
46
46
|
|
|
47
47
|
### as semaphore
|
|
48
48
|
|
|
49
|
-
Just call `withLock(n)` being `n` the maximum of concurrency desired for the lock
|
|
49
|
+
Just call `withLock(n)` being `n` the maximum of concurrency desired for the lock:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
const { setTimeout } = require('timers/promises')
|
|
53
|
+
const { withLock } = require('superlock')
|
|
54
|
+
|
|
55
|
+
const lock = withLock(2)
|
|
56
|
+
|
|
57
|
+
const executions = await Promise.all(
|
|
58
|
+
[...Array(10).keys()].map(index =>
|
|
59
|
+
lock(async () => {
|
|
60
|
+
await setTimeout(Math.random() * 100)
|
|
61
|
+
return index
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
console.log(executions)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### as fair queue
|
|
70
|
+
|
|
71
|
+
`withLock` and `createLock` are one FIFO queue. A caller that enqueues a lot of work keeps taking the next slot.
|
|
72
|
+
|
|
73
|
+
`withFairQueue` is [fair queuing](https://en.wikipedia.org/wiki/Fair_queuing): one queue per key, and a free slot rotates across those keys. The same key is still served in arrival order. Callers that omit the key share one queue.
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
const { withFairQueue } = require('superlock')
|
|
77
|
+
|
|
78
|
+
const queue = withFairQueue()
|
|
79
|
+
|
|
80
|
+
await queue(async () => {
|
|
81
|
+
/* your code execution */
|
|
82
|
+
}, userId)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
A second queued call for the same user waits until each other waiting user has had a turn. Pass an `AbortSignal` as the third argument when the caller can give up. Aborting while the call is queued skips the function.
|
|
50
86
|
|
|
51
87
|
### with cancellation
|
|
52
88
|
|
|
@@ -109,11 +145,122 @@ Type: `AbortSignal`
|
|
|
109
145
|
|
|
110
146
|
When provided, aborting it drops the acquisition while it is still waiting in the queue: no slot is taken and the wrapped function never runs.
|
|
111
147
|
|
|
148
|
+
### createLock([slots=1])
|
|
149
|
+
|
|
150
|
+
The FIFO semaphore behind `withLock`. The promise resolves to a release function you call when the work is done:
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
const { createLock } = require('superlock')
|
|
154
|
+
|
|
155
|
+
const lock = createLock()
|
|
156
|
+
const release = await lock()
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
/* your code execution */
|
|
160
|
+
} finally {
|
|
161
|
+
release()
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
An optional `AbortSignal` is the first argument. If it aborts while the call is queued, the promise resolves `null` and no slot is taken.
|
|
166
|
+
|
|
167
|
+
#### slots
|
|
168
|
+
|
|
169
|
+
Type: `number`<br>
|
|
170
|
+
Default: `1`
|
|
171
|
+
|
|
172
|
+
How many callers can hold the lock at once.
|
|
173
|
+
|
|
174
|
+
### createFairQueue([slots=1])
|
|
175
|
+
|
|
176
|
+
The fair queue behind `withFairQueue`, in the same way `createLock` is behind `withLock`. The promise resolves to a release function you call when the work is done. `createFairLock` is this function.
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
const { createFairQueue } = require('superlock')
|
|
180
|
+
|
|
181
|
+
const queue = createFairQueue()
|
|
182
|
+
const release = await queue(undefined, key)
|
|
183
|
+
|
|
184
|
+
release()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
One key is served in arrival order. Across keys, a free slot goes to the next key that is waiting, then wraps around. A key with nobody left waiting leaves the rotation.
|
|
188
|
+
|
|
189
|
+
A missing key shares one queue. Any other value, including `null`, is its own queue. `null` and a missing signal do not cancel. A value that is not an `AbortSignal` throws `TypeError`. Aborting after the slot was granted does nothing.
|
|
190
|
+
|
|
191
|
+
#### slots
|
|
192
|
+
|
|
193
|
+
Type: `number`<br>
|
|
194
|
+
Default: `1`
|
|
195
|
+
|
|
196
|
+
How many callers can hold the queue at once.
|
|
197
|
+
|
|
198
|
+
#### signal
|
|
199
|
+
|
|
200
|
+
Type: `AbortSignal`
|
|
201
|
+
|
|
202
|
+
Optional first argument. Aborting it while the call is queued resolves `null` and does not take a slot.
|
|
203
|
+
|
|
204
|
+
#### key
|
|
205
|
+
|
|
206
|
+
Type: any
|
|
207
|
+
|
|
208
|
+
Optional second argument. Identifies the queue.
|
|
209
|
+
|
|
210
|
+
### withFairQueue([slots=1])
|
|
211
|
+
|
|
212
|
+
It returns a function that runs your code under [fair queuing](https://en.wikipedia.org/wiki/Fair_queuing). The queue is released when the function finishes, including when it throws.
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
const { withFairQueue } = require('superlock')
|
|
216
|
+
|
|
217
|
+
const queue = withFairQueue()
|
|
218
|
+
|
|
219
|
+
await queue(() => {
|
|
220
|
+
/* your code execution */
|
|
221
|
+
}, key)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
An `AbortSignal` is an optional third argument:
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
await queue(() => {
|
|
228
|
+
/* your code execution */
|
|
229
|
+
}, key, signal)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
If `signal` aborts while the call is still queued, the function is skipped and the call resolves `undefined`.
|
|
233
|
+
|
|
234
|
+
#### slots
|
|
235
|
+
|
|
236
|
+
Type: `number`<br>
|
|
237
|
+
Default: `1`
|
|
238
|
+
|
|
239
|
+
How many callers can hold the queue at once.
|
|
240
|
+
|
|
241
|
+
#### key
|
|
242
|
+
|
|
243
|
+
Type: any
|
|
244
|
+
|
|
245
|
+
Optional second argument. Identifies the queue. A missing key shares one queue.
|
|
246
|
+
|
|
247
|
+
#### signal
|
|
248
|
+
|
|
249
|
+
Type: `AbortSignal`
|
|
250
|
+
|
|
251
|
+
Optional third argument. Aborting it while the call is queued skips the function.
|
|
252
|
+
|
|
112
253
|
### .isLocked()
|
|
113
254
|
|
|
114
255
|
Type: `boolean`
|
|
115
256
|
|
|
116
|
-
It returns `false` if there is at least one free concurrency slots in the lock.
|
|
257
|
+
It returns `false` if there is at least one free concurrency slots in the lock. `createLock`, `createFairQueue`, and `withFairQueue` expose it too.
|
|
258
|
+
|
|
259
|
+
### .awaiting()
|
|
260
|
+
|
|
261
|
+
Type: `number`
|
|
262
|
+
|
|
263
|
+
How many callers are waiting for a slot. On a fair queue that is the total across every key.
|
|
117
264
|
|
|
118
265
|
## License
|
|
119
266
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "superlock",
|
|
3
3
|
"description": "Minimal async mutex and semaphore for serializing critical sections in Node.js.",
|
|
4
4
|
"homepage": "https://github.com/Kikobeats/superlock",
|
|
5
|
-
"version": "1.4.
|
|
5
|
+
"version": "1.4.2",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
7
7
|
"main": "src/index.js",
|
|
8
8
|
"exports": {
|
package/src/fair.js
CHANGED
|
@@ -12,8 +12,10 @@ module.exports = (slots = 1) => {
|
|
|
12
12
|
let last = NONE
|
|
13
13
|
|
|
14
14
|
const drop = entry => {
|
|
15
|
+
const predecessor = entry.ringNode.prev
|
|
15
16
|
ring.remove(entry.ringNode)
|
|
16
17
|
queues.delete(entry.key)
|
|
18
|
+
return predecessor ? predecessor.data : NONE
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
const enqueue = (key, waiter) => {
|
|
@@ -35,8 +37,11 @@ module.exports = (slots = 1) => {
|
|
|
35
37
|
const entry = queues.get(node.data)
|
|
36
38
|
const waiter = entry.queue.dequeue()
|
|
37
39
|
waiting--
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
// A key that just left the ring must not stay as `last`. The next grant
|
|
41
|
+
// would miss it in the map and restart at the head, skipping whoever
|
|
42
|
+
// was behind it.
|
|
43
|
+
if (entry.queue.size() === 0) last = drop(entry)
|
|
44
|
+
else last = entry.key
|
|
40
45
|
waiter.acquire()
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -45,34 +50,46 @@ module.exports = (slots = 1) => {
|
|
|
45
50
|
grant()
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
const acquire =
|
|
53
|
+
const acquire = resolve => {
|
|
49
54
|
--slots
|
|
50
|
-
last = key
|
|
51
55
|
resolve(release)
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
const lock = (signal, key) =>
|
|
55
59
|
new Promise(resolve => {
|
|
56
|
-
if (
|
|
60
|
+
if (
|
|
61
|
+
signal != null &&
|
|
62
|
+
(typeof signal.addEventListener !== 'function' ||
|
|
63
|
+
typeof signal.removeEventListener !== 'function' ||
|
|
64
|
+
typeof signal.aborted !== 'boolean')
|
|
65
|
+
) {
|
|
57
66
|
throw new TypeError('`signal` needs to be an AbortSignal.')
|
|
58
67
|
}
|
|
59
68
|
const id = key === undefined ? SHARED : key
|
|
60
69
|
if (signal?.aborted) return resolve(null)
|
|
61
|
-
if (!lock.isLocked())
|
|
70
|
+
if (!lock.isLocked()) {
|
|
71
|
+
last = id
|
|
72
|
+
return acquire(resolve)
|
|
73
|
+
}
|
|
62
74
|
|
|
63
|
-
const waiter = { acquire: () => acquire(resolve
|
|
75
|
+
const waiter = { acquire: () => acquire(resolve) }
|
|
64
76
|
enqueue(id, waiter)
|
|
65
77
|
|
|
66
78
|
if (signal != null) {
|
|
67
79
|
const onAbort = () => {
|
|
68
|
-
waiter.entry
|
|
80
|
+
const entry = waiter.entry
|
|
81
|
+
entry.queue.remove(waiter.node)
|
|
69
82
|
waiting--
|
|
70
|
-
if (
|
|
83
|
+
if (entry.queue.size() === 0) {
|
|
84
|
+
const current = queues.get(last) === entry
|
|
85
|
+
const cursor = drop(entry)
|
|
86
|
+
if (current) last = cursor
|
|
87
|
+
}
|
|
71
88
|
resolve(null)
|
|
72
89
|
}
|
|
73
90
|
waiter.acquire = () => {
|
|
74
91
|
signal.removeEventListener('abort', onAbort)
|
|
75
|
-
acquire(resolve
|
|
92
|
+
acquire(resolve)
|
|
76
93
|
}
|
|
77
94
|
signal.addEventListener('abort', onAbort, { once: true })
|
|
78
95
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -9,14 +9,27 @@ interface LockInterface extends Lock {
|
|
|
9
9
|
|
|
10
10
|
declare function createLock(slots?: number): LockInterface;
|
|
11
11
|
|
|
12
|
-
declare type
|
|
12
|
+
declare type FairQueue = (signal?: AbortSignal | null, key?: unknown) => Promise<Release | null>;
|
|
13
13
|
|
|
14
|
-
interface
|
|
14
|
+
interface FairQueueInterface extends FairQueue {
|
|
15
15
|
isLocked: () => boolean;
|
|
16
16
|
awaiting: () => number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
declare function
|
|
19
|
+
declare function createFairQueue(slots?: number): FairQueueInterface;
|
|
20
|
+
|
|
21
|
+
declare function createFairLock(slots?: number): FairQueueInterface;
|
|
22
|
+
|
|
23
|
+
interface FairLockInterface extends FairQueueInterface {}
|
|
24
|
+
|
|
25
|
+
declare type WithFairQueue = <T>(fn: () => Promise<T>, key?: unknown, signal?: AbortSignal | null) => Promise<T | undefined>;
|
|
26
|
+
|
|
27
|
+
interface WithFairQueueInterface extends WithFairQueue {
|
|
28
|
+
isLocked: () => boolean;
|
|
29
|
+
awaiting: () => number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
declare function withFairQueue(slots?: number): WithFairQueueInterface;
|
|
20
33
|
|
|
21
34
|
declare type WithLock = <T>(fn: () => Promise<T>, signal?: AbortSignal) => Promise<T | undefined>;
|
|
22
35
|
|
|
@@ -27,4 +40,4 @@ interface WithLockInterface extends WithLock {
|
|
|
27
40
|
|
|
28
41
|
declare function withLock(opts?: number): WithLockInterface;
|
|
29
42
|
|
|
30
|
-
export { withLock, createLock, createFairLock, WithLockInterface, FairLockInterface };
|
|
43
|
+
export { withLock, createLock, withFairQueue, createFairQueue, createFairLock, WithLockInterface, WithFairQueueInterface, FairQueueInterface, FairLockInterface };
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const createFairQueue = require('./fair')
|
|
4
4
|
const createLock = require('./create')
|
|
5
5
|
|
|
6
6
|
const withLock = opts => {
|
|
@@ -22,4 +22,29 @@ const withLock = opts => {
|
|
|
22
22
|
return withLock
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
const withFairQueue = opts => {
|
|
26
|
+
const lock = createFairQueue(opts)
|
|
27
|
+
|
|
28
|
+
const withFairQueue = async (fn, key, signal) => {
|
|
29
|
+
const release = await lock(signal, key)
|
|
30
|
+
if (!release) return
|
|
31
|
+
try {
|
|
32
|
+
return await fn()
|
|
33
|
+
} finally {
|
|
34
|
+
release()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
withFairQueue.isLocked = lock.isLocked
|
|
39
|
+
withFairQueue.awaiting = lock.awaiting
|
|
40
|
+
|
|
41
|
+
return withFairQueue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
withLock,
|
|
46
|
+
createLock,
|
|
47
|
+
withFairQueue,
|
|
48
|
+
createFairQueue,
|
|
49
|
+
createFairLock: createFairQueue
|
|
50
|
+
}
|