Skip to content

Commit 58ca905

Browse files
Simplify JSPromise API
1 parent 55a4bfa commit 58ca905

File tree

4 files changed

+145
-167
lines changed

4 files changed

+145
-167
lines changed

Diff for: IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift

+29
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,32 @@ func expectNotNil<T>(_ value: T?, file: StaticString = #file, line: UInt = #line
101101
throw MessageError("Expect a non-nil value", file: file, line: line, column: column)
102102
}
103103
}
104+
105+
class Expectation {
106+
private(set) var isFulfilled: Bool = false
107+
private let label: String
108+
private let expectedFulfillmentCount: Int
109+
private var fulfillmentCount: Int = 0
110+
111+
init(label: String, expectedFulfillmentCount: Int = 1) {
112+
self.label = label
113+
self.expectedFulfillmentCount = expectedFulfillmentCount
114+
}
115+
116+
func fulfill() {
117+
assert(!isFulfilled, "Too many fulfillment (label: \(label)): expectedFulfillmentCount is \(expectedFulfillmentCount)")
118+
fulfillmentCount += 1
119+
if fulfillmentCount == expectedFulfillmentCount {
120+
isFulfilled = true
121+
}
122+
}
123+
124+
static func wait(_ expectations: [Expectation]) {
125+
var timer: JSTimer!
126+
timer = JSTimer(millisecondsDelay: 5.0, isRepeating: true) {
127+
guard expectations.allSatisfy(\.isFulfilled) else { return }
128+
assert(timer != nil)
129+
timer = nil
130+
}
131+
}
132+
}

Diff for: IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift

+64-4
Original file line numberDiff line numberDiff line change
@@ -528,22 +528,80 @@ try test("Timer") {
528528
}
529529

530530
var timer: JSTimer?
531-
var promise: JSPromise<(), Never>?
531+
var expectations: [Expectation] = []
532532

533533
try test("Promise") {
534+
535+
let p1 = JSPromise.resolve(.null)
536+
let exp1 = Expectation(label: "Promise.then testcase", expectedFulfillmentCount: 4)
537+
p1.then { (value) -> JSValue in
538+
try! expectEqual(value, .null)
539+
exp1.fulfill()
540+
return .number(1.0)
541+
}
542+
.then { value -> JSValue in
543+
try! expectEqual(value, .number(1.0))
544+
exp1.fulfill()
545+
return JSPromise.resolve(.boolean(true)).jsValue()
546+
}
547+
.then { value -> JSValue in
548+
try! expectEqual(value, .boolean(true))
549+
exp1.fulfill()
550+
return .undefined
551+
}
552+
.catch { _ -> JSValue in
553+
fatalError("Not fired due to no throw")
554+
}
555+
.finally { exp1.fulfill() }
556+
557+
let exp2 = Expectation(label: "Promise.catch testcase", expectedFulfillmentCount: 4)
558+
let p2 = JSPromise.reject(JSValue.boolean(false))
559+
p2.then { _ -> JSValue in
560+
fatalError("Not fired due to no success")
561+
}
562+
.catch { reason -> JSValue in
563+
try! expectEqual(reason, .boolean(false))
564+
exp2.fulfill()
565+
return .boolean(true)
566+
}
567+
.then { value -> JSValue in
568+
try! expectEqual(value, .boolean(true))
569+
exp2.fulfill()
570+
return JSPromise.reject(JSValue.number(2.0)).jsValue()
571+
}
572+
.catch { reason -> JSValue in
573+
try! expectEqual(reason, .number(2.0))
574+
exp2.fulfill()
575+
return .undefined
576+
}
577+
.finally { exp2.fulfill() }
578+
579+
534580
let start = JSDate().valueOf()
535581
let timeoutMilliseconds = 5.0
582+
let exp3 = Expectation(label: "Promise and Timer testcae", expectedFulfillmentCount: 2)
536583

537-
promise = JSPromise { resolve in
584+
let p3 = JSPromise { resolve in
538585
timer = JSTimer(millisecondsDelay: timeoutMilliseconds) {
539-
resolve()
586+
exp3.fulfill()
587+
resolve(.success(.undefined))
540588
}
541589
}
542590

543-
promise!.then {
591+
p3.then { _ in
544592
// verify that at least `timeoutMilliseconds` passed since the timer started
545593
try! expectEqual(start + timeoutMilliseconds <= JSDate().valueOf(), true)
594+
exp3.fulfill()
595+
return .undefined
546596
}
597+
598+
let exp4 = Expectation(label: "Promise lifetime")
599+
// Ensure that users don't need to manage JSPromise lifetime
600+
JSPromise.resolve(.boolean(true)).then { _ -> JSValue in
601+
exp4.fulfill()
602+
return .undefined
603+
}
604+
expectations += [exp1, exp2, exp3, exp4]
547605
}
548606

549607
try test("Error") {
@@ -620,3 +678,5 @@ try test("Exception") {
620678
let errorObject3 = JSError(from: ageError as! JSValue)
621679
try expectNotNil(errorObject3)
622680
}
681+
682+
Expectation.wait(expectations)

Diff for: Sources/JavaScriptKit/BasicObjects/JSPromise.swift

+46-162
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This doesn't 100% match the JavaScript API, as `then` overload with two callback
99
It's impossible to unify success and failure types from both callbacks in a single returned promise
1010
without type erasure. You should chain `then` and `catch` in those cases to avoid type erasure.
1111
*/
12-
public final class JSPromise<Success, Failure>: ConvertibleToJSValue, ConstructibleFromJSValue {
12+
public final class JSPromise: JSBridgedClass {
1313
/// The underlying JavaScript `Promise` object.
1414
public let jsObject: JSObject
1515

@@ -18,17 +18,20 @@ public final class JSPromise<Success, Failure>: ConvertibleToJSValue, Constructi
1818
.object(jsObject)
1919
}
2020

21+
public static var constructor: JSFunction {
22+
JSObject.global.Promise.function!
23+
}
24+
2125
/// This private initializer assumes that the passed object is a JavaScript `Promise`
22-
private init(unsafe object: JSObject) {
26+
public init(unsafelyWrapping object: JSObject) {
2327
self.jsObject = object
2428
}
2529

2630
/** Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `jsObject`
2731
is not an instance of JavaScript `Promise`, this initializer will return `nil`.
2832
*/
29-
public init?(_ jsObject: JSObject) {
30-
guard jsObject.isInstanceOf(JSObject.global.Promise.function!) else { return nil }
31-
self.jsObject = jsObject
33+
public convenience init?(_ jsObject: JSObject) {
34+
self.init(from: jsObject)
3235
}
3336

3437
/** Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `value`
@@ -40,73 +43,10 @@ public final class JSPromise<Success, Failure>: ConvertibleToJSValue, Constructi
4043
return Self.init(jsObject)
4144
}
4245

43-
/** Schedules the `success` closure to be invoked on sucessful completion of `self`.
44-
*/
45-
public func then(success: @escaping () -> ()) {
46-
let closure = JSOneshotClosure { _ in
47-
success()
48-
return .undefined
49-
}
50-
_ = jsObject.then!(closure)
51-
}
52-
53-
/** Schedules the `failure` closure to be invoked on either successful or rejected completion of
54-
`self`.
55-
*/
56-
public func finally(successOrFailure: @escaping () -> ()) -> Self {
57-
let closure = JSOneshotClosure { _ in
58-
successOrFailure()
59-
return .undefined
60-
}
61-
return .init(unsafe: jsObject.finally!(closure).object!)
62-
}
63-
}
64-
65-
extension JSPromise where Success == (), Failure == Never {
66-
/** Creates a new `JSPromise` instance from a given `resolver` closure. `resolver` takes
67-
a closure that your code should call to resolve this `JSPromise` instance.
68-
*/
69-
public convenience init(resolver: @escaping (@escaping () -> ()) -> ()) {
70-
let closure = JSOneshotClosure { arguments in
71-
// The arguments are always coming from the `Promise` constructor, so we should be
72-
// safe to assume their type here
73-
resolver { arguments[0].function!() }
74-
return .undefined
75-
}
76-
self.init(unsafe: JSObject.global.Promise.function!.new(closure))
77-
}
78-
}
79-
80-
extension JSPromise where Failure: ConvertibleToJSValue {
81-
/** Creates a new `JSPromise` instance from a given `resolver` closure. `resolver` takes
46+
/** Creates a new `JSPromise` instance from a given `resolver` closure. `resolver` takes
8247
two closure that your code should call to either resolve or reject this `JSPromise` instance.
8348
*/
84-
public convenience init(resolver: @escaping (@escaping (Result<Success, JSError>) -> ()) -> ()) {
85-
let closure = JSOneshotClosure { arguments in
86-
// The arguments are always coming from the `Promise` constructor, so we should be
87-
// safe to assume their type here
88-
let resolve = arguments[0].function!
89-
let reject = arguments[1].function!
90-
91-
resolver {
92-
switch $0 {
93-
case .success:
94-
resolve()
95-
case let .failure(error):
96-
reject(error.jsValue())
97-
}
98-
}
99-
return .undefined
100-
}
101-
self.init(unsafe: JSObject.global.Promise.function!.new(closure))
102-
}
103-
}
104-
105-
extension JSPromise where Success: ConvertibleToJSValue, Failure: JSError {
106-
/** Creates a new `JSPromise` instance from a given `resolver` closure. `resolver` takes
107-
a closure that your code should call to either resolve or reject this `JSPromise` instance.
108-
*/
109-
public convenience init(resolver: @escaping (@escaping (Result<Success, JSError>) -> ()) -> ()) {
49+
public convenience init(resolver: @escaping (@escaping (Result<JSValue, JSValue>) -> ()) -> ()) {
11050
let closure = JSOneshotClosure { arguments in
11151
// The arguments are always coming from the `Promise` constructor, so we should be
11252
// safe to assume their type here
@@ -116,123 +56,67 @@ extension JSPromise where Success: ConvertibleToJSValue, Failure: JSError {
11656
resolver {
11757
switch $0 {
11858
case let .success(success):
119-
resolve(success.jsValue())
59+
resolve(success)
12060
case let .failure(error):
121-
reject(error.jsValue())
61+
reject(error)
12262
}
12363
}
12464
return .undefined
12565
}
126-
self.init(unsafe: JSObject.global.Promise.function!.new(closure))
66+
self.init(unsafelyWrapping: Self.constructor.new(closure))
12767
}
128-
}
12968

130-
extension JSPromise where Success: ConstructibleFromJSValue {
131-
/** Schedules the `success` closure to be invoked on sucessful completion of `self`.
132-
*/
133-
public func then(
134-
success: @escaping (Success) -> (),
135-
file: StaticString = #file,
136-
line: Int = #line
137-
) {
138-
let closure = JSOneshotClosure { arguments in
139-
guard let result = Success.construct(from: arguments[0]) else {
140-
fatalError("\(file):\(line): failed to unwrap success value for `then` callback")
141-
}
142-
success(result)
143-
return .undefined
144-
}
145-
_ = jsObject.then!(closure)
69+
public static func resolve(_ value: JSValue) -> JSPromise {
70+
self.init(unsafelyWrapping: Self.constructor.resolve!(value).object!)
14671
}
14772

148-
/** Returns a new promise created from chaining the current `self` promise with the `success`
149-
closure invoked on sucessful completion of `self`. The returned promise will have a new
150-
`Success` type equal to the return type of `success`.
151-
*/
152-
public func then<ResultType: ConvertibleToJSValue>(
153-
success: @escaping (Success) -> ResultType,
154-
file: StaticString = #file,
155-
line: Int = #line
156-
) -> JSPromise<ResultType, Failure> {
157-
let closure = JSOneshotClosure { arguments -> JSValue in
158-
guard let result = Success.construct(from: arguments[0]) else {
159-
fatalError("\(file):\(line): failed to unwrap success value for `then` callback")
160-
}
161-
return success(result).jsValue()
162-
}
163-
return .init(unsafe: jsObject.then!(closure).object!)
73+
public static func reject(_ reason: JSValue) -> JSPromise {
74+
self.init(unsafelyWrapping: Self.constructor.reject!(reason).object!)
16475
}
16576

166-
/** Returns a new promise created from chaining the current `self` promise with the `success`
167-
closure invoked on sucessful completion of `self`. The returned promise will have a new type
168-
equal to the return type of `success`.
77+
/** Schedules the `success` closure to be invoked on sucessful completion of `self`.
16978
*/
170-
public func then<ResultSuccess: ConvertibleToJSValue, ResultFailure: ConstructibleFromJSValue>(
171-
success: @escaping (Success) -> JSPromise<ResultSuccess, ResultFailure>,
172-
file: StaticString = #file,
173-
line: Int = #line
174-
) -> JSPromise<ResultSuccess, ResultFailure> {
175-
let closure = JSOneshotClosure { arguments -> JSValue in
176-
guard let result = Success.construct(from: arguments[0]) else {
177-
fatalError("\(file):\(line): failed to unwrap success value for `then` callback")
178-
}
179-
return success(result).jsValue()
79+
@discardableResult
80+
public func then(success: @escaping (JSValue) -> JSValue) -> JSPromise {
81+
let closure = JSOneshotClosure {
82+
return success($0[0])
18083
}
181-
return .init(unsafe: jsObject.then!(closure).object!)
84+
return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!)
18285
}
183-
}
18486

185-
extension JSPromise where Failure: ConstructibleFromJSValue {
186-
/** Returns a new promise created from chaining the current `self` promise with the `failure`
187-
closure invoked on rejected completion of `self`. The returned promise will have a new `Success`
188-
type equal to the return type of the callback, while the `Failure` type becomes `Never`.
87+
/** Schedules the `success` closure to be invoked on sucessful completion of `self`.
18988
*/
190-
public func `catch`<ResultSuccess: ConvertibleToJSValue>(
191-
failure: @escaping (Failure) -> ResultSuccess,
192-
file: StaticString = #file,
193-
line: Int = #line
194-
) -> JSPromise<ResultSuccess, Never> {
195-
let closure = JSOneshotClosure { arguments -> JSValue in
196-
guard let error = Failure.construct(from: arguments[0]) else {
197-
fatalError("\(file):\(line): failed to unwrap error value for `catch` callback")
198-
}
199-
return failure(error).jsValue()
89+
@discardableResult
90+
public func then(success: @escaping (JSValue) -> JSValue,
91+
failure: @escaping (JSValue) -> JSValue) -> JSPromise {
92+
let successClosure = JSOneshotClosure {
93+
return success($0[0])
20094
}
201-
return .init(unsafe: jsObject.then!(JSValue.undefined, closure).object!)
95+
let failureClosure = JSOneshotClosure {
96+
return failure($0[0])
97+
}
98+
return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!)
20299
}
203100

204101
/** Schedules the `failure` closure to be invoked on rejected completion of `self`.
205102
*/
206-
public func `catch`(
207-
failure: @escaping (Failure) -> (),
208-
file: StaticString = #file,
209-
line: Int = #line
210-
) {
211-
let closure = JSOneshotClosure { arguments in
212-
guard let error = Failure.construct(from: arguments[0]) else {
213-
fatalError("\(file):\(line): failed to unwrap error value for `catch` callback")
214-
}
215-
failure(error)
216-
return .undefined
103+
@discardableResult
104+
public func `catch`(failure: @escaping (JSValue) -> JSValue) -> JSPromise {
105+
let closure = JSOneshotClosure {
106+
return failure($0[0])
217107
}
218-
_ = jsObject.then!(JSValue.undefined, closure)
108+
return .init(unsafelyWrapping: jsObject.catch!(closure).object!)
219109
}
220110

221-
/** Returns a new promise created from chaining the current `self` promise with the `failure`
222-
closure invoked on rejected completion of `self`. The returned promise will have a new type
223-
equal to the return type of `success`.
111+
/** Schedules the `failure` closure to be invoked on either successful or rejected completion of
112+
`self`.
224113
*/
225-
public func `catch`<ResultSuccess: ConvertibleToJSValue, ResultFailure: ConstructibleFromJSValue>(
226-
failure: @escaping (Failure) -> JSPromise<ResultSuccess, ResultFailure>,
227-
file: StaticString = #file,
228-
line: Int = #line
229-
) -> JSPromise<ResultSuccess, ResultFailure> {
230-
let closure = JSOneshotClosure { arguments -> JSValue in
231-
guard let error = Failure.construct(from: arguments[0]) else {
232-
fatalError("\(file):\(line): failed to unwrap error value for `catch` callback")
233-
}
234-
return failure(error).jsValue()
114+
@discardableResult
115+
public func finally(successOrFailure: @escaping () -> ()) -> JSPromise {
116+
let closure = JSOneshotClosure { _ in
117+
successOrFailure()
118+
return .undefined
235119
}
236-
return .init(unsafe: jsObject.then!(JSValue.undefined, closure).object!)
120+
return .init(unsafelyWrapping: jsObject.finally!(closure).object!)
237121
}
238122
}

0 commit comments

Comments
 (0)