import Foundation /// Name of the JavaScript thread created by React Native. Copied from `RCTJSThreadManager.mm`. private let jsThreadName = "com.facebook.react.runtime.JavaScript" /// Global actor that is used to isolate the code that should only be executed from the JavaScript thread. /// Theoretically it does not act as a real actor; it uses a serial executor that executes jobs **synchronously** /// without hopping to the proper thread. Meaning that running these jobs on the JavaScript thread must be ensured /// externally before switching to the isolated context, e.g. using `schedule` function on `JavaScriptRuntime` /// or enforcing the isolation with `JavaScriptActor.assumeIsolated`. @globalActor public actor JavaScriptActor: GlobalActor { public static let shared = JavaScriptActor() private init() {} nonisolated private let executor = JavaScriptExecutor() nonisolated public var unownedExecutor: UnownedSerialExecutor { return executor.asUnownedSerialExecutor() } /// An equivalent of `MainActor.assumeIsolated`, but for the `JavaScriptActor`. Assumes that the currently executing /// synchronous function is actually executing on the JavaScript thread and invokes an isolated version of the operation, /// allowing synchronous access to JavaScript runtime state without hopping through asynchronous boundaries. /// /// See https://stackoverflow.com/a/79346971 for more information about the implementation. public static func assumeIsolated(_ operation: @JavaScriptActor () throws -> T) rethrows -> T { typealias YesActor = @JavaScriptActor () throws -> T typealias NoActor = () throws -> T // This will crash if the current context cannot be isolated. shared.executor.checkIsolated() // To do the unsafe cast, we have to pretend it's @escaping. return try withoutActuallyEscaping(operation) { (_ fn: @escaping YesActor) throws -> T in let rawFn = unsafeBitCast(fn, to: NoActor.self) return try rawFn() } } /// Stops program execution if the actor's executor is not isolating the current context. public static func checkIsolated() { shared.executor.checkIsolated() } } /// Executor for the `JavaScriptActor` that executes given jobs synchronously and immediately. /// - Note: It does not ensure that given jobs are executed on the JavaScript thread; it must be done externally. internal class JavaScriptExecutor: SerialExecutor, @unchecked Sendable { func enqueue(_ job: UnownedJob) { job.runSynchronously(on: self.asUnownedSerialExecutor()) } /// Converts the executor to the optimized form of borrowed executor reference. func asUnownedSerialExecutor() -> UnownedSerialExecutor { return UnownedSerialExecutor(ordinary: self) } /// Stops program execution if the executor is not isolating the current context. func checkIsolated() { // Using `assert` instead of `precondition` because this check is a heuristic based on // thread name, not a precise isolation guarantee. Worklet runtimes legitimately run on // the UI thread, which would cause a false-positive crash with `precondition`. assert(isIsolatingCurrentContext() == true, "JavaScriptActor operations must be run on the JavaScript thread") } /// Checks whether the executor isolates the current context, i.e. the current thread is a JavaScript thread. /// The condition is also met when running tests and in single threaded environments. func isIsolatingCurrentContext() -> Bool? { // We must be careful as it relies on the thread name given by React Native. return Thread.current.name == jsThreadName || !Thread.isMultiThreaded() || ProcessInfo.processInfo.processName == "xctest" } } /// An actor that is dedicated for the specific runtime. internal actor JavaScriptRuntimeActor { private weak let runtime: JavaScriptRuntime? nonisolated private let executor: JavaScriptExecutor init(runtime: JavaScriptRuntime) { self.runtime = runtime self.executor = JavaScriptRuntimeExecutor(runtime: runtime) } nonisolated var unownedExecutor: UnownedSerialExecutor { return executor.asUnownedSerialExecutor() } func execute(_ operation: @escaping @JavaScriptActor () async throws -> R) async rethrows -> sending R { return try await operation() } } /// Serial executor dedicated for the specific runtime. internal final class JavaScriptRuntimeExecutor: JavaScriptExecutor, @unchecked Sendable { private weak let runtime: JavaScriptRuntime? init(runtime: JavaScriptRuntime) { self.runtime = runtime } override func enqueue(_ job: UnownedJob) { runtime?.schedule(priority: .immediate) { job.runSynchronously(on: self.asUnownedSerialExecutor()) } } }