Files
2026-06-04 12:07:03 -04:00

246 lines
5.0 KiB
Go

package prism
import (
"context"
"errors"
"sync"
"time"
"git.wisehodl.dev/jay/go-roots-ws"
)
// ----------------------------------------------------------------------------
// Observables
// ----------------------------------------------------------------------------
type PublishDispatched struct {
EventID string
At time.Time
}
type PublishAccepted struct {
EventID string
At time.Time
}
type PublishRejected struct {
EventID string
Message string
At time.Time
}
type PublishTimeout struct {
EventID string
At time.Time
}
type PublishSendFailed struct {
EventID string
Err error
At time.Time
}
// ----------------------------------------------------------------------------
// Types
// ----------------------------------------------------------------------------
type publishResult struct {
accepted bool
message string
err error
}
type pendingEntry struct {
eventID string
eventJSON []byte
sent bool
result chan publishResult
mu sync.Mutex
once sync.Once
timer *time.Timer
}
// ----------------------------------------------------------------------------
// Event Publisher
// ----------------------------------------------------------------------------
type EventPublisher struct {
envoy *Envoy
pending map[string]*pendingEntry // event id -> pending entry
inbox <-chan InboxMessage
events <-chan OutboundPoolEvent
ctx context.Context
cancel context.CancelFunc
mu sync.Mutex
wg sync.WaitGroup
}
func NewEventPublisher(e *Envoy) *EventPublisher {
ctx, cancel := context.WithCancel(e.Context())
p := &EventPublisher{
envoy: e,
pending: make(map[string]*pendingEntry),
inbox: e.SubscribeInbox([]string{"OK"}),
events: e.SubscribeEvents(),
ctx: ctx,
cancel: cancel,
}
p.wg.Go(p.routeInbox)
p.wg.Go(p.handleEvents)
return p
}
func (p *EventPublisher) Publish(eventID string, eventJSON []byte, timeout time.Duration) (bool, string, error) {
entry := &pendingEntry{
eventID: eventID,
eventJSON: eventJSON,
result: make(chan publishResult, 1),
}
entry.timer = time.AfterFunc(timeout, func() {
p.mu.Lock()
p.deregister(eventID)
p.mu.Unlock()
p.envoy.Observer().Record(p.envoy.PeerID(),
PublishTimeout{EventID: eventID, At: time.Now()})
p.deliver(entry, publishResult{err: errors.New("publish timeout")})
})
p.mu.Lock()
p.pending[eventID] = entry
connected := p.envoy.IsConnected()
p.mu.Unlock()
if connected {
if err := p.trySend(entry); err != nil {
p.mu.Lock()
p.deregister(eventID)
p.mu.Unlock()
p.deliver(entry, publishResult{err: err})
}
}
r := <-entry.result
return r.accepted, r.message, r.err
}
func (p *EventPublisher) Close() {
p.cancel()
p.mu.Lock()
for id, e := range p.pending {
p.deregister(id)
p.deliver(e, publishResult{err: errors.New("publisher closed")})
}
p.mu.Unlock()
p.wg.Wait()
}
func (p *EventPublisher) trySend(entry *pendingEntry) error {
entry.mu.Lock()
defer entry.mu.Unlock()
if entry.sent {
return nil
}
err := p.envoy.Send([]byte(envelope.EncloseEvent(entry.eventJSON)))
if err != nil {
p.envoy.Observer().Record(p.envoy.PeerID(),
PublishSendFailed{EventID: entry.eventID, Err: err, At: time.Now()})
return err
}
entry.sent = true
p.envoy.Observer().Record(p.envoy.PeerID(),
PublishDispatched{EventID: entry.eventID, At: time.Now()})
return nil
}
func (p *EventPublisher) deliver(entry *pendingEntry, result publishResult) {
entry.once.Do(func() {
entry.timer.Stop()
entry.result <- result
})
}
func (p *EventPublisher) routeInbox() {
for {
select {
case <-p.ctx.Done():
return
case msg := <-p.inbox:
eventID, accepted, message, err := envelope.FindOK(msg.Data)
if err != nil {
continue
}
p.mu.Lock()
entry, ok := p.pending[eventID]
if ok {
p.deregister(eventID)
}
p.mu.Unlock()
if !ok {
continue
}
if accepted {
p.envoy.Observer().Record(msg.ID,
PublishAccepted{EventID: eventID, At: time.Now()})
} else {
p.envoy.Observer().Record(msg.ID,
PublishRejected{EventID: eventID, Message: message, At: time.Now()})
}
p.deliver(entry, publishResult{accepted: accepted, message: message})
}
}
}
func (p *EventPublisher) handleEvents() {
for {
select {
case <-p.ctx.Done():
return
case ev := <-p.events:
switch ev.Kind {
case EventConnected:
p.mu.Lock()
var toSend []*pendingEntry
for _, e := range p.pending {
e.mu.Lock()
if !e.sent {
toSend = append(toSend, e)
}
e.mu.Unlock()
}
p.mu.Unlock()
for _, e := range toSend {
if err := p.trySend(e); err != nil {
p.mu.Lock()
p.deregister(e.eventID)
p.mu.Unlock()
p.deliver(e, publishResult{err: err})
}
}
case EventDisconnected:
p.mu.Lock()
for _, e := range p.pending {
e.mu.Lock()
e.sent = false
e.mu.Unlock()
}
p.mu.Unlock()
}
}
}
}
func (p *EventPublisher) deregister(eventID string) {
delete(p.pending, eventID)
}