git-watchtower 2.3.16 → 2.3.17

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.
@@ -878,7 +878,7 @@ const { parseDiffStats, stash: gitStash, stashPop: gitStashPop } = require('../s
878
878
 
879
879
  // Server process command parsing and static server utilities
880
880
  const { parseCommand } = require('../src/server/process');
881
- const { getMimeType, injectLiveReload, resolveStaticPath } = require('../src/server/static');
881
+ const { getMimeType, injectLiveReload, resolveStaticPath, broadcastReload } = require('../src/server/static');
882
882
 
883
883
  // State (non-store globals)
884
884
  let previousBranchStates = new Map(); // branch name -> commit hash
@@ -2299,9 +2299,13 @@ function restartPolling() {
2299
2299
 
2300
2300
  function notifyClients() {
2301
2301
  if (NO_SERVER) return; // No clients in no-server mode
2302
- clients.forEach(client => client.write('data: reload\n\n'));
2303
- if (clients.size > 0) {
2304
- addLog(`Reloading ${clients.size} browser(s)`, 'info');
2302
+ if (clients.size === 0) return;
2303
+ const { delivered, dropped } = broadcastReload(clients);
2304
+ if (delivered > 0) {
2305
+ addLog(`Reloading ${delivered} browser(s)`, 'info');
2306
+ }
2307
+ if (dropped > 0) {
2308
+ addLog(`Dropped ${dropped} dead live-reload client(s)`, 'warning');
2305
2309
  }
2306
2310
  }
2307
2311
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "2.3.16",
3
+ "version": "2.3.17",
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": {
@@ -125,10 +125,50 @@ function resolveStaticPath(candidate, realStaticDir) {
125
125
  return { status: 'ok', path: realPath };
126
126
  }
127
127
 
128
+ /**
129
+ * Broadcast an SSE frame to every live-reload client, isolating per-client
130
+ * write failures so one dead socket can't abort the whole iteration.
131
+ *
132
+ * Previously this was a one-liner in bin/git-watchtower.js:
133
+ * clients.forEach(c => c.write('data: reload\n\n'));
134
+ * If any client's underlying socket had been destroyed (browser tab
135
+ * closed, network reset, proxy hangup) without 'close' firing yet, the
136
+ * synchronous write threw `ERR_STREAM_DESTROYED` and aborted the
137
+ * forEach mid-iteration — every later client in the Set never received
138
+ * the reload event. Wrapping each write in try/catch and removing the
139
+ * failed client from the Set keeps the broadcast atomic-per-client and
140
+ * also prunes the dead entry so the next call doesn't trip on it again.
141
+ *
142
+ * @param {Set<{write: function, end?: function}>} clients - SSE response objects
143
+ * @param {string} [frame='data: reload\n\n'] - Pre-formatted SSE frame
144
+ * @returns {{delivered: number, dropped: number}}
145
+ */
146
+ function broadcastReload(clients, frame) {
147
+ const message = frame || 'data: reload\n\n';
148
+ let delivered = 0;
149
+ let dropped = 0;
150
+ // Iterate a snapshot — Set.delete during forEach is safe in V8, but
151
+ // copying makes the contract explicit and survives any future
152
+ // iterator-protocol changes.
153
+ for (const client of Array.from(clients)) {
154
+ try {
155
+ client.write(message);
156
+ delivered++;
157
+ } catch (e) {
158
+ // Dead socket. Drop it and keep going so subsequent clients still
159
+ // see the broadcast.
160
+ clients.delete(client);
161
+ dropped++;
162
+ }
163
+ }
164
+ return { delivered, dropped };
165
+ }
166
+
128
167
  module.exports = {
129
168
  MIME_TYPES,
130
169
  getMimeType,
131
170
  LIVE_RELOAD_SCRIPT,
132
171
  injectLiveReload,
133
172
  resolveStaticPath,
173
+ broadcastReload,
134
174
  };