Skip to content

Commit 55a4bfa

Browse files
Merge pull request #113 from swiftwasm/katei/update-jsclosure
Major API change on JSClosure
2 parents 7d7be11 + 0896483 commit 55a4bfa

File tree

10 files changed

+353
-268
lines changed

10 files changed

+353
-268
lines changed

.github/workflows/compatibility.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ jobs:
2020
export PATH="$SWIFTENV_ROOT/bin:$PATH"
2121
eval "$(swiftenv init -)"
2222
make bootstrap
23-
cd Example
23+
cd Example/JavaScriptKitExample
2424
swift build --triple wasm32-unknown-wasi

Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ buttonElement.innerText = "Click me!"
1212
let listener = JSClosure { _ in
1313
alert("Swift is running on browser!")
1414
}
15-
buttonElement.onclick = .function(listener)
15+
buttonElement.onclick = .object(listener)
1616

1717
_ = document.body.appendChild(buttonElement)

IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift

+55-6
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,47 @@ try test("Function Call") {
183183
try expectEqual(func6(true, "OK", 2), .string("OK"))
184184
}
185185

186+
let evalClosure = JSObject.global.globalObject1.eval_closure.function!
187+
188+
try test("Closure Lifetime") {
189+
do {
190+
let c1 = JSClosure { arguments in
191+
return arguments[0]
192+
}
193+
try expectEqual(evalClosure(c1, JSValue.number(1.0)), .number(1.0))
194+
c1.release()
195+
}
196+
197+
do {
198+
let c1 = JSClosure { arguments in
199+
return arguments[0]
200+
}
201+
c1.release()
202+
// Call a released closure
203+
_ = try expectThrow(try evalClosure.throws(c1))
204+
}
205+
206+
do {
207+
let c1 = JSClosure { _ in
208+
// JSClosure will be deallocated before `release()`
209+
_ = JSClosure { _ in .undefined }
210+
return .undefined
211+
}
212+
_ = try expectThrow(try evalClosure.throws(c1))
213+
c1.release()
214+
}
215+
216+
do {
217+
let c1 = JSOneshotClosure { _ in
218+
return .boolean(true)
219+
}
220+
try expectEqual(evalClosure(c1), .boolean(true))
221+
// second call will cause `fatalError` that can be caught as a JavaScript exception
222+
_ = try expectThrow(try evalClosure.throws(c1))
223+
// OneshotClosure won't call fatalError even if it's deallocated before `release`
224+
}
225+
}
226+
186227
try test("Host Function Registration") {
187228
// ```js
188229
// global.globalObject1 = {
@@ -205,7 +246,7 @@ try test("Host Function Registration") {
205246
return .number(1)
206247
}
207248

208-
setJSValue(this: prop_6Ref, name: "host_func_1", value: .function(hostFunc1))
249+
setJSValue(this: prop_6Ref, name: "host_func_1", value: .object(hostFunc1))
209250

210251
let call_host_1 = getJSValue(this: prop_6Ref, name: "call_host_1")
211252
let call_host_1Func = try expectFunction(call_host_1)
@@ -223,8 +264,8 @@ try test("Host Function Registration") {
223264
}
224265
}
225266

226-
try expectEqual(hostFunc2(3), .number(6))
227-
_ = try expectString(hostFunc2(true))
267+
try expectEqual(evalClosure(hostFunc2, 3), .number(6))
268+
_ = try expectString(evalClosure(hostFunc2, true))
228269
hostFunc2.release()
229270
}
230271

@@ -336,7 +377,7 @@ try test("ObjectRef Lifetime") {
336377

337378
let identity = JSClosure { $0[0] }
338379
let ref1 = getJSValue(this: .global, name: "globalObject1").object!
339-
let ref2 = identity(ref1).object!
380+
let ref2 = evalClosure(identity, ref1).object!
340381
try expectEqual(ref1.prop_2, .number(2))
341382
try expectEqual(ref2.prop_2, .number(2))
342383
identity.release()
@@ -449,21 +490,29 @@ try test("Date") {
449490
}
450491

451492
// make the timers global to prevent early deallocation
452-
var timeout: JSTimer?
493+
var timeouts: [JSTimer] = []
453494
var interval: JSTimer?
454495

455496
try test("Timer") {
456497
let start = JSDate().valueOf()
457498
let timeoutMilliseconds = 5.0
458-
499+
var timeout: JSTimer!
459500
timeout = JSTimer(millisecondsDelay: timeoutMilliseconds, isRepeating: false) {
460501
// verify that at least `timeoutMilliseconds` passed since the `timeout` timer started
461502
try! expectEqual(start + timeoutMilliseconds <= JSDate().valueOf(), true)
462503
}
504+
timeouts += [timeout]
505+
506+
timeout = JSTimer(millisecondsDelay: timeoutMilliseconds, isRepeating: false) {
507+
fatalError("timer should be cancelled")
508+
}
509+
timeout = nil
463510

464511
var count = 0.0
465512
let maxCount = 5.0
466513
interval = JSTimer(millisecondsDelay: 5, isRepeating: true) {
514+
// ensure that JSTimer is living
515+
try! expectNotNil(interval)
467516
// verify that at least `timeoutMilliseconds * count` passed since the `timeout`
468517
// timer started
469518
try! expectEqual(start + timeoutMilliseconds * count <= JSDate().valueOf(), true)

IntegrationTests/bin/primary-tests.js

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ global.globalObject1 = {
4747
throw 3.0
4848
},
4949
},
50+
eval_closure: function(fn) {
51+
return fn(arguments[1])
52+
}
5053
};
5154

5255
global.Animal = function (name, age, isCat) {

Sources/JavaScriptKit/BasicObjects/JSPromise.swift

+20-35
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,11 @@ is of actual JavaScript `Error` type, you should use `JSPromise<JSValue, JSValue
88
This doesn't 100% match the JavaScript API, as `then` overload with two callbacks is not available.
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.
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`.
1811
*/
1912
public final class JSPromise<Success, Failure>: ConvertibleToJSValue, ConstructibleFromJSValue {
2013
/// The underlying JavaScript `Promise` object.
2114
public let jsObject: JSObject
2215

23-
private var callbacks = [JSClosure]()
24-
2516
/// The underlying JavaScript `Promise` object wrapped as `JSValue`.
2617
public func jsValue() -> JSValue {
2718
.object(jsObject)
@@ -52,39 +43,37 @@ public final class JSPromise<Success, Failure>: ConvertibleToJSValue, Constructi
5243
/** Schedules the `success` closure to be invoked on sucessful completion of `self`.
5344
*/
5445
public func then(success: @escaping () -> ()) {
55-
let closure = JSClosure { _ in success() }
56-
callbacks.append(closure)
46+
let closure = JSOneshotClosure { _ in
47+
success()
48+
return .undefined
49+
}
5750
_ = jsObject.then!(closure)
5851
}
5952

6053
/** Schedules the `failure` closure to be invoked on either successful or rejected completion of
6154
`self`.
6255
*/
6356
public func finally(successOrFailure: @escaping () -> ()) -> Self {
64-
let closure = JSClosure { _ in
57+
let closure = JSOneshotClosure { _ in
6558
successOrFailure()
59+
return .undefined
6660
}
67-
callbacks.append(closure)
6861
return .init(unsafe: jsObject.finally!(closure).object!)
6962
}
70-
71-
deinit {
72-
callbacks.forEach { $0.release() }
73-
}
7463
}
7564

7665
extension JSPromise where Success == (), Failure == Never {
7766
/** Creates a new `JSPromise` instance from a given `resolver` closure. `resolver` takes
7867
a closure that your code should call to resolve this `JSPromise` instance.
7968
*/
8069
public convenience init(resolver: @escaping (@escaping () -> ()) -> ()) {
81-
let closure = JSClosure { arguments -> () in
70+
let closure = JSOneshotClosure { arguments in
8271
// The arguments are always coming from the `Promise` constructor, so we should be
8372
// safe to assume their type here
8473
resolver { arguments[0].function!() }
74+
return .undefined
8575
}
8676
self.init(unsafe: JSObject.global.Promise.function!.new(closure))
87-
callbacks.append(closure)
8877
}
8978
}
9079

@@ -93,7 +82,7 @@ extension JSPromise where Failure: ConvertibleToJSValue {
9382
two closure that your code should call to either resolve or reject this `JSPromise` instance.
9483
*/
9584
public convenience init(resolver: @escaping (@escaping (Result<Success, JSError>) -> ()) -> ()) {
96-
let closure = JSClosure { arguments -> () in
85+
let closure = JSOneshotClosure { arguments in
9786
// The arguments are always coming from the `Promise` constructor, so we should be
9887
// safe to assume their type here
9988
let resolve = arguments[0].function!
@@ -107,9 +96,9 @@ extension JSPromise where Failure: ConvertibleToJSValue {
10796
reject(error.jsValue())
10897
}
10998
}
99+
return .undefined
110100
}
111101
self.init(unsafe: JSObject.global.Promise.function!.new(closure))
112-
callbacks.append(closure)
113102
}
114103
}
115104

@@ -118,7 +107,7 @@ extension JSPromise where Success: ConvertibleToJSValue, Failure: JSError {
118107
a closure that your code should call to either resolve or reject this `JSPromise` instance.
119108
*/
120109
public convenience init(resolver: @escaping (@escaping (Result<Success, JSError>) -> ()) -> ()) {
121-
let closure = JSClosure { arguments -> () in
110+
let closure = JSOneshotClosure { arguments in
122111
// The arguments are always coming from the `Promise` constructor, so we should be
123112
// safe to assume their type here
124113
let resolve = arguments[0].function!
@@ -132,9 +121,9 @@ extension JSPromise where Success: ConvertibleToJSValue, Failure: JSError {
132121
reject(error.jsValue())
133122
}
134123
}
124+
return .undefined
135125
}
136126
self.init(unsafe: JSObject.global.Promise.function!.new(closure))
137-
callbacks.append(closure)
138127
}
139128
}
140129

@@ -146,13 +135,13 @@ extension JSPromise where Success: ConstructibleFromJSValue {
146135
file: StaticString = #file,
147136
line: Int = #line
148137
) {
149-
let closure = JSClosure { arguments -> () in
138+
let closure = JSOneshotClosure { arguments in
150139
guard let result = Success.construct(from: arguments[0]) else {
151140
fatalError("\(file):\(line): failed to unwrap success value for `then` callback")
152141
}
153142
success(result)
143+
return .undefined
154144
}
155-
callbacks.append(closure)
156145
_ = jsObject.then!(closure)
157146
}
158147

@@ -165,13 +154,12 @@ extension JSPromise where Success: ConstructibleFromJSValue {
165154
file: StaticString = #file,
166155
line: Int = #line
167156
) -> JSPromise<ResultType, Failure> {
168-
let closure = JSClosure { arguments -> JSValue in
157+
let closure = JSOneshotClosure { arguments -> JSValue in
169158
guard let result = Success.construct(from: arguments[0]) else {
170159
fatalError("\(file):\(line): failed to unwrap success value for `then` callback")
171160
}
172161
return success(result).jsValue()
173162
}
174-
callbacks.append(closure)
175163
return .init(unsafe: jsObject.then!(closure).object!)
176164
}
177165

@@ -184,13 +172,12 @@ extension JSPromise where Success: ConstructibleFromJSValue {
184172
file: StaticString = #file,
185173
line: Int = #line
186174
) -> JSPromise<ResultSuccess, ResultFailure> {
187-
let closure = JSClosure { arguments -> JSValue in
175+
let closure = JSOneshotClosure { arguments -> JSValue in
188176
guard let result = Success.construct(from: arguments[0]) else {
189177
fatalError("\(file):\(line): failed to unwrap success value for `then` callback")
190178
}
191179
return success(result).jsValue()
192180
}
193-
callbacks.append(closure)
194181
return .init(unsafe: jsObject.then!(closure).object!)
195182
}
196183
}
@@ -205,13 +192,12 @@ extension JSPromise where Failure: ConstructibleFromJSValue {
205192
file: StaticString = #file,
206193
line: Int = #line
207194
) -> JSPromise<ResultSuccess, Never> {
208-
let closure = JSClosure { arguments -> JSValue in
195+
let closure = JSOneshotClosure { arguments -> JSValue in
209196
guard let error = Failure.construct(from: arguments[0]) else {
210197
fatalError("\(file):\(line): failed to unwrap error value for `catch` callback")
211198
}
212199
return failure(error).jsValue()
213200
}
214-
callbacks.append(closure)
215201
return .init(unsafe: jsObject.then!(JSValue.undefined, closure).object!)
216202
}
217203

@@ -222,13 +208,13 @@ extension JSPromise where Failure: ConstructibleFromJSValue {
222208
file: StaticString = #file,
223209
line: Int = #line
224210
) {
225-
let closure = JSClosure { arguments -> () in
211+
let closure = JSOneshotClosure { arguments in
226212
guard let error = Failure.construct(from: arguments[0]) else {
227213
fatalError("\(file):\(line): failed to unwrap error value for `catch` callback")
228214
}
229215
failure(error)
216+
return .undefined
230217
}
231-
callbacks.append(closure)
232218
_ = jsObject.then!(JSValue.undefined, closure)
233219
}
234220

@@ -241,13 +227,12 @@ extension JSPromise where Failure: ConstructibleFromJSValue {
241227
file: StaticString = #file,
242228
line: Int = #line
243229
) -> JSPromise<ResultSuccess, ResultFailure> {
244-
let closure = JSClosure { arguments -> JSValue in
230+
let closure = JSOneshotClosure { arguments -> JSValue in
245231
guard let error = Failure.construct(from: arguments[0]) else {
246232
fatalError("\(file):\(line): failed to unwrap error value for `catch` callback")
247233
}
248234
return failure(error).jsValue()
249235
}
250-
callbacks.append(closure)
251236
return .init(unsafe: jsObject.then!(JSValue.undefined, closure).object!)
252237
}
253238
}

0 commit comments

Comments
 (0)