/
callback.go
122 lines (105 loc) · 3.5 KB
/
callback.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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()