Add a relay ping and backend status to the iOS app

The relay now answers a stateless ping with pong. The Relay card auto-checks on appear and address changes, showing latency or the connection error; this verifies the endpoint speaks the share protocol, not just that a TCP port is open.
This commit is contained in:
2026-10-27 21:59:27 +08:00
parent decaaa2de1
commit bf5d19900c
6 changed files with 248 additions and 1 deletions
+61
View File
@@ -21,12 +21,16 @@ final class AppModel: ObservableObject {
@Published var errorMessage: String?
@Published var notice: String?
@Published var showAdvanced = false
@Published var relayCheck: RelayCheck?
@Published var isCheckingRelay = false
private let session: ShareSession = ShareNewSession()!
private let receivedDir: URL
private let seedsDir: URL
private var pollTimer: Timer?
private var noticeTask: Task<Void, Never>?
private var relayCheckTask: Task<Void, Never>?
private var relayCheckGeneration = 0
private enum Keys {
static let relay = "share.relay"
@@ -211,6 +215,63 @@ final class AppModel: ObservableObject {
code = ShareGenerateCode()
}
// MARK: - Relay reachability
/// Debounced automatic check used while the Relay card is visible. The
/// short delay keeps every keystroke in the relay field from opening a
/// socket.
func scheduleRelayCheck() {
relayCheckTask?.cancel()
relayCheckGeneration += 1
let generation = relayCheckGeneration
relayCheck = nil
relayCheckTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: 350_000_000)
guard !Task.isCancelled, let self else { return }
await self.performRelayCheck(generation: generation)
}
}
/// Immediate check, used by the refresh button.
func checkRelay() {
relayCheckTask?.cancel()
relayCheckGeneration += 1
let generation = relayCheckGeneration
relayCheckTask = Task { [weak self] in
guard let self else { return }
await self.performRelayCheck(generation: generation)
}
}
private func performRelayCheck(generation: Int) async {
isCheckingRelay = true
let relay = relay
// ShareCheckRelay performs a dial plus the protocol ping/pong. Keep
// that off the main thread; the Go function has its own 5s timeouts.
let json = await withCheckedContinuation { (continuation: CheckedContinuation<String, Never>) in
DispatchQueue.global(qos: .utility).async {
continuation.resume(returning: ShareCheckRelay(relay))
}
}
guard generation == relayCheckGeneration else { return }
isCheckingRelay = false
guard let data = json.data(using: .utf8),
let decoded = try? JSONDecoder().decode(RelayCheck.self, from: data) else {
relayCheck = RelayCheck(
ok: false,
relay: relay.isEmpty ? ShareRelayDefault() : relay,
latencyMs: 0,
error: "Could not read the relay status."
)
return
}
relayCheck = decoded
}
// MARK: - Status polling
private func startPolling() {