From 5db84bd89c8808b0895c7206e8a6a58043f9f8dc Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 18:04:16 +0100
Subject: [PATCH] feat: heartbeat fix, copy messages, copy/share images, hide unknown duration
---
services/websocket.ts | 32 ++++++++++++++++----------------
1 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/services/websocket.ts b/services/websocket.ts
index 4c74e56..895d995 100644
--- a/services/websocket.ts
+++ b/services/websocket.ts
@@ -17,7 +17,7 @@
const MAX_RECONNECT_DELAY = 30000;
const RECONNECT_MULTIPLIER = 2;
const LOCAL_TIMEOUT = 2500;
-const HEARTBEAT_INTERVAL = 20000; // 20s ping to detect zombie sockets
+const HEARTBEAT_INTERVAL = 30000; // 30s ping to detect zombie sockets
export class WebSocketClient {
private ws: WebSocket | null = null;
@@ -27,22 +27,24 @@
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private localTimer: ReturnType<typeof setTimeout> | null = null;
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
- private pongReceived: boolean = true;
+ private lastMessageAt: number = 0; // timestamp of last received message (any type)
private shouldReconnect: boolean = false;
private connected: boolean = false;
private callbacks: WebSocketClientOptions = {};
constructor() {
- // When app comes back to foreground, check if socket is still alive
+ // When app comes back to foreground, verify the connection
AppState.addEventListener("change", (state: AppStateStatus) => {
if (state === "active" && this.shouldReconnect) {
- if (!this.connected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
- // Socket is dead — force immediate reconnect
+ if (!this.ws || this.ws.readyState === WebSocket.CLOSED || this.ws.readyState === WebSocket.CLOSING) {
+ // Socket is definitively dead — force immediate reconnect
this.reconnectDelay = INITIAL_RECONNECT_DELAY;
this.urlIndex = 0;
this.tryUrl();
- } else {
- // Socket looks open but might be zombie — send a ping to verify
+ } else if (this.connected) {
+ // Socket might be alive — send a ping and restart heartbeat
+ // to give it a fresh 2-interval window to prove liveness
+ this.startHeartbeat();
this.sendPing();
}
}
@@ -80,10 +82,11 @@
private startHeartbeat() {
this.stopHeartbeat();
- this.pongReceived = true;
+ this.lastMessageAt = Date.now();
this.heartbeatTimer = setInterval(() => {
- if (!this.pongReceived) {
- // No pong since last ping — socket is zombie, force reconnect
+ const silentMs = Date.now() - this.lastMessageAt;
+ if (silentMs > HEARTBEAT_INTERVAL * 2) {
+ // No message (including pong) for 2 full intervals — zombie socket
this.connected = false;
this.callbacks.onClose?.();
this.reconnectDelay = INITIAL_RECONNECT_DELAY;
@@ -91,6 +94,7 @@
this.tryUrl();
return;
}
+ // Send a ping so the server has something to respond to
this.sendPing();
}, HEARTBEAT_INTERVAL);
}
@@ -101,7 +105,6 @@
private sendPing() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
- this.pongReceived = false;
try {
this.ws.send(JSON.stringify({ type: "ping" }));
} catch {
@@ -147,13 +150,10 @@
ws.onmessage = (event) => {
if (ws !== this.ws) return; // stale
+ this.lastMessageAt = Date.now(); // any message proves the connection is alive
try {
const data = JSON.parse(event.data) as WebSocketMessage;
- // Handle pong responses from heartbeat
- if (data.type === "pong") {
- this.pongReceived = true;
- return;
- }
+ if (data.type === "pong") return; // heartbeat response, don't forward
this.callbacks.onMessage?.(data);
} catch {
this.callbacks.onMessage?.({ type: "text", content: String(event.data) });
--
Gitblit v1.3.1