herdr-remote 0.2.11 → 0.2.12

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/dist/tui.mjs CHANGED
@@ -4015,7 +4015,7 @@ function About({ ctx }) {
4015
4015
  children: updateLabel
4016
4016
  }
4017
4017
  ) }),
4018
- /* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.11" }) }),
4018
+ /* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.12" }) }),
4019
4019
  /* @__PURE__ */ jsx10(Row, { label: t("about.configPath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.configPath)() }) }),
4020
4020
  /* @__PURE__ */ jsx10(Row, { label: t("about.statePath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.stateDir)() }) }),
4021
4021
  /* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("about.docs") }) }),
package/herdr-plugin.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  id = "herdr.remote.web"
2
2
  name = "Herdr Remote Web"
3
- version = "0.2.11"
3
+ version = "0.2.12"
4
4
  min_herdr_version = "0.9.0"
5
5
  description = "Browser access to Herdr workspaces via local or self-hosted relay"
6
6
  platforms = ["linux", "macos"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "herdr-remote",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "Browser access to Herdr workspaces: plugin, host connector, and configuration TUI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,6 +36,19 @@ function closeSocket(ws, reason = 'host connector stopping') {
36
36
  try { ws.close(1000, reason); } catch {}
37
37
  }
38
38
 
39
+ function terminateSocket(ws) {
40
+ if (!ws || ws.readyState === WebSocket.CLOSED) return;
41
+ try {
42
+ if (typeof ws.terminate === 'function') {
43
+ ws.terminate();
44
+ } else if (typeof ws.destroy === 'function') {
45
+ ws.destroy();
46
+ } else if (typeof ws.close === 'function') {
47
+ ws.close();
48
+ }
49
+ } catch {}
50
+ }
51
+
39
52
  function pidAlive(pid) {
40
53
  if (!Number.isInteger(pid) || pid <= 0) return false;
41
54
  try {
@@ -88,6 +101,7 @@ class HostConnector {
88
101
  this.fastFailures = 0;
89
102
  this.reconnectTimer = null;
90
103
  this.heartbeatTimer = null;
104
+ this.keepaliveTimer = null;
91
105
  this.reconnectAttempts = 0;
92
106
  this.clientCount = 0;
93
107
  this.legacyHeartbeat = false;
@@ -154,6 +168,7 @@ class HostConnector {
154
168
  this.stopping = true;
155
169
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
156
170
  if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
171
+ this.stopKeepalive();
157
172
  this.reconnectTimer = null;
158
173
  this.heartbeatTimer = null;
159
174
  sendJson(this.ws, { type: 'host_shutdown' });
@@ -168,6 +183,7 @@ class HostConnector {
168
183
 
169
184
  connect() {
170
185
  if (this.stopping || (this.ws && [WebSocket.OPEN, WebSocket.CONNECTING].includes(this.ws.readyState))) return;
186
+ this.stopKeepalive();
171
187
  let ws;
172
188
  try {
173
189
  ws = new WebSocket(this.relayUrl);
@@ -208,6 +224,7 @@ class HostConnector {
208
224
  this.ws = null;
209
225
  if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
210
226
  this.heartbeatTimer = null;
227
+ this.stopKeepalive();
211
228
  this.ready = false;
212
229
  this.clientCount = 0;
213
230
  this.legacyHeartbeat = false;
@@ -276,6 +293,7 @@ class HostConnector {
276
293
  if (message.type === 'host_ready') {
277
294
  this.ready = true;
278
295
  this.reconnectAttempts = 0;
296
+ this.startKeepalive();
279
297
  if (Object.hasOwn(message, 'clientCount')) {
280
298
  this.legacyHeartbeat = false;
281
299
  this.setClientCount(message.clientCount);
@@ -460,6 +478,49 @@ class HostConnector {
460
478
  this.sendHeartbeat();
461
479
  }
462
480
 
481
+ startKeepalive() {
482
+ this.stopKeepalive();
483
+ if (!this.ws || this.stopping) return;
484
+ this.ws.isAlive = true;
485
+ const interval = this.config?.cleanup?.heartbeatIntervalMs || 30_000;
486
+ this.keepaliveTimer = setInterval(() => this.tickKeepalive(), interval);
487
+ }
488
+
489
+ stopKeepalive() {
490
+ if (this.keepaliveTimer) {
491
+ clearInterval(this.keepaliveTimer);
492
+ this.keepaliveTimer = null;
493
+ }
494
+ }
495
+
496
+ tickKeepalive() {
497
+ const ws = this.ws;
498
+ if (!ws || this.stopping) return;
499
+ if (ws.readyState === WebSocket.CLOSING) {
500
+ terminateSocket(ws);
501
+ return;
502
+ }
503
+ if (ws.readyState !== WebSocket.OPEN) return;
504
+
505
+ if (!ws.isAlive) {
506
+ terminateSocket(ws);
507
+ return;
508
+ }
509
+
510
+ ws.isAlive = false;
511
+ try {
512
+ if (typeof ws.ping === 'function') ws.ping();
513
+ } catch {}
514
+
515
+ if (this.clientCount <= 0 && !this.legacyHeartbeat) {
516
+ sendJson(ws, {
517
+ type: 'heartbeat',
518
+ load: {},
519
+ ptys: [],
520
+ });
521
+ }
522
+ }
523
+
463
524
  setClientCount(value) {
464
525
  const next = Number.isInteger(value) ? Math.max(0, value) : 0;
465
526
  if (next === this.clientCount && (next === 0 || this.heartbeatTimer)) return;