f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
160 lines
4.9 KiB
Go
160 lines
4.9 KiB
Go
package server
|
|
|
|
import (
|
|
gosync "sync"
|
|
"time"
|
|
)
|
|
|
|
// broadcasterBufferCap is the per-subscriber buffer size. A slow
|
|
// client can fall this many events behind before the broadcaster
|
|
// starts dropping events on its channel.
|
|
const broadcasterBufferCap = 8
|
|
|
|
// Event is a refresh signal sent by the sync engine after a pass
|
|
// that wrote data. Scope is advisory — subscribers may filter on
|
|
// it but are free to treat it as "refetch now".
|
|
type Event struct {
|
|
Scope string
|
|
}
|
|
|
|
// Broadcaster fans out Event values from the sync engine to all
|
|
// connected SSE clients. It implements sync.Emitter.
|
|
//
|
|
// Broadcasts are rate-limited with leading+trailing edge semantics:
|
|
// the first emit in a quiet period fires immediately, further emits
|
|
// within minInterval are coalesced into a single trailing broadcast
|
|
// carrying the most recent scope. This keeps first-write latency
|
|
// low while capping refetch work during sustained sync bursts.
|
|
type Broadcaster struct {
|
|
mu gosync.Mutex
|
|
subs map[chan Event]struct{}
|
|
minInterval time.Duration
|
|
lastEmit time.Time
|
|
pending *Event
|
|
timer *time.Timer
|
|
// timerGen increments each time a leading-edge broadcast
|
|
// invalidates the trailing state. A flushTrailing callback
|
|
// captures the generation at schedule time and returns early
|
|
// if the current generation no longer matches. Without this
|
|
// token, a callback whose timer already fired but was still
|
|
// waiting for b.mu could acquire the lock after a leading
|
|
// emit and a subsequent rate-limited emit had installed a
|
|
// new pending+timer, then consume that newer pending as if
|
|
// it were its own and broadcast it immediately — violating
|
|
// the rate limit and orphaning the newly scheduled timer.
|
|
timerGen uint64
|
|
}
|
|
|
|
// NewBroadcaster creates an empty broadcaster. minInterval is the
|
|
// minimum wall-clock time between broadcasts; zero (or any
|
|
// non-positive value) disables coalescing so every Emit fans out
|
|
// immediately.
|
|
func NewBroadcaster(minInterval time.Duration) *Broadcaster {
|
|
return &Broadcaster{
|
|
subs: make(map[chan Event]struct{}),
|
|
minInterval: minInterval,
|
|
}
|
|
}
|
|
|
|
// Emit delivers scope to every subscriber, subject to rate limiting.
|
|
// The first emit after a quiet gap of at least minInterval fans out
|
|
// immediately; emits within the window update the pending scope and
|
|
// schedule one trailing broadcast when the window ends.
|
|
//
|
|
// Delivery is non-blocking: if a subscriber's buffer is full, the
|
|
// event is dropped for that subscriber. The engine never blocks on
|
|
// slow clients.
|
|
func (b *Broadcaster) Emit(scope string) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
if b.minInterval == 0 || b.lastEmit.IsZero() ||
|
|
now.Sub(b.lastEmit) >= b.minInterval {
|
|
// Leading edge. Invalidate any in-flight trailing state:
|
|
// bumping timerGen makes a flushTrailing callback whose
|
|
// timer already fired (but was still waiting for b.mu)
|
|
// return without touching state. Clearing pending and
|
|
// stopping the timer handle the common cases where the
|
|
// callback has not yet started; the generation token
|
|
// covers the narrower race where it has.
|
|
b.pending = nil
|
|
if b.timer != nil {
|
|
b.timer.Stop()
|
|
b.timer = nil
|
|
}
|
|
b.timerGen++
|
|
b.lastEmit = now
|
|
b.broadcastLocked(Event{Scope: scope})
|
|
return
|
|
}
|
|
|
|
b.pending = &Event{Scope: scope}
|
|
if b.timer == nil {
|
|
gen := b.timerGen
|
|
wait := b.minInterval - now.Sub(b.lastEmit)
|
|
b.timer = time.AfterFunc(wait, func() {
|
|
b.flushTrailing(gen)
|
|
})
|
|
}
|
|
}
|
|
|
|
// flushTrailing is invoked by the trailing-edge timer. It delivers
|
|
// the most recent coalesced scope (if any) and clears the timer so
|
|
// future emits can schedule a new one. gen is the generation the
|
|
// timer captured at schedule time; a mismatch means a leading-edge
|
|
// broadcast has since invalidated this callback, so it returns
|
|
// without touching any state.
|
|
func (b *Broadcaster) flushTrailing(gen uint64) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
if gen != b.timerGen {
|
|
return
|
|
}
|
|
b.timer = nil
|
|
if b.pending == nil {
|
|
return
|
|
}
|
|
ev := *b.pending
|
|
b.pending = nil
|
|
b.lastEmit = time.Now()
|
|
b.broadcastLocked(ev)
|
|
}
|
|
|
|
// broadcastLocked sends ev to every subscriber using a non-blocking
|
|
// select so a full buffer drops the event for that subscriber only.
|
|
// Must be called with b.mu held; holding the lock is safe because
|
|
// sends never block.
|
|
func (b *Broadcaster) broadcastLocked(ev Event) {
|
|
for ch := range b.subs {
|
|
select {
|
|
case ch <- ev:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// Subscribe returns a receive channel for events and an unsubscribe
|
|
// function. Calling unsubscribe closes the channel and removes the
|
|
// subscription. It is safe to call unsubscribe multiple times.
|
|
func (b *Broadcaster) Subscribe() (<-chan Event, func()) {
|
|
ch := make(chan Event, broadcasterBufferCap)
|
|
b.mu.Lock()
|
|
b.subs[ch] = struct{}{}
|
|
b.mu.Unlock()
|
|
|
|
var once gosync.Once
|
|
unsub := func() {
|
|
once.Do(func() {
|
|
b.mu.Lock()
|
|
if _, ok := b.subs[ch]; ok {
|
|
delete(b.subs, ch)
|
|
close(ch)
|
|
}
|
|
b.mu.Unlock()
|
|
})
|
|
}
|
|
return ch, unsub
|
|
}
|