diff options
Diffstat (limited to 'libgo/go/syscall/js/callback.go')
-rw-r--r-- | libgo/go/syscall/js/callback.go | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/libgo/go/syscall/js/callback.go b/libgo/go/syscall/js/callback.go new file mode 100644 index 00000000000..9d573074cbd --- /dev/null +++ b/libgo/go/syscall/js/callback.go @@ -0,0 +1,122 @@ +// 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() |