First Commit
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s

This commit is contained in:
dela
2026-10-09 08:38:16 +08:00
commit 0335d572de
756 changed files with 201663 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
package server
import (
"sync"
"github.com/Autumn-27/artex/db"
)
// Broadcaster is a per-task in-process pub/sub for live activity events. The
// engine publishes each appended activity at its single emit point; SSE handlers
// subscribe per task. Storage (activity table) and the live stream come from the
// same Publish call, so they never diverge.
type Broadcaster struct {
mu sync.Mutex
subs map[string]map[chan db.Activity]struct{} // task id -> set of subscriber channels
}
func NewBroadcaster() *Broadcaster {
return &Broadcaster{subs: map[string]map[chan db.Activity]struct{}{}}
}
// Subscribe returns a buffered channel of activities for a task plus an
// unsubscribe func the caller must invoke (defer) to release it.
func (b *Broadcaster) Subscribe(task string) (<-chan db.Activity, func()) {
ch := make(chan db.Activity, 256)
b.mu.Lock()
if b.subs[task] == nil {
b.subs[task] = map[chan db.Activity]struct{}{}
}
b.subs[task][ch] = struct{}{}
b.mu.Unlock()
var once sync.Once
return ch, func() {
once.Do(func() {
b.mu.Lock()
if m := b.subs[task]; m != nil {
delete(m, ch)
if len(m) == 0 {
delete(b.subs, task)
}
}
b.mu.Unlock()
close(ch)
})
}
}
// Publish fans an activity out to all subscribers of a task. Non-blocking: if a
// subscriber's buffer is full the event is dropped — the client reconnects with
// its last seq cursor and catches up the gap from the DB, so liveness never
// stalls the engine.
func (b *Broadcaster) Publish(task string, a db.Activity) {
b.mu.Lock()
defer b.mu.Unlock()
for ch := range b.subs[task] {
select {
case ch <- a:
default:
}
}
}