Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 43 additions & 9 deletions v3/pkg/application/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ type eventListener struct {
delete bool // Flag to indicate that this listener should be deleted
}

// dispatchQueueSize buffers EventProcessor's dispatch queues (see Emit) well
// above realistic emit bursts, so Emit() stays non-blocking in practice. Also
// bounds in-flight dispatch, unlike the old unbounded goroutine-per-Emit.
const dispatchQueueSize = 4096

// EventProcessor handles custom events
type EventProcessor struct {
// Go event listeners
Expand All @@ -104,13 +109,48 @@ type EventProcessor struct {
dispatchEventToWindows func(*CustomEvent)
hooks map[string][]*hook
hookLock sync.RWMutex

// Feed the two single-consumer dispatch workers below. Emit() used to hand
// each dispatch to its own bare goroutine, and Go gives no ordering
// guarantee between them — two events emitted back-to-back could reach
// ExecJS in either order. A single ordered worker per queue fixes that,
// while keeping listeners and windows independent of each other.
listenerQueue chan *CustomEvent
windowQueue chan *CustomEvent
}

func NewWailsEventProcessor(dispatchEventToWindows func(*CustomEvent)) *EventProcessor {
return &EventProcessor{
e := &EventProcessor{
listeners: make(map[string][]*eventListener),
dispatchEventToWindows: dispatchEventToWindows,
hooks: make(map[string][]*hook),
listenerQueue: make(chan *CustomEvent, dispatchQueueSize),
windowQueue: make(chan *CustomEvent, dispatchQueueSize),
}
go e.runListenerDispatch()
go e.runWindowDispatch()
return e
}

// runListenerDispatch drains listenerQueue in order for the process lifetime.
// Each event gets its own recover so one panic can't kill the worker and
// stop delivery to everything emitted after it.
func (e *EventProcessor) runListenerDispatch() {
for event := range e.listenerQueue {
func() {
defer handlePanic()
e.dispatchEventToListeners(event)
}()
}
}

// runWindowDispatch: same as runListenerDispatch, for the window/IPC side.
func (e *EventProcessor) runWindowDispatch() {
for event := range e.windowQueue {
func() {
defer handlePanic()
e.dispatchEventToWindows(event)
}()
}
}

Expand Down Expand Up @@ -157,14 +197,8 @@ func (e *EventProcessor) Emit(thisEvent *CustomEvent) error {
}
}

go func() {
defer handlePanic()
e.dispatchEventToListeners(thisEvent)
}()
go func() {
defer handlePanic()
e.dispatchEventToWindows(thisEvent)
}()
e.listenerQueue <- thisEvent
e.windowQueue <- thisEvent
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return nil
}
Expand Down
19 changes: 14 additions & 5 deletions v3/pkg/application/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package application_test

import (
"sync"
"sync/atomic"
"testing"

"github.com/wailsapp/wails/v3/pkg/application"
Expand All @@ -10,14 +11,21 @@ import (
)

type mockNotifier struct {
mu sync.Mutex
Events []*application.CustomEvent
}

// mu: dispatch now runs on a persistent worker (see runWindowDispatch), so it
// can race with a test's Reset() right after Emit(). Mutex makes that safe.
func (m *mockNotifier) dispatchEventToWindows(event *application.CustomEvent) {
m.mu.Lock()
defer m.mu.Unlock()
m.Events = append(m.Events, event)
}

func (m *mockNotifier) Reset() {
m.mu.Lock()
defer m.mu.Unlock()
m.Events = []*application.CustomEvent{}
}

Expand Down Expand Up @@ -99,12 +107,13 @@ func Test_EventsOnMultiple(t *testing.T) {

// Test OnApplicationEvent
eventName := "test"
counter := 0
// atomic: 2 of the 3 Emit()s below can invoke this callback concurrently.
var counter atomic.Int32
var wg sync.WaitGroup
wg.Add(2)
unregisterFn := eventProcessor.OnMultiple(eventName, func(event *application.CustomEvent) {
// This is called in a goroutine
counter++
counter.Add(1)
wg.Done()
}, 2)
_ = eventProcessor.Emit(&application.CustomEvent{
Expand All @@ -120,16 +129,16 @@ func Test_EventsOnMultiple(t *testing.T) {
Data: "test payload",
})
wg.Wait()
i.Equal(2, counter)
i.Equal(int32(2), counter.Load())

// Unregister
notifier.Reset()
unregisterFn()
counter = 0
counter.Store(0)
_ = eventProcessor.Emit(&application.CustomEvent{
Name: "test",
Data: "test payload",
})
i.Equal(0, counter)
i.Equal(int32(0), counter.Load())

}
40 changes: 32 additions & 8 deletions v3/pkg/application/linux_cgo_gtk3.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,37 @@ static WebKitWebView* webkit_web_view(GtkWidget *webview) {
return WEBKIT_WEB_VIEW(webview);
}

// Discards the result/error; execute_js_sync's callers never need one.
static void on_evaluate_javascript_finish(GObject *source, GAsyncResult *result, gpointer user_data) {
gboolean *done = (gboolean *)user_data;
GError *error = NULL;
JSCValue *value = webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(source), result, &error);
if (error != NULL) {
g_error_free(error);
}
if (value != NULL) {
g_object_unref(value);
}
*done = TRUE;
}

// Blocks until WebKit confirms `script` actually finished — callback=NULL
// (the old behavior) only confirms WebKit accepted the request, not that
// back-to-back calls execute in the order they were issued. Same nested
// main-loop technique as clipboard_get_text_sync (linux_cgo.c): blocking on
// a condvar instead would freeze the loop our own completion callback needs
// to run on. `done` is stack-local, so concurrent calls stay independent.
static void execute_js_sync(WebKitWebView *web_view, const char *script, gssize length,
const char *world_name, const char *source_uri) {
gboolean done = FALSE;
webkit_web_view_evaluate_javascript(web_view, script, length, world_name, source_uri,
NULL, on_evaluate_javascript_finish, &done);
GMainContext *ctx = g_main_context_default();
while (!done) {
g_main_context_iteration(ctx, TRUE);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Wrapper for the WEBKIT_IS_USER_MEDIA_PERMISSION_REQUEST macro
static int is_user_media_permission_request(WebKitPermissionRequest *request) {
return WEBKIT_IS_USER_MEDIA_PERMISSION_REQUEST(request) ? 1 : 0;
Expand Down Expand Up @@ -1151,14 +1182,7 @@ func (w *linuxWebviewWindow) disableDND() {
func (w *linuxWebviewWindow) execJS(js string) {
InvokeAsync(func() {
value := C.CString(js)
C.webkit_web_view_evaluate_javascript(w.webKitWebView(),
value,
C.long(len(js)),
nil,
emptyWorldName,
nil,
nil,
nil)
C.execute_js_sync(w.webKitWebView(), value, C.long(len(js)), nil, emptyWorldName)
C.free(unsafe.Pointer(value))
})
}
Expand Down
Loading