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

Asynchronous calls in JSClosure #157

Closed
mhavu opened this issue Jan 28, 2022 · 5 comments · Fixed by #159
Closed

Asynchronous calls in JSClosure #157

mhavu opened this issue Jan 28, 2022 · 5 comments · Fixed by #159
Labels
documentation Improvements or additions to documentation enhancement New feature or request

Comments

@mhavu
Copy link

mhavu commented Jan 28, 2022

What is the best way to handle calls to asynchronous code inside a JSClosure, especially if one needs to return the result of such call from the JSClosure? Consider the following example:

let closure = .object(JSClosure { (arguments: [JSValue]) in
    guard let url = arguments.first?.string else {
        return JSValue.undefined
    }
    return await fetch(url)
})

This results in the following error:

cannot pass function of type '([JSValue]) async -> JSValue' to parameter expecting synchronous function type

The reason is obvious, but is there a nice way to work around this?

@j-f1
Copy link
Member

j-f1 commented Jan 28, 2022

That would be a great feature to add! I would imagine it as either an overload of JSClosure.init that takes an async closure, or a separate JSClosure.async static method (for clarity) that does something like this:

static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure {
    JSClosure { arguments in
        JSPromise { resolver in
            Task {
                do {
                    let result = try await body(arguments)
                    resolver(.success(result))
                } catch {
                    if let jsError = error as? JSError {
                        resolver(.failure(jsError.jsValue()))
                    } else {
                        resolver(.failure(JSError(message: String(describing: error)).jsValue()))
                    }
                }
            }
        }.jsValue()
    }
}

@MaxDesiatov MaxDesiatov added documentation Improvements or additions to documentation enhancement New feature or request labels Jan 28, 2022
@mhavu
Copy link
Author

mhavu commented Jan 28, 2022

Indeed. If one doesn't need to return the result of the asynchronous call, using Task inside a JSClosure should work as it is, right? If this is the case, wrapping the asynchronous call (inside Task) to a synchronous function that calls a completion handler should do the trick when the result needs to be returned:

let closure = .object(JSClosure { (arguments: [JSValue]) in
    guard let url = arguments.first?.string else {
        return JSValue.undefined
    }
    func wrappedFetch(_ url: String, completion: @escaping (String) -> ()) {
        Task {
            let data = await fetch(url)
            completion(data)
        }    
    }
    var result: String
    wrappedFetch(url) { data in
        result = data
    }
    return result
})

What you suggest above would definitely be cleaner.

@j-f1
Copy link
Member

j-f1 commented Jan 28, 2022

I don’t think that code would work. Specifically, I get two compiler errors: “Variable 'result' captured by a closure before being initialized” and “Variable 'result' used before being initialized.” However, you’re correct that if you don’t need to return the result of the asynchronous call (and if you don’t need the JS code to be able to wait for your code to finish running), running the async code in a Task would work.

@mhavu
Copy link
Author

mhavu commented Jan 28, 2022

Ah, too bad. Now that you mention it, it is clear that the closure can reach the return statement before the completion handler has set the result. Returning a JSPromise should work, though.

@mhavu
Copy link
Author

mhavu commented Jan 30, 2022

This does indeed work:

let closure = .object(JSClosure { (arguments: [JSValue]) in
    guard let url = arguments.first?.string else {
        return JSValue.undefined
    }
    func wrappedFetch(_ url: String, completion: @escaping (String) -> ()) {
        Task {
            let data = try await fetch(url)
            completion(data)
        }    
    }
    let result = JSPromise { resolve in
         wrappedFetch(url) { data in
            resolve(.success(data))
        }
    }
    return result.jsValue()
})

@j-f1 j-f1 mentioned this issue Feb 3, 2022
1 task
@j-f1 j-f1 closed this as completed in #159 Jun 5, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants