5558396bb7
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.
122 lines
3.1 KiB
Go
122 lines
3.1 KiB
Go
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
|
|
}
|