Files
soft_quay/core/application/event_relay.go
T

139 lines
3.1 KiB
Go

package application
import (
"context"
"errors"
"fmt"
"sync"
)
var (
ErrEventRelayClosed = errors.New("application event relay closed")
ErrEventRelayInvalid = errors.New("application event relay is invalid")
)
// EventRelay is a bounded FIFO between background event pumps and the UI frame.
// Submit applies lossless backpressure; Drain must only run on the UI goroutine.
type EventRelay struct {
events chan Event
slots chan struct{}
done chan struct{}
mu sync.Mutex
closed bool
closeOnce sync.Once
}
// NewEventRelay creates a relay with a strictly positive bounded capacity.
func NewEventRelay(capacity int) (*EventRelay, error) {
if capacity <= 0 {
return nil, fmt.Errorf(
"%w: capacity must be positive",
ErrEventRelayInvalid,
)
}
relay := &EventRelay{
events: make(chan Event, capacity),
slots: make(chan struct{}, capacity),
done: make(chan struct{}),
}
for index := 0; index < capacity; index++ {
relay.slots <- struct{}{}
}
return relay, nil
}
// Submit queues one event or returns when the context/relay closes.
func (relay *EventRelay) Submit(ctx context.Context, event Event) error {
if relay == nil {
return fmt.Errorf("%w: nil relay", ErrEventRelayInvalid)
}
if !event.Type.Valid() {
return fmt.Errorf(
"%w: unknown type %q",
ErrInvalidEvent,
event.Type,
)
}
select {
case <-relay.done:
return ErrEventRelayClosed
case <-ctx.Done():
return ctx.Err()
case <-relay.slots:
}
relay.mu.Lock()
defer relay.mu.Unlock()
if relay.closed {
relay.slots <- struct{}{}
return ErrEventRelayClosed
}
if err := ctx.Err(); err != nil {
relay.slots <- struct{}{}
return err
}
relay.events <- event
return nil
}
// Drain applies the events present at entry without extending a UI frame forever.
func (relay *EventRelay) Drain(apply func(Event) error) error {
if relay == nil || apply == nil {
return fmt.Errorf("%w: nil relay or apply function", ErrEventRelayInvalid)
}
limit := len(relay.events)
var applyErrors []error
for index := 0; index < limit; index++ {
select {
case event := <-relay.events:
relay.slots <- struct{}{}
if err := apply(event); err != nil {
applyErrors = append(applyErrors, err)
}
default:
return errors.Join(applyErrors...)
}
}
return errors.Join(applyErrors...)
}
// Close unblocks pending submissions. Queued events remain available to Drain.
func (relay *EventRelay) Close() {
if relay == nil {
return
}
relay.closeOnce.Do(func() {
relay.mu.Lock()
relay.closed = true
close(relay.done)
relay.mu.Unlock()
})
}
// PumpEvents forwards application events to a relay and requests a UI frame.
// invalidate may be called concurrently; no UI state may be mutated here.
func PumpEvents(
ctx context.Context,
events <-chan Event,
relay *EventRelay,
invalidate func(),
) error {
if events == nil || relay == nil || invalidate == nil {
return fmt.Errorf("%w: incomplete event pump", ErrEventRelayInvalid)
}
for {
select {
case <-ctx.Done():
return ctx.Err()
case event, open := <-events:
if !open {
return nil
}
if err := relay.Submit(ctx, event); err != nil {
return err
}
invalidate()
}
}
}