blob: 9d573074cbd3990de97c4f13bc368dd7a2640863 [file] [log] [blame]
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build js,wasm
package js
import "sync"
var (
pendingCallbacks = Global().Get("Array").New()
makeCallbackHelper = Global().Get("Go").Get("_makeCallbackHelper")
makeEventCallbackHelper = Global().Get("Go").Get("_makeEventCallbackHelper")
)
var (
callbacksMu sync.Mutex
callbacks = make(map[uint32]func([]Value))
nextCallbackID uint32 = 1
)
// Callback is a Go function that got wrapped for use as a JavaScript callback.
type Callback struct {
Value // the JavaScript function that queues the callback for execution
id uint32
}
// NewCallback returns a wrapped callback function.
//
// Invoking the callback in JavaScript will queue the Go function fn for execution.
// This execution happens asynchronously on a special goroutine that handles all callbacks and preserves
// the order in which the callbacks got called.
// As a consequence, if one callback blocks this goroutine, other callbacks will not be processed.
// A blocking callback should therefore explicitly start a new goroutine.
//
// Callback.Release must be called to free up resources when the callback will not be used any more.
func NewCallback(fn func(args []Value)) Callback {
callbackLoopOnce.Do(func() {
go callbackLoop()
})
callbacksMu.Lock()
id := nextCallbackID
nextCallbackID++
callbacks[id] = fn
callbacksMu.Unlock()
return Callback{
Value: makeCallbackHelper.Invoke(id, pendingCallbacks, jsGo),
id: id,
}
}
type EventCallbackFlag int
const (
// PreventDefault can be used with NewEventCallback to call event.preventDefault synchronously.
PreventDefault EventCallbackFlag = 1 << iota
// StopPropagation can be used with NewEventCallback to call event.stopPropagation synchronously.
StopPropagation
// StopImmediatePropagation can be used with NewEventCallback to call event.stopImmediatePropagation synchronously.
StopImmediatePropagation
)
// NewEventCallback returns a wrapped callback function, just like NewCallback, but the callback expects to have
// exactly one argument, the event. Depending on flags, it will synchronously call event.preventDefault,
// event.stopPropagation and/or event.stopImmediatePropagation before queuing the Go function fn for execution.
func NewEventCallback(flags EventCallbackFlag, fn func(event Value)) Callback {
c := NewCallback(func(args []Value) {
fn(args[0])
})
return Callback{
Value: makeEventCallbackHelper.Invoke(
flags&PreventDefault != 0,
flags&StopPropagation != 0,
flags&StopImmediatePropagation != 0,
c,
),
id: c.id,
}
}
// Release frees up resources allocated for the callback.
// The callback must not be invoked after calling Release.
func (c Callback) Release() {
callbacksMu.Lock()
delete(callbacks, c.id)
callbacksMu.Unlock()
}
var callbackLoopOnce sync.Once
func callbackLoop() {
for !jsGo.Get("_callbackShutdown").Bool() {
sleepUntilCallback()
for {
cb := pendingCallbacks.Call("shift")
if cb == Undefined() {
break
}
id := uint32(cb.Get("id").Int())
callbacksMu.Lock()
f, ok := callbacks[id]
callbacksMu.Unlock()
if !ok {
Global().Get("console").Call("error", "call to closed callback")
continue
}
argsObj := cb.Get("args")
args := make([]Value, argsObj.Length())
for i := range args {
args[i] = argsObj.Index(i)
}
f(args)
}
}
}
// sleepUntilCallback is defined in the runtime package
func sleepUntilCallback()