From 3bf4b5c39b385b4fe258912a43e6b98292b54799 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Mon, 16 Jan 2023 20:34:16 -0800 Subject: [PATCH] extension: Implement an EventBroker (#315) Signed-off-by: James Hillyerd Signed-off-by: James Hillyerd --- pkg/extension/broker.go | 57 +++++++++++++++ pkg/extension/broker_test.go | 134 +++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 pkg/extension/broker.go create mode 100644 pkg/extension/broker_test.go diff --git a/pkg/extension/broker.go b/pkg/extension/broker.go new file mode 100644 index 0000000..93caad3 --- /dev/null +++ b/pkg/extension/broker.go @@ -0,0 +1,57 @@ +package extension + +import "sync" + +// EventBroker maintains a list of listeners interested in a specific type +// of event. +type EventBroker[E any, R comparable] struct { + sync.RWMutex + listenerNames []string // Ordered listener names. + listenerFuncs []func(E) *R // Ordered listener functions. +} + +// Emit sends the provided event to each registered listener in order, until +// one returns a non-nil result. That result will be returned to the caller. +func (eb *EventBroker[E, R]) Emit(event *E) *R { + eb.RLock() + defer eb.RUnlock() + + for _, l := range eb.listenerFuncs { + // Events are copied to minimize the risk of mutation. + if result := l(*event); result != nil { + return result + } + } + + return nil +} + +// AddListener registers the named listener, replacing one with a duplicate +// name if present. Listeners should be added in order of priority, most +// significant first. +func (eb *EventBroker[E, R]) AddListener(name string, listener func(E) *R) { + eb.Lock() + defer eb.Unlock() + + eb.lockedRemoveListener(name) + eb.listenerNames = append(eb.listenerNames, name) + eb.listenerFuncs = append(eb.listenerFuncs, listener) +} + +// RemoveListener unregisters the named listener. +func (eb *EventBroker[E, R]) RemoveListener(name string) { + eb.Lock() + defer eb.Unlock() + + eb.lockedRemoveListener(name) +} + +func (eb *EventBroker[E, R]) lockedRemoveListener(name string) { + for i, entry := range eb.listenerNames { + if entry == name { + eb.listenerNames = append(eb.listenerNames[:i], eb.listenerNames[i+1:]...) + eb.listenerFuncs = append(eb.listenerFuncs[:i], eb.listenerFuncs[i+1:]...) + break + } + } +} diff --git a/pkg/extension/broker_test.go b/pkg/extension/broker_test.go new file mode 100644 index 0000000..2d6d6b7 --- /dev/null +++ b/pkg/extension/broker_test.go @@ -0,0 +1,134 @@ +package extension_test + +import ( + "testing" + + "github.com/inbucket/inbucket/pkg/extension" +) + +func testBrokerEmitCallsOneListener(t *testing.T) { + broker := &extension.EventBroker[string, bool]{} + + // Setup listener. + var got string + listener := func(s string) *bool { + got = s + return nil + } + broker.AddListener("x", listener) + + want := "bacon" + broker.Emit(&want) + if got != want { + t.Errorf("Emit got %q, want %q", got, want) + } +} + +func testBrokerEmitCallsMultipleListeners(t *testing.T) { + broker := &extension.EventBroker[string, bool]{} + + // Setup listeners. + var first_got, second_got string + first := func(s string) *bool { + first_got = s + return nil + } + second := func(s string) *bool { + second_got = s + return nil + } + + broker.AddListener("1", first) + broker.AddListener("2", second) + + want := "hi" + broker.Emit(&want) + if first_got != want { + t.Errorf("first got %q, want %q", first_got, want) + } + if second_got != want { + t.Errorf("second got %q, want %q", second_got, want) + } +} + +func testBrokerEmitCapturesFirstResult(t *testing.T) { + broker := &extension.EventBroker[struct{}, string]{} + + // Setup listeners. + makeListener := func(result *string) func(struct{}) *string { + return func(s struct{}) *string { return result } + } + first := "first" + second := "second" + broker.AddListener("0", makeListener(nil)) + broker.AddListener("1", makeListener(&first)) + broker.AddListener("2", makeListener(&second)) + + want := first + got := broker.Emit(&struct{}{}) + if got == nil { + t.Errorf("Emit got nil, want %q", want) + } else if *got != want { + t.Errorf("Emit got %q, want %q", *got, want) + } +} + +func testBrokerAddingDuplicateNameReplacesPrevious(t *testing.T) { + broker := &extension.EventBroker[string, bool]{} + + // Setup listeners. + var first_got, second_got string + first := func(s string) *bool { + first_got = s + return nil + } + second := func(s string) *bool { + second_got = s + return nil + } + + broker.AddListener("dup", first) + broker.AddListener("dup", second) + + want := "hi" + broker.Emit(&want) + if first_got != "" { + t.Errorf("first got %q, want empty string", first_got) + } + if second_got != want { + t.Errorf("second got %q, want %q", second_got, want) + } +} + +func testBrokerRemovingListenerSuccessful(t *testing.T) { + broker := &extension.EventBroker[string, bool]{} + + // Setup listeners. + var first_got, second_got string + first := func(s string) *bool { + first_got = s + return nil + } + second := func(s string) *bool { + second_got = s + return nil + } + + broker.AddListener("1", first) + broker.AddListener("2", second) + broker.RemoveListener("1") + + want := "hi" + broker.Emit(&want) + if first_got != "" { + t.Errorf("first got %q, want empty string", first_got) + } + if second_got != want { + t.Errorf("second got %q, want %q", second_got, want) + } +} + +func testBrokerRemovingMissingListener(t *testing.T) { + broker := &extension.EventBroker[string, bool]{} + broker.RemoveListener("doesn't crash") +}