fix(app): ping the relay, retry stuck messages, drop duplicates

A message sent into a dead socket ended up as failed — attachments even
stayed on "sending" forever — and only the user could send it again by
hand. Mobile NAT drops idle TCP silently, so neither side knew.

- pingInterval on the WebSocket, so the client notices a dead link itself
- outbox: after every reconnect, stuck messages are re-sent (batch of 30,
  up to a day old); permanent errors are not retried
- sendAttachment gained the try/catch it was missing, otherwise the
  attachment never reached the outbox
- de-duplicate by message id: a retry re-sends the SAME message, which
  would otherwise show up twice on the other side
This commit is contained in:
Julius Joska
2026-08-13 12:23:24 +02:00
parent b82f50f888
commit 9985b79aa6
5 changed files with 259 additions and 25 deletions
+12 -2
View File
@@ -21,6 +21,15 @@ import 'package:socks5_proxy/socks_client.dart';
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
/// Jak často posílat WS ping a jak dlouho čekat na pong.
///
/// Bez pingu se mrtvé spojení tváří jako živé: mobilní NAT zahodí nečinné TCP
/// tiše, appka dál hlásí „připojeno" a píše do prázdna — zprávy pak odejdou až
/// po restartu appky, všechny naráz. `dart:io` při nastaveném intervalu ping
/// posílá a když pong nedorazí včas, spojení zavře → [RelayClient] se
/// reconnectne sám. Relay pinguje protisměrně po 30 s (ws.rs PING_INTERVAL).
const kWsPingInterval = Duration(seconds: 30);
/// Abstrakce nad tím, jak klient otevírá spojení k relayi.
abstract class RelayTransport {
/// Sdílený HTTP klient pro REST (registrace, prekey bundle, přílohy…).
@@ -41,7 +50,8 @@ class DirectTransport implements RelayTransport {
http.Client httpClient() => _client ??= http.Client();
@override
WebSocketChannel connectWs(Uri url) => WebSocketChannel.connect(url);
WebSocketChannel connectWs(Uri url) =>
IOWebSocketChannel.connect(url, pingInterval: kWsPingInterval);
@override
void close() {
@@ -84,7 +94,7 @@ class TorTransport implements RelayTransport {
@override
WebSocketChannel connectWs(Uri url) =>
IOWebSocketChannel.connect(_withExplicitPort(url),
customClient: _sharedHttpClient());
customClient: _sharedHttpClient(), pingInterval: kWsPingInterval);
/// `Uri` pro `ws`/`wss` nezná výchozí port → `url.port` vrací 0 a SOCKS
/// CONNECT by cílil na port 0 (serverError). Doplníme ho podle schématu