Skip to content

Fix JavaScriptEventLoop not building with Embedded Swift #354

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 1, 2025
11 changes: 4 additions & 7 deletions Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,21 +105,20 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
return eventLoop
}

@MainActor private static var didInstallGlobalExecutor = false
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() {
MainActor.assumeIsolated {
Self.installGlobalExecutorIsolated()
}
Self.installGlobalExecutorIsolated()
}

@MainActor private static func installGlobalExecutorIsolated() {
private static func installGlobalExecutorIsolated() {
guard !didInstallGlobalExecutor else { return }
didInstallGlobalExecutor = true

#if compiler(>=5.9)
typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) (
Expand Down Expand Up @@ -188,8 +187,6 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
swift_task_enqueueMainExecutor_hook_impl,
to: UnsafeMutableRawPointer?.self
)

didInstallGlobalExecutor = true
}

private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) {
Expand Down
33 changes: 19 additions & 14 deletions Sources/JavaScriptKit/BasicObjects/JSPromise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,21 +84,22 @@ public final class JSPromise: JSBridgedClass {
}
#endif

#if !hasFeature(Embedded)
/// Schedules the `success` closure to be invoked on successful completion of `self`.
@discardableResult
public func then(success: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise {
public func then(success: @escaping (JSValue) -> JSValue) -> JSPromise {
let closure = JSOneshotClosure {
success($0[0]).jsValue
}
return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!)
}

#if compiler(>=5.5)
#if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
/// Schedules the `success` closure to be invoked on successful completion of `self`.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@discardableResult
public func then(success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
public func then(
success: sending @escaping (sending JSValue) async throws -> JSValue
) -> JSPromise {
let closure = JSOneshotClosure.async {
try await success($0[0]).jsValue
}
Expand All @@ -109,8 +110,8 @@ public final class JSPromise: JSBridgedClass {
/// Schedules the `success` closure to be invoked on successful completion of `self`.
@discardableResult
public func then(
success: @escaping (sending JSValue) -> ConvertibleToJSValue,
failure: @escaping (sending JSValue) -> ConvertibleToJSValue
success: @escaping (sending JSValue) -> JSValue,
failure: @escaping (sending JSValue) -> JSValue
) -> JSPromise {
let successClosure = JSOneshotClosure {
success($0[0]).jsValue
Expand All @@ -121,13 +122,13 @@ public final class JSPromise: JSBridgedClass {
return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!)
}

#if compiler(>=5.5)
#if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
/// Schedules the `success` closure to be invoked on successful completion of `self`.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@discardableResult
public func then(
success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue,
failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue
success: sending @escaping (sending JSValue) async throws -> JSValue,
failure: sending @escaping (sending JSValue) async throws -> JSValue
) -> JSPromise {
let successClosure = JSOneshotClosure.async {
try await success($0[0]).jsValue
Expand All @@ -141,19 +142,24 @@ public final class JSPromise: JSBridgedClass {

/// Schedules the `failure` closure to be invoked on rejected completion of `self`.
@discardableResult
public func `catch`(failure: @escaping (sending JSValue) -> ConvertibleToJSValue) -> JSPromise {
public func `catch`(
failure: @escaping (sending JSValue) -> JSValue
)
-> JSPromise
{
let closure = JSOneshotClosure {
failure($0[0]).jsValue
}
return .init(unsafelyWrapping: jsObject.catch!(closure).object!)
}

#if compiler(>=5.5)
#if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
/// Schedules the `failure` closure to be invoked on rejected completion of `self`.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@discardableResult
public func `catch`(failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise
{
public func `catch`(
failure: sending @escaping (sending JSValue) async throws -> JSValue
) -> JSPromise {
let closure = JSOneshotClosure.async {
try await failure($0[0]).jsValue
}
Expand All @@ -171,5 +177,4 @@ public final class JSPromise: JSBridgedClass {
}
return .init(unsafelyWrapping: jsObject.finally!(closure).object!)
}
#endif
}
9 changes: 6 additions & 3 deletions Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import _CJavaScriptKit
#if hasFeature(Embedded) && os(WASI)
import _Concurrency
#endif

/// `JSClosureProtocol` wraps Swift closure objects for use in JavaScript. Conforming types
/// are responsible for managing the lifetime of the closure they wrap, but can delegate that
Expand Down Expand Up @@ -40,7 +43,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
fatalError("JSOneshotClosure does not support dictionary literal initialization")
}

#if compiler(>=5.5) && !hasFeature(Embedded)
#if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public static func async(_ body: sending @escaping (sending [JSValue]) async throws -> JSValue) -> JSOneshotClosure
{
Expand Down Expand Up @@ -132,7 +135,7 @@ public class JSClosure: JSFunction, JSClosureProtocol {
fatalError("JSClosure does not support dictionary literal initialization")
}

#if compiler(>=5.5) && !hasFeature(Embedded)
#if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public static func async(_ body: @Sendable @escaping (sending [JSValue]) async throws -> JSValue) -> JSClosure {
JSClosure(makeAsyncClosure(body))
Expand All @@ -148,7 +151,7 @@ public class JSClosure: JSFunction, JSClosureProtocol {
#endif
}

#if compiler(>=5.5) && !hasFeature(Embedded)
#if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
private func makeAsyncClosure(
_ body: sending @escaping (sending [JSValue]) async throws -> JSValue
Expand Down
6 changes: 3 additions & 3 deletions Tests/JavaScriptEventLoopTests/JSPromiseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ final class JSPromiseTests: XCTestCase {
p1 = p1.then { value in
XCTAssertEqual(value, .null)
continuation.resume()
return JSValue.number(1.0)
return JSValue.number(1.0).jsValue
}
}
await withCheckedContinuation { continuation in
p1 = p1.then { value in
XCTAssertEqual(value, .number(1.0))
continuation.resume()
return JSPromise.resolve(JSValue.boolean(true))
return JSPromise.resolve(JSValue.boolean(true)).jsValue
}
}
await withCheckedContinuation { continuation in
Expand Down Expand Up @@ -48,7 +48,7 @@ final class JSPromiseTests: XCTestCase {
p2 = p2.then { value in
XCTAssertEqual(value, .boolean(true))
continuation.resume()
return JSPromise.reject(JSValue.number(2.0))
return JSPromise.reject(JSValue.number(2.0)).jsValue
}
}
await withCheckedContinuation { continuation in
Expand Down
4 changes: 2 additions & 2 deletions Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ final class JavaScriptEventLoopTests: XCTestCase {
}
let promise2 = promise.then { result in
try await Task.sleep(nanoseconds: 100_000_000)
return String(result.number!)
return .string(String(result.number!))
}
let thenDiff = try await measureTime {
let result = try await promise2.value
Expand All @@ -171,7 +171,7 @@ final class JavaScriptEventLoopTests: XCTestCase {
100
)
}
let failingPromise2 = failingPromise.then { _ in
let failingPromise2 = failingPromise.then { _ -> JSValue in
throw MessageError("Should not be called", file: #file, line: #line, column: #column)
} failure: { err in
return err
Expand Down