Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion BrowserKit/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ let package = Package(
name: "WebEngine",
dependencies: ["Common",
.product(name: "GCDWebServers", package: "GCDWebServer")],
swiftSettings: [.unsafeFlags(["-enable-testing"])]),
swiftSettings: [.unsafeFlags(["-enable-testing"]),
.enableExperimentalFeature("StrictConcurrency")]),
.testTarget(
name: "WebEngineTests",
dependencies: ["WebEngine"]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import Foundation
public protocol MenuHelperWebViewInterface {
/// Used to add a find in page menu option on webview textfields
@objc
optional func menuHelperFindInPage()
optional nonisolated func menuHelperFindInPage()

/// Used to add a search with "client" menu option on the webview textfields
@objc
optional func menuHelperSearchWith()
optional nonisolated func menuHelperSearchWith()
}

/// Used to pass in the Client strings for the webview textfields menu options
Expand Down
4 changes: 1 addition & 3 deletions BrowserKit/Sources/WebEngine/Engine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,18 @@ import Foundation

/// The engine used to create an `EngineView` and `EngineSession`.
/// There is only when engine view to be created, but multiple sessions can exists.
@MainActor
public protocol Engine {
/// Creates a new view for rendering web content.
/// - Returns: The created `EngineView`
@MainActor
func createView() -> EngineView

/// Creates a new engine session.
/// - Parameter dependencies: Pass in the required session dependencies on creation
/// - Returns: The created `EngineSession`
@MainActor
func createSession(dependencies: EngineSessionDependencies) throws -> EngineSession

/// Warm the `Engine` whenever we move the application to foreground
@MainActor
func warmEngine()

/// Idle the `Engine` whenever we move the application to background
Expand Down
5 changes: 2 additions & 3 deletions BrowserKit/Sources/WebEngine/EngineSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import Foundation
import UIKit

/// Protocol representing a single engine session. In browsers usually a session corresponds to a tab.
@MainActor
public protocol EngineSession: NSObject {
/// Engine session delegate
var delegate: EngineSessionDelegate? { get set }

/// Proxy object for handling telemetry events.
var telemetryProxy: EngineTelemetryProxy? { get set }
nonisolated var telemetryProxy: EngineTelemetryProxy? { get set }

/// Whether the engine session is currently being rendered
var isActive: Bool { get set }
Expand All @@ -34,7 +35,6 @@ public protocol EngineSession: NSObject {
func goForward()

/// Scroll the session to the top.
@MainActor
func scrollToTop()

/// Show the web view's built-in find interaction.
Expand Down Expand Up @@ -63,7 +63,6 @@ public protocol EngineSession: NSObject {
func restore(state: Data)

/// Close the session. This may free underlying objects. Call this when you are finished using this session.
@MainActor
func close()

/// Switch to standard tracking protection mode.
Expand Down
1 change: 1 addition & 0 deletions BrowserKit/Sources/WebEngine/FullscreenDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Foundation
///
/// Due to limited documentation on this behavior, the following methods handle
/// fullscreen transitions based on trial and error.
@MainActor
public protocol FullscreenDelegate: AnyObject {
/// Called when the web view enters fullscreen mode.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import Common
import Foundation

/// Helper to get the metadata fetched out of a `WKEngineSession`
@MainActor
protocol MetadataFetcherHelper {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should something that "fetches" data really be fully main actor isolated? I imagine it has to do some background stuff... 🤔 (same concern for the annotation below also)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, theoritically yes I agree, but the thing is that metadata is fetched with evaluateJavascript method, and that's marked as @MainActor in WKWebView package.
Screenshot 2025-09-26 at 4 57 31 PM

And also, since to other changes, this whole class screams red if it's not main actor 😆
Screenshot 2025-09-26 at 4 56 31 PM

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh myyyy 😆 Ok yep keep on as you were then hahaha!! Just ignore me 😂

var delegate: MetadataFetcherDelegate? { get set }
func fetch(fromSession session: WKEngineSession, url: URL)
}

@MainActor
protocol MetadataFetcherDelegate: AnyObject {
func didLoad(pageMetadata: EnginePageMetadata)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,22 @@ public enum ReadabilityOperationResult {
case timeout
}

@MainActor
protocol ReaderModeNavigationDelegate: AnyObject {
func didFailWithError(error: Error)
func didFinish()
}

// TODO: FXIOS-11373 - finish handling reader mode in WebEngine - this class is to be tested
@MainActor
final class WKReadabilityOperation: Operation,
@unchecked Sendable,
ReaderModeNavigationDelegate,
WKReaderModeDelegate {
var url: URL
var semaphore: DispatchSemaphore
// TODO: FXIOS-11373 - The original code had a semaphore, but it's removed here since with @MainActor we
// don't want to suspense the main thread. So this class needs to be retought if this project is picked back again.
// var semaphore: DispatchSemaphore
var result: ReadabilityOperationResult?
var session: WKEngineSession?
var readerModeCache: ReaderModeCache
Expand All @@ -36,7 +40,6 @@ final class WKReadabilityOperation: Operation,
logger: Logger = DefaultLogger.shared
) {
self.url = url
self.semaphore = DispatchSemaphore(value: 0)
self.readerModeCache = readerModeCache
self.mainQueue = mainQueue
self.logger = logger
Expand All @@ -46,34 +49,30 @@ final class WKReadabilityOperation: Operation,
if self.isCancelled {
return
}
// TODO: FXIOS-11373 - finish handling reader mode in WebEngine - This isn't concurrency safe

// Setup a new session and kick all this off on the main thread since UIKit
// and WebKit are not safe from other threads.
Task { @MainActor in
let configProvider = DefaultWKEngineConfigurationProvider()
let parameters = WKWebViewParameters()
let dependencies = EngineSessionDependencies(webviewParameters: parameters)
let session = WKEngineSession.sessionFactory(userScriptManager: DefaultUserScriptManager(),
dependencies: dependencies,
configurationProvider: configProvider,
readerModeDelegate: self)
session?.navigationHandler.readerModeNavigationDelegate = self
self.session = session

// Load the page in the session. This either fails with a navigation error, or we
// get a readability callback. Or it takes too long, in which case the semaphore
// times out. The script on the page will retry every 500ms for 10 seconds.
let context = BrowsingContext(type: .internalNavigation, url: self.url)
guard let browserURL = BrowserURL(browsingContext: context) else { return }
session?.load(browserURL: browserURL)
}

let timeout = DispatchTime.now() + .seconds(10)
if semaphore.wait(timeout: timeout) == .timedOut {
result = ReadabilityOperationResult.timeout
}

processResult()
// Task { @MainActor in
// let configProvider = DefaultWKEngineConfigurationProvider()
// let parameters = WKWebViewParameters()
// let dependencies = EngineSessionDependencies(webviewParameters: parameters)
// let session = WKEngineSession.sessionFactory(userScriptManager: DefaultUserScriptManager(),
// dependencies: dependencies,
// configurationProvider: configProvider,
// readerModeDelegate: self)
// session?.navigationHandler.readerModeNavigationDelegate = self
// self.session = session
//
// // Load the page in the session. This either fails with a navigation error, or we
// // get a readability callback. Or it takes too long, in which case the semaphore
// // times out. The script on the page will retry every 500ms for 10 seconds.
// let context = BrowsingContext(type: .internalNavigation, url: self.url)
// guard let browserURL = BrowserURL(browsingContext: context) else { return }
// session?.load(browserURL: browserURL)
// }
//
// processResult()
}

private func processResult() {
Expand Down Expand Up @@ -106,7 +105,7 @@ final class WKReadabilityOperation: Operation,

func didFailWithError(error: Error) {
result = ReadabilityOperationResult.error(error as NSError)
semaphore.signal()
// semaphore.signal()
}

func didFinish() {
Expand Down Expand Up @@ -140,6 +139,6 @@ final class WKReadabilityOperation: Operation,
guard session == self.session else { return }

result = ReadabilityOperationResult.success(readabilityResult)
semaphore.signal()
// semaphore.signal()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import Foundation

// TODO: FXIOS-11373 - finish handling reader mode in WebEngine - this class is to be tested
@MainActor
final class WKReadabilityService {
private let ReadabilityServiceDefaultConcurrency = 1
var queue: OperationQueue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Common
import Foundation

/// Delegate that contains callbacks that we have added on top of the built-in WKWebViewDelegate
@MainActor
public protocol WKReaderModeDelegate: AnyObject {
func readerMode(
_ readerMode: ReaderModeStyleSetter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class WKReaderModeHandlers: WKReaderModeHandlersProtocol, Notifiable {
}
}

@MainActor
private func handleReaderModeError(url: URL,
readerModeConfiguration: ReaderModeConfiguration) -> GCDWebServerResponse? {
WKReadabilityService().process(url, cache: readerModeCache)
Expand Down
5 changes: 1 addition & 4 deletions BrowserKit/Sources/WebEngine/WKWebview/WKEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Common
import Foundation
import WebKit

@MainActor
public class WKEngine: Engine {
private let sourceTimerFactory: DispatchSourceTimerFactory
private var shutdownWebServerTimer: DispatchSourceInterface?
Expand All @@ -15,7 +16,6 @@ public class WKEngine: Engine {
private let configProvider: WKEngineConfigurationProvider

// TODO: FXIOS-13670 With Swift 6 we can use default params in the init
@MainActor
public static func factory(engineDependencies: EngineDependencies) -> WKEngine {
let configProvider = DefaultWKEngineConfigurationProvider()
let userScriptManager = DefaultUserScriptManager()
Expand All @@ -29,7 +29,6 @@ public class WKEngine: Engine {
)
}

@MainActor
init(userScriptManager: WKUserScriptManager,
webServerUtil: WKWebServerUtil,
sourceTimerFactory: DispatchSourceTimerFactory,
Expand All @@ -48,7 +47,6 @@ public class WKEngine: Engine {
return WKEngineView.factory(frame: .zero)
}

@MainActor
public func createSession(dependencies: EngineSessionDependencies) throws -> EngineSession {
guard let session = WKEngineSession.sessionFactory(userScriptManager: userScriptManager,
dependencies: dependencies,
Expand All @@ -59,7 +57,6 @@ public class WKEngine: Engine {
return session
}

@MainActor
public func warmEngine() {
shutdownWebServerTimer?.cancel()
shutdownWebServerTimer = nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@ import Common
import Foundation
@preconcurrency import WebKit

@MainActor
protocol SessionHandler: AnyObject {
func commitURLChange()
func fetchMetadata(withURL url: URL)
func received(error: NSError, forURL url: URL)
}

@MainActor
protocol WKJavascriptInterface: AnyObject {
/// Calls a javascript method.
/// - Parameter method: The method signature to be called in javascript world.
/// - Parameter scope: An optional string defining the scope in which the method should be called.
func callJavascriptMethod(_ method: String, scope: String?)
}

@MainActor
class WKEngineSession: NSObject,
EngineSession,
WKEngineWebViewDelegate,
Expand All @@ -30,7 +33,7 @@ class WKEngineSession: NSObject,
uiHandler.delegate = delegate
}
}
weak var telemetryProxy: EngineTelemetryProxy?
nonisolated(unsafe) weak var telemetryProxy: EngineTelemetryProxy?
weak var fullscreenDelegate: FullscreenDelegate?

private(set) var webView: WKEngineWebView
Expand Down
Loading