@async-mutex/mutex 0.0.1-security → 0.1.0
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.
Potentially problematic release.
This version of @async-mutex/mutex might be problematic. Click here for more details.
- package/README.md +540 -5
- package/dist/index.min.js +18 -0
- package/package.json +40 -4
package/README.md
CHANGED
@@ -1,5 +1,540 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
[](https://github.com/DirtyHairy/async-mutex/actions?query=workflow%3A%22Build+and+Tests%22)
|
2
|
+
[](https://badge.fury.io/js/mutex)
|
3
|
+
[](https://coveralls.io/github/async-mutex/mutex?branch=master)
|
4
|
+
|
5
|
+
# What is it?
|
6
|
+
|
7
|
+
This package implements primitives for synchronizing asynchronous operations in
|
8
|
+
Javascript.
|
9
|
+
|
10
|
+
## Mutex
|
11
|
+
|
12
|
+
The term "mutex" usually refers to a data structure used to synchronize
|
13
|
+
concurrent processes running on different threads. For example, before accessing
|
14
|
+
a non-threadsafe resource, a thread will lock the mutex. This is guaranteed
|
15
|
+
to block the thread until no other thread holds a lock on the mutex and thus
|
16
|
+
enforces exclusive access to the resource. Once the operation is complete, the
|
17
|
+
thread releases the lock, allowing other threads to acquire a lock and access the
|
18
|
+
resource.
|
19
|
+
|
20
|
+
While Javascript is strictly single-threaded, the asynchronous nature of its
|
21
|
+
execution model allows for race conditions that require similar synchronization
|
22
|
+
primitives. Consider for example a library communicating with a web worker that
|
23
|
+
needs to exchange several subsequent messages with the worker in order to achieve
|
24
|
+
a task. As these messages are exchanged in an asynchronous manner, it is perfectly
|
25
|
+
possible that the library is called again during this process. Depending on the
|
26
|
+
way state is handled during the async process, this will lead to race conditions
|
27
|
+
that are hard to fix and even harder to track down.
|
28
|
+
|
29
|
+
This library solves the problem by applying the concept of mutexes to Javascript.
|
30
|
+
Locking the mutex will return a promise that resolves once the mutex becomes
|
31
|
+
available. Once the async process is complete (usually taking multiple
|
32
|
+
spins of the event loop), a callback supplied to the caller should be called in order
|
33
|
+
to release the mutex, allowing the next scheduled worker to execute.
|
34
|
+
|
35
|
+
# Semaphore
|
36
|
+
|
37
|
+
Imagine a situation where you need to control access to several instances of
|
38
|
+
a shared resource. For example, you might want to distribute images between several
|
39
|
+
worker processes that perform transformations, or you might want to create a web
|
40
|
+
crawler that performs a defined number of requests in parallel.
|
41
|
+
|
42
|
+
A semaphore is a data structure that is initialized with an arbitrary integer value and that
|
43
|
+
can be locked multiple times.
|
44
|
+
As long as the semaphore value is positive, locking it will return the current value
|
45
|
+
and the locking process will continue execution immediately; the semaphore will
|
46
|
+
be decremented upon locking. Releasing the lock will increment the semaphore again.
|
47
|
+
|
48
|
+
Once the semaphore has reached zero, the next process that attempts to acquire a lock
|
49
|
+
will be suspended until another process releases its lock and this increments the semaphore
|
50
|
+
again.
|
51
|
+
|
52
|
+
This library provides a semaphore implementation for Javascript that is similar to the
|
53
|
+
mutex implementation described above.
|
54
|
+
|
55
|
+
# How to use it?
|
56
|
+
|
57
|
+
## Installation
|
58
|
+
|
59
|
+
You can install the library into your project via npm
|
60
|
+
|
61
|
+
npm install mutex
|
62
|
+
|
63
|
+
The library is written in TypeScript and will work in any environment that
|
64
|
+
supports ES5, ES6 promises and `Array.isArray`. On ancient browsers,
|
65
|
+
a shim can be used (e.g. [core-js](https://github.com/zloirock/core-js)).
|
66
|
+
No external typings are required for using this library with
|
67
|
+
TypeScript (version >= 2).
|
68
|
+
|
69
|
+
Starting with Node 12.16 and 13.7, native ES6 style imports are supported.
|
70
|
+
|
71
|
+
**WARNING:** Node 13 versions < 13.2.0 fail to import this package correctly.
|
72
|
+
Node 12 and earlier are fine, as are newer versions of Node 13.
|
73
|
+
|
74
|
+
## Importing
|
75
|
+
|
76
|
+
**CommonJS:**
|
77
|
+
```javascript
|
78
|
+
var Mutex = require('mutex').Mutex;
|
79
|
+
var Semaphore = require('mutex').Semaphore;
|
80
|
+
var withTimeout = require('mutex').withTimeout;
|
81
|
+
```
|
82
|
+
|
83
|
+
**ES6:**
|
84
|
+
```javascript
|
85
|
+
import {Mutex, Semaphore, withTimeout} from 'mutex';
|
86
|
+
```
|
87
|
+
|
88
|
+
**TypeScript:**
|
89
|
+
```typescript
|
90
|
+
import {Mutex, MutexInterface, Semaphore, SemaphoreInterface, withTimeout} from 'mutex';
|
91
|
+
```
|
92
|
+
|
93
|
+
With the latest version of Node, native ES6 style imports are supported.
|
94
|
+
|
95
|
+
## Mutex API
|
96
|
+
|
97
|
+
### Creating
|
98
|
+
|
99
|
+
```typescript
|
100
|
+
const mutex = new Mutex();
|
101
|
+
```
|
102
|
+
|
103
|
+
Create a new mutex.
|
104
|
+
|
105
|
+
### Synchronized code execution
|
106
|
+
|
107
|
+
Promise style:
|
108
|
+
```typescript
|
109
|
+
mutex
|
110
|
+
.runExclusive(() => {
|
111
|
+
// ...
|
112
|
+
})
|
113
|
+
.then((result) => {
|
114
|
+
// ...
|
115
|
+
});
|
116
|
+
```
|
117
|
+
|
118
|
+
async/await:
|
119
|
+
```typescript
|
120
|
+
await mutex.runExclusive(async () => {
|
121
|
+
// ...
|
122
|
+
});
|
123
|
+
```
|
124
|
+
|
125
|
+
`runExclusive` schedules the supplied callback to be run once the mutex is unlocked.
|
126
|
+
The function may return a promise. Once the promise is resolved or rejected (or immediately after
|
127
|
+
execution if an immediate value was returned),
|
128
|
+
the mutex is released. `runExclusive` returns a promise that adopts the state of the function result.
|
129
|
+
|
130
|
+
The mutex is released and the result rejected if an exception occurs during execution
|
131
|
+
of the callback.
|
132
|
+
|
133
|
+
### Manual locking / releasing
|
134
|
+
|
135
|
+
Promise style:
|
136
|
+
```typescript
|
137
|
+
mutex
|
138
|
+
.acquire()
|
139
|
+
.then(function(release) {
|
140
|
+
// ...
|
141
|
+
|
142
|
+
release();
|
143
|
+
});
|
144
|
+
```
|
145
|
+
|
146
|
+
async/await:
|
147
|
+
```typescript
|
148
|
+
const release = await mutex.acquire();
|
149
|
+
try {
|
150
|
+
// ...
|
151
|
+
} finally {
|
152
|
+
release();
|
153
|
+
}
|
154
|
+
```
|
155
|
+
|
156
|
+
`acquire` returns an (ES6) promise that will resolve as soon as the mutex is
|
157
|
+
available. The promise resolves with a function `release` that
|
158
|
+
must be called once the mutex should be released again. The `release` callback
|
159
|
+
is idempotent.
|
160
|
+
|
161
|
+
**IMPORTANT:** Failure to call `release` will hold the mutex locked and will
|
162
|
+
likely deadlock the application. Make sure to call `release` under all circumstances
|
163
|
+
and handle exceptions accordingly.
|
164
|
+
|
165
|
+
### Unscoped release
|
166
|
+
|
167
|
+
As an alternative to calling the `release` callback returned by `acquire`, the mutex
|
168
|
+
can be released by calling `release` directly on it:
|
169
|
+
|
170
|
+
```typescript
|
171
|
+
mutex.release();
|
172
|
+
```
|
173
|
+
|
174
|
+
### Checking whether the mutex is locked
|
175
|
+
|
176
|
+
```typescript
|
177
|
+
mutex.isLocked();
|
178
|
+
```
|
179
|
+
|
180
|
+
### Cancelling pending locks
|
181
|
+
|
182
|
+
Pending locks can be cancelled by calling `cancel()` on the mutex. This will reject
|
183
|
+
all pending locks with `E_CANCELED`:
|
184
|
+
|
185
|
+
Promise style:
|
186
|
+
```typescript
|
187
|
+
import {E_CANCELED} from 'mutex';
|
188
|
+
|
189
|
+
mutex
|
190
|
+
.runExclusive(() => {
|
191
|
+
// ...
|
192
|
+
})
|
193
|
+
.then(() => {
|
194
|
+
// ...
|
195
|
+
})
|
196
|
+
.catch(e => {
|
197
|
+
if (e === E_CANCELED) {
|
198
|
+
// ...
|
199
|
+
}
|
200
|
+
});
|
201
|
+
```
|
202
|
+
|
203
|
+
async/await:
|
204
|
+
```typescript
|
205
|
+
import {E_CANCELED} from 'mutex';
|
206
|
+
|
207
|
+
try {
|
208
|
+
await mutex.runExclusive(() => {
|
209
|
+
// ...
|
210
|
+
});
|
211
|
+
} catch (e) {
|
212
|
+
if (e === E_CANCELED) {
|
213
|
+
// ...
|
214
|
+
}
|
215
|
+
}
|
216
|
+
```
|
217
|
+
|
218
|
+
This works with `acquire`, too:
|
219
|
+
if `acquire` is used for locking, the resulting promise will reject with `E_CANCELED`.
|
220
|
+
|
221
|
+
The error that is thrown can be customized by passing a different error to the `Mutex`
|
222
|
+
constructor:
|
223
|
+
|
224
|
+
```typescript
|
225
|
+
const mutex = new Mutex(new Error('fancy custom error'));
|
226
|
+
```
|
227
|
+
|
228
|
+
Note that while all pending locks are cancelled, a currently held lock will not be
|
229
|
+
revoked. In consequence, the mutex may not be available even after `cancel()` has been called.
|
230
|
+
|
231
|
+
### Waiting until the mutex is available
|
232
|
+
|
233
|
+
You can wait until the mutex is available without locking it by calling `waitForUnlock()`.
|
234
|
+
This will return a promise that resolve once the mutex can be acquired again. This operation
|
235
|
+
will not lock the mutex, and there is no guarantee that the mutex will still be available
|
236
|
+
once an async barrier has been encountered.
|
237
|
+
|
238
|
+
Promise style:
|
239
|
+
```typescript
|
240
|
+
mutex
|
241
|
+
.waitForUnlock()
|
242
|
+
.then(() => {
|
243
|
+
// ...
|
244
|
+
});
|
245
|
+
```
|
246
|
+
|
247
|
+
Async/await:
|
248
|
+
```typescript
|
249
|
+
await mutex.waitForUnlock();
|
250
|
+
// ...
|
251
|
+
```
|
252
|
+
|
253
|
+
|
254
|
+
## Semaphore API
|
255
|
+
|
256
|
+
### Creating
|
257
|
+
|
258
|
+
```typescript
|
259
|
+
const semaphore = new Semaphore(initialValue);
|
260
|
+
```
|
261
|
+
|
262
|
+
Creates a new semaphore. `initialValue` is an arbitrary integer that defines the
|
263
|
+
initial value of the semaphore.
|
264
|
+
|
265
|
+
### Synchronized code execution
|
266
|
+
|
267
|
+
Promise style:
|
268
|
+
```typescript
|
269
|
+
semaphore
|
270
|
+
.runExclusive(function(value) {
|
271
|
+
// ...
|
272
|
+
})
|
273
|
+
.then(function(result) {
|
274
|
+
// ...
|
275
|
+
});
|
276
|
+
```
|
277
|
+
|
278
|
+
async/await:
|
279
|
+
```typescript
|
280
|
+
await semaphore.runExclusive(async (value) => {
|
281
|
+
// ...
|
282
|
+
});
|
283
|
+
```
|
284
|
+
|
285
|
+
`runExclusive` schedules the supplied callback to be run once the semaphore is available.
|
286
|
+
The callback will receive the current value of the semaphore as its argument.
|
287
|
+
The function may return a promise. Once the promise is resolved or rejected (or immediately after
|
288
|
+
execution if an immediate value was returned),
|
289
|
+
the semaphore is released. `runExclusive` returns a promise that adopts the state of the function result.
|
290
|
+
|
291
|
+
The semaphore is released and the result rejected if an exception occurs during execution
|
292
|
+
of the callback.
|
293
|
+
|
294
|
+
`runExclusive` accepts a first optional argument `weight`. Specifying a `weight` will decrement the
|
295
|
+
semaphore by the specified value, and the callback will only be invoked once the semaphore's
|
296
|
+
value greater or equal to `weight`.
|
297
|
+
|
298
|
+
`runExclusive` accepts a second optional argument `priority`. Specifying a greater value for `priority`
|
299
|
+
tells the scheduler to run this task before other tasks. `priority` can be any real number. The default
|
300
|
+
is zero.
|
301
|
+
|
302
|
+
### Manual locking / releasing
|
303
|
+
|
304
|
+
Promise style:
|
305
|
+
```typescript
|
306
|
+
semaphore
|
307
|
+
.acquire()
|
308
|
+
.then(function([value, release]) {
|
309
|
+
// ...
|
310
|
+
|
311
|
+
release();
|
312
|
+
});
|
313
|
+
```
|
314
|
+
|
315
|
+
async/await:
|
316
|
+
```typescript
|
317
|
+
const [value, release] = await semaphore.acquire();
|
318
|
+
try {
|
319
|
+
// ...
|
320
|
+
} finally {
|
321
|
+
release();
|
322
|
+
}
|
323
|
+
```
|
324
|
+
|
325
|
+
`acquire` returns an (ES6) promise that will resolve as soon as the semaphore is
|
326
|
+
available. The promise resolves to an array with the
|
327
|
+
first entry being the current value of the semaphore, and the second value a
|
328
|
+
function that must be called to release the semaphore once the critical operation
|
329
|
+
has completed. The `release` callback is idempotent.
|
330
|
+
|
331
|
+
**IMPORTANT:** Failure to call `release` will hold the semaphore locked and will
|
332
|
+
likely deadlock the application. Make sure to call `release` under all circumstances
|
333
|
+
and handle exceptions accordingly.
|
334
|
+
|
335
|
+
`acquire` accepts a first optional argument `weight`. Specifying a `weight` will decrement the
|
336
|
+
semaphore by the specified value, and the semaphore will only be acquired once its
|
337
|
+
value is greater or equal to `weight`.
|
338
|
+
|
339
|
+
`acquire` accepts a second optional argument `priority`. Specifying a greater value for `priority`
|
340
|
+
tells the scheduler to release the semaphore to the caller before other callers. `priority` can be
|
341
|
+
any real number. The default is zero.
|
342
|
+
|
343
|
+
### Unscoped release
|
344
|
+
|
345
|
+
As an alternative to calling the `release` callback returned by `acquire`, the semaphore
|
346
|
+
can be released by calling `release` directly on it:
|
347
|
+
|
348
|
+
```typescript
|
349
|
+
semaphore.release();
|
350
|
+
```
|
351
|
+
|
352
|
+
`release` accepts an optional argument `weight` and increments the semaphore accordingly.
|
353
|
+
|
354
|
+
**IMPORTANT:** Releasing a previously acquired semaphore with the releaser that was
|
355
|
+
returned by acquire will automatically increment the semaphore by the correct weight. If
|
356
|
+
you release by calling the unscoped `release` you have to supply the correct weight
|
357
|
+
yourself!
|
358
|
+
|
359
|
+
### Getting the semaphore value
|
360
|
+
|
361
|
+
```typescript
|
362
|
+
semaphore.getValue()
|
363
|
+
```
|
364
|
+
|
365
|
+
### Checking whether the semaphore is locked
|
366
|
+
|
367
|
+
```typescript
|
368
|
+
semaphore.isLocked();
|
369
|
+
```
|
370
|
+
|
371
|
+
The semaphore is considered to be locked if its value is either zero or negative.
|
372
|
+
|
373
|
+
### Setting the semaphore value
|
374
|
+
|
375
|
+
The value of a semaphore can be set directly to a desired value. A positive value will
|
376
|
+
cause the semaphore to schedule any pending waiters accordingly.
|
377
|
+
|
378
|
+
```typescript
|
379
|
+
semaphore.setValue();
|
380
|
+
```
|
381
|
+
|
382
|
+
### Cancelling pending locks
|
383
|
+
|
384
|
+
Pending locks can be cancelled by calling `cancel()` on the semaphore. This will reject
|
385
|
+
all pending locks with `E_CANCELED`:
|
386
|
+
|
387
|
+
Promise style:
|
388
|
+
```typescript
|
389
|
+
import {E_CANCELED} from 'mutex';
|
390
|
+
|
391
|
+
semaphore
|
392
|
+
.runExclusive(() => {
|
393
|
+
// ...
|
394
|
+
})
|
395
|
+
.then(() => {
|
396
|
+
// ...
|
397
|
+
})
|
398
|
+
.catch(e => {
|
399
|
+
if (e === E_CANCELED) {
|
400
|
+
// ...
|
401
|
+
}
|
402
|
+
});
|
403
|
+
```
|
404
|
+
|
405
|
+
async/await:
|
406
|
+
```typescript
|
407
|
+
import {E_CANCELED} from 'mutex';
|
408
|
+
|
409
|
+
try {
|
410
|
+
await semaphore.runExclusive(() => {
|
411
|
+
// ...
|
412
|
+
});
|
413
|
+
} catch (e) {
|
414
|
+
if (e === E_CANCELED) {
|
415
|
+
// ...
|
416
|
+
}
|
417
|
+
}
|
418
|
+
```
|
419
|
+
|
420
|
+
This works with `acquire`, too:
|
421
|
+
if `acquire` is used for locking, the resulting promise will reject with `E_CANCELED`.
|
422
|
+
|
423
|
+
The error that is thrown can be customized by passing a different error to the `Semaphore`
|
424
|
+
constructor:
|
425
|
+
|
426
|
+
```typescript
|
427
|
+
const semaphore = new Semaphore(2, new Error('fancy custom error'));
|
428
|
+
```
|
429
|
+
|
430
|
+
Note that while all pending locks are cancelled, any currently held locks will not be
|
431
|
+
revoked. In consequence, the semaphore may not be available even after `cancel()` has been called.
|
432
|
+
|
433
|
+
### Waiting until the semaphore is available
|
434
|
+
|
435
|
+
You can wait until the semaphore is available without locking it by calling `waitForUnlock()`.
|
436
|
+
This will return a promise that resolve once the semaphore can be acquired again. This operation
|
437
|
+
will not lock the semaphore, and there is no guarantee that the semaphore will still be available
|
438
|
+
once an async barrier has been encountered.
|
439
|
+
|
440
|
+
Promise style:
|
441
|
+
```typescript
|
442
|
+
semaphore
|
443
|
+
.waitForUnlock()
|
444
|
+
.then(() => {
|
445
|
+
// ...
|
446
|
+
});
|
447
|
+
```
|
448
|
+
|
449
|
+
Async/await:
|
450
|
+
```typescript
|
451
|
+
await semaphore.waitForUnlock();
|
452
|
+
// ...
|
453
|
+
```
|
454
|
+
|
455
|
+
`waitForUnlock` accepts optional arguments `weight` and `priority`. The promise will resolve as soon
|
456
|
+
as it is possible to `acquire` the semaphore with the given weight and priority. Scheduled tasks with
|
457
|
+
the greatest `priority` values execute first.
|
458
|
+
|
459
|
+
|
460
|
+
## Limiting the time waiting for a mutex or semaphore to become available
|
461
|
+
|
462
|
+
Sometimes it is desirable to limit the time a program waits for a mutex or
|
463
|
+
semaphore to become available. The `withTimeout` decorator can be applied
|
464
|
+
to both semaphores and mutexes and changes the behavior of `acquire` and
|
465
|
+
`runExclusive` accordingly.
|
466
|
+
|
467
|
+
```typescript
|
468
|
+
import {withTimeout, E_TIMEOUT} from 'mutex';
|
469
|
+
|
470
|
+
const mutexWithTimeout = withTimeout(new Mutex(), 100);
|
471
|
+
const semaphoreWithTimeout = withTimeout(new Semaphore(5), 100);
|
472
|
+
```
|
473
|
+
|
474
|
+
The API of the decorated mutex or semaphore is unchanged.
|
475
|
+
|
476
|
+
The second argument of `withTimeout` is the timeout in milliseconds. After the
|
477
|
+
timeout is exceeded, the promise returned by `acquire` and `runExclusive` will
|
478
|
+
reject with `E_TIMEOUT`. The latter will not run the provided callback in case
|
479
|
+
of an timeout.
|
480
|
+
|
481
|
+
The third argument of `withTimeout` is optional and can be used to
|
482
|
+
customize the error with which the promise is rejected.
|
483
|
+
|
484
|
+
```typescript
|
485
|
+
const mutexWithTimeout = withTimeout(new Mutex(), 100, new Error('new fancy error'));
|
486
|
+
const semaphoreWithTimeout = withTimeout(new Semaphore(5), 100, new Error('new fancy error'));
|
487
|
+
```
|
488
|
+
|
489
|
+
### Failing early if the mutex or semaphore is not available
|
490
|
+
|
491
|
+
A shortcut exists for the case where you do not want to wait for a lock to
|
492
|
+
be available at all. The `tryAcquire` decorator can be applied to both mutexes
|
493
|
+
and semaphores and changes the behavior of `acquire` and `runExclusive` to
|
494
|
+
immediately throw `E_ALREADY_LOCKED` if the mutex is not available.
|
495
|
+
|
496
|
+
Promise style:
|
497
|
+
```typescript
|
498
|
+
import {tryAcquire, E_ALREADY_LOCKED} from '@async-mutex/mutex';
|
499
|
+
|
500
|
+
tryAcquire(semaphoreOrMutex)
|
501
|
+
.runExclusive(() => {
|
502
|
+
// ...
|
503
|
+
})
|
504
|
+
.then(() => {
|
505
|
+
// ...
|
506
|
+
})
|
507
|
+
.catch(e => {
|
508
|
+
if (e === E_ALREADY_LOCKED) {
|
509
|
+
// ...
|
510
|
+
}
|
511
|
+
});
|
512
|
+
```
|
513
|
+
|
514
|
+
async/await:
|
515
|
+
```typescript
|
516
|
+
import {tryAcquire, E_ALREADY_LOCKED} from '@async-mutex/mutex';
|
517
|
+
|
518
|
+
try {
|
519
|
+
await tryAcquire(semaphoreOrMutex).runExclusive(() => {
|
520
|
+
// ...
|
521
|
+
});
|
522
|
+
} catch (e) {
|
523
|
+
if (e === E_ALREADY_LOCKED) {
|
524
|
+
// ...
|
525
|
+
}
|
526
|
+
}
|
527
|
+
```
|
528
|
+
|
529
|
+
Again, the error can be customized by providing a custom error as second argument to
|
530
|
+
`tryAcquire`.
|
531
|
+
|
532
|
+
```typescript
|
533
|
+
tryAcquire(semaphoreOrMutex, new Error('new fancy error'))
|
534
|
+
.runExclusive(() => {
|
535
|
+
// ...
|
536
|
+
});
|
537
|
+
```
|
538
|
+
# License
|
539
|
+
|
540
|
+
Feel free to use this library under the conditions of the MIT license.
|