feat(docker-watcher): phase 11 - frontend embed & SSE
Embed SvelteKit static build in Go binary via go:embed. Event bus for pub/sub with deploy log, instance status, and deploy status events. SSE endpoints for real-time streaming. Frontend SSE client with exponential backoff reconnection. Makefile for build pipeline. Update Phase 12 auth plan with OAuth2/OIDC support.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// EventType identifies the kind of event being published.
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
// EventDeployLog is emitted when a new deploy log line is appended.
|
||||
EventDeployLog EventType = "deploy_log"
|
||||
|
||||
// EventInstanceStatus is emitted when an instance status changes.
|
||||
EventInstanceStatus EventType = "instance_status"
|
||||
|
||||
// EventDeployStatus is emitted when a deploy status changes.
|
||||
EventDeployStatus EventType = "deploy_status"
|
||||
)
|
||||
|
||||
// Event is a single event published on the bus.
|
||||
type Event struct {
|
||||
Type EventType `json:"type"`
|
||||
Payload any `json:"payload"`
|
||||
}
|
||||
|
||||
// DeployLogPayload is the payload for EventDeployLog events.
|
||||
type DeployLogPayload struct {
|
||||
DeployID string `json:"deploy_id"`
|
||||
Message string `json:"message"`
|
||||
Level string `json:"level"`
|
||||
}
|
||||
|
||||
// InstanceStatusPayload is the payload for EventInstanceStatus events.
|
||||
type InstanceStatusPayload struct {
|
||||
InstanceID string `json:"instance_id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
StageID string `json:"stage_id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// DeployStatusPayload is the payload for EventDeployStatus events.
|
||||
type DeployStatusPayload struct {
|
||||
DeployID string `json:"deploy_id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
StageID string `json:"stage_id"`
|
||||
ImageTag string `json:"image_tag"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Subscriber is a channel that receives events.
|
||||
type Subscriber chan Event
|
||||
|
||||
// Bus is a simple in-process pub/sub event bus.
|
||||
// It supports topic-based filtering and per-subscriber buffering.
|
||||
type Bus struct {
|
||||
mu sync.RWMutex
|
||||
subscribers map[Subscriber]subscriberInfo
|
||||
}
|
||||
|
||||
type subscriberInfo struct {
|
||||
filter func(Event) bool
|
||||
}
|
||||
|
||||
// New creates a new event bus.
|
||||
func New() *Bus {
|
||||
return &Bus{
|
||||
subscribers: make(map[Subscriber]subscriberInfo),
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe registers a new subscriber with an optional filter.
|
||||
// If filter is nil, the subscriber receives all events.
|
||||
// The returned channel is buffered to avoid blocking publishers.
|
||||
func (b *Bus) Subscribe(filter func(Event) bool) Subscriber {
|
||||
ch := make(Subscriber, 64)
|
||||
b.mu.Lock()
|
||||
b.subscribers[ch] = subscriberInfo{filter: filter}
|
||||
b.mu.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
// Unsubscribe removes a subscriber and closes its channel.
|
||||
func (b *Bus) Unsubscribe(ch Subscriber) {
|
||||
b.mu.Lock()
|
||||
if _, ok := b.subscribers[ch]; ok {
|
||||
delete(b.subscribers, ch)
|
||||
close(ch)
|
||||
}
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
// Publish sends an event to all matching subscribers.
|
||||
// If a subscriber's buffer is full, the event is dropped for that subscriber
|
||||
// to avoid blocking the publisher.
|
||||
func (b *Bus) Publish(evt Event) {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
for ch, info := range b.subscribers {
|
||||
if info.filter != nil && !info.filter(evt) {
|
||||
continue
|
||||
}
|
||||
// Non-blocking send — drop if subscriber is slow.
|
||||
select {
|
||||
case ch <- evt:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalEvent serializes an event to a JSON string suitable for SSE data lines.
|
||||
func MarshalEvent(evt Event) (string, error) {
|
||||
data, err := json.Marshal(evt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
Reference in New Issue
Block a user