ios_client
This commit is contained in:
161
ios_client 0.8.5/Kecalek/AppState.swift
Normal file
161
ios_client 0.8.5/Kecalek/AppState.swift
Normal file
@@ -0,0 +1,161 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum ConnectionStatus: Equatable {
|
||||
case disconnected
|
||||
case connecting
|
||||
case connected
|
||||
case reconnecting
|
||||
}
|
||||
|
||||
@Observable
|
||||
final class AppState {
|
||||
var isLoggedIn = false
|
||||
var currentUser: User?
|
||||
var connectionStatus: ConnectionStatus = .disconnected
|
||||
var email: String = ""
|
||||
|
||||
let chatClient = ChatClient()
|
||||
|
||||
private var reconnectTask: Task<Void, Never>?
|
||||
private var notificationTask: Task<Void, Never>?
|
||||
private var isReconnecting = false
|
||||
private var backgroundedAt: Date?
|
||||
|
||||
/// Start listening for connection state changes (call after login)
|
||||
func startConnectionMonitor() {
|
||||
notificationTask?.cancel()
|
||||
notificationTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
let stream = await chatClient.makeNotificationStream()
|
||||
for await notification in stream {
|
||||
guard !Task.isCancelled else { break }
|
||||
if case .connectionStateChanged(let connected) = notification {
|
||||
await MainActor.run {
|
||||
if connected {
|
||||
self.connectionStatus = .connected
|
||||
self.isReconnecting = false
|
||||
self.reconnectTask?.cancel()
|
||||
self.reconnectTask = nil
|
||||
} else if self.isLoggedIn, !self.isReconnecting {
|
||||
// Only start reconnect if not already reconnecting
|
||||
// (reconnect() internally calls disconnect() which fires this)
|
||||
self.connectionStatus = .disconnected
|
||||
self.attemptReconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt reconnect with exponential backoff; immediate logout on auth failure
|
||||
@MainActor
|
||||
private func attemptReconnect() {
|
||||
reconnectTask?.cancel()
|
||||
isReconnecting = true
|
||||
reconnectTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
let maxAttempts = 5
|
||||
var delay: TimeInterval = Constants.reconnectBaseDelay
|
||||
|
||||
for attempt in 1...maxAttempts {
|
||||
guard !Task.isCancelled, self.isLoggedIn else { return }
|
||||
|
||||
self.connectionStatus = .reconnecting
|
||||
#if DEBUG
|
||||
print("DEBUG AppState: reconnect attempt \(attempt)/\(maxAttempts), delay=\(delay)s")
|
||||
#endif
|
||||
|
||||
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
|
||||
guard !Task.isCancelled, self.isLoggedIn else { return }
|
||||
|
||||
let result = await self.chatClient.reconnect()
|
||||
switch result {
|
||||
case .success:
|
||||
self.connectionStatus = .connected
|
||||
self.isReconnecting = false
|
||||
#if DEBUG
|
||||
print("DEBUG AppState: reconnected on attempt \(attempt)")
|
||||
#endif
|
||||
return
|
||||
case .authFailed:
|
||||
// Keys rotated or invalid — logout immediately, don't retry
|
||||
self.isReconnecting = false
|
||||
#if DEBUG
|
||||
print("DEBUG AppState: auth failed (keys likely rotated), logging out immediately")
|
||||
#endif
|
||||
await self.logout()
|
||||
return
|
||||
case .networkError:
|
||||
// Network issue — retry with backoff
|
||||
delay = min(delay * 2, Constants.reconnectMaxDelay)
|
||||
}
|
||||
}
|
||||
|
||||
// All network retries exhausted → force logout
|
||||
self.isReconnecting = false
|
||||
guard !Task.isCancelled, self.isLoggedIn else { return }
|
||||
#if DEBUG
|
||||
print("DEBUG AppState: reconnect failed after \(maxAttempts) attempts, logging out")
|
||||
#endif
|
||||
await self.logout()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - App Lifecycle
|
||||
|
||||
func handleEnteredBackground() {
|
||||
backgroundedAt = Date()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func handleBecameActive() {
|
||||
guard isLoggedIn, !isReconnecting else { return }
|
||||
let wasInBackground = backgroundedAt.map { Date().timeIntervalSince($0) } ?? 0
|
||||
backgroundedAt = nil
|
||||
|
||||
Task {
|
||||
let alive = await chatClient.isConnectionAlive()
|
||||
if !alive {
|
||||
#if DEBUG
|
||||
print("DEBUG AppState: foreground — connection dead, reconnecting")
|
||||
#endif
|
||||
await MainActor.run {
|
||||
guard !self.isReconnecting else { return }
|
||||
self.connectionStatus = .reconnecting
|
||||
self.attemptReconnect()
|
||||
}
|
||||
} else if wasInBackground > 30 {
|
||||
// Connection appears alive but was backgrounded a long time —
|
||||
// force reconnect to ensure fresh state
|
||||
#if DEBUG
|
||||
print("DEBUG AppState: foreground — stale connection (\(Int(wasInBackground))s), reconnecting")
|
||||
#endif
|
||||
await MainActor.run {
|
||||
guard !self.isReconnecting else { return }
|
||||
self.connectionStatus = .reconnecting
|
||||
self.attemptReconnect()
|
||||
}
|
||||
} else {
|
||||
#if DEBUG
|
||||
print("DEBUG AppState: foreground — connection alive (\(Int(wasInBackground))s in bg)")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logout() async {
|
||||
isReconnecting = false
|
||||
reconnectTask?.cancel()
|
||||
reconnectTask = nil
|
||||
notificationTask?.cancel()
|
||||
notificationTask = nil
|
||||
await chatClient.disconnect()
|
||||
KeychainService.deleteCredentials()
|
||||
isLoggedIn = false
|
||||
currentUser = nil
|
||||
connectionStatus = .disconnected
|
||||
email = ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user