internal/events decouples the parts of JobSearch that cause events from
the parts that react to them. bus.go has Bus: Subscribe, and a synchronous
Publish that calls each handler in turn on the publisher's goroutine, so
handlers mustn't block (the hub doesn't: it queues per client). events.go
has the event types and their payloads, starting with OpportunitiesUpdated.
With fetcher.WithEvents, a save that stores new opportunities, or new
releases of stored ones, publishes OpportunitiesUpdated with their UUIDs and
counts. An opportunity whose row is only rewritten, as when it is fetched
again from the newest stored release, isn't counted as updated, and nothing
is published when nothing changed. store.SaveResult now reports
NewOpportunity for each opportunity saved.
job-search serve subscribes its WebSocket hub to the bus, so every client
gets {"type":"event","eventType":"OpportunitiesUpdated","payload":{"new":
[...],"updated":[...],"summary":{"new":n,"updated":n}}}. The Envelope gains
an eventType field, and Hub.PushEvent and Server.PushEvent send events.
91 lines
1.6 KiB
Go
91 lines
1.6 KiB
Go
package events
|
|
|
|
import (
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
)
|
|
|
|
// Every subscriber gets every event, in the order they subscribed.
|
|
func TestPublish(t *testing.T) {
|
|
var bus Bus
|
|
var got []string
|
|
for _, name := range []string{"first", "second"} {
|
|
bus.Subscribe(func(e Event) {
|
|
got = append(got, name+":"+e.Type)
|
|
})
|
|
}
|
|
|
|
bus.Publish(Event{
|
|
Type: "a",
|
|
})
|
|
bus.Publish(Event{
|
|
Type: "b",
|
|
})
|
|
|
|
want := []string{"first:a", "second:a", "first:b", "second:b"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %v; want %v", got, want)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("got %v; want %v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Publishing with no subscribers does nothing.
|
|
func TestPublishNoSubscribers(t *testing.T) {
|
|
var bus Bus
|
|
bus.Publish(Event{
|
|
Type: OpportunitiesUpdated,
|
|
})
|
|
}
|
|
|
|
// A handler may subscribe or publish without deadlocking.
|
|
func TestHandlerReenters(t *testing.T) {
|
|
var bus Bus
|
|
var inner atomic.Int32
|
|
bus.Subscribe(func(e Event) {
|
|
if e.Type == "outer" {
|
|
bus.Subscribe(func(Event) {
|
|
inner.Add(1)
|
|
})
|
|
bus.Publish(Event{
|
|
Type: "inner",
|
|
})
|
|
}
|
|
})
|
|
|
|
bus.Publish(Event{
|
|
Type: "outer",
|
|
})
|
|
if inner.Load() != 1 {
|
|
t.Errorf("inner handler ran %d times; want 1", inner.Load())
|
|
}
|
|
}
|
|
|
|
// Publish and Subscribe are safe from several goroutines at once (run with
|
|
// -race).
|
|
func TestConcurrent(t *testing.T) {
|
|
var bus Bus
|
|
var count atomic.Int32
|
|
var wg sync.WaitGroup
|
|
for range 10 {
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
bus.Subscribe(func(Event) {
|
|
count.Add(1)
|
|
})
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
bus.Publish(Event{
|
|
Type: "x",
|
|
})
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|