Use an AsyncStream to avoid sending @MainActor restricted AppDelegate into other isolation domains

This commit is contained in:
Caleb Jasik 2025-02-25 13:19:39 -06:00
parent df4c3a51b8
commit a32d17705c
No known key found for this signature in database
4 changed files with 28 additions and 14 deletions

View file

@ -150,7 +150,7 @@ let statusString: [NEVPNStatus: String] = [
] ]
// Represents a site that was pulled out of the system configuration // Represents a site that was pulled out of the system configuration
class Site: Codable { class Site: Codable, @unchecked Sendable {
// Stored in manager // Stored in manager
var name: String var name: String
var id: String var id: String

View file

@ -1,10 +1,10 @@
import MobileNebula @preconcurrency import MobileNebula
enum APIClientError: Error { enum APIClientError: Error {
case invalidCredentials case invalidCredentials
} }
class APIClient { struct APIClient: Sendable {
let apiClient: MobileNebulaAPIClient let apiClient: MobileNebulaAPIClient
let json = JSONDecoder() let json = JSONDecoder()

View file

@ -25,17 +25,19 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
dnUpdater.updateAllLoop { site in Task {
// Signal the site has changed in case the current site details screen is active for await site in dnUpdater.siteUpdates {
let container = self.sites?.getContainer(id: site.id) // Signal the site has changed in case the current site details screen is active
if container != nil { let container = self.sites?.getContainer(id: site.id)
// Update references to the site with the new site config if container != nil {
container!.site = site // Update references to the site with the new site config
container!.updater.update(connected: site.connected ?? false, replaceSite: site) container!.site = site
} container!.updater.update(connected: site.connected ?? false, replaceSite: site)
}
// Signal to the main screen to reload // Signal to the main screen to reload
self.ui?.invokeMethod("refreshSites", arguments: nil) self.ui?.invokeMethod("refreshSites", arguments: nil)
}
} }
guard let controller = window?.rootViewController as? FlutterViewController else { guard let controller = window?.rootViewController as? FlutterViewController else {

View file

@ -1,7 +1,7 @@
import Foundation import Foundation
import os.log import os.log
class DNUpdater { class DNUpdater: @unchecked Sendable {
private let apiClient = APIClient() private let apiClient = APIClient()
private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes
private let log = Logger(subsystem: "net.defined.mobileNebula", category: "DNUpdater") private let log = Logger(subsystem: "net.defined.mobileNebula", category: "DNUpdater")
@ -30,6 +30,18 @@ class DNUpdater {
timer.resume() timer.resume()
} }
// Site updates provides an async/await alternative to `.updateAllLoop` that doesn't require a sendable closure.
// https://developer.apple.com/documentation/swift/asyncstream
var siteUpdates: AsyncStream<Site> {
AsyncStream { continuation in
self.updateAllLoop(onUpdate: { site in
continuation.yield(site)
})
}
}
func updateSingleLoop(site: Site, onUpdate: @Sendable @escaping (Site) -> Void) { func updateSingleLoop(site: Site, onUpdate: @Sendable @escaping (Site) -> Void) {
timer.eventHandler = { timer.eventHandler = {
self.updateSite(site: site, onUpdate: onUpdate) self.updateSite(site: site, onUpdate: onUpdate)