golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.18k stars 17.57k forks source link

proposal: syscall/js: method or function to await a js.Value #69720

Open jcbhmr opened 3 hours ago

jcbhmr commented 3 hours ago

Proposal Details

Let's say I have an async JavaScript function:

globalThis.myAsyncFunction = async () => 42;

Ideally I would be able to do one of these:

var n int
n = js.Global().Call("myAsyncFunction").Wait().Int()
n = js.Global().Call("myAsyncFunction").Await().Int()
n = js.Await(js.Global().Call("myAsyncFunction")).Int()
n = (<-js.Global().Call("myAsyncFunction").Chan()).Int()

Creating a promise in Go code for use by JavaScript code is already reasonably easy: just use the new Promise(callback) constructor with a Go-defined js.Func callback.

func DoThingAsync() js.Value {
    return promiseConstructor.New(js.FuncOf(func(this Value, args []Value) interface{} {
        r := <-myChannelFromSomewhereThatReturnsValueAndErr
        if r.err == nil {
            args[0].Call(r.value)
        } else {
            args[1].Call(r.err)
        }
        return nil
    }))
}

The other direction -- unwrapping a JavaScript Promise instance on the Go-side by waiting for it -- is a bit lacking.

// Ugh. I have to manage this intermediate channel myself and remember to handle some edge cases.
var n int
p := promiseConstructor.Call("resolve", js.Global().Call("myAsyncFunction"))
type result struct {
    value js.Value
    err error
}
ch := make(chan result, 1)
onFulfilled := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
    ch <- result{args[0], nil}
    close(ch)
    return nil
})
defer onFulfilled.Release()
onRejected := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
    ch <- result{js.Value{}, js.Error{args[0]}}
    close(ch)
    return nil
})
defer onRejected.Release()
p.Call("then", onFulfilled, onRejected)
r := <-ch
if r.err != nil {
    panic(r.err)
}
n = r.value.Int()

Here's the algorithm for the ECMAScript 2025 Await( value ) abstract operation: https://tc39.es/ecma262/multipage/control-abstraction-objects.html#await

It seems like the .Wait() method convention is already in the standard library with sync.WaitGroup wg.Wait(). Here's an idea for how that might look in Go code. I'm not a Go channels wizard so this might be the completely wrong way to do this.

// Wait waits for the thenable v to fulfill or reject and returns the resulting value or error.
// This is equivalent to the await operator in JavaScript.
func (v js.Value) Wait() (js.Value, error) {
    p := promiseConstructor.Call("resolve", v)
    type result struct {
        value js.Value
        err error
    }
    ch := make(chan result, 1)
    onFulfilled := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        ch <- result{args[0], nil}
        close(ch)
        return nil
    })
    defer onFulfilled.Release()
    onRejected := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        ch <- result{js.Value{}, js.Error{args[0]}}
        close(ch)
        return nil
    })
    defer onRejected.Release()
    p.Call("then", onFulfilled, onRejected)
    r := <-ch
    // Unsure if want to panic on error, or return the error.
    // If a synchronous function throws an error it panics. Don't know whether to stick
    // to that convention or to return a (js.Value, error) multivalue.
    return r.value, r.err
    // OR
    if r.err == nil {
        return r.value
    } else {
        panic(r.err)
    }
}

There's already some of this promise stuff in the Go standard library https://github.com/golang/go/blob/1d0f5c478ac176fa99d0f3d6bd540e5fb422187a/src/net/http/roundtrip_js.go#L129-L245 so it seems like this is a thing that people need to do. I think that providing a .Wait() or Await(v) or something would be a good way to "one good way to do it"-ify this.

gabyhelp commented 3 hours ago

Related Issues and Documentation

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

jcbhmr commented 3 hours ago

That issue https://github.com/golang/go/issues/28911 that @gabyhelp linked is kinda what I want but it went unresolved. https://go-review.googlesource.com/c/go/+/150917 which was spawned from the issue linked was abandoned. The reasoning given seemed to be of the "this doesn't belong in the standard library" vibe.

I am a bit wary of adding new API like this. We can already achieve this by manually calling the "then" function ourselves, which the net/http library already does.

This is just syntactic sugar and I am afraid might add precedent for adding more helpers like this.

I'm in the same boat: I think this is API creep. And I (now) feel the same about https://go-review.googlesource.com/c/go/+/144384/, particularly if we start citing that as precedent for changes like this.

I think that a .Await() or something to get the await JavaScript keyword algorithm available to Go code is a good idea. Await is similar to but not identical to just calling .then(); there's the Promise.resolve() native-ification step too. Related reading: https://www.zhenghao.io/posts/await-vs-promise

ianlancetaylor commented 3 hours ago

CC @golang/js