| // 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() |