From 9c049491cb38d332e43edbc73fa948447e41937d Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 28 Jan 2022 13:44:41 -0500 Subject: [PATCH 01/14] Update JSPromise docs --- .../BasicObjects/JSPromise.swift | 48 +++++++------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 0aa44cadd..19652ea64 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -1,14 +1,4 @@ -/** A wrapper around [the JavaScript `Promise` class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) -that exposes its functions in a type-safe and Swifty way. The `JSPromise` API is generic over both -`Success` and `Failure` types, which improves compatibility with other statically-typed APIs such -as Combine. If you don't know the exact type of your `Success` value, you should use `JSValue`, e.g. -`JSPromise`. In the rare case, where you can't guarantee that the error thrown -is of actual JavaScript `Error` type, you should use `JSPromise`. - -This doesn't 100% match the JavaScript API, as `then` overload with two callbacks is not available. -It's impossible to unify success and failure types from both callbacks in a single returned promise -without type erasure. You should chain `then` and `catch` in those cases to avoid type erasure. -*/ +/// A wrapper around [the JavaScript `Promise` class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) public final class JSPromise: JSBridgedClass { /// The underlying JavaScript `Promise` object. public let jsObject: JSObject @@ -27,25 +17,27 @@ public final class JSPromise: JSBridgedClass { self.jsObject = object } - /** Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `jsObject` - is not an instance of JavaScript `Promise`, this initializer will return `nil`. - */ + /// Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `jsObject` + /// is not an instance of JavaScript `Promise`, this initializer will return `nil`. public convenience init?(_ jsObject: JSObject) { self.init(from: jsObject) } - /** Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `value` - is not an object and is not an instance of JavaScript `Promise`, this function will - return `nil`. - */ + /// Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `value` + /// is not an object and is not an instance of JavaScript `Promise`, this function will + /// return `nil`. public static func construct(from value: JSValue) -> Self? { guard case let .object(jsObject) = value else { return nil } return Self.init(jsObject) } - /** Creates a new `JSPromise` instance from a given `resolver` closure. `resolver` takes - two closure that your code should call to either resolve or reject this `JSPromise` instance. - */ + /// Creates a new `JSPromise` instance from a given `resolver` closure. + /// The closure is passed a completion handler. Passing a successful + /// `Result` to the completion handler will cause the promise to resolve + /// with the corresponding value; passing a failure `Result` will cause the + /// promise to reject with the corresponding value. + /// Calling the completion handler more than once will have no effect + /// (per the JavaScript specification). public convenience init(resolver: @escaping (@escaping (Result) -> ()) -> ()) { let closure = JSOneshotClosure { arguments in // The arguments are always coming from the `Promise` constructor, so we should be @@ -74,8 +66,7 @@ public final class JSPromise: JSBridgedClass { self.init(unsafelyWrapping: Self.constructor.reject!(reason).object!) } - /** Schedules the `success` closure to be invoked on sucessful completion of `self`. - */ + /// Schedules the `success` closure to be invoked on sucessful completion of `self`. @discardableResult public func then(success: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure { @@ -84,8 +75,7 @@ public final class JSPromise: JSBridgedClass { return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } - /** Schedules the `success` closure to be invoked on sucessful completion of `self`. - */ + /// Schedules the `success` closure to be invoked on sucessful completion of `self`. @discardableResult public func then(success: @escaping (JSValue) -> ConvertibleToJSValue, failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { @@ -98,8 +88,7 @@ public final class JSPromise: JSBridgedClass { return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) } - /** Schedules the `failure` closure to be invoked on rejected completion of `self`. - */ + /// Schedules the `failure` closure to be invoked on rejected completion of `self`. @discardableResult public func `catch`(failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure { @@ -108,9 +97,8 @@ public final class JSPromise: JSBridgedClass { return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } - /** Schedules the `failure` closure to be invoked on either successful or rejected completion of - `self`. - */ + /// Schedules the `failure` closure to be invoked on either successful or rejected + /// completion of `self`. @discardableResult public func finally(successOrFailure: @escaping () -> ()) -> JSPromise { let closure = JSOneshotClosure { _ in From 34f22bf34bf089642d5e2fa62ba7a933992fd33e Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 28 Jan 2022 13:48:18 -0500 Subject: [PATCH 02/14] Add a JSClosure.async {} initializer --- .../FundamentalObjects/JSClosure.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index cbd44bd6e..9cdd8cd2e 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -84,6 +84,27 @@ public class JSClosure: JSObject, JSClosureProtocol { Self.sharedClosures[hostFuncRef] = (self, body) } + #if compiler(>=5.5) + static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure { + JSClosure { arguments in + JSPromise { resolver in + Task { + do { + let result = try await body(arguments) + resolver(.success(result)) + } catch { + if let jsError = error as? JSError { + resolver(.failure(jsError.jsValue())) + } else { + resolver(.failure(JSError(message: String(describing: error)).jsValue())) + } + } + } + }.jsValue() + } + } + #endif + #if JAVASCRIPTKIT_WITHOUT_WEAKREFS deinit { guard isReleased else { From e81349972eec94c014fcf78f11c27477f4f297a6 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 28 Jan 2022 13:49:10 -0500 Subject: [PATCH 03/14] spelling fix --- Sources/JavaScriptKit/BasicObjects/JSPromise.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 19652ea64..cee01d301 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -66,7 +66,7 @@ public final class JSPromise: JSBridgedClass { self.init(unsafelyWrapping: Self.constructor.reject!(reason).object!) } - /// Schedules the `success` closure to be invoked on sucessful completion of `self`. + /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult public func then(success: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure { @@ -75,7 +75,7 @@ public final class JSPromise: JSBridgedClass { return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } - /// Schedules the `success` closure to be invoked on sucessful completion of `self`. + /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult public func then(success: @escaping (JSValue) -> ConvertibleToJSValue, failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { From 86ab3551f696b710bacb15c95d7e48413eb55953 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 28 Jan 2022 13:52:51 -0500 Subject: [PATCH 04/14] Async JSOneshotClosure --- .../FundamentalObjects/JSClosure.swift | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 9cdd8cd2e..7951a9acb 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -30,6 +30,12 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { }) } + #if compiler(>=5.5) + static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSOneshotClosure { + JSOneshotClosure(makeAsyncClosure(body)) + } + #endif + /// Release this function resource. /// After calling `release`, calling this function from JavaScript will fail. public func release() { @@ -86,22 +92,7 @@ public class JSClosure: JSObject, JSClosureProtocol { #if compiler(>=5.5) static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure { - JSClosure { arguments in - JSPromise { resolver in - Task { - do { - let result = try await body(arguments) - resolver(.success(result)) - } catch { - if let jsError = error as? JSError { - resolver(.failure(jsError.jsValue())) - } else { - resolver(.failure(JSError(message: String(describing: error)).jsValue())) - } - } - } - }.jsValue() - } + JSClosure(makeAsyncClosure(body)) } #endif @@ -114,6 +105,25 @@ public class JSClosure: JSObject, JSClosureProtocol { #endif } +private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSValue) -> (([JSValue]) -> JSValue) { + { arguments in + JSPromise { resolver in + Task { + do { + let result = try await body(arguments) + resolver(.success(result)) + } catch { + if let jsError = error as? JSError { + resolver(.failure(jsError.jsValue())) + } else { + resolver(.failure(JSError(message: String(describing: error)).jsValue())) + } + } + } + }.jsValue() + } +} + // MARK: - `JSClosure` mechanism note // From e0603021c03ed02d135397837ead4af5e7dad631 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 28 Jan 2022 13:53:10 -0500 Subject: [PATCH 05/14] async JSPromise callbacks --- .../BasicObjects/JSPromise.swift | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index cee01d301..12761f0bc 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -75,6 +75,15 @@ public final class JSPromise: JSBridgedClass { return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } + /// Schedules the `success` closure to be invoked on successful completion of `self`. + @discardableResult + public func then(success: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { + let closure = JSOneshotClosure.async { + return await success($0[0]).jsValue() + } + return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) + } + /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult public func then(success: @escaping (JSValue) -> ConvertibleToJSValue, @@ -88,6 +97,19 @@ public final class JSPromise: JSBridgedClass { return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) } + /// Schedules the `success` closure to be invoked on successful completion of `self`. + @discardableResult + public func then(success: @escaping (JSValue) async -> ConvertibleToJSValue, + failure: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { + let successClosure = JSOneshotClosure.async { + return await success($0[0]).jsValue() + } + let failureClosure = JSOneshotClosure.async { + return await failure($0[0]).jsValue() + } + return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) + } + /// Schedules the `failure` closure to be invoked on rejected completion of `self`. @discardableResult public func `catch`(failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { @@ -97,6 +119,15 @@ public final class JSPromise: JSBridgedClass { return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } + /// Schedules the `failure` closure to be invoked on rejected completion of `self`. + @discardableResult + public func `catch`(failure: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { + let closure = JSOneshotClosure.async { + return await failure($0[0]).jsValue() + } + return .init(unsafelyWrapping: jsObject.catch!(closure).object!) + } + /// Schedules the `failure` closure to be invoked on either successful or rejected /// completion of `self`. @discardableResult From 2ca262aa9038cd699db2b1e92f9bdca73b0630e0 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Wed, 2 Feb 2022 23:07:16 -0500 Subject: [PATCH 06/14] require compiler v5.5 for async code --- Sources/JavaScriptKit/BasicObjects/JSPromise.swift | 6 ++++++ Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 12761f0bc..6425d3f10 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -75,6 +75,7 @@ public final class JSPromise: JSBridgedClass { return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } + #if compiler(>=5.5) /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult public func then(success: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { @@ -83,6 +84,7 @@ public final class JSPromise: JSBridgedClass { } return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } + #endif /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult @@ -97,6 +99,7 @@ public final class JSPromise: JSBridgedClass { return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) } + #if compiler(>=5.5) /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult public func then(success: @escaping (JSValue) async -> ConvertibleToJSValue, @@ -109,6 +112,7 @@ public final class JSPromise: JSBridgedClass { } return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) } + #endif /// Schedules the `failure` closure to be invoked on rejected completion of `self`. @discardableResult @@ -119,6 +123,7 @@ public final class JSPromise: JSBridgedClass { return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } + #if compiler(>=5.5) /// Schedules the `failure` closure to be invoked on rejected completion of `self`. @discardableResult public func `catch`(failure: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { @@ -127,6 +132,7 @@ public final class JSPromise: JSBridgedClass { } return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } + #endif /// Schedules the `failure` closure to be invoked on either successful or rejected /// completion of `self`. diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 7951a9acb..ae2730188 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -105,6 +105,7 @@ public class JSClosure: JSObject, JSClosureProtocol { #endif } +#if compiler(>=5.5) private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSValue) -> (([JSValue]) -> JSValue) { { arguments in JSPromise { resolver in @@ -123,7 +124,7 @@ private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSVa }.jsValue() } } - +#endif // MARK: - `JSClosure` mechanism note // From 3e5408e149455e5f87dc5384a8ecb56c13a2a7ea Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 1 Apr 2022 14:36:38 -0400 Subject: [PATCH 07/14] Add async JSClosure test --- .../Sources/ConcurrencyTests/main.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index 5447032fe..736e40b5c 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -97,6 +97,24 @@ func entrypoint() async throws { _ = await (t3.value, t4.value, t5.value) try expectEqual(context.completed, ["t4", "t3", "t5"]) } + + try await asyncTest("Async JSClosure") { + let delayClosure = JSClosure.async { _ -> JSValue in + try await Task.sleep(nanoseconds: 2_000_000_000) + return JSValue.number(3) + } + let delayObject = JSObject.global.Object.function!.new() + delayObject.closure = delayClosure.jsValue() + + let start = time(nil) + let promise = JSPromise(from: delayObject.closure!()) + try expectNotNil(promise) + let result = try await promise!.value + let diff = difftime(time(nil), start) + try expectEqual(diff >= 2, true) + try expectEqual(result, .number(3)) + } + // FIXME(katei): Somehow it doesn't work due to a mysterious unreachable inst // at the end of thunk. // This issue is not only on JS host environment, but also on standalone coop executor. From 3ca829a3d028a7a973102d5f2777e28608c14b3a Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 1 Apr 2022 14:39:16 -0400 Subject: [PATCH 08/14] Add availability annotations --- Sources/JavaScriptKit/BasicObjects/JSPromise.swift | 3 +++ Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 2 ++ 2 files changed, 5 insertions(+) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 6425d3f10..da0b8e426 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -77,6 +77,7 @@ public final class JSPromise: JSBridgedClass { #if compiler(>=5.5) /// Schedules the `success` closure to be invoked on successful completion of `self`. + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) @discardableResult public func then(success: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure.async { @@ -101,6 +102,7 @@ public final class JSPromise: JSBridgedClass { #if compiler(>=5.5) /// Schedules the `success` closure to be invoked on successful completion of `self`. + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) @discardableResult public func then(success: @escaping (JSValue) async -> ConvertibleToJSValue, failure: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { @@ -125,6 +127,7 @@ public final class JSPromise: JSBridgedClass { #if compiler(>=5.5) /// Schedules the `failure` closure to be invoked on rejected completion of `self`. + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) @discardableResult public func `catch`(failure: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure.async { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index ae2730188..d561595b9 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -91,6 +91,7 @@ public class JSClosure: JSObject, JSClosureProtocol { } #if compiler(>=5.5) + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure { JSClosure(makeAsyncClosure(body)) } @@ -106,6 +107,7 @@ public class JSClosure: JSObject, JSClosureProtocol { } #if compiler(>=5.5) +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSValue) -> (([JSValue]) -> JSValue) { { arguments in JSPromise { resolver in From bf7b52aff261d1613ccfe8d83e1e2e9401975789 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 1 Apr 2022 15:11:55 -0400 Subject: [PATCH 09/14] fix availability annotations --- Sources/JavaScriptKit/BasicObjects/JSPromise.swift | 6 +++--- Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index da0b8e426..8418705bd 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -77,7 +77,7 @@ public final class JSPromise: JSBridgedClass { #if compiler(>=5.5) /// Schedules the `success` closure to be invoked on successful completion of `self`. - @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult public func then(success: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure.async { @@ -102,7 +102,7 @@ public final class JSPromise: JSBridgedClass { #if compiler(>=5.5) /// Schedules the `success` closure to be invoked on successful completion of `self`. - @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult public func then(success: @escaping (JSValue) async -> ConvertibleToJSValue, failure: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { @@ -127,7 +127,7 @@ public final class JSPromise: JSBridgedClass { #if compiler(>=5.5) /// Schedules the `failure` closure to be invoked on rejected completion of `self`. - @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult public func `catch`(failure: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure.async { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index d561595b9..2b4892790 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -31,6 +31,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { } #if compiler(>=5.5) + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSOneshotClosure { JSOneshotClosure(makeAsyncClosure(body)) } @@ -91,7 +92,7 @@ public class JSClosure: JSObject, JSClosureProtocol { } #if compiler(>=5.5) - @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure { JSClosure(makeAsyncClosure(body)) } @@ -107,7 +108,7 @@ public class JSClosure: JSObject, JSClosureProtocol { } #if compiler(>=5.5) -@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSValue) -> (([JSValue]) -> JSValue) { { arguments in JSPromise { resolver in From ce61808485d34869466bf812a20a2a1021cac7d6 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 30 Apr 2022 14:00:08 -0400 Subject: [PATCH 10/14] Drop jsValue() parens --- .../TestSuites/Sources/ConcurrencyTests/main.swift | 2 +- Sources/JavaScriptKit/BasicObjects/JSPromise.swift | 8 ++++---- Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index 736e40b5c..aabeacf75 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -104,7 +104,7 @@ func entrypoint() async throws { return JSValue.number(3) } let delayObject = JSObject.global.Object.function!.new() - delayObject.closure = delayClosure.jsValue() + delayObject.closure = delayClosure.jsValue let start = time(nil) let promise = JSPromise(from: delayObject.closure!()) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 8418705bd..b25f591db 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -81,7 +81,7 @@ public final class JSPromise: JSBridgedClass { @discardableResult public func then(success: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure.async { - return await success($0[0]).jsValue() + return await success($0[0]).jsValue } return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } @@ -107,10 +107,10 @@ public final class JSPromise: JSBridgedClass { public func then(success: @escaping (JSValue) async -> ConvertibleToJSValue, failure: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { let successClosure = JSOneshotClosure.async { - return await success($0[0]).jsValue() + return await success($0[0]).jsValue } let failureClosure = JSOneshotClosure.async { - return await failure($0[0]).jsValue() + return await failure($0[0]).jsValue } return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) } @@ -131,7 +131,7 @@ public final class JSPromise: JSBridgedClass { @discardableResult public func `catch`(failure: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure.async { - return await failure($0[0]).jsValue() + return await failure($0[0]).jsValue } return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 2b4892790..c45b5c9b4 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -118,9 +118,9 @@ private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSVa resolver(.success(result)) } catch { if let jsError = error as? JSError { - resolver(.failure(jsError.jsValue())) + resolver(.failure(jsError.jsValue)) } else { - resolver(.failure(JSError(message: String(describing: error)).jsValue())) + resolver(.failure(JSError(message: String(describing: error)).jsValue)) } } } From 9b95b9833a5b3f5d1985084684bcd1793de2426c Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 30 Apr 2022 14:00:22 -0400 Subject: [PATCH 11/14] Mark JS[Oneshot]Closure.async as public --- Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index c45b5c9b4..0cffed666 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -32,7 +32,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { #if compiler(>=5.5) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSOneshotClosure { + public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSOneshotClosure { JSOneshotClosure(makeAsyncClosure(body)) } #endif @@ -93,7 +93,7 @@ public class JSClosure: JSObject, JSClosureProtocol { #if compiler(>=5.5) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure { + public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure { JSClosure(makeAsyncClosure(body)) } #endif From 77584a39513e77dae0cb02409f554d33587be45d Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 4 Jun 2022 09:19:26 -0400 Subject: [PATCH 12/14] Allow JSPromise chained async callbacks to throw --- .../JavaScriptKit/BasicObjects/JSPromise.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 4e9d8d03b..4b366d812 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -79,9 +79,9 @@ public final class JSPromise: JSBridgedClass { /// 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: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { + public func then(success: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure.async { - await success($0[0]).jsValue + try await success($0[0]).jsValue } return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } @@ -106,14 +106,14 @@ public final class JSPromise: JSBridgedClass { /// 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: @escaping (JSValue) async -> ConvertibleToJSValue, - failure: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise + public func then(success: @escaping (JSValue) async throws -> ConvertibleToJSValue, + failure: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise { let successClosure = JSOneshotClosure.async { - await success($0[0]).jsValue + try await success($0[0]).jsValue } let failureClosure = JSOneshotClosure.async { - await failure($0[0]).jsValue + try await failure($0[0]).jsValue } return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) } @@ -132,9 +132,9 @@ public final class JSPromise: JSBridgedClass { /// 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: @escaping (JSValue) async -> ConvertibleToJSValue) -> JSPromise { + public func `catch`(failure: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure.async { - await failure($0[0]).jsValue + try await failure($0[0]).jsValue } return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } From 7d67669c3466445fc3ffb0f2b46942e2c03d3b05 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 4 Jun 2022 09:29:43 -0400 Subject: [PATCH 13/14] Add expectGTE helper function --- .../Sources/ConcurrencyTests/UnitTestUtils.swift | 12 ++++++++++++ .../TestSuites/Sources/ConcurrencyTests/main.swift | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift index 1557764a2..40c3165da 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift @@ -41,6 +41,18 @@ struct MessageError: Error { } } +func expectGTE( + _ lhs: T, _ rhs: T, + file: StaticString = #file, line: UInt = #line, column: UInt = #column +) throws { + if lhs < rhs { + throw MessageError( + "Expected \(lhs) to be greater than or equal to \(rhs)", + file: file, line: line, column: column + ) + } +} + func expectEqual( _ lhs: T, _ rhs: T, file: StaticString = #file, line: UInt = #line, column: UInt = #column diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index aabeacf75..dda99031b 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -62,7 +62,7 @@ func entrypoint() async throws { let start = time(nil) try await Task.sleep(nanoseconds: 2_000_000_000) let diff = difftime(time(nil), start); - try expectEqual(diff >= 2, true) + try expectGTE(diff, 2) } try await asyncTest("Job reordering based on priority") { @@ -111,7 +111,7 @@ func entrypoint() async throws { try expectNotNil(promise) let result = try await promise!.value let diff = difftime(time(nil), start) - try expectEqual(diff >= 2, true) + try expectGTE(diff, 2) try expectEqual(result, .number(3)) } From 2305f7e4da6e3a37b0ee56d5137f42fe2c1cdc7a Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 4 Jun 2022 09:29:48 -0400 Subject: [PATCH 14/14] Add remaining tests --- .../Sources/ConcurrencyTests/main.swift | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index dda99031b..3bce2aa8d 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -115,6 +115,67 @@ func entrypoint() async throws { try expectEqual(result, .number(3)) } + try await asyncTest("Async JSPromise: then") { + let promise = JSPromise { resolve in + _ = JSObject.global.setTimeout!( + JSClosure { _ in + resolve(.success(JSValue.number(3))) + return .undefined + }.jsValue, + 1_000 + ) + } + let promise2 = promise.then { result in + try await Task.sleep(nanoseconds: 1_000_000_000) + return String(result.number!) + } + let start = time(nil) + let result = try await promise2.value + let diff = difftime(time(nil), start) + try expectGTE(diff, 2) + try expectEqual(result, .string("3.0")) + } + + try await asyncTest("Async JSPromise: then(success:failure:)") { + let promise = JSPromise { resolve in + _ = JSObject.global.setTimeout!( + JSClosure { _ in + resolve(.failure(JSError(message: "test").jsValue)) + return .undefined + }.jsValue, + 1_000 + ) + } + let promise2 = promise.then { _ in + throw JSError(message: "should not succeed") + } failure: { err in + return err + } + let result = try await promise2.value + try expectEqual(result.object?.message, .string("test")) + } + + try await asyncTest("Async JSPromise: catch") { + let promise = JSPromise { resolve in + _ = JSObject.global.setTimeout!( + JSClosure { _ in + resolve(.failure(JSError(message: "test").jsValue)) + return .undefined + }.jsValue, + 1_000 + ) + } + let promise2 = promise.catch { err in + try await Task.sleep(nanoseconds: 1_000_000_000) + return err + } + let start = time(nil) + let result = try await promise2.value + let diff = difftime(time(nil), start) + try expectGTE(diff, 2) + try expectEqual(result.object?.message, .string("test")) + } + // FIXME(katei): Somehow it doesn't work due to a mysterious unreachable inst // at the end of thunk. // This issue is not only on JS host environment, but also on standalone coop executor.