Skip to content

fix(v3): guarantee event dispatch order under rapid Emit() bursts#5757

Open
DevLumuz wants to merge 2 commits into
wailsapp:masterfrom
DevLumuz:fix-event-dispatch-order
Open

fix(v3): guarantee event dispatch order under rapid Emit() bursts#5757
DevLumuz wants to merge 2 commits into
wailsapp:masterfrom
DevLumuz:fix-event-dispatch-order

Conversation

@DevLumuz

@DevLumuz DevLumuz commented Jul 6, 2026

Copy link
Copy Markdown

Problem

Events emitted rapidly in succession (e.g. a Go backend streaming many small
events back-to-back) can reach the frontend out of order, or with visible
interleaving. Two independent causes:

  1. EventProcessor.Emit (v3/pkg/application/events.go) spawns two bare
    goroutines per call for dispatch (one for Go listeners, one for windows).
    Go gives no ordering guarantee between separately spawned goroutines, so
    two events emitted back-to-back can have their window-dispatch (and
    therefore their ExecJS call) run in either order.

  2. linux/gtk3 execJS (v3/pkg/application/linux_cgo_gtk3.go) calls
    webkit_web_view_evaluate_javascript with callback=NULL ("fire and
    forget"). WebKit confirms the request was accepted, not that it
    finished executing — WebKit evaluates scripts asynchronously via its
    WebProcess, and without a completion callback nothing guarantees that
    rapid, back-to-back evaluate_javascript calls execute in the same order
    they were dispatched.

Fix

  1. Replaced the two bare goroutines in Emit() with one buffered, ordered
    worker per dispatch target (listeners, windows). Emit() stays
    non-blocking under normal load (buffer of 4096, well above realistic
    bursts) — dispatch is now strict FIFO per target, and also bounded,
    unlike the previous unbounded goroutine-per-Emit() fan-out.

  2. execJS now waits for the real WebKit completion callback before
    returning, using the same nested main-loop-iteration technique already
    used by clipboard_get_text_sync (linux_cgo.c) to make an async
    GLib/GTK call appear synchronous from the calling C code — blocking on a
    condition variable instead would freeze the very loop that has to keep
    running for the completion callback to ever be delivered.

Also fixed two pre-existing latent races in events_test.go's mock notifier
and counter that -race now catches reliably against the new, more
deterministic dispatch timing (they were always technically possible, just
rarely triggered by the old bare-goroutine timing).

Verification

Isolated repro: 500 Emit() calls in a tight loop, no app-specific code
involved, checking arrival order in the page.

Before After
Out of order 478/500 (95.6%) 0/500
Wall-clock to issue all 500 Emit() calls 2.16ms 244µs (faster — Emit() no longer spawns two goroutines per call)

go test -race ./pkg/application/... passes clean.

Summary by CodeRabbit

  • Bug Fixes
    • Improved event delivery stability by switching to queued, continuously running dispatch for listener and window events, reducing missed or inconsistent callbacks.
    • Improved Linux JavaScript execution reliability by waiting for WebKit to finish evaluating scripts before continuing.
  • Tests
    • Updated event-dispatch test coverage to be race-safe under concurrent callback execution and reset behavior.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 777d401d-4c7a-4d9e-a623-511b07ffac70

📥 Commits

Reviewing files that changed from the base of the PR and between 5f00091 and 6825ea2.

📒 Files selected for processing (3)
  • v3/pkg/application/events.go
  • v3/pkg/application/events_test.go
  • v3/pkg/application/linux_cgo_gtk3.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • v3/pkg/application/linux_cgo_gtk3.go
  • v3/pkg/application/events_test.go
  • v3/pkg/application/events.go

Walkthrough

Event dispatch now uses buffered queues and persistent workers, with concurrency-safe test updates. On Linux GTK3, JavaScript execution now waits for WebKit completion through a new synchronous C helper.

Changes

Queue-based Event Dispatch

Layer / File(s) Summary
Dispatch queue and worker implementation
v3/pkg/application/events.go
Adds dispatchQueueSize, listenerQueue/windowQueue, persistent worker goroutines with panic recovery, and updates Emit to enqueue events instead of spawning goroutines.
Concurrency-safe test updates
v3/pkg/application/events_test.go
Adds mutex protection to mockNotifier and switches Test_EventsOnMultiple to atomic.Int32 with atomic reads and writes.

Linux Synchronous JavaScript Execution

Layer / File(s) Summary
Synchronous JS evaluation helper and wiring
v3/pkg/application/linux_cgo_gtk3.go
Adds execute_js_sync and on_evaluate_javascript_finish, then updates execJS to call the new helper instead of the direct WebKit evaluation API.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant EventProcessor
  participant ListenerWorker
  participant WindowWorker
  Caller->>EventProcessor: Emit(event)
  EventProcessor->>EventProcessor: enqueue to listenerQueue
  EventProcessor->>EventProcessor: enqueue to windowQueue
  ListenerWorker->>ListenerWorker: dispatchEventToListeners(event)
  WindowWorker->>WindowWorker: dispatchEventToWindows(event)
Loading
sequenceDiagram
  participant linuxWebviewWindow
  participant execute_js_sync
  participant WebKit
  participant GMainContext
  linuxWebviewWindow->>execute_js_sync: C.execute_js_sync(js, world)
  execute_js_sync->>WebKit: webkit_web_view_evaluate_javascript(...)
  WebKit->>execute_js_sync: on_evaluate_javascript_finish()
  execute_js_sync->>GMainContext: iterate until completion flag set
  execute_js_sync-->>linuxWebviewWindow: return after completion
Loading

Possibly related issues

Possibly related PRs

Suggested labels: Bug, go, v3, Linux

Suggested reviewers: leaanthony

Poem

A rabbit hops by queue-lined trails,
No racey paws, no flaky tales.
WebKit waits till JS is through,
Then hops returns with cleaner glue.
🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the problem, fix, and verification, but it omits required template sections like Fixes #, type of change, testing checklist, and test configuration. Add the missing template sections: link the issue with Fixes #, select the change type, complete the test checklist, and paste wails doctor output.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: fixing event ordering under rapid Emit bursts.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain modules listed in go.work or their selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
v3/pkg/application/events.go (1)

122-155: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a shutdown path for EventProcessor

NewWailsEventProcessor starts two goroutines that block forever on unclosed queues. That is fine for the app singleton, but short-lived processors in tests and benchmarks—like BenchmarkOnMultipleEvent inside b.Loop()—keep accumulating workers and skew benchmark results. Add Close/Stop and stop the dispatch loops with a done channel or by closing the queues.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/pkg/application/events.go` around lines 122 - 155, Add a shutdown path to
EventProcessor so the goroutines started by NewWailsEventProcessor do not leak
for short-lived uses. Introduce a Close/Stop method on EventProcessor, track a
done signal or close listenerQueue/windowQueue safely, and make
runListenerDispatch and runWindowDispatch exit when shutdown is requested.
Update NewWailsEventProcessor and the dispatch loops to use the new lifecycle
control so tests and benchmarks like BenchmarkOnMultipleEvent can stop workers
cleanly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@v3/pkg/application/events.go`:
- Around line 200-201: `EventManager.Emit` is currently blocked by synchronous
sends to both `listenerQueue` and `windowQueue`, so a slow window delivery path
can stall callers. Update the `Emit` flow in `EventManager` to avoid blocking on
`windowQueue` backpressure by using a non-blocking enqueue strategy with
overflow handling, such as logging and dropping or otherwise degrading safely,
while keeping `listenerQueue` behavior consistent with the intended delivery
model. Refer to `Emit`, `listenerQueue`, `windowQueue`, and the
`EventIPCTransport.DispatchWailsEvent`/`WebviewWindow.DispatchWailsEvent` path
when making the change.

In `@v3/pkg/application/linux_cgo_gtk3.go`:
- Around line 124-139: `execute_js_sync` can still allow out-of-order `execJS`
completion because it blocks with a nested main loop while another
`webkit_web_view_evaluate_javascript` is already in flight. Update the
`execute_js_sync`/`on_evaluate_javascript_finish` flow so JS executions are
serialized through a single queue or by chaining the next request from the
previous callback. Make sure only one `WebKitWebView` evaluation is active at a
time, and keep the existing synchronous behavior at the `execute_js_sync` call
site.

---

Nitpick comments:
In `@v3/pkg/application/events.go`:
- Around line 122-155: Add a shutdown path to EventProcessor so the goroutines
started by NewWailsEventProcessor do not leak for short-lived uses. Introduce a
Close/Stop method on EventProcessor, track a done signal or close
listenerQueue/windowQueue safely, and make runListenerDispatch and
runWindowDispatch exit when shutdown is requested. Update NewWailsEventProcessor
and the dispatch loops to use the new lifecycle control so tests and benchmarks
like BenchmarkOnMultipleEvent can stop workers cleanly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7346d191-d362-4292-a4a9-bec0db30deff

📥 Commits

Reviewing files that changed from the base of the PR and between a84a490 and 5f00091.

📒 Files selected for processing (3)
  • v3/pkg/application/events.go
  • v3/pkg/application/events_test.go
  • v3/pkg/application/linux_cgo_gtk3.go

Comment thread v3/pkg/application/events.go
Comment thread v3/pkg/application/linux_cgo_gtk3.go
Two related fixes for Wails events reaching the page out of order when
Emit() is called rapidly in succession (e.g. streaming many small events
back-to-back):

1. EventProcessor.Emit spawned two bare goroutines per call for dispatch
   (listeners, windows), with no ordering guarantee between goroutines from
   successive calls. Replaced with one buffered, ordered worker per target —
   Emit() stays non-blocking, dispatch is now strict FIFO. Also bounds
   in-flight dispatch (dispatchQueueSize), unlike the old unbounded fan-out.

2. linux/gtk3 execJS passed callback=NULL to
   webkit_web_view_evaluate_javascript ("fire and forget"): WebKit confirms
   the request was accepted, not that it finished executing, so nothing
   guaranteed successive evaluate_javascript calls ran in the order they
   were dispatched. execJS now waits for the real completion callback, via
   the same nested-main-loop technique already used by
   clipboard_get_text_sync (linux_cgo.c).

Verified with an isolated repro (500 Emit() calls in a tight loop, no AI/LLM
or app-specific code involved): before, 478/500 (95.6%) arrived out of
order in the page. After, 0/500. Also faster to issue: 2.16ms -> 244µs total
for the 500 calls, since Emit() no longer spawns two goroutines per call.

go test -race ./pkg/application/... passes clean (also fixed two
pre-existing latent races in events_test.go's mock that -race now catches
reliably against the new, more deterministic dispatch timing).
@DevLumuz
DevLumuz force-pushed the fix-event-dispatch-order branch from 5f00091 to 6825ea2 Compare July 6, 2026 00:49
@heixiaoma

Copy link
Copy Markdown

That’s awesome! Right now I’m outputting SSE data to the frontend and relying on sleep delays to maintain the order. With this solution, everything will work perfectly without any issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants