Skip to content

feat: tray event client#639

Merged
wolf31o2 merged 1 commit intomainfrom
feature/tray-event-client
Mar 2, 2026
Merged

feat: tray event client#639
wolf31o2 merged 1 commit intomainfrom
feature/tray-event-client

Conversation

@wolf31o2
Copy link
Copy Markdown
Member

@wolf31o2 wolf31o2 commented Mar 1, 2026


Summary by cubic

Adds a tray EventClient that streams events from adder’s /events WebSocket, auto-reconnects, and exposes connection status. Includes a thread-safe StatusTracker and optional event type filtering.

  • New Features
    • Connects to the /events endpoint and emits parsed events on a buffered channel.
    • Automatic reconnect with exponential backoff (capped at 30s).
    • StatusTracker with starting, connected, reconnecting, stopped, and error states, plus change observer.
    • Non-blocking delivery; Stop closes the connection and events channel.
    • Server-side type filtering via a ?types=... query.
    • Tests cover event flow, reconnection, type filter, double-start error, stop without start, and shutdown.

Written for commit e439225. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • WebSocket event streaming with automatic reconnection (exponential backoff) and non-blocking delivery.
    • Thread-safe status tracking with states: Stopped, Starting, Connected, Reconnecting, Error.
    • Optional event-type filtering and status-change callbacks for real-time monitoring.
    • Safe start/stop lifecycle and graceful shutdown handling.
  • Tests

    • Comprehensive tests covering event delivery, reconnection, filtering, lifecycle, and status-tracker behavior.

@wolf31o2 wolf31o2 requested a review from a team as a code owner March 1, 2026 17:13
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f2f1ef and e439225.

📒 Files selected for processing (4)
  • tray/events.go
  • tray/events_test.go
  • tray/status.go
  • tray/status_test.go

📝 Walkthrough

Walkthrough

Adds an EventClient that connects to an /events WebSocket endpoint, parses incoming JSON into event.Event values, delivers them on a buffered channel, and automatically reconnects with exponential backoff. Provides options for event-type filtering and optional StatusTracker integration. Introduces a concurrency-safe StatusTracker with observer callbacks and stringified status constants. Includes comprehensive tests for event delivery, reconnection, filtering, lifecycle behavior, and status tracking.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: tray event client' clearly and concisely summarizes the main change: introducing a new EventClient for the tray package that connects to WebSocket events.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/tray-event-client

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 and usage tips.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tray/events_test.go">

<violation number="1" location="tray/events_test.go:173">
P2: `receivedPath` is written from the HTTP handler goroutine and read later in the test without synchronization, which will trigger a data race under the race detector. Consider protecting it with a mutex or using a channel to pass the value.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

var receivedPath string

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedPath = r.URL.RawQuery
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 1, 2026

Choose a reason for hiding this comment

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

P2: receivedPath is written from the HTTP handler goroutine and read later in the test without synchronization, which will trigger a data race under the race detector. Consider protecting it with a mutex or using a channel to pass the value.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tray/events_test.go, line 173:

<comment>`receivedPath` is written from the HTTP handler goroutine and read later in the test without synchronization, which will trigger a data race under the race detector. Consider protecting it with a mutex or using a channel to pass the value.</comment>

<file context>
@@ -0,0 +1,288 @@
+	var receivedPath string
+
+	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		receivedPath = r.URL.RawQuery
+		conn, err := testUpgrader.Upgrade(w, r, nil)
+		if err != nil {
</file context>
Fix with Cubic

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
tray/events.go (2)

59-62: Defensively copy types to avoid external mutation side effects.

The option currently stores the caller’s slice reference directly. Copying avoids accidental post-construction mutation and race-prone behavior.

💡 Suggested refactor
 func WithEventTypes(types []string) EventClientOption {
+	typeFilter := append([]string(nil), types...)
 	return func(c *EventClient) {
-		c.typeFilter = types
+		c.typeFilter = typeFilter
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tray/events.go` around lines 59 - 62, WithEventTypes currently stores the
caller's slice reference into EventClient.typeFilter, which permits external
mutation; modify WithEventTypes to defensively copy the input slice before
assigning (e.g., allocate a new slice with make/append or copy) so
EventClient.typeFilter holds an independent slice and cannot be mutated by
callers after construction.

248-253: Add observability for dropped events under backpressure.

Dropping on full channel is valid, but currently silent. A counter or debug log here would make pressure and loss visible in production.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tray/events.go` around lines 248 - 253, The non-blocking send to c.events
silently drops events when the channel is full; update the default branch to
record this loss by incrementing an observability metric and emitting a
debug/info log. Specifically, in the select default path where the code
currently comments "// channel full, drop event", call your metrics counter
(e.g., EventsDropped.Inc or a Prometheus counter) and log the dropped event with
the component logger (e.g., c.logger.Debugf or similar) including identifying
details from evt so backpressure and loss are visible in production.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tray/events_test.go`:
- Around line 170-174: The test currently writes to receivedPath in the HTTP
handler goroutine and reads it from the test goroutine, causing race-detector
failures; fix by synchronizing access — e.g., have the handler send the path
string over a channel (or store it in an atomic.Value or protect with a
sync.Mutex) instead of writing a shared variable, and have the test goroutine
receive/read from that channel/atomic/mutex-protected variable when asserting;
update the handler and the test code that reads receivedPath (references:
receivedPath variable, handler http.HandlerFunc, and testUpgrader usage) to use
this synchronization.

In `@tray/events.go`:
- Around line 67-70: WithEventStatusTracker should guard against a nil
StatusTracker: change the returned option to check if t == nil and, in that
case, return a no-op option (e.g., func(c *EventClient) {}) instead of setting
c.status to nil; otherwise assign c.status = t. This prevents
Start()/connectLoop() from panicking when they call c.status.Set(...) while
preserving existing behavior when a valid StatusTracker is provided.

In `@tray/status.go`:
- Around line 72-84: The observer callback in StatusTracker.Set is invoked
inline (obs(s)) which can block callers; change it to dispatch the callback
asynchronously by launching it in a new goroutine (e.g., go func() { ... }()),
capturing the copied obs and s variables already taken from StatusTracker, and
protect the goroutine by recovering from panics so a faulty observer cannot
crash the process; update the Set method (referenced as StatusTracker.Set and
the local obs variable) to perform non-blocking, panic-safe invocation of the
observer.

---

Nitpick comments:
In `@tray/events.go`:
- Around line 59-62: WithEventTypes currently stores the caller's slice
reference into EventClient.typeFilter, which permits external mutation; modify
WithEventTypes to defensively copy the input slice before assigning (e.g.,
allocate a new slice with make/append or copy) so EventClient.typeFilter holds
an independent slice and cannot be mutated by callers after construction.
- Around line 248-253: The non-blocking send to c.events silently drops events
when the channel is full; update the default branch to record this loss by
incrementing an observability metric and emitting a debug/info log.
Specifically, in the select default path where the code currently comments "//
channel full, drop event", call your metrics counter (e.g., EventsDropped.Inc or
a Prometheus counter) and log the dropped event with the component logger (e.g.,
c.logger.Debugf or similar) including identifying details from evt so
backpressure and loss are visible in production.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17676cb and 7f2f1ef.

📒 Files selected for processing (4)
  • tray/events.go
  • tray/events_test.go
  • tray/status.go
  • tray/status_test.go

Comment on lines +170 to +174
var receivedPath string

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedPath = r.URL.RawQuery
conn, err := testUpgrader.Upgrade(w, r, nil)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Synchronize receivedPath access to avoid race-detector failures.

receivedPath is written from the server handler goroutine and read from the test goroutine without synchronization.

💡 Suggested fix
 func TestEventClient_TypeFilter(t *testing.T) {
 	// Verify that the type filter is sent as a query parameter
-	var receivedPath string
+	var (
+		receivedPath string
+		pathMu       sync.Mutex
+	)

 	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		receivedPath = r.URL.RawQuery
+		pathMu.Lock()
+		receivedPath = r.URL.RawQuery
+		pathMu.Unlock()
 		conn, err := testUpgrader.Upgrade(w, r, nil)
 		if err != nil {
 			return
@@
 	waitForStatus(t, client.status, StatusConnected, 2*time.Second)

-	assert.Contains(t, receivedPath, "types=")
-	assert.Contains(t, receivedPath, "input.block")
-	assert.Contains(t, receivedPath, "input.transaction")
+	pathMu.Lock()
+	gotPath := receivedPath
+	pathMu.Unlock()
+	assert.Contains(t, gotPath, "types=")
+	assert.Contains(t, gotPath, "input.block")
+	assert.Contains(t, gotPath, "input.transaction")
 }

Also applies to: 201-203

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tray/events_test.go` around lines 170 - 174, The test currently writes to
receivedPath in the HTTP handler goroutine and reads it from the test goroutine,
causing race-detector failures; fix by synchronizing access — e.g., have the
handler send the path string over a channel (or store it in an atomic.Value or
protect with a sync.Mutex) instead of writing a shared variable, and have the
test goroutine receive/read from that channel/atomic/mutex-protected variable
when asserting; update the handler and the test code that reads receivedPath
(references: receivedPath variable, handler http.HandlerFunc, and testUpgrader
usage) to use this synchronization.

Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
@wolf31o2 wolf31o2 force-pushed the feature/tray-event-client branch from 7f2f1ef to e439225 Compare March 2, 2026 13:30
@wolf31o2
Copy link
Copy Markdown
Member Author

wolf31o2 commented Mar 2, 2026

@cubic-dev-ai review

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Mar 2, 2026

@cubic-dev-ai review

@wolf31o2 I have started the AI code review. It will take a few minutes to complete.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 4 files

@wolf31o2 wolf31o2 merged commit 1e90d71 into main Mar 2, 2026
11 checks passed
@wolf31o2 wolf31o2 deleted the feature/tray-event-client branch March 2, 2026 19:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants