@i14ks/ccv 0.7.0 → 0.7.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.
Files changed (2) hide show
  1. package/bundle/ccv.mjs +64 -3
  2. package/package.json +1 -1
package/bundle/ccv.mjs CHANGED
@@ -4229,6 +4229,9 @@ var RELAY_BROADCAST = "*";
4229
4229
  // ../host/dist/relay-link.js
4230
4230
  var RETRY_MIN_MS = 1e3;
4231
4231
  var RETRY_MAX_MS = 6e4;
4232
+ var HANDSHAKE_TIMEOUT_MS = 15e3;
4233
+ var CONNECT_TIMEOUT_MS = 3e4;
4234
+ var SILENCE_TIMEOUT_MS = 95e3;
4232
4235
  function jitter(delay) {
4233
4236
  return Math.round(delay * (0.8 + Math.random() * 0.4));
4234
4237
  }
@@ -4237,6 +4240,17 @@ var RelayLink = class extends EventEmitter3 {
4237
4240
  #socket = null;
4238
4241
  #retry = RETRY_MIN_MS;
4239
4242
  #retryTimer = null;
4243
+ /**
4244
+ * Сторож живого соединения — один таймер на три фазы жизни сокета.
4245
+ *
4246
+ * До `open` он сторожит подключение, после — тишину, и переставляется каждым
4247
+ * пришедшим кадром. Один таймер, а не три: у сокета в каждый момент есть
4248
+ * ровно один способ подвести, и держать под него отдельные заводные механизмы
4249
+ * значило бы гасить их по очереди в каждой ветке.
4250
+ */
4251
+ #watchdog = null;
4252
+ /** Сокет рвём мы сами — чтобы не подменить свою причину отказа причиной от `ws`. */
4253
+ #aborting = false;
4240
4254
  #clients = /* @__PURE__ */ new Set();
4241
4255
  #error = null;
4242
4256
  #closed = false;
@@ -4422,10 +4436,13 @@ var RelayLink = class extends EventEmitter3 {
4422
4436
  clearTimeout(this.#retryTimer);
4423
4437
  this.#retryTimer = null;
4424
4438
  }
4439
+ this.#disarm();
4425
4440
  const socket = this.#socket;
4426
4441
  this.#socket = null;
4427
4442
  if (socket) {
4428
4443
  socket.removeAllListeners();
4444
+ socket.on("error", () => {
4445
+ });
4429
4446
  socket.close();
4430
4447
  }
4431
4448
  for (const clientId of this.#clients)
@@ -4437,9 +4454,12 @@ var RelayLink = class extends EventEmitter3 {
4437
4454
  if (!credentials || this.#closed || this.#socket)
4438
4455
  return;
4439
4456
  const wsUrl = `${credentials.url.replace(/^http/, "ws")}/relay/host`;
4440
- const socket = new WebSocket(wsUrl);
4457
+ this.#aborting = false;
4458
+ const socket = new WebSocket(wsUrl, { handshakeTimeout: HANDSHAKE_TIMEOUT_MS });
4441
4459
  this.#socket = socket;
4460
+ this.#arm(socket, CONNECT_TIMEOUT_MS, "relay \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043D\u0430 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435");
4442
4461
  socket.on("open", () => {
4462
+ this.#arm(socket, SILENCE_TIMEOUT_MS, "relay \u0437\u0430\u043C\u043E\u043B\u0447\u0430\u043B");
4443
4463
  const hello = {
4444
4464
  t: "host.hello",
4445
4465
  protocol: RELAY_PROTOCOL_VERSION,
@@ -4449,13 +4469,19 @@ var RelayLink = class extends EventEmitter3 {
4449
4469
  };
4450
4470
  socket.send(JSON.stringify(hello));
4451
4471
  });
4452
- socket.on("message", (raw) => this.#onMessage(socket, String(raw)));
4472
+ socket.on("message", (raw) => {
4473
+ this.#arm(socket, SILENCE_TIMEOUT_MS, "relay \u0437\u0430\u043C\u043E\u043B\u0447\u0430\u043B");
4474
+ this.#onMessage(socket, String(raw));
4475
+ });
4453
4476
  socket.on("error", (error) => {
4454
- this.#error = describe5(error);
4477
+ if (this.#aborting)
4478
+ return;
4479
+ this.#error = humanize(error);
4455
4480
  });
4456
4481
  socket.on("close", (code, reason) => {
4457
4482
  if (this.#socket !== socket)
4458
4483
  return;
4484
+ this.#disarm();
4459
4485
  this.#socket = null;
4460
4486
  for (const clientId of this.#clients)
4461
4487
  this.emit("detached", clientId);
@@ -4469,6 +4495,34 @@ var RelayLink = class extends EventEmitter3 {
4469
4495
  this.#scheduleRetry(rejected);
4470
4496
  });
4471
4497
  }
4498
+ /**
4499
+ * Взвести сторожа: не дождались — рвём сокет и уходим на общий круг повтора.
4500
+ *
4501
+ * `terminate()`, а не `close()`: закрытие по правилам ждёт ответного кадра от
4502
+ * стороны, которая как раз и не отвечает, — а нам нужен `close` на своей
4503
+ * стороне здесь и сейчас, потому что именно он запускает переподключение.
4504
+ * Для сокета, застрявшего в CONNECTING, `ws` тем же вызовом обрывает
4505
+ * рукопожатие — и это тот самый случай, ради которого всё написано.
4506
+ */
4507
+ #arm(socket, ms, reason) {
4508
+ this.#disarm();
4509
+ this.#watchdog = setTimeout(() => {
4510
+ this.#watchdog = null;
4511
+ if (this.#socket !== socket)
4512
+ return;
4513
+ this.#error = `${reason} \u0437\u0430 ${Math.round(ms / 1e3)} \u0441 \u2014 \u043F\u0440\u043E\u0431\u0443\u0435\u043C \u0441\u043D\u043E\u0432\u0430`;
4514
+ this.#log(this.#error);
4515
+ this.#aborting = true;
4516
+ socket.terminate();
4517
+ }, ms);
4518
+ this.#watchdog.unref();
4519
+ }
4520
+ #disarm() {
4521
+ if (!this.#watchdog)
4522
+ return;
4523
+ clearTimeout(this.#watchdog);
4524
+ this.#watchdog = null;
4525
+ }
4472
4526
  #scheduleRetry(rejected) {
4473
4527
  if (this.#closed || !this.#credentials || this.#retryTimer)
4474
4528
  return;
@@ -4560,6 +4614,13 @@ var RelayLink = class extends EventEmitter3 {
4560
4614
  function describe5(error) {
4561
4615
  return error instanceof Error ? error.message : String(error);
4562
4616
  }
4617
+ function humanize(error) {
4618
+ const message = describe5(error);
4619
+ if (/handshake has timed out/i.test(message)) {
4620
+ return `relay \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043D\u0430 \u0440\u0443\u043A\u043E\u043F\u043E\u0436\u0430\u0442\u0438\u0435 \u0437\u0430 ${Math.round(HANDSHAKE_TIMEOUT_MS / 1e3)} \u0441 \u2014 \u043F\u0440\u043E\u0431\u0443\u0435\u043C \u0441\u043D\u043E\u0432\u0430`;
4621
+ }
4622
+ return message;
4623
+ }
4563
4624
 
4564
4625
  // ../host/dist/terminal.js
4565
4626
  import { EventEmitter as EventEmitter4 } from "node:events";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@i14ks/ccv",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "license": "UNLICENSED",
5
5
  "description": "GUI для Claude Code: сессии агента в каталогах проектов, дерево файлов и редактор рядом с разговором.",
6
6
  "homepage": "https://claudevisual.com",