|
| 1 | +// Implementation of custom executors for JavaScript event loop |
| 2 | +// This file implements the ExecutorFactory protocol to provide custom main and global executors |
| 3 | +// for Swift concurrency in JavaScript environment. |
| 4 | +// See: https://github.com/swiftlang/swift/pull/80266 |
| 5 | +// See: https://forums.swift.org/t/pitch-2-custom-main-and-global-executors/78437 |
| 6 | + |
| 7 | +import _CJavaScriptKit |
| 8 | + |
| 9 | +#if compiler(>=6.2) |
| 10 | + |
| 11 | +// MARK: - MainExecutor Implementation |
| 12 | +// MainExecutor is used by the main actor to execute tasks on the main thread |
| 13 | +@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) |
| 14 | +extension JavaScriptEventLoop: MainExecutor { |
| 15 | + public func run() throws { |
| 16 | + // This method is called from `swift_task_asyncMainDrainQueueImpl`. |
| 17 | + // https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a/stdlib/public/Concurrency/ExecutorImpl.swift#L28 |
| 18 | + // Yield control to the JavaScript event loop to skip the `exit(0)` |
| 19 | + // call by `swift_task_asyncMainDrainQueueImpl`. |
| 20 | + swjs_unsafe_event_loop_yield() |
| 21 | + } |
| 22 | + public func stop() {} |
| 23 | +} |
| 24 | + |
| 25 | +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) |
| 26 | +extension JavaScriptEventLoop: TaskExecutor {} |
| 27 | + |
| 28 | +@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) |
| 29 | +extension JavaScriptEventLoop: SchedulableExecutor { |
| 30 | + public func enqueue<C: Clock>( |
| 31 | + _ job: consuming ExecutorJob, |
| 32 | + after delay: C.Duration, |
| 33 | + tolerance: C.Duration?, |
| 34 | + clock: C |
| 35 | + ) { |
| 36 | + let milliseconds = Self.delayInMilliseconds(from: delay, clock: clock) |
| 37 | + self.enqueue( |
| 38 | + UnownedJob(job), |
| 39 | + withDelay: milliseconds |
| 40 | + ) |
| 41 | + } |
| 42 | + |
| 43 | + private static func delayInMilliseconds<C: Clock>(from duration: C.Duration, clock: C) -> Double { |
| 44 | + let swiftDuration = clock.convert(from: duration)! |
| 45 | + let (seconds, attoseconds) = swiftDuration.components |
| 46 | + return Double(seconds) * 1_000 + (Double(attoseconds) / 1_000_000_000_000_000) |
| 47 | + } |
| 48 | +} |
| 49 | + |
| 50 | +// MARK: - ExecutorFactory Implementation |
| 51 | +@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) |
| 52 | +extension JavaScriptEventLoop: ExecutorFactory { |
| 53 | + // Forward all operations to the current thread's JavaScriptEventLoop instance |
| 54 | + final class CurrentThread: TaskExecutor, SchedulableExecutor, MainExecutor, SerialExecutor { |
| 55 | + func checkIsolated() {} |
| 56 | + |
| 57 | + func enqueue(_ job: consuming ExecutorJob) { |
| 58 | + JavaScriptEventLoop.shared.enqueue(job) |
| 59 | + } |
| 60 | + |
| 61 | + func enqueue<C: Clock>( |
| 62 | + _ job: consuming ExecutorJob, |
| 63 | + after delay: C.Duration, |
| 64 | + tolerance: C.Duration?, |
| 65 | + clock: C |
| 66 | + ) { |
| 67 | + JavaScriptEventLoop.shared.enqueue( |
| 68 | + job, |
| 69 | + after: delay, |
| 70 | + tolerance: tolerance, |
| 71 | + clock: clock |
| 72 | + ) |
| 73 | + } |
| 74 | + func run() throws { |
| 75 | + try JavaScriptEventLoop.shared.run() |
| 76 | + } |
| 77 | + func stop() { |
| 78 | + JavaScriptEventLoop.shared.stop() |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + public static var mainExecutor: any MainExecutor { |
| 83 | + CurrentThread() |
| 84 | + } |
| 85 | + |
| 86 | + public static var defaultExecutor: any TaskExecutor { |
| 87 | + CurrentThread() |
| 88 | + } |
| 89 | +} |
| 90 | + |
| 91 | +#endif // compiler(>=6.2) |
0 commit comments