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 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 & Semaphore patterns supported
12
+ - **Powerful**: Mutex, semaphore, and fair queuing
13
13
  - **Secure**: Auto lock release toa void dead locks
14
- - **Lightweight**: No dependencies, just ~50 LOC
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.1",
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
- if (entry.queue.size() === 0) drop(entry)
39
- last = entry.key
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 = (resolve, key) => {
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 (signal != null && typeof signal.addEventListener !== 'function') {
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()) return acquire(resolve, id)
70
+ if (!lock.isLocked()) {
71
+ last = id
72
+ return acquire(resolve)
73
+ }
62
74
 
63
- const waiter = { acquire: () => acquire(resolve, id) }
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.queue.remove(waiter.node)
80
+ const entry = waiter.entry
81
+ entry.queue.remove(waiter.node)
69
82
  waiting--
70
- if (waiter.entry.queue.size() === 0) drop(waiter.entry)
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, id)
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 FairLock = (signal: AbortSignal | undefined | null, key?: unknown) => Promise<Release | null>;
12
+ declare type FairQueue = (signal?: AbortSignal | null, key?: unknown) => Promise<Release | null>;
13
13
 
14
- interface FairLockInterface extends FairLock {
14
+ interface FairQueueInterface extends FairQueue {
15
15
  isLocked: () => boolean;
16
16
  awaiting: () => number;
17
17
  }
18
18
 
19
- declare function createFairLock(slots?: number): FairLockInterface;
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 createFairLock = require('./fair')
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
- module.exports = { withLock, createLock, createFairLock }
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
+ }