feat(#392): publish track-like + request-status events to SSE bus
Slice 2 of #392 — wires the first producers onto the bus that slice 1 built. After this commit, an SSE subscriber sees real events fire: - track.liked / track.unliked when the user toggles the heart on a track (handleLikeTrack / handleUnlikeTrack). Album + artist like events intentionally deferred — they're symmetric trivial follow-ups but the operator's primary like surface is tracks. - request.status_changed when a Lidarr request is created, cancelled, approved, or rejected. Auto-approve will fire twice (pending then approved) in rapid succession, which is semantically correct; client invalidation handles that fine. Events are user-scoped via row.UserID so admin approve/reject route to the requester, not the admin acting. Helpers live in events_publish.go so the wire shape (kind names, payload keys) stays in one place — future producers in slice 3 reuse the same pattern. events_publish.go is no-op when h.eventbus is nil so tests that construct handlers without a bus continue to pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -125,6 +125,7 @@ func (h *handlers) handleApproveRequest(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
h.publishRequestStatusChanged(row)
|
||||
writeJSON(w, http.StatusOK, requestViewFrom(row))
|
||||
}
|
||||
|
||||
@@ -164,5 +165,6 @@ func (h *handlers) handleRejectRequest(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
h.publishRequestStatusChanged(row)
|
||||
writeJSON(w, http.StatusOK, requestViewFrom(row))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Small helpers for posting live events to the SSE bus (#392). Producers
|
||||
// call these from their handler success paths so the wire shape stays in
|
||||
// one place. Each helper is a no-op when h.eventbus is nil (older callers
|
||||
// or tests that don't construct a bus); production always has one.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/eventbus"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
// publishLikeEvent broadcasts a track/album/artist like or unlike. The
|
||||
// event is scoped to the user (so only their own clients invalidate); the
|
||||
// payload includes the entity_type and entity_id so the dispatcher can
|
||||
// invalidate the right query keys.
|
||||
func (h *handlers) publishLikeEvent(userID, entityID pgtype.UUID, entityType string, liked bool) {
|
||||
if h.eventbus == nil {
|
||||
return
|
||||
}
|
||||
kind := "track.liked"
|
||||
if !liked {
|
||||
kind = "track.unliked"
|
||||
}
|
||||
if entityType == "album" {
|
||||
if liked {
|
||||
kind = "album.liked"
|
||||
} else {
|
||||
kind = "album.unliked"
|
||||
}
|
||||
}
|
||||
if entityType == "artist" {
|
||||
if liked {
|
||||
kind = "artist.liked"
|
||||
} else {
|
||||
kind = "artist.unliked"
|
||||
}
|
||||
}
|
||||
h.eventbus.Publish(eventbus.Event{
|
||||
Kind: kind,
|
||||
UserID: uuidToString(userID),
|
||||
Data: map[string]any{
|
||||
"entity_type": entityType,
|
||||
"entity_id": uuidToString(entityID),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// publishRequestStatusChanged broadcasts a Lidarr request status flip to
|
||||
// the request's original requester so their /requests page reflects the
|
||||
// new state without manual refresh. Admin actors (approve / reject) still
|
||||
// route to the requester's user_id, not the admin's — the original user
|
||||
// is who needs to see the update.
|
||||
func (h *handlers) publishRequestStatusChanged(row dbq.LidarrRequest) {
|
||||
if h.eventbus == nil {
|
||||
return
|
||||
}
|
||||
h.eventbus.Publish(eventbus.Event{
|
||||
Kind: "request.status_changed",
|
||||
UserID: uuidToString(row.UserID),
|
||||
Data: map[string]any{
|
||||
"request_id": uuidToString(row.ID),
|
||||
"status": string(row.Status),
|
||||
"kind": string(row.Kind),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -60,6 +60,7 @@ func (h *handlers) handleLikeTrack(w http.ResponseWriter, r *http.Request) {
|
||||
_ = playevents.CaptureContextualLikeIfPlaying(r.Context(), q, user.ID, id, h.logger)
|
||||
}
|
||||
h.logLikeChange(r, syncpkg.EntityLikeTrack, user.ID, id, syncpkg.OpUpsert)
|
||||
h.publishLikeEvent(user.ID, id, "track", true)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -83,6 +84,7 @@ func (h *handlers) handleUnlikeTrack(w http.ResponseWriter, r *http.Request) {
|
||||
// Don't fail the response — soft-delete is best-effort.
|
||||
}
|
||||
h.logLikeChange(r, syncpkg.EntityLikeTrack, user.ID, id, syncpkg.OpDelete)
|
||||
h.publishLikeEvent(user.ID, id, "track", false)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@ func (h *handlers) handleCreateRequest(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
h.publishRequestStatusChanged(row)
|
||||
writeJSON(w, http.StatusCreated, requestViewFrom(row))
|
||||
}
|
||||
|
||||
@@ -254,5 +255,6 @@ func (h *handlers) handleCancelRequest(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
h.publishRequestStatusChanged(row)
|
||||
writeJSON(w, http.StatusOK, requestViewFrom(row))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user