git-watchtower 1.14.18 → 2.0.1

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.
@@ -1753,7 +1753,7 @@ const pollMutex = new Mutex();
1753
1753
  async function pollGitChanges() {
1754
1754
  // Skip if a poll is already in progress (don't queue)
1755
1755
  if (pollMutex.isLocked()) return;
1756
- await pollMutex.acquire();
1756
+ const pollToken = await pollMutex.acquire();
1757
1757
  store.setState({ isPolling: true, pollingStatus: 'fetching' });
1758
1758
 
1759
1759
  // Casino mode: start slot reels spinning (no sound - too annoying)
@@ -2122,7 +2122,7 @@ async function pollGitChanges() {
2122
2122
  }
2123
2123
  } finally {
2124
2124
  store.setState({ isPolling: false });
2125
- pollMutex.release();
2125
+ pollMutex.release(pollToken);
2126
2126
  render();
2127
2127
  }
2128
2128
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.14.18",
3
+ "version": "2.0.1",
4
4
  "description": "Terminal-based Git branch monitor with activity sparklines and optional dev server with live reload",
5
5
  "main": "bin/git-watchtower.js",
6
6
  "bin": {
@@ -6,22 +6,33 @@
6
6
  /**
7
7
  * Simple mutex for preventing concurrent operations
8
8
  * Use this to prevent race conditions in polling and server operations
9
+ *
10
+ * Mutual exclusion is enforced by ownership tokens: acquire() returns a
11
+ * unique Symbol, and release() requires the caller to hand that same
12
+ * token back. Previously release() was just a zero-arg "drain the next
13
+ * waiter" operation — so a double-release or a stray release() without
14
+ * a matching acquire() would advance the queue twice, handing the lock
15
+ * to two owners concurrently and silently breaking the invariant the
16
+ * mutex exists to protect. Tokens make that misuse throw instead.
9
17
  */
10
18
  class Mutex {
11
19
  constructor() {
12
- this.locked = false;
20
+ /** @type {symbol | null} */
21
+ this._heldBy = null;
22
+ /** @type {Array<(token: symbol) => void>} */
13
23
  this.queue = [];
14
24
  }
15
25
 
16
26
  /**
17
- * Acquire the lock. Returns a promise that resolves when lock is acquired.
18
- * @returns {Promise<void>}
27
+ * Acquire the lock. Resolves with an opaque token that must be passed
28
+ * back to release(). Hold on to it and don't share it across callers.
29
+ * @returns {Promise<symbol>}
19
30
  */
20
31
  async acquire() {
21
32
  return new Promise((resolve) => {
22
- if (!this.locked) {
23
- this.locked = true;
24
- resolve();
33
+ if (this._heldBy === null) {
34
+ this._heldBy = Symbol('mutex-token');
35
+ resolve(this._heldBy);
25
36
  } else {
26
37
  this.queue.push(resolve);
27
38
  }
@@ -29,14 +40,28 @@ class Mutex {
29
40
  }
30
41
 
31
42
  /**
32
- * Release the lock. If there are waiting acquirers, the next one gets the lock.
43
+ * Release the lock. The token must be the one returned by the
44
+ * corresponding acquire(). Throws for:
45
+ * - release() on an unlocked mutex (release-without-acquire)
46
+ * - release() with a token that doesn't match the current holder
47
+ * (double-release, or release from the wrong caller)
48
+ *
49
+ * @param {symbol} token
33
50
  */
34
- release() {
51
+ release(token) {
52
+ if (this._heldBy === null) {
53
+ throw new Error('Mutex.release(): called on an unlocked mutex');
54
+ }
55
+ if (token !== this._heldBy) {
56
+ throw new Error('Mutex.release(): token does not match the current holder');
57
+ }
35
58
  if (this.queue.length > 0) {
36
59
  const next = this.queue.shift();
37
- next();
60
+ const nextToken = Symbol('mutex-token');
61
+ this._heldBy = nextToken;
62
+ next(nextToken);
38
63
  } else {
39
- this.locked = false;
64
+ this._heldBy = null;
40
65
  }
41
66
  }
42
67
 
@@ -47,11 +72,11 @@ class Mutex {
47
72
  * @returns {Promise<T>}
48
73
  */
49
74
  async withLock(fn) {
50
- await this.acquire();
75
+ const token = await this.acquire();
51
76
  try {
52
77
  return await fn();
53
78
  } finally {
54
- this.release();
79
+ this.release(token);
55
80
  }
56
81
  }
57
82
 
@@ -60,7 +85,7 @@ class Mutex {
60
85
  * @returns {boolean}
61
86
  */
62
87
  isLocked() {
63
- return this.locked;
88
+ return this._heldBy !== null;
64
89
  }
65
90
 
66
91
  /**