139 lines
3.1 KiB
Go
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()
|
|
}
|
|
}
|
|
}
|