1// Copyright 2018 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5//go:build js && wasm
6
7package js
8
9import "sync"
10
11var (
12	funcsMu    sync.Mutex
13	funcs             = make(map[uint32]func(Value, []Value) any)
14	nextFuncID uint32 = 1
15)
16
17// Func is a wrapped Go function to be called by JavaScript.
18type Func struct {
19	Value // the JavaScript function that invokes the Go function
20	id    uint32
21}
22
23// FuncOf returns a function to be used by JavaScript.
24//
25// The Go function fn is called with the value of JavaScript's "this" keyword and the
26// arguments of the invocation. The return value of the invocation is
27// the result of the Go function mapped back to JavaScript according to ValueOf.
28//
29// Invoking the wrapped Go function from JavaScript will
30// pause the event loop and spawn a new goroutine.
31// Other wrapped functions which are triggered during a call from Go to JavaScript
32// get executed on the same goroutine.
33//
34// As a consequence, if one wrapped function blocks, JavaScript's event loop
35// is blocked until that function returns. Hence, calling any async JavaScript
36// API, which requires the event loop, like fetch (http.Client), will cause an
37// immediate deadlock. Therefore a blocking function should explicitly start a
38// new goroutine.
39//
40// Func.Release must be called to free up resources when the function will not be invoked any more.
41func FuncOf(fn func(this Value, args []Value) any) Func {
42	funcsMu.Lock()
43	id := nextFuncID
44	nextFuncID++
45	funcs[id] = fn
46	funcsMu.Unlock()
47	return Func{
48		id:    id,
49		Value: jsGo.Call("_makeFuncWrapper", id),
50	}
51}
52
53// Release frees up resources allocated for the function.
54// The function must not be invoked after calling Release.
55// It is allowed to call Release while the function is still running.
56func (c Func) Release() {
57	funcsMu.Lock()
58	delete(funcs, c.id)
59	funcsMu.Unlock()
60}
61
62// setEventHandler is defined in the runtime package.
63func setEventHandler(fn func() bool)
64
65func init() {
66	setEventHandler(handleEvent)
67}
68
69// handleEvent retrieves the pending event (window._pendingEvent) and calls the js.Func on it.
70// It returns true if an event was handled.
71func handleEvent() bool {
72	// Retrieve the event from js
73	cb := jsGo.Get("_pendingEvent")
74	if cb.IsNull() {
75		return false
76	}
77	jsGo.Set("_pendingEvent", Null())
78
79	id := uint32(cb.Get("id").Int())
80	if id == 0 { // zero indicates deadlock
81		select {}
82	}
83
84	// Retrieve the associated js.Func
85	funcsMu.Lock()
86	f, ok := funcs[id]
87	funcsMu.Unlock()
88	if !ok {
89		Global().Get("console").Call("error", "call to released function")
90		return true
91	}
92
93	// Call the js.Func with arguments
94	this := cb.Get("this")
95	argsObj := cb.Get("args")
96	args := make([]Value, argsObj.Length())
97	for i := range args {
98		args[i] = argsObj.Index(i)
99	}
100	result := f(this, args)
101
102	// Return the result to js
103	cb.Set("result", result)
104	return true
105}
106