All checks were successful
Dev Build / build-test (pull_request) Successful in 2m9s
93 lines
2.8 KiB
Go
93 lines
2.8 KiB
Go
// 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: <type>
|
|
// data: <json>
|
|
//
|
|
func (e Event) toSSE() string {
|
|
data, _ := json.Marshal(e)
|
|
return "event: " + string(e.Type) + "\n" + "data: " + string(data) + "\n\n"
|
|
}
|