Skip to content
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

Add a generic JSPromise implementation #62

Merged
merged 8 commits into from
Sep 24, 2020
136 changes: 136 additions & 0 deletions Sources/JavaScriptKit/BasicObjects/JSPromise.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/** A wrapper around [the JavaScript `Promise` class](https://developer.mozilla.org/ja/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<JSValue, JSError>`. In the rare case, where you can't guarantee that the error thrown
is of actual JavaScript `Error` type, you should use `JSPromise<JSValue, JSValue>`.

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.
*/
public final class JSPromise<Success, Failure>: JSValueConvertible, JSValueConstructible
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note that this class must have the same lifetime with the actual Promise object in JS environment because callback handlers will be deallocated when JSPromise.deinit.

If the actual Promise object in JS environment lives longer than this JSPromise, it may call deallocated JSClosure.

where Success: JSValueConstructible, Failure: JSValueConstructible {
/// The underlying JavaScript `Promise` object.
public let jsObject: JSObject

private var callbacks = [JSClosure]()

/// The underlying JavaScript `Promise` object wrapped as `JSValue`.
public func jsValue() -> JSValue {
.object(jsObject)
}

/// This private initializer assumes that the passed object is a JavaScript `Promise`
private init(unsafe 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
}

/** 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)
}

/** Schedules the `success` closure to be invoked on sucessful completion of `self`.
*/
public func then(success: @escaping (Success) -> ()) {
let closure = JSClosure {
success(Success.construct(from: $0[0])!)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If fail to construct, we should throw error or report it with a detailed message. Force unwrapping fatal messages is not quite useful to see the error reason for JavaScriptKit users.

}
callbacks.append(closure)
jsObject.then.function!(closure)
}

/** 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: JSValueConvertible>(
success: @escaping (Success) -> ResultType
) -> JSPromise<ResultType, Failure> {
let closure = JSClosure {
success(Success.construct(from: $0[0])!).jsValue()
}
callbacks.append(closure)
return .init(unsafe: jsObject.then.function!(closure).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`.
*/
public func then<ResultSuccess: JSValueConvertible, ResultFailure: JSValueConstructible>(
success: @escaping (Success) -> JSPromise<ResultSuccess, ResultFailure>
) -> JSPromise<ResultSuccess, ResultFailure> {
let closure = JSClosure {
success(Success.construct(from: $0[0])!).jsValue()
}
callbacks.append(closure)
return .init(unsafe: jsObject.then.function!(closure).object!)
}

/** Schedules the `failure` closure to be invoked on rejected completion of `self`.
*/
public func `catch`(failure: @escaping (Failure) -> ()) {
let closure = JSClosure {
failure(Failure.construct(from: $0[0])!)
}
callbacks.append(closure)
jsObject.then.function!(JSValue.undefined, closure)
j-f1 marked this conversation as resolved.
Show resolved Hide resolved
}

/** 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.
*/
public func `catch`<ResultSuccess: JSValueConvertible>(
failure: @escaping (Failure) -> ResultSuccess
) -> JSPromise<ResultSuccess, Never> {
let closure = JSClosure {
failure(Failure.construct(from: $0[0])!).jsValue()
}
callbacks.append(closure)
return .init(unsafe: jsObject.then.function!(JSValue.undefined, 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`.
*/
public func `catch`<ResultSuccess: JSValueConvertible, ResultFailure: JSValueConstructible>(
failure: @escaping (Failure) -> JSPromise<ResultSuccess, ResultFailure>
) -> JSPromise<ResultSuccess, ResultFailure> {
let closure = JSClosure {
failure(Failure.construct(from: $0[0])!).jsValue()
}
callbacks.append(closure)
return .init(unsafe: jsObject.then.function!(JSValue.undefined, closure).object!)
}

/** Schedules the `failure` closure to be invoked on either successful or rejected completion of
`self`.
*/
public func finally(successOrFailure: @escaping () -> ()) -> Self {
let closure = JSClosure { _ in
successOrFailure()
}
callbacks.append(closure)
return .init(unsafe: jsObject.finally.function!(closure).object!)
}

deinit {
callbacks.forEach { $0.release() }
}
}
6 changes: 6 additions & 0 deletions Sources/JavaScriptKit/JSValueConstructible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,9 @@ extension UInt64: JSValueConstructible {
value.number.map(Self.init)
}
}

extension Never: JSValueConstructible {
public static func construct(from value: JSValue) -> Never? {
fatalError()
}
}