// // WebViewPool.swift // Prisme // // Author: Simon-Pierre Boucher // import Foundation import WebKit /// Reusable `WKWebView` instances, partitioned by identity container. /// Never instantiate one web view per tab (CLAUDE.md §8): tabs check a view /// out while visible and check it back in when they leave the screen. @MainActor final class WebViewPool { private static let maxIdlePerContainer = 3 private let containers: IdentityContainerStore private let rules: ContentRuleManager private var idle: [IdentityContainer.ID: [WKWebView]] = [:] /// Weak set of every view we ever vended, so freshly compiled content /// rules can be applied to views already on screen. private let live = NSHashTable.weakObjects() init(containers: IdentityContainerStore, rules: ContentRuleManager) { self.containers = containers self.rules = rules } func checkout(for containerID: IdentityContainer.ID) -> WKWebView { if let reused = idle[containerID]?.popLast() { return reused } let webView = makeWebView(for: containerID) live.add(webView) return webView } func checkin(_ webView: WKWebView, containerID: IdentityContainer.ID) { webView.stopLoading() var stack = idle[containerID] ?? [] guard stack.count < Self.maxIdlePerContainer else { return } stack.append(webView) idle[containerID] = stack } /// Apply a compiled rule list to all current and future web views. func apply(_ ruleList: WKContentRuleList) { for webView in live.allObjects { webView.configuration.userContentController.add(ruleList) } } /// Extractor source is read once; it only defines functions, so the /// injection cost at documentEnd is negligible (CLAUDE.md §8). private static let extractorSource: String? = { guard let url = Bundle.main.url(forResource: "extractor", withExtension: "js") else { return nil } return try? String(contentsOf: url, encoding: .utf8) }() private func makeWebView(for containerID: IdentityContainer.ID) -> WKWebView { let configuration = WKWebViewConfiguration() configuration.websiteDataStore = containers.dataStore(for: containerID) configuration.allowsInlineMediaPlayback = true if let ruleList = rules.ruleList { configuration.userContentController.add(ruleList) } if let source = Self.extractorSource { configuration.userContentController.addUserScript( WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true) ) } let webView = PrismeWebView(frame: .zero, configuration: configuration) webView.allowsBackForwardNavigationGestures = true webView.isFindInteractionEnabled = true webView.scrollView.contentInsetAdjustmentBehavior = .always return webView } }