feat: sync interview content in real time
Publish shared-document and note events through RabbitMQ, stream token-scoped document updates to participants, and update connected interviewer and participant browsers through resilient SSE subscriptions. Add snapshots, heartbeats, reconnection behavior, proxy streaming configuration, privacy filtering, and end-to-end RabbitMQ coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
e4fee06e2f
commit
c050815900
16 changed files with 438 additions and 101 deletions
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/danyel/go-guess/backend/internal/database/repository"
|
||||
"github.com/danyel/go-guess/backend/internal/eventbus"
|
||||
"github.com/danyel/go-guess/backend/internal/security"
|
||||
"github.com/danyel/go-guess/backend/internal/service"
|
||||
servicemodel "github.com/danyel/go-guess/backend/internal/service/model"
|
||||
|
|
@ -511,6 +512,20 @@ func (h *Handler) InterviewEvents(w http.ResponseWriter, r *http.Request) {
|
|||
if handleServiceError(w, r, err) {
|
||||
return
|
||||
}
|
||||
streamInterviewEvents(w, r, events)
|
||||
}
|
||||
|
||||
func (h *Handler) ParticipantMeetingEvents(w http.ResponseWriter, r *http.Request) {
|
||||
events, err := h.interviews.SubscribeParticipant(r.Context(), chi.URLParam(r, "token"))
|
||||
if handleServiceError(w, r, err) {
|
||||
return
|
||||
}
|
||||
streamInterviewEvents(w, r, events)
|
||||
}
|
||||
|
||||
func streamInterviewEvents(
|
||||
w http.ResponseWriter, r *http.Request, events <-chan eventbus.InterviewEvent,
|
||||
) {
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
internalError(w, r, errors.New("streaming is unsupported"))
|
||||
|
|
@ -519,24 +534,29 @@ func (h *Handler) InterviewEvents(w http.ResponseWriter, r *http.Request) {
|
|||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
flusher.Flush()
|
||||
heartbeat := time.NewTicker(20 * time.Second)
|
||||
defer heartbeat.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-heartbeat.C:
|
||||
if _, err := io.WriteString(w, ": keepalive\n\n"); err != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
case event, ok := <-events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if event.InterviewID != idParam(r) {
|
||||
continue
|
||||
}
|
||||
payload, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "event: interview-note\ndata: %s\n\n", payload); err != nil {
|
||||
if _, err := fmt.Fprintf(w, "event: interview-event\ndata: %s\n\n", payload); err != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ func New(h *handler.Handler, tokens security.ITokenManager, frontendURL string)
|
|||
router.Put("/api/interviews/{token}/answers/{questionId}", h.SaveAnswer)
|
||||
router.Post("/api/interviews/{token}/finish", h.FinishInterview)
|
||||
router.Get("/api/participant-meetings/{token}", h.GetParticipantMeeting)
|
||||
router.Get("/api/participant-meetings/{token}/events", h.ParticipantMeetingEvents)
|
||||
router.Group(func(protected chi.Router) {
|
||||
protected.Use(authenticate(tokens))
|
||||
protected.Get("/api/jobs", h.ListJobs)
|
||||
|
|
|
|||
|
|
@ -128,8 +128,11 @@ func (interviewStub) UpdateInbox(context.Context, uint, uint, string) (model.Sch
|
|||
func (interviewStub) ListCalendar(context.Context, uint) ([]model.ScheduledInterview, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (interviewStub) Subscribe(context.Context, uint, uint) (<-chan eventbus.InterviewNoteEvent, error) {
|
||||
return make(chan eventbus.InterviewNoteEvent), nil
|
||||
func (interviewStub) Subscribe(context.Context, uint, uint) (<-chan eventbus.InterviewEvent, error) {
|
||||
return make(chan eventbus.InterviewEvent), nil
|
||||
}
|
||||
func (interviewStub) SubscribeParticipant(context.Context, string) (<-chan eventbus.InterviewEvent, error) {
|
||||
return make(chan eventbus.InterviewEvent), nil
|
||||
}
|
||||
|
||||
type interviewCapture struct {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue