Files
tiny-forge/internal/events/bus.go
T
alexei.dolgolyov 5558396bb7 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.
2026-03-27 22:30:25 +03:00

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
}