Files
steipete--codexbar/Sources/CodexBarCore/WebKit/WebKitTeardown.swift
T
2026-07-13 12:22:33 +08:00

109 lines
3.6 KiB
Swift

#if os(macOS)
import AppKit
import Foundation
import WebKit
@MainActor
public 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