Skip to content

Commit

Permalink
up: update name match logic and add more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
inhere committed Jun 7, 2023
1 parent 6a692b4 commit 65c8754
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 49 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ Lightweight event management, dispatch tool library implemented by Go

- Support for custom definition event objects
- Support for adding multiple listeners to an event
- Supports setting the priority of the event listener. The higher the priority, the higher the trigger.
- Support setting the priority of the event listener, the higher the priority, the first to trigger
- Support for a set of event listeners based on the event name prefix `PREFIX.*`.
- add `app.*` event listen, trigger `app.run` `app.end`, Both will trigger the `app.*` event at the same time
- `ModeSimple` - `app.*` event listen, trigger `app.run` `app.end`, Both will trigger the `app.*` event at the same time
- `ModePath` - `db.**` event listen, trigger `db.run` `db.end`, Only trigger the `db.**` event once
- Support for using the wildcard `*` to listen for triggers for all events
- Support async trigger event by `go` channel consumers. use `Async(), FireAsync()`
- Complete unit testing, unit coverage `> 95%`

## [中文说明](README.zh-CN.md)
Expand All @@ -22,7 +24,7 @@ Lightweight event management, dispatch tool library implemented by Go

## GoDoc

- [Godoc for github](https://pkg.go.dev/github.com/gookit/event)
- [Godoc for GitHub](https://pkg.go.dev/github.com/gookit/event)

## Install

Expand All @@ -38,7 +40,9 @@ go get github.com/gookit/event
- `MustTrigger/MustFire(name string, params M) Event` Trigger event, there will be panic if there is an error
- `FireEvent(e Event) (err error)` Trigger an event based on a given event instance
- `FireBatch(es ...interface{}) (ers []error)` Trigger multiple events at once
- `AsyncFire(e Event)` Async fire event by 'go' keywords
- `Async/FireC(name string, params M)` Push event to `chan`, asynchronous consumption processing
- `FireAsync(e Event)` Push event to `chan`, asynchronous consumption processing
- `AsyncFire(e Event)` Async fire event by 'go' keywords

## Quick start

Expand Down
22 changes: 13 additions & 9 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
[![Coverage Status](https://coveralls.io/repos/github/gookit/event/badge.svg?branch=master)](https://coveralls.io/github/gookit/event?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/gookit/event)](https://goreportcard.com/report/github.com/gookit/event)

Go 实现的轻量级的事件管理、调度工具库
`event` Go 实现的轻量级的事件管理、调度工具库

- 支持自定义定义事件对象
- 支持自定义创建预定义的事件对象
- 支持对一个事件添加多个监听器
- 支持设置事件监听器的优先级,优先级越高越先触发
- 支持根据事件名称前缀 `PREFIX.*` 来进行一组事件监听.
- 注册`app.*` 事件的监听,触发 `app.run` `app.end` 时,都将同时会触发 `app.*` 事件
- 支持使用通配符 `*` 来监听全部事件的触发
- 支持通过通配符 `*` 来进行一组事件的匹配监听.
- `ModeSimple` - 注册 `app.*` 事件的监听,触发 `app.run` `app.end` 时,都将同时会触发 `app.*` 事件
- `ModePath` - **NEW** `*` 只匹配一段非 `.` 的字符,可以进行更精细的监听; `**` 匹配任意多个字符,只能用于开头或结尾
- 支持直接使用通配符 `*` 来监听全部事件的触发
- 支持触发事件时投递到 `chan`, 异步进行消费处理. 触发: `Async(), FireAsync()`
- 完善的单元测试,单元覆盖率 `> 95%`

## [English](README.md)
Expand All @@ -22,7 +24,7 @@ English introduction, please see **[EN README](README.md)**

## GoDoc

- [Godoc for github](https://pkg.go.dev/github.com/gookit/event)
- [Godoc for GitHub](https://pkg.go.dev/github.com/gookit/event)

## 安装

Expand All @@ -35,10 +37,12 @@ go get github.com/gookit/event
- `On/Listen(name string, listener Listener, priority ...int)` 注册事件监听
- `Subscribe/AddSubscriber(sbr Subscriber)` 订阅,支持注册多个事件监听
- `Trigger/Fire(name string, params M) (error, Event)` 触发事件
- `MustTrigger/MustFire(name string, params M) Event` 触发事件,有错误则会panic
- `FireEvent(e Event) (err error)` 根据给定的事件实例,触发事件
- `MustTrigger/MustFire(name string, params M) Event` 触发事件,有错误则会panic
- `FireEvent(e Event) (err error)` 根据给定的事件实例,触发事件
- `FireBatch(es ...interface{}) (ers []error)` 一次触发多个事件
- `AsyncFire(e Event)` Async fire event by 'go' keywords
- `Async/FireC(name string, params M)` 投递触事件到 `chan`,异步消费处理
- `FireAsync(e Event)` 投递触事件到 `chan`,异步消费处理
- `AsyncFire(e Event)` 简单的通过 `go` 异步触发事件

## 快速使用

Expand Down
14 changes: 8 additions & 6 deletions event.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Package event is lightweight event manager and dispatcher implements by Go.
package event

// Wildcard event name
const Wildcard = "*"
const AnyNode = "*"
const AllNode = "**"
// wildcard event name
const (
Wildcard = "*"
AnyNode = "*"
AllNode = "**"
)

const (
// ModeSimple old mode, simple match group listener.
Expand Down Expand Up @@ -44,8 +46,8 @@ type ManagerFace interface {
type Options struct {
// EnableLock enable lock on fire event.
EnableLock bool
// ChanLength for fire events by goroutine
ChanLength int
// ChannelSize for fire events by goroutine
ChannelSize int
ConsumerNum int
// MatchMode event name match mode. default is ModeSimple
MatchMode uint8
Expand Down
99 changes: 71 additions & 28 deletions manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import (
"sync"
)

const (
defaultChannelSize = 100
defaultConsumerNum = 5
)

// Manager event manager definition. for manage events and listeners
type Manager struct {
Options
Expand Down Expand Up @@ -44,8 +49,8 @@ func NewManager(name string, fns ...OptionFn) *Manager {
}

// for async fire by goroutine
em.ConsumerNum = 5
em.ChanLength = 10
em.ConsumerNum = defaultConsumerNum
em.ChannelSize = defaultChannelSize

// apply options
return em.WithOptions(fns...)
Expand Down Expand Up @@ -154,6 +159,29 @@ func (em *Manager) Trigger(name string, params M) (error, Event) {

// Fire trigger event by name. if not found listener, will return (nil, nil)
func (em *Manager) Fire(name string, params M) (err error, e Event) {
// call listeners handle event
e, err = em.fireByName(name, params, false)
return
}

// Async fire event by go channel.
func (em *Manager) Async(name string, params M) {
_, _ = em.fireByName(name, params, true)
}

// FireC async fire event by go channel. alias of the method Async()
func (em *Manager) FireC(name string, params M) {
_, _ = em.fireByName(name, params, true)
}

// fire event by name.
//
// if useCh is true, will async fire by channel. always return (nil, nil)
//
// On useCh=false:
// - will call listeners handle event.
// - if not found listener, will return (nil, nil)
func (em *Manager) fireByName(name string, params M, useCh bool) (e Event, err error) {
name = goodName(name, false)

// use pre-defined Event
Expand All @@ -163,10 +191,16 @@ func (em *Manager) Fire(name string, params M) (err error, e Event) {
e.SetData(params)
}
} else {
// create a basic event instance
// create new basic event instance
e = em.newBasicEvent(name, params)
}

// fire by channel
if useCh {
em.FireAsync(e)
return nil, nil
}

// call listeners handle event
err = em.FireEvent(e)
return
Expand All @@ -183,17 +217,6 @@ func (em *Manager) FireEvent(e Event) (err error) {
e.Abort(false)
name := e.Name()

// fire direct matched listeners. eg: db.user.add
if lq, ok := em.listeners[name]; ok {
// sort by priority before call.
for _, li := range lq.Sort().Items() {
err = li.Listener.Handle(e)
if err != nil || e.IsAborted() {
return
}
}
}

// fire group listeners by wildcard. eg "db.user.*"
if em.MatchMode == ModePath {
err = em.firePathMode(name, e)
Expand Down Expand Up @@ -223,6 +246,17 @@ func (em *Manager) FireEvent(e Event) (err error) {
// Example:
// - event "db.user.add" will trigger listeners on the "db.user.*"
func (em *Manager) fireSimpleMode(name string, e Event) (err error) {
// fire direct matched listeners. eg: db.user.add
if lq, ok := em.listeners[name]; ok {
// sort by priority before call.
for _, li := range lq.Sort().Items() {
err = li.Listener.Handle(e)
if err != nil || e.IsAborted() {
return
}
}
}

pos := strings.LastIndexByte(name, '.')

if pos > 0 && pos < len(name) {
Expand All @@ -247,12 +281,8 @@ func (em *Manager) fireSimpleMode(name string, e Event) (err error) {
// - event "db.user.add" will trigger listeners on the "db.**"
// - event "db.user.add" will trigger listeners on the "db.user.*"
func (em *Manager) firePathMode(name string, e Event) (err error) {
if strings.IndexByte(name, '.') < 0 {
return nil
}

for pattern, lq := range em.listeners {
if pattern != name && matchNodePath(pattern, name, ".") {
if pattern == name || matchNodePath(pattern, name, ".") {
for _, li := range lq.Sort().Items() {
err = li.Listener.Handle(e)
if err != nil || e.IsAborted() {
Expand All @@ -265,14 +295,18 @@ func (em *Manager) firePathMode(name string, e Event) (err error) {
return nil
}

// Fire2 alias of the method Fire()
func (em *Manager) Fire2(name string, params M) (error, Event) {
return em.Fire(name, params)
}
// FireAsync async fire event by go channel.
//
// Note: if you want to use this method, you should call the method Close() after all events are fired.
func (em *Manager) FireAsync(e Event) {
if em.ConsumerNum <= 0 {
em.ConsumerNum = defaultConsumerNum
}
if em.ChannelSize <= 0 {
em.ChannelSize = defaultChannelSize
}

// FireByChan async fire event by go channel
func (em *Manager) FireByChan(e Event) {
// once make
// once make consumers
em.once.Do(func() {
em.makeConsumers()
})
Expand All @@ -283,18 +317,27 @@ func (em *Manager) FireByChan(e Event) {

// async fire event by 'go' keywords
func (em *Manager) makeConsumers() {
em.ch = make(chan Event, em.ChanLength)
em.ch = make(chan Event, em.ChannelSize)

// make event consumers
for i := 0; i < em.ConsumerNum; i++ {
go func() {
// keep running until channel closed
for e := range em.ch {
_ = em.FireEvent(e) // ignore async fire error
}
}()
}
}

// Close manager and event channel
func (em *Manager) Close() error {
if em.ch != nil {
close(em.ch)
}
return nil
}

// FireBatch fire multi event at once.
//
// Usage:
Expand All @@ -316,7 +359,7 @@ func (em *Manager) FireBatch(es ...any) (ers []error) {
return
}

// AsyncFire async fire event by 'go' keywords
// AsyncFire simple async fire event by 'go' keywords
func (em *Manager) AsyncFire(e Event) {
go func(e Event) {
_ = em.FireEvent(e)
Expand Down
70 changes: 70 additions & 0 deletions manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"strings"
"testing"
"time"

"github.com/gookit/event"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -77,4 +78,73 @@ func TestManager_Fire_usePathMode(t *testing.T) {
assert.Contains(t, str, "db.*.update|")
assert.True(t, strings.Count(str, "|") == 3)
})
buf.Reset()

t.Run("not-exist", func(t *testing.T) {
err, e := em.Fire("not-exist", event.M{"user": "inhere"})
assert.NoError(t, err)
assert.Equal(t, "inhere", e.Get("user"))
str := buf.String()
fmt.Println(str)
assert.Equal(t, "*|", str)
})
}

func TestManager_Fire_AllNode(t *testing.T) {
em := event.NewManager("test", event.UsePathMode, event.EnableLock)

buf := new(bytes.Buffer)
em.Listen("**.add", event.ListenerFunc(func(e event.Event) error {
_, _ = buf.WriteString("**.add|")
return nil
}))

err, e := em.Trigger("db.user.add", event.M{"user": "inhere"})
assert.NoError(t, err)
assert.Equal(t, "inhere", e.Get("user"))
str := buf.String()
assert.Equal(t, "**.add|", str)
}

func TestManager_FireC(t *testing.T) {
em := event.NewManager("test", event.UsePathMode, event.EnableLock)
defer func(em *event.Manager) {
_ = em.Close()
}(em)

buf := new(bytes.Buffer)
em.Listen("db.user.*", event.ListenerFunc(func(e event.Event) error {
_, _ = buf.WriteString("db.user.*|")
return nil
}))
em.Listen("db.**", event.ListenerFunc(func(e event.Event) error {
_, _ = buf.WriteString("db.**|")
return nil
}), 1)

em.Listen("db.user.add", event.ListenerFunc(func(e event.Event) error {
_, _ = buf.WriteString("db.user.add|")
return nil
}), 2)

assert.True(t, em.HasListeners("db.user.*"))

em.FireC("db.user.add", event.M{"user": "inhere"})
time.Sleep(time.Millisecond * 100) // wait for async

str := buf.String()
fmt.Println(str)
assert.Contains(t, str, "db.**|")
assert.Contains(t, str, "db.user.*|")
assert.Contains(t, str, "db.user.add|")
assert.True(t, strings.Count(str, "|") == 3)
buf.Reset()

em.WithOptions(func(o *event.Options) {
o.ChannelSize = 0
o.ConsumerNum = 0
})
em.Async("not-exist", event.M{"user": "inhere"})
time.Sleep(time.Millisecond * 100) // wait for async
assert.Empty(t, buf.String())
}
10 changes: 8 additions & 2 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,14 @@ func goodName(name string, isReg bool) string {
panic("event: the event name cannot be empty")
}

if isReg && (name == AllNode || name == Wildcard) {
return Wildcard
// on add listener
if isReg {
if name == AllNode || name == Wildcard {
return Wildcard
}
if strings.HasPrefix(name, AllNode) {
return name
}
}

if !goodNameReg.MatchString(name) {
Expand Down

0 comments on commit 65c8754

Please sign in to comment.