From 58ca905b1edb7c002491e1f2deea6bc1365581b6 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 4 Jan 2021 11:11:19 +0900 Subject: [PATCH 1/4] Simplify JSPromise API --- .../Sources/PrimaryTests/UnitTestUtils.swift | 29 +++ .../Sources/PrimaryTests/main.swift | 68 +++++- .../BasicObjects/JSPromise.swift | 208 ++++-------------- Sources/JavaScriptKit/JSBridgedType.swift | 7 +- 4 files changed, 145 insertions(+), 167 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift index eefa7194e..07d20a4e1 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift @@ -101,3 +101,32 @@ func expectNotNil(_ value: T?, file: StaticString = #file, line: UInt = #line throw MessageError("Expect a non-nil value", file: file, line: line, column: column) } } + +class Expectation { + private(set) var isFulfilled: Bool = false + private let label: String + private let expectedFulfillmentCount: Int + private var fulfillmentCount: Int = 0 + + init(label: String, expectedFulfillmentCount: Int = 1) { + self.label = label + self.expectedFulfillmentCount = expectedFulfillmentCount + } + + func fulfill() { + assert(!isFulfilled, "Too many fulfillment (label: \(label)): expectedFulfillmentCount is \(expectedFulfillmentCount)") + fulfillmentCount += 1 + if fulfillmentCount == expectedFulfillmentCount { + isFulfilled = true + } + } + + static func wait(_ expectations: [Expectation]) { + var timer: JSTimer! + timer = JSTimer(millisecondsDelay: 5.0, isRepeating: true) { + guard expectations.allSatisfy(\.isFulfilled) else { return } + assert(timer != nil) + timer = nil + } + } +} diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index 66daeec0a..ab060817e 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -528,22 +528,80 @@ try test("Timer") { } var timer: JSTimer? -var promise: JSPromise<(), Never>? +var expectations: [Expectation] = [] try test("Promise") { + + let p1 = JSPromise.resolve(.null) + let exp1 = Expectation(label: "Promise.then testcase", expectedFulfillmentCount: 4) + p1.then { (value) -> JSValue in + try! expectEqual(value, .null) + exp1.fulfill() + return .number(1.0) + } + .then { value -> JSValue in + try! expectEqual(value, .number(1.0)) + exp1.fulfill() + return JSPromise.resolve(.boolean(true)).jsValue() + } + .then { value -> JSValue in + try! expectEqual(value, .boolean(true)) + exp1.fulfill() + return .undefined + } + .catch { _ -> JSValue in + fatalError("Not fired due to no throw") + } + .finally { exp1.fulfill() } + + let exp2 = Expectation(label: "Promise.catch testcase", expectedFulfillmentCount: 4) + let p2 = JSPromise.reject(JSValue.boolean(false)) + p2.then { _ -> JSValue in + fatalError("Not fired due to no success") + } + .catch { reason -> JSValue in + try! expectEqual(reason, .boolean(false)) + exp2.fulfill() + return .boolean(true) + } + .then { value -> JSValue in + try! expectEqual(value, .boolean(true)) + exp2.fulfill() + return JSPromise.reject(JSValue.number(2.0)).jsValue() + } + .catch { reason -> JSValue in + try! expectEqual(reason, .number(2.0)) + exp2.fulfill() + return .undefined + } + .finally { exp2.fulfill() } + + let start = JSDate().valueOf() let timeoutMilliseconds = 5.0 + let exp3 = Expectation(label: "Promise and Timer testcae", expectedFulfillmentCount: 2) - promise = JSPromise { resolve in + let p3 = JSPromise { resolve in timer = JSTimer(millisecondsDelay: timeoutMilliseconds) { - resolve() + exp3.fulfill() + resolve(.success(.undefined)) } } - promise!.then { + p3.then { _ in // verify that at least `timeoutMilliseconds` passed since the timer started try! expectEqual(start + timeoutMilliseconds <= JSDate().valueOf(), true) + exp3.fulfill() + return .undefined } + + let exp4 = Expectation(label: "Promise lifetime") + // Ensure that users don't need to manage JSPromise lifetime + JSPromise.resolve(.boolean(true)).then { _ -> JSValue in + exp4.fulfill() + return .undefined + } + expectations += [exp1, exp2, exp3, exp4] } try test("Error") { @@ -620,3 +678,5 @@ try test("Exception") { let errorObject3 = JSError(from: ageError as! JSValue) try expectNotNil(errorObject3) } + +Expectation.wait(expectations) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 02a6b27c0..ed472ec87 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -9,7 +9,7 @@ This doesn't 100% match the JavaScript API, as `then` overload with two callback 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. */ -public final class JSPromise: ConvertibleToJSValue, ConstructibleFromJSValue { +public final class JSPromise: JSBridgedClass { /// The underlying JavaScript `Promise` object. public let jsObject: JSObject @@ -18,17 +18,20 @@ public final class JSPromise: ConvertibleToJSValue, Constructi .object(jsObject) } + public static var constructor: JSFunction { + JSObject.global.Promise.function! + } + /// This private initializer assumes that the passed object is a JavaScript `Promise` - private init(unsafe object: JSObject) { + public init(unsafelyWrapping object: JSObject) { 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`. */ - public init?(_ jsObject: JSObject) { - guard jsObject.isInstanceOf(JSObject.global.Promise.function!) else { return nil } - self.jsObject = jsObject + public convenience init?(_ jsObject: JSObject) { + self.init(from: jsObject) } /** Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `value` @@ -40,73 +43,10 @@ public final class JSPromise: ConvertibleToJSValue, Constructi return Self.init(jsObject) } - /** Schedules the `success` closure to be invoked on sucessful completion of `self`. - */ - public func then(success: @escaping () -> ()) { - let closure = JSOneshotClosure { _ in - success() - return .undefined - } - _ = jsObject.then!(closure) - } - - /** Schedules the `failure` closure to be invoked on either successful or rejected completion of - `self`. - */ - public func finally(successOrFailure: @escaping () -> ()) -> Self { - let closure = JSOneshotClosure { _ in - successOrFailure() - return .undefined - } - return .init(unsafe: jsObject.finally!(closure).object!) - } -} - -extension JSPromise where Success == (), Failure == Never { - /** Creates a new `JSPromise` instance from a given `resolver` closure. `resolver` takes - a closure that your code should call to resolve this `JSPromise` instance. - */ - public convenience init(resolver: @escaping (@escaping () -> ()) -> ()) { - let closure = JSOneshotClosure { arguments in - // The arguments are always coming from the `Promise` constructor, so we should be - // safe to assume their type here - resolver { arguments[0].function!() } - return .undefined - } - self.init(unsafe: JSObject.global.Promise.function!.new(closure)) - } -} - -extension JSPromise where Failure: ConvertibleToJSValue { - /** Creates a new `JSPromise` instance from a given `resolver` closure. `resolver` takes + /** 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. */ - 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 - // safe to assume their type here - let resolve = arguments[0].function! - let reject = arguments[1].function! - - resolver { - switch $0 { - case .success: - resolve() - case let .failure(error): - reject(error.jsValue()) - } - } - return .undefined - } - self.init(unsafe: JSObject.global.Promise.function!.new(closure)) - } -} - -extension JSPromise where Success: ConvertibleToJSValue, Failure: JSError { - /** Creates a new `JSPromise` instance from a given `resolver` closure. `resolver` takes - a closure that your code should call to either resolve or reject this `JSPromise` instance. - */ - public convenience init(resolver: @escaping (@escaping (Result) -> ()) -> ()) { + 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 // safe to assume their type here @@ -116,123 +56,67 @@ extension JSPromise where Success: ConvertibleToJSValue, Failure: JSError { resolver { switch $0 { case let .success(success): - resolve(success.jsValue()) + resolve(success) case let .failure(error): - reject(error.jsValue()) + reject(error) } } return .undefined } - self.init(unsafe: JSObject.global.Promise.function!.new(closure)) + self.init(unsafelyWrapping: Self.constructor.new(closure)) } -} -extension JSPromise where Success: ConstructibleFromJSValue { - /** Schedules the `success` closure to be invoked on sucessful completion of `self`. - */ - public func then( - success: @escaping (Success) -> (), - file: StaticString = #file, - line: Int = #line - ) { - let closure = JSOneshotClosure { arguments in - guard let result = Success.construct(from: arguments[0]) else { - fatalError("\(file):\(line): failed to unwrap success value for `then` callback") - } - success(result) - return .undefined - } - _ = jsObject.then!(closure) + public static func resolve(_ value: JSValue) -> JSPromise { + self.init(unsafelyWrapping: Self.constructor.resolve!(value).object!) } - /** Returns a new promise created from chaining the current `self` promise with the `success` - closure invoked on sucessful completion of `self`. The returned promise will have a new - `Success` type equal to the return type of `success`. - */ - public func then( - success: @escaping (Success) -> ResultType, - file: StaticString = #file, - line: Int = #line - ) -> JSPromise { - let closure = JSOneshotClosure { arguments -> JSValue in - guard let result = Success.construct(from: arguments[0]) else { - fatalError("\(file):\(line): failed to unwrap success value for `then` callback") - } - return success(result).jsValue() - } - return .init(unsafe: jsObject.then!(closure).object!) + public static func reject(_ reason: JSValue) -> JSPromise { + self.init(unsafelyWrapping: Self.constructor.reject!(reason).object!) } - /** Returns a new promise created from chaining the current `self` promise with the `success` - closure invoked on sucessful completion of `self`. The returned promise will have a new type - equal to the return type of `success`. + /** Schedules the `success` closure to be invoked on sucessful completion of `self`. */ - public func then( - success: @escaping (Success) -> JSPromise, - file: StaticString = #file, - line: Int = #line - ) -> JSPromise { - let closure = JSOneshotClosure { arguments -> JSValue in - guard let result = Success.construct(from: arguments[0]) else { - fatalError("\(file):\(line): failed to unwrap success value for `then` callback") - } - return success(result).jsValue() + @discardableResult + public func then(success: @escaping (JSValue) -> JSValue) -> JSPromise { + let closure = JSOneshotClosure { + return success($0[0]) } - return .init(unsafe: jsObject.then!(closure).object!) + return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } -} -extension JSPromise where Failure: ConstructibleFromJSValue { - /** Returns a new promise created from chaining the current `self` promise with the `failure` - closure invoked on rejected completion of `self`. The returned promise will have a new `Success` - type equal to the return type of the callback, while the `Failure` type becomes `Never`. + /** Schedules the `success` closure to be invoked on sucessful completion of `self`. */ - public func `catch`( - failure: @escaping (Failure) -> ResultSuccess, - file: StaticString = #file, - line: Int = #line - ) -> JSPromise { - let closure = JSOneshotClosure { arguments -> JSValue in - guard let error = Failure.construct(from: arguments[0]) else { - fatalError("\(file):\(line): failed to unwrap error value for `catch` callback") - } - return failure(error).jsValue() + @discardableResult + public func then(success: @escaping (JSValue) -> JSValue, + failure: @escaping (JSValue) -> JSValue) -> JSPromise { + let successClosure = JSOneshotClosure { + return success($0[0]) } - return .init(unsafe: jsObject.then!(JSValue.undefined, closure).object!) + let failureClosure = JSOneshotClosure { + return failure($0[0]) + } + return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) } /** Schedules the `failure` closure to be invoked on rejected completion of `self`. */ - public func `catch`( - failure: @escaping (Failure) -> (), - file: StaticString = #file, - line: Int = #line - ) { - let closure = JSOneshotClosure { arguments in - guard let error = Failure.construct(from: arguments[0]) else { - fatalError("\(file):\(line): failed to unwrap error value for `catch` callback") - } - failure(error) - return .undefined + @discardableResult + public func `catch`(failure: @escaping (JSValue) -> JSValue) -> JSPromise { + let closure = JSOneshotClosure { + return failure($0[0]) } - _ = jsObject.then!(JSValue.undefined, closure) + return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } - /** Returns a new promise created from chaining the current `self` promise with the `failure` - closure invoked on rejected completion of `self`. The returned promise will have a new type - equal to the return type of `success`. + /** Schedules the `failure` closure to be invoked on either successful or rejected completion of + `self`. */ - public func `catch`( - failure: @escaping (Failure) -> JSPromise, - file: StaticString = #file, - line: Int = #line - ) -> JSPromise { - let closure = JSOneshotClosure { arguments -> JSValue in - guard let error = Failure.construct(from: arguments[0]) else { - fatalError("\(file):\(line): failed to unwrap error value for `catch` callback") - } - return failure(error).jsValue() + @discardableResult + public func finally(successOrFailure: @escaping () -> ()) -> JSPromise { + let closure = JSOneshotClosure { _ in + successOrFailure() + return .undefined } - return .init(unsafe: jsObject.then!(JSValue.undefined, closure).object!) + return .init(unsafelyWrapping: jsObject.finally!(closure).object!) } } diff --git a/Sources/JavaScriptKit/JSBridgedType.swift b/Sources/JavaScriptKit/JSBridgedType.swift index 029a26663..7532f367b 100644 --- a/Sources/JavaScriptKit/JSBridgedType.swift +++ b/Sources/JavaScriptKit/JSBridgedType.swift @@ -35,7 +35,12 @@ public protocol JSBridgedClass: JSBridgedType { extension JSBridgedClass { public var value: JSValue { jsObject.jsValue() } public init?(from value: JSValue) { - guard let object = value.object, object.isInstanceOf(Self.constructor) else { return nil } + guard let object = value.object else { return nil } + self.init(from: object) + } + + public init?(from object: JSObject) { + guard object.isInstanceOf(Self.constructor) else { return nil } self.init(unsafelyWrapping: object) } } From a85ee3f8eb779baf359382eb10ebcdc3070ccae8 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 6 Jan 2021 14:14:33 +0900 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Jed Fox --- Sources/JavaScriptKit/BasicObjects/JSPromise.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index ed472ec87..ae159d1f0 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -46,7 +46,7 @@ public final class JSPromise: JSBridgedClass { /** 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. */ - public convenience init(resolver: @escaping (@escaping (Result) -> ()) -> ()) { + public convenience init(resolver: @escaping (@escaping (Result< JSValueConvertible, JSValueConvertible >) -> ()) -> ()) { let closure = JSOneshotClosure { arguments in // The arguments are always coming from the `Promise` constructor, so we should be // safe to assume their type here @@ -66,18 +66,18 @@ public final class JSPromise: JSBridgedClass { self.init(unsafelyWrapping: Self.constructor.new(closure)) } - public static func resolve(_ value: JSValue) -> JSPromise { + public static func resolve(_ value: JSValueConvertible) -> JSPromise { self.init(unsafelyWrapping: Self.constructor.resolve!(value).object!) } - public static func reject(_ reason: JSValue) -> JSPromise { + public static func reject(_ reason: JSValueConvertible) -> JSPromise { self.init(unsafelyWrapping: Self.constructor.reject!(reason).object!) } /** Schedules the `success` closure to be invoked on sucessful completion of `self`. */ @discardableResult - public func then(success: @escaping (JSValue) -> JSValue) -> JSPromise { + public func then(success: @escaping (JSValue) -> JSValueConvertible) -> JSPromise { let closure = JSOneshotClosure { return success($0[0]) } @@ -87,8 +87,8 @@ public final class JSPromise: JSBridgedClass { /** Schedules the `success` closure to be invoked on sucessful completion of `self`. */ @discardableResult - public func then(success: @escaping (JSValue) -> JSValue, - failure: @escaping (JSValue) -> JSValue) -> JSPromise { + public func then(success: @escaping (JSValue) -> JSValueConvertible, + failure: @escaping (JSValue) -> JSValueConvertible) -> JSPromise { let successClosure = JSOneshotClosure { return success($0[0]) } @@ -101,7 +101,7 @@ public final class JSPromise: JSBridgedClass { /** Schedules the `failure` closure to be invoked on rejected completion of `self`. */ @discardableResult - public func `catch`(failure: @escaping (JSValue) -> JSValue) -> JSPromise { + public func `catch`(failure: @escaping (JSValue) -> JSValueConvertible) -> JSPromise { let closure = JSOneshotClosure { return failure($0[0]) } From 6561e158eb128674a1cba10d3c101ede24ffce01 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 6 Jan 2021 14:17:36 +0900 Subject: [PATCH 3/4] Revert "Apply suggestions from code review" This reverts commit a85ee3f8eb779baf359382eb10ebcdc3070ccae8. --- Sources/JavaScriptKit/BasicObjects/JSPromise.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index ae159d1f0..ed472ec87 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -46,7 +46,7 @@ public final class JSPromise: JSBridgedClass { /** 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. */ - public convenience init(resolver: @escaping (@escaping (Result< JSValueConvertible, JSValueConvertible >) -> ()) -> ()) { + 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 // safe to assume their type here @@ -66,18 +66,18 @@ public final class JSPromise: JSBridgedClass { self.init(unsafelyWrapping: Self.constructor.new(closure)) } - public static func resolve(_ value: JSValueConvertible) -> JSPromise { + public static func resolve(_ value: JSValue) -> JSPromise { self.init(unsafelyWrapping: Self.constructor.resolve!(value).object!) } - public static func reject(_ reason: JSValueConvertible) -> JSPromise { + public static func reject(_ reason: JSValue) -> JSPromise { self.init(unsafelyWrapping: Self.constructor.reject!(reason).object!) } /** Schedules the `success` closure to be invoked on sucessful completion of `self`. */ @discardableResult - public func then(success: @escaping (JSValue) -> JSValueConvertible) -> JSPromise { + public func then(success: @escaping (JSValue) -> JSValue) -> JSPromise { let closure = JSOneshotClosure { return success($0[0]) } @@ -87,8 +87,8 @@ public final class JSPromise: JSBridgedClass { /** Schedules the `success` closure to be invoked on sucessful completion of `self`. */ @discardableResult - public func then(success: @escaping (JSValue) -> JSValueConvertible, - failure: @escaping (JSValue) -> JSValueConvertible) -> JSPromise { + public func then(success: @escaping (JSValue) -> JSValue, + failure: @escaping (JSValue) -> JSValue) -> JSPromise { let successClosure = JSOneshotClosure { return success($0[0]) } @@ -101,7 +101,7 @@ public final class JSPromise: JSBridgedClass { /** Schedules the `failure` closure to be invoked on rejected completion of `self`. */ @discardableResult - public func `catch`(failure: @escaping (JSValue) -> JSValueConvertible) -> JSPromise { + public func `catch`(failure: @escaping (JSValue) -> JSValue) -> JSPromise { let closure = JSOneshotClosure { return failure($0[0]) } From 39abdbc120e24d9d91bb7e0de2db1467427c132d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 6 Jan 2021 14:32:09 +0900 Subject: [PATCH 4/4] Use ConvertibleToJSValue for return value type --- .../Sources/PrimaryTests/main.swift | 32 +++++++++---------- .../BasicObjects/JSPromise.swift | 20 ++++++------ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index ab060817e..e93dfc1b6 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -532,22 +532,22 @@ var expectations: [Expectation] = [] try test("Promise") { - let p1 = JSPromise.resolve(.null) + let p1 = JSPromise.resolve(JSValue.null) let exp1 = Expectation(label: "Promise.then testcase", expectedFulfillmentCount: 4) - p1.then { (value) -> JSValue in + p1.then { value in try! expectEqual(value, .null) exp1.fulfill() - return .number(1.0) + return JSValue.number(1.0) } - .then { value -> JSValue in + .then { value in try! expectEqual(value, .number(1.0)) exp1.fulfill() - return JSPromise.resolve(.boolean(true)).jsValue() + return JSPromise.resolve(JSValue.boolean(true)) } - .then { value -> JSValue in + .then { value in try! expectEqual(value, .boolean(true)) exp1.fulfill() - return .undefined + return JSValue.undefined } .catch { _ -> JSValue in fatalError("Not fired due to no throw") @@ -559,20 +559,20 @@ try test("Promise") { p2.then { _ -> JSValue in fatalError("Not fired due to no success") } - .catch { reason -> JSValue in + .catch { reason in try! expectEqual(reason, .boolean(false)) exp2.fulfill() - return .boolean(true) + return JSValue.boolean(true) } - .then { value -> JSValue in + .then { value in try! expectEqual(value, .boolean(true)) exp2.fulfill() - return JSPromise.reject(JSValue.number(2.0)).jsValue() + return JSPromise.reject(JSValue.number(2.0)) } - .catch { reason -> JSValue in + .catch { reason in try! expectEqual(reason, .number(2.0)) exp2.fulfill() - return .undefined + return JSValue.undefined } .finally { exp2.fulfill() } @@ -592,14 +592,14 @@ try test("Promise") { // verify that at least `timeoutMilliseconds` passed since the timer started try! expectEqual(start + timeoutMilliseconds <= JSDate().valueOf(), true) exp3.fulfill() - return .undefined + return JSValue.undefined } let exp4 = Expectation(label: "Promise lifetime") // Ensure that users don't need to manage JSPromise lifetime - JSPromise.resolve(.boolean(true)).then { _ -> JSValue in + JSPromise.resolve(JSValue.boolean(true)).then { _ in exp4.fulfill() - return .undefined + return JSValue.undefined } expectations += [exp1, exp2, exp3, exp4] } diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index ed472ec87..5b0d47dd4 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -66,20 +66,20 @@ public final class JSPromise: JSBridgedClass { self.init(unsafelyWrapping: Self.constructor.new(closure)) } - public static func resolve(_ value: JSValue) -> JSPromise { + public static func resolve(_ value: ConvertibleToJSValue) -> JSPromise { self.init(unsafelyWrapping: Self.constructor.resolve!(value).object!) } - public static func reject(_ reason: JSValue) -> JSPromise { + public static func reject(_ reason: ConvertibleToJSValue) -> JSPromise { self.init(unsafelyWrapping: Self.constructor.reject!(reason).object!) } /** Schedules the `success` closure to be invoked on sucessful completion of `self`. */ @discardableResult - public func then(success: @escaping (JSValue) -> JSValue) -> JSPromise { + public func then(success: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure { - return success($0[0]) + return success($0[0]).jsValue() } return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } @@ -87,13 +87,13 @@ public final class JSPromise: JSBridgedClass { /** Schedules the `success` closure to be invoked on sucessful completion of `self`. */ @discardableResult - public func then(success: @escaping (JSValue) -> JSValue, - failure: @escaping (JSValue) -> JSValue) -> JSPromise { + public func then(success: @escaping (JSValue) -> ConvertibleToJSValue, + failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { let successClosure = JSOneshotClosure { - return success($0[0]) + return success($0[0]).jsValue() } let failureClosure = JSOneshotClosure { - return failure($0[0]) + return failure($0[0]).jsValue() } return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) } @@ -101,9 +101,9 @@ public final class JSPromise: JSBridgedClass { /** Schedules the `failure` closure to be invoked on rejected completion of `self`. */ @discardableResult - public func `catch`(failure: @escaping (JSValue) -> JSValue) -> JSPromise { + public func `catch`(failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure { - return failure($0[0]) + return failure($0[0]).jsValue() } return .init(unsafelyWrapping: jsObject.catch!(closure).object!) }