Skip to content

Commit

Permalink
Merge pull request #115 from swiftwasm/katei/simplify-jspromise
Browse files Browse the repository at this point in the history
Simplify JSPromise API
  • Loading branch information
kateinoigakukun authored Jan 10, 2021
2 parents d583439 + 39abdbc commit b9984f8
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 167 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,32 @@ func expectNotNil<T>(_ 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
}
}
}
68 changes: 64 additions & 4 deletions IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -528,22 +528,80 @@ try test("Timer") {
}

var timer: JSTimer?
var promise: JSPromise<(), Never>?
var expectations: [Expectation] = []

try test("Promise") {

let p1 = JSPromise.resolve(JSValue.null)
let exp1 = Expectation(label: "Promise.then testcase", expectedFulfillmentCount: 4)
p1.then { value in
try! expectEqual(value, .null)
exp1.fulfill()
return JSValue.number(1.0)
}
.then { value in
try! expectEqual(value, .number(1.0))
exp1.fulfill()
return JSPromise.resolve(JSValue.boolean(true))
}
.then { value in
try! expectEqual(value, .boolean(true))
exp1.fulfill()
return JSValue.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 in
try! expectEqual(reason, .boolean(false))
exp2.fulfill()
return JSValue.boolean(true)
}
.then { value in
try! expectEqual(value, .boolean(true))
exp2.fulfill()
return JSPromise.reject(JSValue.number(2.0))
}
.catch { reason in
try! expectEqual(reason, .number(2.0))
exp2.fulfill()
return JSValue.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 JSValue.undefined
}

let exp4 = Expectation(label: "Promise lifetime")
// Ensure that users don't need to manage JSPromise lifetime
JSPromise.resolve(JSValue.boolean(true)).then { _ in
exp4.fulfill()
return JSValue.undefined
}
expectations += [exp1, exp2, exp3, exp4]
}

try test("Error") {
Expand Down Expand Up @@ -620,3 +678,5 @@ try test("Exception") {
let errorObject3 = JSError(from: ageError as! JSValue)
try expectNotNil(errorObject3)
}

Expectation.wait(expectations)
208 changes: 46 additions & 162 deletions Sources/JavaScriptKit/BasicObjects/JSPromise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Success, Failure>: ConvertibleToJSValue, ConstructibleFromJSValue {
public final class JSPromise: JSBridgedClass {
/// The underlying JavaScript `Promise` object.
public let jsObject: JSObject

Expand All @@ -18,17 +18,20 @@ public final class JSPromise<Success, Failure>: 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`
Expand All @@ -40,73 +43,10 @@ public final class JSPromise<Success, Failure>: 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<Success, JSError>) -> ()) -> ()) {
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<Success, JSError>) -> ()) -> ()) {
public convenience init(resolver: @escaping (@escaping (Result<JSValue, JSValue>) -> ()) -> ()) {
let closure = JSOneshotClosure { arguments in
// The arguments are always coming from the `Promise` constructor, so we should be
// safe to assume their type here
Expand All @@ -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: ConvertibleToJSValue) -> 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<ResultType: ConvertibleToJSValue>(
success: @escaping (Success) -> ResultType,
file: StaticString = #file,
line: Int = #line
) -> JSPromise<ResultType, Failure> {
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: ConvertibleToJSValue) -> 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<ResultSuccess: ConvertibleToJSValue, ResultFailure: ConstructibleFromJSValue>(
success: @escaping (Success) -> JSPromise<ResultSuccess, ResultFailure>,
file: StaticString = #file,
line: Int = #line
) -> JSPromise<ResultSuccess, ResultFailure> {
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) -> ConvertibleToJSValue) -> JSPromise {
let closure = JSOneshotClosure {
return success($0[0]).jsValue()
}
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`<ResultSuccess: ConvertibleToJSValue>(
failure: @escaping (Failure) -> ResultSuccess,
file: StaticString = #file,
line: Int = #line
) -> JSPromise<ResultSuccess, Never> {
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) -> ConvertibleToJSValue,
failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise {
let successClosure = JSOneshotClosure {
return success($0[0]).jsValue()
}
return .init(unsafe: jsObject.then!(JSValue.undefined, closure).object!)
let failureClosure = JSOneshotClosure {
return failure($0[0]).jsValue()
}
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) -> ConvertibleToJSValue) -> JSPromise {
let closure = JSOneshotClosure {
return failure($0[0]).jsValue()
}
_ = 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`<ResultSuccess: ConvertibleToJSValue, ResultFailure: ConstructibleFromJSValue>(
failure: @escaping (Failure) -> JSPromise<ResultSuccess, ResultFailure>,
file: StaticString = #file,
line: Int = #line
) -> JSPromise<ResultSuccess, ResultFailure> {
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!)
}
}
Loading

0 comments on commit b9984f8

Please sign in to comment.