108 lines · 3.6 KB · Swift
RawDownload
#if os(macOS)import AppKitimport Foundationimport WebKit @MainActorpublic enum WebKitTeardown {    private static var retained: [ObjectIdentifier: AnyObject] = [:]    private static var scheduled: Set<ObjectIdentifier> = []    private static let log = CodexBarLog.logger(LogCategories.webkitTeardown)     #if arch(x86_64)    private static let retainAfterCleanup = true    #else    private static let retainAfterCleanup = false    #endif    private static let cleanupDelay: TimeInterval = 0.25    private static let releasePollInterval: TimeInterval = 0.2    private static let releaseMinimumDelay: TimeInterval = 2.0    #if arch(x86_64)    private static let releaseMaximumDelay: TimeInterval = 8.0    #else    private static let releaseMaximumDelay: TimeInterval = 2.0    #endif     public static func retain(_ owner: AnyObject) {        self.retained[ObjectIdentifier(owner)] = owner    }     public static func scheduleCleanup(        owner: AnyObject,        window: NSWindow?,        webView: WKWebView?,        closeWindow: (() -> Void)? = nil)    {        let id = ObjectIdentifier(owner)        self.retained[id] = owner        guard !self.scheduled.contains(id) else { return }        self.scheduled.insert(id)        self.log.debug(            "WebKit cleanup scheduled",            metadata: ["window": "\(window != nil)", "webView": "\(webView != nil)"])         Task { @MainActor in            // Let WebKit unwind delegate callbacks before teardown on Intel.            await Task.yield()            try? await Task.sleep(nanoseconds: self.nanoseconds(self.cleanupDelay))            self.cleanup(window: window, webView: webView, closeWindow: closeWindow)            self.scheduleRelease(id: id, window: window, webView: webView)        }    }     #if DEBUG    static func isRetainedForTesting(_ owner: AnyObject) -> Bool {        self.retained[ObjectIdentifier(owner)] != nil    }     static func isScheduledForTesting(_ owner: AnyObject) -> Bool {        self.scheduled.contains(ObjectIdentifier(owner))    }     static func resetForTesting() {        self.retained.removeAll()        self.scheduled.removeAll()    }    #endif     private static func cleanup(window: NSWindow?, webView: WKWebView?, closeWindow: (() -> Void)?) {        self.log.debug(            "WebKit cleanup",            metadata: ["window": "\(window != nil)", "webView": "\(webView != nil)"])        webView?.stopLoading()        webView?.navigationDelegate = nil        window?.delegate = nil         if self.retainAfterCleanup {            window?.orderOut(nil)        } else if let closeWindow {            closeWindow()        } else {            window?.close()        }    }     private static func scheduleRelease(id: ObjectIdentifier, window: NSWindow?, webView: WKWebView?) {        Task { @MainActor in            let start = Date()            while true {                let elapsed = Date().timeIntervalSince(start)                let windowVisible = window?.isVisible ?? false                let webViewLoading = webView?.isLoading ?? false                let inPlay = windowVisible || webViewLoading                if elapsed >= self.releaseMaximumDelay || (!inPlay && elapsed >= self.releaseMinimumDelay) {                    break                }                try? await Task.sleep(nanoseconds: self.nanoseconds(self.releasePollInterval))            }            self.retained.removeValue(forKey: id)            self.scheduled.remove(id)            self.log.debug("WebKit cleanup released")        }    }     private static func nanoseconds(_ interval: TimeInterval) -> UInt64 {        UInt64(interval * 1_000_000_000)    }}#endif