Reconnect at once when the Mac wakes or its network changes
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Network
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
private let staleAfter: TimeInterval = 40
|
private let staleAfter: TimeInterval = 40
|
||||||
@@ -26,8 +25,7 @@ final class EventStream {
|
|||||||
private var retry: Task<Void, Never>?
|
private var retry: Task<Void, Never>?
|
||||||
private var checker: Task<Void, Never>?
|
private var checker: Task<Void, Never>?
|
||||||
private var generation = 0
|
private var generation = 0
|
||||||
private let monitor = NWPathMonitor()
|
private var networkObserver: (any NSObjectProtocol)?
|
||||||
private var online: Bool?
|
|
||||||
private var activeObserver: NotificationCenter.ObservationToken?
|
private var activeObserver: NotificationCenter.ObservationToken?
|
||||||
|
|
||||||
init(api: API, after: Int64, lead: Bool, onEvent: @escaping (ByreEvent) -> Void, onState: @escaping (Bool) -> Void, onReady: @escaping () -> Void) {
|
init(api: API, after: Int64, lead: Bool, onEvent: @escaping (ByreEvent) -> Void, onState: @escaping (Bool) -> Void, onReady: @escaping () -> Void) {
|
||||||
@@ -43,13 +41,9 @@ final class EventStream {
|
|||||||
self?.check()
|
self?.check()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
monitor.pathUpdateHandler = { [weak self] path in
|
networkObserver = NetworkWatch.shared.observe { [weak self] in
|
||||||
let up = path.status == .satisfied
|
self?.connect()
|
||||||
Task { @MainActor in
|
|
||||||
self?.pathChanged(up)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
monitor.start(queue: .main)
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
activeObserver = NotificationCenter.default.addObserver(of: NSApplication.self, for: .didBecomeActive) { [weak self] _ in
|
activeObserver = NotificationCenter.default.addObserver(of: NSApplication.self, for: .didBecomeActive) { [weak self] _ in
|
||||||
self?.check()
|
self?.check()
|
||||||
@@ -67,21 +61,16 @@ final class EventStream {
|
|||||||
checker?.cancel()
|
checker?.cancel()
|
||||||
retry?.cancel()
|
retry?.cancel()
|
||||||
reading?.cancel()
|
reading?.cancel()
|
||||||
monitor.cancel()
|
if let networkObserver {
|
||||||
|
NotificationCenter.default.removeObserver(networkObserver)
|
||||||
|
}
|
||||||
|
networkObserver = nil
|
||||||
if let activeObserver {
|
if let activeObserver {
|
||||||
NotificationCenter.default.removeObserver(activeObserver)
|
NotificationCenter.default.removeObserver(activeObserver)
|
||||||
}
|
}
|
||||||
activeObserver = nil
|
activeObserver = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func pathChanged(_ up: Bool) {
|
|
||||||
let was = online
|
|
||||||
online = up
|
|
||||||
if was == false && up {
|
|
||||||
connect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func check() {
|
private func check() {
|
||||||
if Date().timeIntervalSince(heard) < staleAfter {
|
if Date().timeIntervalSince(heard) < staleAfter {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import Foundation
|
||||||
|
import Network
|
||||||
|
#if os(macOS)
|
||||||
|
import AppKit
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private let networkChanged = Notification.Name("run.fc.byre.networkChanged")
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class NetworkWatch {
|
||||||
|
static let shared = NetworkWatch()
|
||||||
|
|
||||||
|
private let monitor = NWPathMonitor()
|
||||||
|
private var seen = false
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
monitor.pathUpdateHandler = { [weak self] path in
|
||||||
|
let summary = "\(path.status) \(path.availableInterfaces.map(\.name).joined(separator: " "))"
|
||||||
|
Task { @MainActor in
|
||||||
|
self?.pathChanged(summary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
monitor.start(queue: .main)
|
||||||
|
#if os(macOS)
|
||||||
|
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.didWakeNotification, object: nil, queue: .main) { [weak self] _ in
|
||||||
|
Task { @MainActor in
|
||||||
|
log.notice("woke from sleep, reconnecting")
|
||||||
|
self?.post()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
func observe(_ handler: @escaping @MainActor () -> Void) -> any NSObjectProtocol {
|
||||||
|
NotificationCenter.default.addObserver(forName: networkChanged, object: self, queue: .main) { _ in
|
||||||
|
MainActor.assumeIsolated {
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pathChanged(_ summary: String) {
|
||||||
|
guard seen else {
|
||||||
|
seen = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.notice("network changed (\(summary, privacy: .public)), reconnecting")
|
||||||
|
post()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func post() {
|
||||||
|
NotificationCenter.default.post(name: networkChanged, object: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ final class Remote {
|
|||||||
private var stopped = false
|
private var stopped = false
|
||||||
private var heard = Date()
|
private var heard = Date()
|
||||||
private var checker: Task<Void, Never>?
|
private var checker: Task<Void, Never>?
|
||||||
|
private var networkObserver: (any NSObjectProtocol)?
|
||||||
|
|
||||||
init(api: API) {
|
init(api: API) {
|
||||||
self.api = api
|
self.api = api
|
||||||
@@ -69,16 +70,30 @@ final class Remote {
|
|||||||
self?.check()
|
self?.check()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
networkObserver = NetworkWatch.shared.observe { [weak self] in
|
||||||
|
self?.restart()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
stopped = true
|
stopped = true
|
||||||
checker?.cancel()
|
checker?.cancel()
|
||||||
|
if let networkObserver {
|
||||||
|
NotificationCenter.default.removeObserver(networkObserver)
|
||||||
|
}
|
||||||
|
networkObserver = nil
|
||||||
ws?.cancel(with: .normalClosure, reason: nil)
|
ws?.cancel(with: .normalClosure, reason: nil)
|
||||||
ws = nil
|
ws = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func restart() {
|
||||||
|
let old = ws
|
||||||
|
ws = nil
|
||||||
|
old?.cancel(with: .goingAway, reason: nil)
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
// A socket byre closed while this Mac slept stays open here until something is sent on it.
|
// A socket byre closed while this Mac slept stays open here until something is sent on it.
|
||||||
private func check() {
|
private func check() {
|
||||||
guard let task = ws else {
|
guard let task = ws else {
|
||||||
@@ -122,11 +137,13 @@ final class Remote {
|
|||||||
log.notice("client connection closed:\(errorText(error), privacy: .public)")
|
log.notice("client connection closed:\(errorText(error), privacy: .public)")
|
||||||
self.ws = nil
|
self.ws = nil
|
||||||
try? await Task.sleep(for: retryAfter)
|
try? await Task.sleep(for: retryAfter)
|
||||||
|
if self.ws == nil {
|
||||||
self.start()
|
self.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func handle(_ task: URLSessionWebSocketTask, _ data: Data) {
|
private func handle(_ task: URLSessionWebSocketTask, _ data: Data) {
|
||||||
let req: ClientRequest
|
let req: ClientRequest
|
||||||
|
|||||||
Reference in New Issue
Block a user