viveworker 0.8.7 → 0.8.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viveworker",
3
- "version": "0.8.7",
3
+ "version": "0.8.8",
4
4
  "description": "Mobile control plane for AI agents. Approve, answer, and hand off work for Codex, Claude, MCP-ready tools, and A2A from one phone.",
5
5
  "author": "Yuta Hoshino <hoshino.lireneo@gmail.com>",
6
6
  "license": "MIT",
@@ -48,7 +48,6 @@ import {
48
48
  decode,
49
49
  encodeData,
50
50
  encodeAck,
51
- encodePing,
52
51
  encodeResumeReq,
53
52
  generateMid,
54
53
  FRAME_DATA,
@@ -79,7 +78,11 @@ export const STATE = Object.freeze({
79
78
  FAILED: "failed",
80
79
  });
81
80
 
82
- const DEFAULT_PING_INTERVAL_MS = 30_000; // CF idle timeout is ~100s
81
+ export const TEXT_KEEPALIVE_PING = "viveworker.keepalive.ping.v1";
82
+ export const TEXT_KEEPALIVE_PONG = "viveworker.keepalive.pong.v1";
83
+
84
+ const DEFAULT_PING_INTERVAL_MS = 75_000; // CF idle timeout is ~100s
85
+ const MIN_PING_INTERVAL_MS = 5_000;
83
86
  const DEFAULT_BACKOFF_MS = [1_000, 2_000, 4_000, 8_000, 16_000, 30_000];
84
87
  const DEFAULT_HANDSHAKE_TIMEOUT_MS = 30_000;
85
88
  const MAX_PRE_CONNECT_BACKOFF_MS = 4_000;
@@ -169,6 +172,7 @@ export class RemotePairingTransport {
169
172
  this._onHandshakeComplete = opts.onHandshakeComplete ?? noop;
170
173
 
171
174
  this._pingIntervalMs = opts.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS;
175
+ if (Number(this._pingIntervalMs) <= 0) this._pingIntervalMs = 0;
172
176
  this._backoffMs = (opts.backoffMs ?? DEFAULT_BACKOFF_MS).slice();
173
177
  if (this._backoffMs.length === 0) this._backoffMs = [1_000];
174
178
  this._handshakeTimeoutMs = opts.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
@@ -207,7 +211,7 @@ export class RemotePairingTransport {
207
211
  this._reconnectAttempt = 0;
208
212
  /** @type {ReturnType<typeof setTimeout> | null} */
209
213
  this._reconnectTimer = null;
210
- /** @type {ReturnType<typeof setInterval> | null} */
214
+ /** @type {ReturnType<typeof setTimeout> | null} */
211
215
  this._pingTimer = null;
212
216
  /** @type {ReturnType<typeof setTimeout> | null} */
213
217
  this._handshakeTimer = null;
@@ -229,6 +233,7 @@ export class RemotePairingTransport {
229
233
  // ---- sequencing (monotonic across the transport lifetime) ----
230
234
  this._outboundSeq = 0;
231
235
  this._lastSeenPeerSeq = 0;
236
+ this._lastActivityAtMs = 0;
232
237
  }
233
238
 
234
239
  // -------------------------------------------------------------------------
@@ -395,6 +400,7 @@ export class RemotePairingTransport {
395
400
  try { this._ws?.close(CLOSE_NORMAL, "closed during open"); } catch {}
396
401
  return;
397
402
  }
403
+ this._markActivity({ reschedulePing: false });
398
404
  if (this._session) {
399
405
  // We have a live noise session from a previous connection — try to
400
406
  // resume rather than re-handshaking. `lastSeenPeerSeq` tells the relay
@@ -421,6 +427,21 @@ export class RemotePairingTransport {
421
427
  }
422
428
 
423
429
  _handleMessage(evt) {
430
+ if (typeof evt?.data === "string") {
431
+ if (evt.data === TEXT_KEEPALIVE_PONG) {
432
+ this._markActivity();
433
+ return;
434
+ }
435
+ if (evt.data === TEXT_KEEPALIVE_PING) {
436
+ this._markActivity();
437
+ this._wireSend(TEXT_KEEPALIVE_PONG);
438
+ return;
439
+ }
440
+ this._onError(new Error("unexpected text websocket frame"));
441
+ return;
442
+ }
443
+
444
+ this._markActivity();
424
445
  let frame;
425
446
  try {
426
447
  frame = decode(asU8(evt.data));
@@ -720,18 +741,41 @@ export class RemotePairingTransport {
720
741
 
721
742
  _startPing() {
722
743
  this._stopPing();
723
- this._pingTimer = setInterval(() => {
724
- this._sendPing();
725
- }, this._pingIntervalMs);
744
+ if (!this._pingIntervalMs) return;
745
+ if (!this._lastActivityAtMs) this._lastActivityAtMs = Date.now();
746
+ this._schedulePing();
726
747
  }
727
748
 
728
749
  _stopPing() {
729
750
  if (this._pingTimer != null) {
730
- clearInterval(this._pingTimer);
751
+ clearTimeout(this._pingTimer);
731
752
  this._pingTimer = null;
732
753
  }
733
754
  }
734
755
 
756
+ _schedulePing() {
757
+ if (!this._pingIntervalMs || this._closed || !this._ws || this._ws.readyState !== 1) return;
758
+ const interval = Math.max(MIN_PING_INTERVAL_MS, Number(this._pingIntervalMs) || DEFAULT_PING_INTERVAL_MS);
759
+ const idleFor = Date.now() - (this._lastActivityAtMs || Date.now());
760
+ const delay = Math.max(1, interval - idleFor);
761
+ this._pingTimer = setTimeout(() => {
762
+ this._pingTimer = null;
763
+ if (this._closed || !this._ws || this._ws.readyState !== 1) return;
764
+ const nextIdleFor = Date.now() - (this._lastActivityAtMs || Date.now());
765
+ if (nextIdleFor >= interval) {
766
+ this._sendPing();
767
+ }
768
+ this._schedulePing();
769
+ }, delay);
770
+ }
771
+
772
+ _markActivity({ reschedulePing = true } = {}) {
773
+ this._lastActivityAtMs = Date.now();
774
+ if (!reschedulePing || this._pingTimer == null) return;
775
+ this._stopPing();
776
+ this._schedulePing();
777
+ }
778
+
735
779
  // -------------------------------------------------------------------------
736
780
  // Frame sends
737
781
  // -------------------------------------------------------------------------
@@ -751,22 +795,27 @@ export class RemotePairingTransport {
751
795
  }
752
796
 
753
797
  _sendPing() {
754
- this._wireSend(encodePing());
798
+ if (this._wireSend(TEXT_KEEPALIVE_PING, { countAsActivity: false })) {
799
+ this._markActivity({ reschedulePing: false });
800
+ }
755
801
  }
756
802
 
757
803
  _sendResumeReq(lastSeenSeq) {
758
804
  this._wireSend(encodeResumeReq(lastSeenSeq));
759
805
  }
760
806
 
761
- _wireSend(bytes) {
807
+ _wireSend(bytes, { countAsActivity = true } = {}) {
762
808
  if (!this._ws || this._ws.readyState !== 1 /* OPEN */) {
763
809
  this._log.warn?.("send while WS not open — dropping");
764
- return;
810
+ return false;
765
811
  }
766
812
  try {
767
813
  this._ws.send(bytes);
814
+ if (countAsActivity) this._markActivity();
815
+ return true;
768
816
  } catch (err) {
769
817
  this._onError(err);
818
+ return false;
770
819
  }
771
820
  }
772
821