diff options
Diffstat (limited to 'src/syscall/js/callback.go')
| -rw-r--r-- | src/syscall/js/callback.go | 145 |
1 files changed, 145 insertions, 0 deletions
diff --git a/src/syscall/js/callback.go b/src/syscall/js/callback.go new file mode 100644 index 0000000000..2c693240fa --- /dev/null +++ b/src/syscall/js/callback.go @@ -0,0 +1,145 @@ +// 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() + +var makeCallbackHelper = Global.Call("eval", ` + (function(id, pendingCallbacks, resolveCallbackPromise) { + return function() { + pendingCallbacks.push({ id: id, args: arguments }); + resolveCallbackPromise(); + }; + }) +`) + +var makeEventCallbackHelper = Global.Call("eval", ` + (function(preventDefault, stopPropagation, stopImmediatePropagation, fn) { + return function(event) { + if (preventDefault) { + event.preventDefault(); + } + if (stopPropagation) { + event.stopPropagation(); + } + if (stopImmediatePropagation) { + event.stopImmediatePropagation(); + } + fn(event); + }; + }) +`) + +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. +// A Callback can be passed to functions of this package that accept interface{}, +// for example Value.Set and Value.Call. +type Callback struct { + id uint32 + enqueueFn Value // the JavaScript function that queues the callback for execution +} + +// NewCallback returns a wrapped callback function. It can be passed to functions of this package +// that accept interface{}, for example Value.Set and Value.Call. +// +// 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.Close 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{ + id: id, + enqueueFn: makeCallbackHelper.Invoke(id, pendingCallbacks, resolveCallbackPromise), + } +} + +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{ + id: c.id, + enqueueFn: makeEventCallbackHelper.Invoke( + flags&PreventDefault != 0, + flags&StopPropagation != 0, + flags&StopImmediatePropagation != 0, + c, + ), + } +} + +func (c Callback) Close() { + callbacksMu.Lock() + delete(callbacks, c.id) + callbacksMu.Unlock() +} + +var callbackLoopOnce sync.Once + +func callbackLoop() { + for { + 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() |
