superlock 1.2.10 → 1.3.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.
- package/LICENSE.md +0 -0
- package/README.md +36 -0
- package/package.json +21 -16
- package/src/create.js +40 -23
- package/src/index.d.ts +4 -2
- package/src/index.js +3 -2
package/LICENSE.md
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -48,6 +48,26 @@ console.log(executions)
|
|
|
48
48
|
|
|
49
49
|
Just call `withLock(n)` being `n` the maximum of concurrency desired for the lock.
|
|
50
50
|
|
|
51
|
+
### with cancellation
|
|
52
|
+
|
|
53
|
+
Pass an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) to drop an acquisition that is still waiting in the queue — handy for shedding work whose caller already gave up (a timed-out request, a disconnected client):
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
const { withLock } = require('superlock')
|
|
57
|
+
|
|
58
|
+
const lock = withLock()
|
|
59
|
+
const controller = new AbortController()
|
|
60
|
+
|
|
61
|
+
const pending = lock(() => doWork(), controller.signal)
|
|
62
|
+
|
|
63
|
+
// caller no longer needs the result: give the queued slot back
|
|
64
|
+
controller.abort()
|
|
65
|
+
|
|
66
|
+
await pending // resolves `undefined`; `doWork` never runs
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If the signal aborts **before** the slot is granted, the acquisition is dropped: it never takes a slot and the wrapped function never runs. Aborting **after** the slot is granted is a no-op (the work is already running). Aborting is silent — it resolves instead of throwing — so shedding a lot of queued work never produces rejections to handle.
|
|
70
|
+
|
|
51
71
|
## API
|
|
52
72
|
|
|
53
73
|
### withLock([concurrency=1])
|
|
@@ -66,6 +86,16 @@ await lock(() => {
|
|
|
66
86
|
|
|
67
87
|
The lock will be automatically released after your code execution is done even if an error occurred, avoiding [deadlock](https://en.wikipedia.org/wiki/Deadlock) situations.
|
|
68
88
|
|
|
89
|
+
It also accepts an optional `AbortSignal` as second argument:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
await lock(() => {
|
|
93
|
+
/* your code execution */
|
|
94
|
+
}, signal)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
If `signal` aborts while the call is still queued, the function is skipped and the call resolves `undefined` instead of running. Aborting after the slot is granted has no effect.
|
|
98
|
+
|
|
69
99
|
#### concurrency
|
|
70
100
|
|
|
71
101
|
Type: `number`<br>
|
|
@@ -73,6 +103,12 @@ Default: `1`
|
|
|
73
103
|
|
|
74
104
|
It sets the maximum of concurrency allowed for the lock.
|
|
75
105
|
|
|
106
|
+
#### signal
|
|
107
|
+
|
|
108
|
+
Type: `AbortSignal`
|
|
109
|
+
|
|
110
|
+
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
|
+
|
|
76
112
|
### .isLocked()
|
|
77
113
|
|
|
78
114
|
Type: `boolean`
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "superlock",
|
|
3
3
|
"description": "The most simple mutex/semaphore implementation",
|
|
4
4
|
"homepage": "https://github.com/Kikobeats/superlock",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.3.0",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
7
7
|
"main": "src/index.js",
|
|
8
8
|
"exports": {
|
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
"name": "Kiko Beats",
|
|
15
15
|
"url": "https://kikobeats.com"
|
|
16
16
|
},
|
|
17
|
-
"contributors": [
|
|
17
|
+
"contributors": [
|
|
18
|
+
{
|
|
19
|
+
"name": "Tycho Bokdam",
|
|
20
|
+
"email": "tycho@palmtreecoding.com"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
18
23
|
"repository": {
|
|
19
24
|
"type": "git",
|
|
20
25
|
"url": "git+https://github.com/Kikobeats/superlock.git"
|
|
@@ -52,19 +57,6 @@
|
|
|
52
57
|
"files": [
|
|
53
58
|
"src"
|
|
54
59
|
],
|
|
55
|
-
"scripts": {
|
|
56
|
-
"clean": "rm -rf node_modules",
|
|
57
|
-
"contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true",
|
|
58
|
-
"coverage": "c8 report --reporter=text-lcov > coverage/lcov.info",
|
|
59
|
-
"lint": "standard",
|
|
60
|
-
"postrelease": "npm run release:tags && npm run release:github && (ci-publish || npm publish --access=public)",
|
|
61
|
-
"prerelease": "npm run contributors",
|
|
62
|
-
"pretest": "npm run lint",
|
|
63
|
-
"release": "standard-version -a",
|
|
64
|
-
"release:github": "github-generate-release",
|
|
65
|
-
"release:tags": "git push --follow-tags origin HEAD:master",
|
|
66
|
-
"test": "c8 ava"
|
|
67
|
-
},
|
|
68
60
|
"license": "MIT",
|
|
69
61
|
"ava": {
|
|
70
62
|
"files": [
|
|
@@ -94,5 +86,18 @@
|
|
|
94
86
|
"simple-git-hooks": {
|
|
95
87
|
"commit-msg": "npx commitlint --edit",
|
|
96
88
|
"pre-commit": "npx nano-staged"
|
|
89
|
+
},
|
|
90
|
+
"scripts": {
|
|
91
|
+
"clean": "rm -rf node_modules",
|
|
92
|
+
"contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true",
|
|
93
|
+
"coverage": "c8 report --reporter=text-lcov > coverage/lcov.info",
|
|
94
|
+
"lint": "standard",
|
|
95
|
+
"postrelease": "npm run release:tags && npm run release:github && (ci-publish || npm publish --access=public)",
|
|
96
|
+
"prerelease": "npm run contributors",
|
|
97
|
+
"pretest": "npm run lint",
|
|
98
|
+
"release": "standard-version -a",
|
|
99
|
+
"release:github": "github-generate-release",
|
|
100
|
+
"release:tags": "git push --follow-tags origin HEAD:master",
|
|
101
|
+
"test": "c8 ava"
|
|
97
102
|
}
|
|
98
|
-
}
|
|
103
|
+
}
|
package/src/create.js
CHANGED
|
@@ -7,43 +7,42 @@ class Node {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
class LinkedList {
|
|
10
|
+
constructor () {
|
|
11
|
+
this.length = 0
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
enqueue (data) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
current = current.next
|
|
17
|
-
}
|
|
18
|
-
current.next = new Node(data)
|
|
19
|
-
}
|
|
15
|
+
const node = new Node(data)
|
|
16
|
+
if (this.head) this.tail.next = node
|
|
17
|
+
else this.head = node
|
|
18
|
+
this.tail = node
|
|
19
|
+
this.length++
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
dequeue () {
|
|
23
23
|
if (!this.head) return
|
|
24
24
|
const data = this.head.data
|
|
25
25
|
this.head = this.head.next
|
|
26
|
+
this.length--
|
|
26
27
|
return data
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
size () {
|
|
30
|
-
|
|
31
|
-
let current = this.head
|
|
32
|
-
while (current) {
|
|
33
|
-
count++
|
|
34
|
-
current = current.next
|
|
35
|
-
}
|
|
36
|
-
return count
|
|
31
|
+
return this.length
|
|
37
32
|
}
|
|
38
33
|
}
|
|
39
34
|
|
|
40
35
|
module.exports = (slots = 1) => {
|
|
41
36
|
const queue = new LinkedList()
|
|
37
|
+
let cancelled = 0
|
|
42
38
|
|
|
43
39
|
const release = () => {
|
|
44
40
|
++slots
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
let waiter
|
|
42
|
+
while ((waiter = queue.dequeue()) !== undefined) {
|
|
43
|
+
if (waiter.cancelled) --cancelled
|
|
44
|
+
else return waiter.acquire()
|
|
45
|
+
}
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
const acquire = resolve => {
|
|
@@ -51,14 +50,32 @@ module.exports = (slots = 1) => {
|
|
|
51
50
|
resolve(release)
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
const lock =
|
|
55
|
-
new Promise(resolve =>
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
const lock = signal =>
|
|
54
|
+
new Promise(resolve => {
|
|
55
|
+
if (signal?.aborted) return resolve(null)
|
|
56
|
+
if (!lock.isLocked()) return acquire(resolve)
|
|
57
|
+
|
|
58
|
+
const waiter = { cancelled: false, acquire: () => acquire(resolve) }
|
|
59
|
+
|
|
60
|
+
if (signal !== undefined) {
|
|
61
|
+
const onAbort = () => {
|
|
62
|
+
waiter.cancelled = true
|
|
63
|
+
++cancelled
|
|
64
|
+
resolve(null)
|
|
65
|
+
}
|
|
66
|
+
waiter.acquire = () => {
|
|
67
|
+
signal.removeEventListener('abort', onAbort)
|
|
68
|
+
acquire(resolve)
|
|
69
|
+
}
|
|
70
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
queue.enqueue(waiter)
|
|
74
|
+
})
|
|
58
75
|
|
|
59
76
|
lock.isLocked = () => slots === 0
|
|
60
77
|
|
|
61
|
-
lock.awaiting = () => queue.size()
|
|
78
|
+
lock.awaiting = () => queue.size() - cancelled
|
|
62
79
|
|
|
63
80
|
return lock
|
|
64
81
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
declare type Release = () => void;
|
|
2
2
|
declare type Acquire = (resolve: (release: Release) => void) => void;
|
|
3
|
-
declare type Lock = () => Promise<Release>;
|
|
3
|
+
declare type Lock = (signal?: AbortSignal) => Promise<Release | null>;
|
|
4
4
|
|
|
5
5
|
interface LockInterface extends Lock {
|
|
6
6
|
isLocked: () => boolean;
|
|
7
|
+
awaiting: () => number;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
declare function createLock(slots?: number): LockInterface;
|
|
10
11
|
|
|
11
|
-
declare type WithLock = <T>(fn: () => Promise<T
|
|
12
|
+
declare type WithLock = <T>(fn: () => Promise<T>, signal?: AbortSignal) => Promise<T | undefined>;
|
|
12
13
|
|
|
13
14
|
interface WithLockInterface extends WithLock {
|
|
14
15
|
isLocked: () => boolean;
|
|
16
|
+
awaiting: () => number;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
declare function withLock(opts?: number): WithLockInterface;
|
package/src/index.js
CHANGED
|
@@ -5,8 +5,9 @@ const createLock = require('./create')
|
|
|
5
5
|
const withLock = opts => {
|
|
6
6
|
const lock = createLock(opts)
|
|
7
7
|
|
|
8
|
-
const withLock = async fn => {
|
|
9
|
-
const release = await lock()
|
|
8
|
+
const withLock = async (fn, signal) => {
|
|
9
|
+
const release = await lock(signal)
|
|
10
|
+
if (!release) return
|
|
10
11
|
try {
|
|
11
12
|
return await fn()
|
|
12
13
|
} finally {
|