Get value from JS Promise/async function from within a JSContext
Asked Answered
B

2

8

I'm executing a JavaScript SDK from within a JSContext, I can't get values out of any of the SDK's asynchronous functions however. I can get a JavaScript promise out of the JSContext, but I can't figure out how to resolve it. I have tried many ways of getting the value from the Promise, but every one has failed.

If I try something like the following I get [object Promise] back:

return self.jsContext.evaluateScript("new Promise(resolve => { setTimeout(300, () => resolve([1, 2, 3])) })")!

If I chain then directly onto the JS I get [object Promise] still:

return self.jsContext.evaluateScript("new Promise(resolve => { setTimeout(300, () => resolve([1, 2, 3])) }).then(val => val.json())")

If I try to invoke the method from Swift, I get still get [object Promise]:

let jsPromise = self.jsContext.evaluateScript("new Promise(resolve => { setTimeout(300, () => resolve([1, 2, 3])) })")
let promiseResult = jsPromise?.invokeMethod("then", withArguments: ["val => { return val.json() }"])
return promiseResult!

If I declare a JS variable outside of the Promise, then pass the value to it from a Swift-invoked then call, I get the original value set to it (as expected but worth a try):

self.jsContext.evaluateScript("let tempVar = 'Nothing has happened yet!'")
let jsPromise = self.jsContext.evaluateScript("new Promise(resolve => { setTimeout(300, () => resolve([1, 2, 3])) })")
let promiseResult = jsPromise?.invokeMethod("then", withArguments: ["val => { tempVar = val }"])
let tempVar = self.jsContext.evaluateScript("tempVar")
return tempVar!

If I try and use top-level await and resolve the Promise to a variable, then pull that variable out of the JSContext, IU get a EXC_BAD_INSTRUCTION error:

let jsPromise = self.jsContext.evaluateScript("let someVar = await new Promise(resolve => { setTimeout(300, () => resolve([1, 2, 3])) })")
return self.jsContext.evaluateScript("someVar")!

Thanks in advance, and sorry if I'm missing something, still very new to Swift.

Blastoderm answered 12/9, 2017 at 16:53 Comment(1)
What do you see in the object if you debug it with a breakpoint?Saiz
G
4

There are issues while mocking the Promise work flow inside JSContext. The functions like setTimout,setInterval etc. are not available in JSContext.

However, you can call Swift code from Javascript by passing block into the JSContext. Here's a code snippet, that shows how you can find out errors in JSContext.

var logValue = "" {
    didSet {
        print(logValue)
    }
}
//block we can pass to JSContext as JS function
let showLogScript: @convention(block) (String) -> Void = { value in
    logValue = value
}
let jsContext = JSContext()

//set exceptionHandler block
jsContext?.exceptionHandler = {
    (ctx: JSContext!, value: JSValue!) in
    print(value)
}
//make showLog function available to JSContext
jsContext?.setObject(unsafeBitCast(showLogScript, to: AnyObject.self), forKeyedSubscript: "showLog" as (NSCopying & NSObjectProtocol))

jsContext!.evaluateScript("showLog('this is my first name')") //this works
jsContext!.evaluateScript("showLog(setTimeout.name)") //it has issue
Gamber answered 20/9, 2017 at 3:53 Comment(0)
P
2

To get resolved values (and rejected errors) from asynchronous javascript code you should operate with javascript's Promise and its then method:

Promise.prototype.then()

then(onFulfilled)
then(onFulfilled, onRejected)

then(
  (value) => { /* fulfilment handler */ },
  (reason) => { /* rejection handler */ },
)

We can call then with the fulfilment handler which provides a resolved value from above, so let's implement that:

let script = """
new Promise(resolve => {
    setTimeout(() => resolve([1, 2, 3]), 300)
})
"""
let promise = context.evaluateScript(script)

let onFulfilled: @convention(block) (JSValue) -> Void = {
    print($0) // Prints: 1,2,3
}
promise?.invokeMethod("then", withArguments: [unsafeBitCast(onFulfilled, to: JSValue.self)])

This approach works for async functions as well because they operate with Promise under the hood, for instance:

let script = """
async function load() {
    return [1, 2, 3];
}
"""
context.evaluateScript(script)

let promise = context.evaluateScript("load()")

let onFulfilled: @convention(block) (JSValue) -> Void = {
    print($0) // Prints: 1,2,3
}
promise?.invokeMethod("then", withArguments: [unsafeBitCast(onFulfilled, to: JSValue.self)])
Poinsettia answered 9/3, 2023 at 21:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.