forked from swiftwasm/JavaScriptKit
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathJavaScriptEventLoop.swift
139 lines (120 loc) · 5.78 KB
/
JavaScriptEventLoop.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import JavaScriptKit
import _CJavaScriptEventLoop
// NOTE: `@available` annotations are semantically wrong, but they make it easier to develop applications targeting WebAssembly in Xcode.
#if compiler(>=5.5)
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.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
let queueMicrotask: @Sendable (@escaping () -> Void) -> Void
/// A function that invokes a given closure after a specified number of milliseconds.
let setTimeout: @Sendable (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: @Sendable @escaping (@escaping () -> Void) -> Void,
setTimeout: @Sendable @escaping (Double, @escaping () -> Void) -> Void
) {
self.queueMicrotask = queueTask
self.setTimeout = setTimeout
}
/// A singleton instance of the Executor
public static let shared: JavaScriptEventLoop = {
let promise = JSPromise(resolver: { resolver -> Void in
resolver(.success(.undefined))
})
let setTimeout = JSObject.global.setTimeout.function!
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 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 the custom-executor will be introduced officially.
/// See also: https://github.com/rjmccall/swift-evolution/blob/custom-executors/proposals/0000-custom-executors.md#the-default-global-concurrent-executor
public static func installGlobalExecutor() {
guard !didInstallGlobalExecutor else { return }
typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) -> Void
let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in
JavaScriptEventLoop.shared.enqueue(job)
}
swift_task_enqueueGlobal_hook = unsafeBitCast(swift_task_enqueueGlobal_hook_impl, to: UnsafeMutableRawPointer?.self)
typealias swift_task_enqueueGlobalWithDelay_hook_Fn = @convention(thin) (UInt64, UnownedJob, swift_task_enqueueGlobalWithDelay_original) -> Void
let swift_task_enqueueGlobalWithDelay_hook_impl: swift_task_enqueueGlobalWithDelay_hook_Fn = { delay, job, original in
JavaScriptEventLoop.shared.enqueue(job, withDelay: delay)
}
swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast(swift_task_enqueueGlobalWithDelay_hook_impl, to: UnsafeMutableRawPointer?.self)
typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueMainExecutor_original) -> Void
let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in
JavaScriptEventLoop.shared.enqueue(job)
}
swift_task_enqueueMainExecutor_hook = unsafeBitCast(swift_task_enqueueMainExecutor_hook_impl, to: UnsafeMutableRawPointer?.self)
didInstallGlobalExecutor = true
}
private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) {
let milliseconds = nanoseconds / 1_000_000
setTimeout(Double(milliseconds), {
job._runSynchronously(on: self.asUnownedSerialExecutor())
})
}
public func enqueue(_ job: UnownedJob) {
insertJobQueue(job: job)
}
public func asUnownedSerialExecutor() -> UnownedSerialExecutor {
return UnownedSerialExecutor(ordinary: self)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public extension JSPromise {
/// Wait for the promise to complete, returning (or throwing) its result.
var value: JSValue {
get async throws {
try await withUnsafeThrowingContinuation { [self] continuation in
self.then(
success: {
continuation.resume(returning: $0)
return JSValue.undefined
},
failure: {
continuation.resume(throwing: $0)
return JSValue.undefined
}
)
}
}
}
/// Wait for the promise to complete, returning its result or exception as a Result.
var result: Result<JSValue, JSValue> {
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