Files
yattee/Yattee/Services/Navigation/ConnectivityMonitor.swift
Arkadiusz Fal e094770047 Fence the last two caches and undo failed logins properly
Home feeds and subscription providers join the subscriptions feed in
dropping results that belong to a login the app has since moved off.
Clearing a cache does not cancel what is already awaiting the server,
and an Apple-account switch discarded providers without invalidating the
fetches holding them.

A sign-in whose credential the Keychain refused to store is now rolled
back rather than deleted. Creating an account also queues the permanent
marker that stops every other device synthesizing an account for that
instance's pre-revamp login; leaving it behind would suppress a still
valid legacy login everywhere on the strength of an account nobody has.
An account the server already confirmed is still deleted properly,
tombstone and all.

Caches with no recorded owner are discarded even when the identity
cannot be resolved at all — that is the same first launch after
upgrading, only offline.

An account whose credential is missing no longer hides a signed-in
sibling on the same instance, which made the whole instance report
itself as logged out. And connectivity returning recomputes statuses, so
an account marked unreachable retries instead of waiting for a
foreground transition it has no way to cause.
2026-08-27 20:45:54 +02:00

74 lines
2.2 KiB
Swift

//
// ConnectivityMonitor.swift
// Yattee
//
// Network connectivity monitoring for offline mode support.
//
import Foundation
import Network
/// Monitors network connectivity status.
@Observable
final class ConnectivityMonitor: @unchecked Sendable {
/// Whether the device is currently online.
private(set) var isOnline: Bool = true
/// Whether we're on a cellular connection.
private(set) var isCellular: Bool = false
/// Whether the connection is considered expensive (cellular or hotspot).
private(set) var isExpensive: Bool = false
/// Whether the connection is constrained (low data mode).
private(set) var isConstrained: Bool = false
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "stream.yattee.connectivity")
// MARK: - Lifecycle
init() {
startMonitoring()
}
deinit {
stopMonitoring()
}
// MARK: - Monitoring
/// Start monitoring network connectivity.
func startMonitoring() {
monitor.pathUpdateHandler = { [weak self] path in
Task { @MainActor [weak self] in
let wasOffline = self?.isOnline == false
self?.isOnline = path.status == .satisfied
// Anything that gave up while there was no network needs to
// hear that there is one again — an account marked unreachable
// stops reads from going out at all, so nothing else would ever
// discover it.
if wasOffline, path.status == .satisfied {
NotificationCenter.default.post(name: .connectivityDidReturn, object: nil)
}
self?.isCellular = path.usesInterfaceType(.cellular)
self?.isExpensive = path.isExpensive
self?.isConstrained = path.isConstrained
}
}
monitor.start(queue: queue)
}
/// Stop monitoring network connectivity.
func stopMonitoring() {
monitor.cancel()
}
}
extension Notification.Name {
/// Posted when the device goes from offline to online.
static let connectivityDidReturn = Notification.Name("stream.yattee.connectivityDidReturn")
}