Skip to content

Commit c52ea09

Browse files
authoredSep 24, 2020
Add a generic JSPromise implementation (swiftwasm#62)
This provides three overloads each for `then` and `catch`, and I'm not sure if that's good for the type-checker performance and usability. I was thinking of naming the promise-returning callback version `flatMap`, but ultimately decided against it. `then` overload with two callbacks is not available, because it's impossible to unify success and failure types from both callbacks in a single returned promise without type erasure. I think users should just chain `then` and `catch` in those cases so that type erasure is avoided.
1 parent 87c8f73 commit c52ea09

File tree

5 files changed

+281
-4
lines changed

5 files changed

+281
-4
lines changed
 

‎IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift

+19
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,25 @@ try test("Timer") {
466466
}
467467
}
468468

469+
var timer: JSTimer?
470+
var promise: JSPromise<(), Never>?
471+
472+
try test("Promise") {
473+
let start = JSDate().valueOf()
474+
let timeoutMilliseconds = 5.0
475+
476+
promise = JSPromise { resolve in
477+
timer = JSTimer(millisecondsDelay: timeoutMilliseconds) {
478+
resolve()
479+
}
480+
}
481+
482+
promise!.then {
483+
// verify that at least `timeoutMilliseconds` passed since the timer started
484+
try! expectEqual(start + timeoutMilliseconds <= JSDate().valueOf(), true)
485+
}
486+
}
487+
469488
try test("Error") {
470489
let message = "test error"
471490
let error = JSError(message: message)

‎Sources/JavaScriptKit/BasicObjects/JSArray.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/// A wrapper around [the JavaScript Array class](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array)
1+
/// A wrapper around [the JavaScript Array class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)
22
/// that exposes its properties in a type-safe and Swifty way.
33
public class JSArray: JSBridgedClass {
44
public static let constructor = JSObject.global.Array.function!

‎Sources/JavaScriptKit/BasicObjects/JSError.swift

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) that
33
exposes its properties in a type-safe way.
44
*/
5-
public final class JSError: Error, JSBridgedClass {
6-
/// The constructor function used to create new `Error` objects.
5+
public final class JSError: Error, JSValueConvertible {
6+
/// The constructor function used to create new JavaScript `Error` objects.
77
public static let constructor = JSObject.global.Error.function!
88

99
/// The underlying JavaScript `Error` object.
@@ -32,6 +32,11 @@ public final class JSError: Error, JSBridgedClass {
3232
public var stack: String? {
3333
jsObject.stack.string
3434
}
35+
36+
/// Creates a new `JSValue` from this `JSError` instance.
37+
public func jsValue() -> JSValue {
38+
.object(jsObject)
39+
}
3540
}
3641

3742
extension JSError: CustomStringConvertible {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
/** A wrapper around [the JavaScript `Promise` class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)
2+
that exposes its functions in a type-safe and Swifty way. The `JSPromise` API is generic over both
3+
`Success` and `Failure` types, which improves compatibility with other statically-typed APIs such
4+
as Combine. If you don't know the exact type of your `Success` value, you should use `JSValue`, e.g.
5+
`JSPromise<JSValue, JSError>`. In the rare case, where you can't guarantee that the error thrown
6+
is of actual JavaScript `Error` type, you should use `JSPromise<JSValue, JSValue>`.
7+
8+
This doesn't 100% match the JavaScript API, as `then` overload with two callbacks is not available.
9+
It's impossible to unify success and failure types from both callbacks in a single returned promise
10+
without type erasure. You should chain `then` and `catch` in those cases to avoid type erasure.
11+
12+
**IMPORTANT**: instances of this class must have the same lifetime as the actual `Promise` object in
13+
the JavaScript environment, because callback handlers will be deallocated when `JSPromise.deinit` is
14+
executed.
15+
16+
If the actual `Promise` object in JavaScript environment lives longer than this `JSPromise`, it may
17+
attempt to call a deallocated `JSClosure`.
18+
*/
19+
public final class JSPromise<Success, Failure>: JSValueConvertible, JSValueConstructible {
20+
/// The underlying JavaScript `Promise` object.
21+
public let jsObject: JSObject
22+
23+
private var callbacks = [JSClosure]()
24+
25+
/// The underlying JavaScript `Promise` object wrapped as `JSValue`.
26+
public func jsValue() -> JSValue {
27+
.object(jsObject)
28+
}
29+
30+
/// This private initializer assumes that the passed object is a JavaScript `Promise`
31+
private init(unsafe object: JSObject) {
32+
self.jsObject = object
33+
}
34+
35+
/** Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `jsObject`
36+
is not an instance of JavaScript `Promise`, this initializer will return `nil`.
37+
*/
38+
public init?(_ jsObject: JSObject) {
39+
guard jsObject.isInstanceOf(JSObject.global.Promise.function!) else { return nil }
40+
self.jsObject = jsObject
41+
}
42+
43+
/** Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `value`
44+
is not an object and is not an instance of JavaScript `Promise`, this function will
45+
return `nil`.
46+
*/
47+
public static func construct(from value: JSValue) -> Self? {
48+
guard case let .object(jsObject) = value else { return nil }
49+
return Self.init(jsObject)
50+
}
51+
52+
/** Schedules the `success` closure to be invoked on sucessful completion of `self`.
53+
*/
54+
public func then(success: @escaping () -> ()) {
55+
let closure = JSClosure { _ in success() }
56+
callbacks.append(closure)
57+
_ = jsObject.then!(closure)
58+
}
59+
60+
/** Schedules the `failure` closure to be invoked on either successful or rejected completion of
61+
`self`.
62+
*/
63+
public func finally(successOrFailure: @escaping () -> ()) -> Self {
64+
let closure = JSClosure { _ in
65+
successOrFailure()
66+
}
67+
callbacks.append(closure)
68+
return .init(unsafe: jsObject.finally!(closure).object!)
69+
}
70+
71+
deinit {
72+
callbacks.forEach { $0.release() }
73+
}
74+
}
75+
76+
extension JSPromise where Success == (), Failure == Never {
77+
/** Creates a new `JSPromise` instance from a given `resolver` closure. `resolver` takes
78+
a closure that your code should call to resolve this `JSPromise` instance.
79+
*/
80+
public convenience init(resolver: @escaping (@escaping () -> ()) -> ()) {
81+
let closure = JSClosure { arguments -> () in
82+
// The arguments are always coming from the `Promise` constructor, so we should be
83+
// safe to assume their type here
84+
resolver { arguments[0].function!() }
85+
}
86+
self.init(unsafe: JSObject.global.Promise.function!.new(closure))
87+
callbacks.append(closure)
88+
}
89+
}
90+
91+
extension JSPromise where Failure: JSValueConvertible {
92+
/** Creates a new `JSPromise` instance from a given `resolver` closure. `resolver` takes
93+
two closure that your code should call to either resolve or reject this `JSPromise` instance.
94+
*/
95+
public convenience init(resolver: @escaping (@escaping (Result<Success, JSError>) -> ()) -> ()) {
96+
let closure = JSClosure { arguments -> () in
97+
// The arguments are always coming from the `Promise` constructor, so we should be
98+
// safe to assume their type here
99+
let resolve = arguments[0].function!
100+
let reject = arguments[1].function!
101+
102+
resolver {
103+
switch $0 {
104+
case .success:
105+
resolve()
106+
case let .failure(error):
107+
reject(error.jsValue())
108+
}
109+
}
110+
}
111+
self.init(unsafe: JSObject.global.Promise.function!.new(closure))
112+
callbacks.append(closure)
113+
}
114+
}
115+
116+
extension JSPromise where Success: JSValueConvertible, Failure: JSError {
117+
/** Creates a new `JSPromise` instance from a given `resolver` closure. `resolver` takes
118+
a closure that your code should call to either resolve or reject this `JSPromise` instance.
119+
*/
120+
public convenience init(resolver: @escaping (@escaping (Result<Success, JSError>) -> ()) -> ()) {
121+
let closure = JSClosure { arguments -> () in
122+
// The arguments are always coming from the `Promise` constructor, so we should be
123+
// safe to assume their type here
124+
let resolve = arguments[0].function!
125+
let reject = arguments[1].function!
126+
127+
resolver {
128+
switch $0 {
129+
case let .success(success):
130+
resolve(success.jsValue())
131+
case let .failure(error):
132+
reject(error.jsValue())
133+
}
134+
}
135+
}
136+
self.init(unsafe: JSObject.global.Promise.function!.new(closure))
137+
callbacks.append(closure)
138+
}
139+
}
140+
141+
extension JSPromise where Success: JSValueConstructible {
142+
/** Schedules the `success` closure to be invoked on sucessful completion of `self`.
143+
*/
144+
public func then(
145+
success: @escaping (Success) -> (),
146+
file: StaticString = #file,
147+
line: Int = #line
148+
) {
149+
let closure = JSClosure { arguments -> () in
150+
guard let result = Success.construct(from: arguments[0]) else {
151+
fatalError("\(file):\(line): failed to unwrap success value for `then` callback")
152+
}
153+
success(result)
154+
}
155+
callbacks.append(closure)
156+
_ = jsObject.then!(closure)
157+
}
158+
159+
/** Returns a new promise created from chaining the current `self` promise with the `success`
160+
closure invoked on sucessful completion of `self`. The returned promise will have a new
161+
`Success` type equal to the return type of `success`.
162+
*/
163+
public func then<ResultType: JSValueConvertible>(
164+
success: @escaping (Success) -> ResultType,
165+
file: StaticString = #file,
166+
line: Int = #line
167+
) -> JSPromise<ResultType, Failure> {
168+
let closure = JSClosure { arguments -> JSValue in
169+
guard let result = Success.construct(from: arguments[0]) else {
170+
fatalError("\(file):\(line): failed to unwrap success value for `then` callback")
171+
}
172+
return success(result).jsValue()
173+
}
174+
callbacks.append(closure)
175+
return .init(unsafe: jsObject.then!(closure).object!)
176+
}
177+
178+
/** Returns a new promise created from chaining the current `self` promise with the `success`
179+
closure invoked on sucessful completion of `self`. The returned promise will have a new type
180+
equal to the return type of `success`.
181+
*/
182+
public func then<ResultSuccess: JSValueConvertible, ResultFailure: JSValueConstructible>(
183+
success: @escaping (Success) -> JSPromise<ResultSuccess, ResultFailure>,
184+
file: StaticString = #file,
185+
line: Int = #line
186+
) -> JSPromise<ResultSuccess, ResultFailure> {
187+
let closure = JSClosure { arguments -> JSValue in
188+
guard let result = Success.construct(from: arguments[0]) else {
189+
fatalError("\(file):\(line): failed to unwrap success value for `then` callback")
190+
}
191+
return success(result).jsValue()
192+
}
193+
callbacks.append(closure)
194+
return .init(unsafe: jsObject.then!(closure).object!)
195+
}
196+
}
197+
198+
extension JSPromise where Failure: JSValueConstructible {
199+
/** Returns a new promise created from chaining the current `self` promise with the `failure`
200+
closure invoked on rejected completion of `self`. The returned promise will have a new `Success`
201+
type equal to the return type of the callback, while the `Failure` type becomes `Never`.
202+
*/
203+
public func `catch`<ResultSuccess: JSValueConvertible>(
204+
failure: @escaping (Failure) -> ResultSuccess,
205+
file: StaticString = #file,
206+
line: Int = #line
207+
) -> JSPromise<ResultSuccess, Never> {
208+
let closure = JSClosure { arguments -> JSValue in
209+
guard let error = Failure.construct(from: arguments[0]) else {
210+
fatalError("\(file):\(line): failed to unwrap error value for `catch` callback")
211+
}
212+
return failure(error).jsValue()
213+
}
214+
callbacks.append(closure)
215+
return .init(unsafe: jsObject.then!(JSValue.undefined, closure).object!)
216+
}
217+
218+
/** Schedules the `failure` closure to be invoked on rejected completion of `self`.
219+
*/
220+
public func `catch`(
221+
failure: @escaping (Failure) -> (),
222+
file: StaticString = #file,
223+
line: Int = #line
224+
) {
225+
let closure = JSClosure { arguments -> () in
226+
guard let error = Failure.construct(from: arguments[0]) else {
227+
fatalError("\(file):\(line): failed to unwrap error value for `catch` callback")
228+
}
229+
failure(error)
230+
}
231+
callbacks.append(closure)
232+
_ = jsObject.then!(JSValue.undefined, closure)
233+
}
234+
235+
/** Returns a new promise created from chaining the current `self` promise with the `failure`
236+
closure invoked on rejected completion of `self`. The returned promise will have a new type
237+
equal to the return type of `success`.
238+
*/
239+
public func `catch`<ResultSuccess: JSValueConvertible, ResultFailure: JSValueConstructible>(
240+
failure: @escaping (Failure) -> JSPromise<ResultSuccess, ResultFailure>,
241+
file: StaticString = #file,
242+
line: Int = #line
243+
) -> JSPromise<ResultSuccess, ResultFailure> {
244+
let closure = JSClosure { arguments -> JSValue in
245+
guard let error = Failure.construct(from: arguments[0]) else {
246+
fatalError("\(file):\(line): failed to unwrap error value for `catch` callback")
247+
}
248+
return failure(error).jsValue()
249+
}
250+
callbacks.append(closure)
251+
return .init(unsafe: jsObject.then!(JSValue.undefined, closure).object!)
252+
}
253+
}

‎Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public protocol TypedArrayElement: JSValueConvertible, JSValueConstructible {
1010
static var typedArrayClass: JSFunction { get }
1111
}
1212

13-
/// A wrapper around all JavaScript [TypedArray](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) classes that exposes their properties in a type-safe way.
13+
/// A wrapper around all JavaScript [TypedArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) classes that exposes their properties in a type-safe way.
1414
/// FIXME: the BigInt-based TypedArrays are not supported (https://github.com/swiftwasm/JavaScriptKit/issues/56)
1515
public class JSTypedArray<Element>: JSBridgedClass, ExpressibleByArrayLiteral where Element: TypedArrayElement {
1616
public static var constructor: JSFunction { Element.typedArrayClass }

0 commit comments

Comments
 (0)
Please sign in to comment.