git-watchtower 2.0.0 → 2.0.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/bin/git-watchtower.js +2 -2
- package/package.json +1 -1
- package/src/utils/async.js +58 -16
package/bin/git-watchtower.js
CHANGED
|
@@ -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
package/src/utils/async.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
18
|
-
*
|
|
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 (
|
|
23
|
-
this.
|
|
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.
|
|
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
|
-
|
|
60
|
+
const nextToken = Symbol('mutex-token');
|
|
61
|
+
this._heldBy = nextToken;
|
|
62
|
+
next(nextToken);
|
|
38
63
|
} else {
|
|
39
|
-
this.
|
|
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.
|
|
88
|
+
return this._heldBy !== null;
|
|
64
89
|
}
|
|
65
90
|
|
|
66
91
|
/**
|
|
@@ -181,17 +206,24 @@ function debounce(fn, delay) {
|
|
|
181
206
|
|
|
182
207
|
/**
|
|
183
208
|
* Throttle a function - execute at most once per interval
|
|
209
|
+
*
|
|
210
|
+
* Mirrors debounce() in returning a wrapped function with a `.cancel()`
|
|
211
|
+
* method. If a trailing call has been scheduled (throttle fires on the
|
|
212
|
+
* leading edge and coalesces further calls during the interval into a
|
|
213
|
+
* single trailing call), cancel() drops it. Wire cancel() into shutdown
|
|
214
|
+
* paths so a UI-event-driven throttle can't fire a deferred call after
|
|
215
|
+
* the target module has torn down and leave state partially written.
|
|
216
|
+
*
|
|
184
217
|
* @template {(...args: any[]) => void} T
|
|
185
218
|
* @param {T} fn - Function to throttle
|
|
186
219
|
* @param {number} interval - Minimum interval between calls in milliseconds
|
|
187
|
-
* @returns {T}
|
|
220
|
+
* @returns {T & { cancel: () => void }}
|
|
188
221
|
*/
|
|
189
222
|
function throttle(fn, interval) {
|
|
190
223
|
let lastCall = 0;
|
|
191
224
|
let timeoutId = null;
|
|
192
225
|
|
|
193
|
-
|
|
194
|
-
return (...args) => {
|
|
226
|
+
const throttled = (...args) => {
|
|
195
227
|
const now = Date.now();
|
|
196
228
|
const timeSinceLastCall = now - lastCall;
|
|
197
229
|
|
|
@@ -207,6 +239,16 @@ function throttle(fn, interval) {
|
|
|
207
239
|
}, interval - timeSinceLastCall);
|
|
208
240
|
}
|
|
209
241
|
};
|
|
242
|
+
|
|
243
|
+
throttled.cancel = () => {
|
|
244
|
+
if (timeoutId) {
|
|
245
|
+
clearTimeout(timeoutId);
|
|
246
|
+
timeoutId = null;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// @ts-ignore - TypeScript can't verify generic function augmentation
|
|
251
|
+
return throttled;
|
|
210
252
|
}
|
|
211
253
|
|
|
212
254
|
module.exports = {
|