// Package sse provides Server-Sent Events infrastructure for real-time updates. // Includes event types, a central broadcaster, and an HTTP handler. package sse import ( "encoding/json" "time" ) // EventType identifies the category of an SSE event. type EventType string const ( EventPrinterStatus EventType = "printer.status" EventJobStarted EventType = "job.started" EventJobCompleted EventType = "job.completed" EventFilamentLow EventType = "filament.low" ) // Event is a JSON-serializable SSE event pushed through the broadcaster. type Event struct { Type EventType `json:"type"` Payload json.RawMessage `json:"payload"` Timestamp time.Time `json:"timestamp"` } // PrinterStatusPayload carries printer online/offline/printing state. type PrinterStatusPayload struct { PrinterID int `json:"printer_id"` PrinterName string `json:"printer_name"` Status string `json:"status"` // "online", "offline", "printing" } // JobStartedPayload carries initial print job info. type JobStartedPayload struct { JobID int `json:"job_id"` JobName string `json:"job_name"` PrinterID int `json:"printer_id"` SpoolID *int `json:"spool_id,omitempty"` } // JobCompletedPayload carries final print job data including usage. type JobCompletedPayload struct { JobID int `json:"job_id"` JobName string `json:"job_name"` PrinterID int `json:"printer_id"` DurationSeconds *int `json:"duration_seconds,omitempty"` TotalGramsUsed *float64 `json:"total_grams_used,omitempty"` TotalCostUSD *float64 `json:"total_cost_usd,omitempty"` } // FilamentLowPayload alerts that a spool is below its threshold. type FilamentLowPayload struct { SpoolID int `json:"spool_id"` SpoolName string `json:"spool_name"` RemainingGrams int `json:"remaining_grams"` ThresholdGrams int `json:"threshold_grams"` } // NewEvent creates an Event with the current timestamp from a typed payload. func NewEvent(eventType EventType, payload any) (Event, error) { raw, err := json.Marshal(payload) if err != nil { return Event{}, err } return Event{ Type: eventType, Payload: raw, Timestamp: time.Now().UTC(), }, nil } // MustEvent creates an Event and panics on marshal failure (for use with // known-good payloads in tests and internal wiring). func MustEvent(eventType EventType, payload any) Event { ev, err := NewEvent(eventType, payload) if err != nil { panic("sse.MustEvent: failed to marshal payload: " + err.Error()) } return ev } // toSSE formats this Event as a standard SSE message string ready to be // written to a response writer. The format is: // // event: // data: // func (e Event) toSSE() string { data, _ := json.Marshal(e) return "event: " + string(e.Type) + "\n" + "data: " + string(data) + "\n\n" }