superlock 1.2.9 → 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 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.2.9",
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
- if (!this.head) {
12
- this.head = new Node(data)
13
- } else {
14
- let current = this.head
15
- while (current.next) {
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
- let count = 0
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
- const fn = queue.dequeue()
46
- if (fn !== undefined) fn()
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
- lock.isLocked() ? queue.enqueue(() => acquire(resolve)) : acquire(resolve)
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>) => 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 {