import JavaScriptKit #if compiler(>=6.4) || (swift(>=6.3) && arch(wasm32)) @_spi(ExperimentalCustomExecutors) import _Concurrency #else import _Concurrency #endif import _CJavaScriptEventLoop import _CJavaScriptKit // NOTE: `@available` annotations are semantically wrong, but they make it easier to develop applications targeting WebAssembly in Xcode. #if compiler(>=5.5) /// Singleton type responsible for integrating JavaScript event loop as a Swift concurrency executor, conforming to /// `SerialExecutor` protocol from the standard library. To utilize it: /// /// 1. Make sure that your target depends on `JavaScriptEventLoop` in your `Packages.swift`: /// /// ```swift /// .target( /// name: "JavaScriptKitExample", /// dependencies: [ /// "JavaScriptKit", /// .product(name: "JavaScriptEventLoop", package: "JavaScriptKit") /// ] /// ) /// ``` /// /// 2. Add an explicit import in the code that executes **before* you start using `await` and/or `Task` /// APIs (most likely in `main.swift`): /// /// ```swift /// import JavaScriptEventLoop /// ``` /// /// 3. Run this function **before* you start using `await` and/or `Task` APIs (again, most likely in /// `main.swift`): /// /// ```swift /// JavaScriptEventLoop.installGlobalExecutor() /// ``` @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { /// A function that queues a given closure as a microtask into JavaScript event loop. /// See also: https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide public var queueMicrotask: (@escaping () -> Void) -> Void /// A function that invokes a given closure after a specified number of milliseconds. public var setTimeout: (Double, @escaping () -> Void) -> Void /// A mutable state to manage internal job queue /// Note that this should be guarded atomically when supporting multi-threaded environment. var queueState = QueueState() private init( queueTask: @escaping (@escaping () -> Void) -> Void, setTimeout: @escaping (Double, @escaping () -> Void) -> Void ) { self.queueMicrotask = queueTask self.setTimeout = setTimeout } /// A per-thread singleton instance of the Executor public static var shared: JavaScriptEventLoop { return _shared } #if compiler(>=6.1) && _runtime(_multithreaded) // In multi-threaded environment, we have an event loop executor per // thread (per Web Worker). A job enqueued in one thread should be // executed in the same thread under this global executor. private static var _shared: JavaScriptEventLoop { if let tls = swjs_thread_local_event_loop { let eventLoop = Unmanaged.fromOpaque(tls).takeUnretainedValue() return eventLoop } let eventLoop = create() swjs_thread_local_event_loop = Unmanaged.passRetained(eventLoop).toOpaque() return eventLoop } #else private static let _shared: JavaScriptEventLoop = create() #endif private static func create() -> JavaScriptEventLoop { let promise = JSPromise(resolver: { resolver -> Void in resolver(.success(.undefined)) }) let setTimeout = JSObject.global.setTimeout.object! let eventLoop = JavaScriptEventLoop( queueTask: { job in // TODO(katei): Should prefer `queueMicrotask` if available? // We should measure if there is performance advantage. promise.then { _ in job() return JSValue.undefined } }, setTimeout: { delay, job in setTimeout( JSOneshotClosure { _ in job() return JSValue.undefined }, delay ) } ) return eventLoop } private nonisolated(unsafe) static var didInstallGlobalExecutor = false /// Set JavaScript event loop based executor to be the global executor /// Note that this should be called before any of the jobs are created. /// This installation step will be unnecessary after custom executor are /// introduced officially. See also [a draft proposal for custom /// executors](https://github.com/rjmccall/swift-evolution/blob/custom-executors/proposals/0000-custom-executors.md#the-default-global-concurrent-executor) public static func installGlobalExecutor() { Self.installGlobalExecutorIsolated() } private static func installGlobalExecutorIsolated() { guard !didInstallGlobalExecutor else { return } didInstallGlobalExecutor = true #if (compiler(>=6.4) || (swift(>=6.3) && arch(wasm32))) && !hasFeature(Embedded) if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) { // For Swift 6.4 and above, we can use the new `ExecutorFactory` API _Concurrency._createExecutors(factory: JavaScriptEventLoop.self) } #else // For Swift 6.1 and below, or Embedded Swift, we need to install // the global executor by hook API. The ExecutorFactory mechanism // does not work in Embedded Swift because ExecutorImpl.swift is // excluded from the embedded Concurrency library. installByLegacyHook() #endif } internal func enqueue(_ job: UnownedJob, withDelay milliseconds: Double) { setTimeout( milliseconds, { #if compiler(>=5.9) job.runSynchronously(on: self.asUnownedSerialExecutor()) #else job._runSynchronously(on: self.asUnownedSerialExecutor()) #endif } ) } internal func unsafeEnqueue(_ job: UnownedJob) { #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) guard swjs_get_worker_thread_id_cached() == SWJS_MAIN_THREAD_ID else { // Notify the main thread to execute the job when a job is // enqueued from a Web Worker thread but without an executor preference. // This is usually the case when hopping back to the main thread // at the end of a task. let jobBitPattern = unsafeBitCast(job, to: UInt.self) swjs_send_job_to_main_thread(jobBitPattern) return } // If the current thread is the main thread, do nothing special. #endif insertJobQueue(job: job) } #if compiler(>=5.9) @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) public func enqueue(_ job: consuming ExecutorJob) { // NOTE: Converting a `ExecutorJob` to an ``UnownedJob`` and invoking // ``UnownedJob/runSynchronously(_:)` on it multiple times is undefined behavior. unsafeEnqueue(UnownedJob(job)) } #else public func enqueue(_ job: UnownedJob) { unsafeEnqueue(job) } #endif public func asUnownedSerialExecutor() -> UnownedSerialExecutor { return UnownedSerialExecutor(ordinary: self) } } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension JSPromise { /// Wait for the promise to complete, returning (or throwing) its result. public var value: JSValue { get async throws(JSException) { try await withUnsafeContinuation { [self] continuation in self.then( success: { continuation.resume(returning: Swift.Result.success($0)) return JSValue.undefined }, failure: { continuation.resume(returning: Swift.Result.failure(.init($0))) return JSValue.undefined } ) }.get() } } /// Wait for the promise to complete, returning its result or exception as a Result. /// /// - Note: Calling this function does not switch from the caller's isolation domain. public func value(isolation: isolated (any Actor)? = #isolation) async throws(JSException) -> JSValue { try await withUnsafeContinuation(isolation: isolation) { [self] continuation in self.then( success: { continuation.resume(returning: Swift.Result.success($0)) return JSValue.undefined }, failure: { continuation.resume(returning: Swift.Result.failure(.init($0))) return JSValue.undefined } ) }.get() } /// Wait for the promise to complete, returning its result or exception as a Result. public var result: JSPromise.Result { get async { await withUnsafeContinuation { [self] continuation in self.then( success: { continuation.resume(returning: .success($0)) return JSValue.undefined }, failure: { continuation.resume(returning: .failure($0)) return JSValue.undefined } ) } } } } #endif