mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 08:36:37 +08:00
feat(core): add typed eventbus and domain extension points
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
package core_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/core"
|
||||
)
|
||||
|
||||
type UserRegisteredEvent struct {
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type OrderCreatedEvent struct {
|
||||
OrderID string `json:"order_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
func TestEventBusPublishSubscribe(t *testing.T) {
|
||||
bus := core.NewEventBus()
|
||||
var receivedID string
|
||||
|
||||
disposer := bus.On("user:registered", func(ctx context.Context, e UserRegisteredEvent) error {
|
||||
receivedID = e.UserID
|
||||
return nil
|
||||
})
|
||||
require.NotNil(t, disposer)
|
||||
|
||||
assert.Equal(t, []string{"user:registered"}, bus.Topics())
|
||||
|
||||
err := bus.Emit(context.Background(), "user:registered", UserRegisteredEvent{UserID: "u_999", Username: "alice"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "u_999", receivedID)
|
||||
|
||||
// Emit to empty topic returns nil error
|
||||
err = bus.Emit(nil, "empty:topic", nil)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEventBusGenericSubscribe(t *testing.T) {
|
||||
bus := core.NewEventBus()
|
||||
var receivedOrder string
|
||||
|
||||
assert.Panics(t, func() {
|
||||
core.Subscribe[OrderCreatedEvent](nil, "order:created", func(ctx context.Context, e OrderCreatedEvent) error {
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
disposer := core.Subscribe(bus, "order:created", func(ctx context.Context, e OrderCreatedEvent) error {
|
||||
receivedOrder = e.OrderID
|
||||
return nil
|
||||
})
|
||||
require.NotNil(t, disposer)
|
||||
|
||||
err := bus.Emit(context.Background(), "order:created", OrderCreatedEvent{OrderID: "ord_123", Amount: 99.5})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ord_123", receivedOrder)
|
||||
|
||||
// Test unsubscribe via disposer
|
||||
err = disposer()
|
||||
assert.NoError(t, err)
|
||||
|
||||
receivedOrder = ""
|
||||
err = bus.Emit(context.Background(), "order:created", OrderCreatedEvent{OrderID: "ord_456", Amount: 100})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, receivedOrder, "handler should not be called after disposal")
|
||||
}
|
||||
|
||||
func TestEventBusHandlerSignatures(t *testing.T) {
|
||||
bus := core.NewEventBus()
|
||||
|
||||
var (
|
||||
calledWithCtxPayloadErr atomic.Bool
|
||||
calledWithCtxPayload atomic.Bool
|
||||
calledWithPayloadErr atomic.Bool
|
||||
calledWithPayload atomic.Bool
|
||||
calledWithCtxErr atomic.Bool
|
||||
calledWithCtx atomic.Bool
|
||||
calledWithNoArgsErr atomic.Bool
|
||||
calledWithNoArgs atomic.Bool
|
||||
)
|
||||
|
||||
bus.On("test:sig", func(ctx context.Context, e UserRegisteredEvent) error {
|
||||
calledWithCtxPayloadErr.Store(true)
|
||||
assert.Equal(t, "u_1", e.UserID)
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.On("test:sig", func(ctx context.Context, e UserRegisteredEvent) {
|
||||
calledWithCtxPayload.Store(true)
|
||||
assert.Equal(t, "u_1", e.UserID)
|
||||
})
|
||||
|
||||
bus.On("test:sig", func(e UserRegisteredEvent) error {
|
||||
calledWithPayloadErr.Store(true)
|
||||
assert.Equal(t, "u_1", e.UserID)
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.On("test:sig", func(e UserRegisteredEvent) {
|
||||
calledWithPayload.Store(true)
|
||||
assert.Equal(t, "u_1", e.UserID)
|
||||
})
|
||||
|
||||
bus.On("test:sig", func(ctx context.Context) error {
|
||||
calledWithCtxErr.Store(true)
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.On("test:sig", func(ctx context.Context) {
|
||||
calledWithCtx.Store(true)
|
||||
})
|
||||
|
||||
bus.On("test:sig", func() error {
|
||||
calledWithNoArgsErr.Store(true)
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.On("test:sig", func() {
|
||||
calledWithNoArgs.Store(true)
|
||||
})
|
||||
|
||||
err := bus.Emit(context.Background(), "test:sig", UserRegisteredEvent{UserID: "u_1", Username: "test"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.True(t, calledWithCtxPayloadErr.Load())
|
||||
assert.True(t, calledWithCtxPayload.Load())
|
||||
assert.True(t, calledWithPayloadErr.Load())
|
||||
assert.True(t, calledWithPayload.Load())
|
||||
assert.True(t, calledWithCtxErr.Load())
|
||||
assert.True(t, calledWithCtx.Load())
|
||||
assert.True(t, calledWithNoArgsErr.Load())
|
||||
assert.True(t, calledWithNoArgs.Load())
|
||||
}
|
||||
|
||||
func TestEventBusPointerAndValueConversion(t *testing.T) {
|
||||
bus := core.NewEventBus()
|
||||
|
||||
var (
|
||||
receivedFromValueToPtr atomic.Bool
|
||||
receivedFromPtrToValue atomic.Bool
|
||||
receivedFromNilPtr atomic.Bool
|
||||
)
|
||||
|
||||
// Handler expects pointer, payload emitted as value
|
||||
bus.On("test:ptr", func(ctx context.Context, e *UserRegisteredEvent) error {
|
||||
if e != nil && e.UserID == "u_ptr" {
|
||||
receivedFromValueToPtr.Store(true)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
err := bus.Emit(context.Background(), "test:ptr", UserRegisteredEvent{UserID: "u_ptr"})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, receivedFromValueToPtr.Load())
|
||||
|
||||
// Handler expects value, payload emitted as pointer
|
||||
bus.On("test:val", func(ctx context.Context, e UserRegisteredEvent) error {
|
||||
if e.UserID == "u_val" {
|
||||
receivedFromPtrToValue.Store(true)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
err = bus.Emit(context.Background(), "test:val", &UserRegisteredEvent{UserID: "u_val"})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, receivedFromPtrToValue.Load())
|
||||
|
||||
// Handler expects value, payload is nil pointer
|
||||
var nilEvent *UserRegisteredEvent
|
||||
bus.On("test:nil_ptr", func(ctx context.Context, e UserRegisteredEvent) error {
|
||||
assert.Equal(t, "", e.UserID)
|
||||
receivedFromNilPtr.Store(true)
|
||||
return nil
|
||||
})
|
||||
err = bus.Emit(context.Background(), "test:nil_ptr", nilEvent)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, receivedFromNilPtr.Load())
|
||||
|
||||
// Convertible type test (int to int64)
|
||||
var receivedConvert int64
|
||||
bus.On("test:conv", func(e int64) {
|
||||
receivedConvert = e
|
||||
})
|
||||
err = bus.Emit(context.Background(), "test:conv", int(42))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(42), receivedConvert)
|
||||
}
|
||||
|
||||
func TestEventBusErrorCollectionAndPanicRecovery(t *testing.T) {
|
||||
bus := core.NewEventBus()
|
||||
|
||||
errHandler1 := errors.New("handler 1 failed")
|
||||
errHandler2 := errors.New("handler 2 failed")
|
||||
|
||||
bus.On("test:err", func() error {
|
||||
return errHandler1
|
||||
})
|
||||
|
||||
bus.On("test:err", func() {
|
||||
panic("something went horribly wrong")
|
||||
})
|
||||
|
||||
bus.On("test:err", func() error {
|
||||
return errHandler2
|
||||
})
|
||||
|
||||
err := bus.Emit(context.Background(), "test:err", nil)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, errHandler1) || errors.Is(err, errHandler2))
|
||||
assert.Contains(t, err.Error(), "handler 1 failed")
|
||||
assert.Contains(t, err.Error(), "handler 2 failed")
|
||||
assert.Contains(t, err.Error(), "panic")
|
||||
}
|
||||
|
||||
func TestEventBusInvalidHandlerPanics(t *testing.T) {
|
||||
bus := core.NewEventBus()
|
||||
|
||||
assert.Panics(t, func() {
|
||||
bus.On("test:invalid", nil)
|
||||
})
|
||||
|
||||
assert.Panics(t, func() {
|
||||
bus.On("test:invalid", "not-a-func")
|
||||
})
|
||||
|
||||
assert.Panics(t, func() {
|
||||
// More than 2 arguments
|
||||
bus.On("test:invalid", func(a, b, c string) {})
|
||||
})
|
||||
|
||||
assert.Panics(t, func() {
|
||||
// 2 args, but first is not context
|
||||
bus.On("test:invalid", func(a string, b int) {})
|
||||
})
|
||||
|
||||
assert.Panics(t, func() {
|
||||
// More than 1 return value
|
||||
bus.On("test:invalid", func() (int, error) { return 0, nil })
|
||||
})
|
||||
|
||||
assert.Panics(t, func() {
|
||||
// Return value is not error
|
||||
bus.On("test:invalid", func() int { return 0 })
|
||||
})
|
||||
}
|
||||
|
||||
func TestEventBusListenersCountAndDisposerIdempotence(t *testing.T) {
|
||||
bus := core.NewEventBus()
|
||||
|
||||
assert.Equal(t, 0, bus.Listeners("topic1"))
|
||||
|
||||
d1 := bus.On("topic1", func() {})
|
||||
d2 := bus.On("topic1", func() {})
|
||||
assert.Equal(t, 2, bus.Listeners("topic1"))
|
||||
|
||||
_ = d1()
|
||||
assert.Equal(t, 1, bus.Listeners("topic1"))
|
||||
|
||||
// Calling disposer again should be no-op
|
||||
_ = d1()
|
||||
assert.Equal(t, 1, bus.Listeners("topic1"))
|
||||
|
||||
_ = d2()
|
||||
assert.Equal(t, 0, bus.Listeners("topic1"))
|
||||
}
|
||||
|
||||
func TestEventBusConcurrentAccess(t *testing.T) {
|
||||
bus := core.NewEventBus()
|
||||
var wg sync.WaitGroup
|
||||
|
||||
var receivedCount atomic.Int64
|
||||
|
||||
// Concurrently subscribe and emit
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
topic := fmt.Sprintf("topic:%d", idx%5)
|
||||
disposer := bus.On(topic, func(ctx context.Context, e UserRegisteredEvent) error {
|
||||
receivedCount.Add(1)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Emit some events
|
||||
_ = bus.Emit(context.Background(), topic, UserRegisteredEvent{UserID: fmt.Sprintf("u_%d", idx)})
|
||||
|
||||
// Randomly dispose
|
||||
if idx%2 == 0 {
|
||||
_ = disposer()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
topic := fmt.Sprintf("topic:%d", idx%5)
|
||||
_ = bus.Emit(context.Background(), topic, UserRegisteredEvent{UserID: fmt.Sprintf("u_%d", idx)})
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
assert.Greater(t, receivedCount.Load(), int64(0))
|
||||
}
|
||||
Reference in New Issue
Block a user