diff --git a/README.md b/README.md index c25e1b3..12a6b66 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 @@ -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 diff --git a/README.zh-CN.md b/README.zh-CN.md index bdb2726..acad699 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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) @@ -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) ## 安装 @@ -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` 异步触发事件 ## 快速使用 diff --git a/event.go b/event.go index ad7c0a5..bed0bcc 100644 --- a/event.go +++ b/event.go @@ -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. @@ -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 diff --git a/manager.go b/manager.go index 95f505f..d43c8b4 100644 --- a/manager.go +++ b/manager.go @@ -6,6 +6,11 @@ import ( "sync" ) +const ( + defaultChannelSize = 100 + defaultConsumerNum = 5 +) + // Manager event manager definition. for manage events and listeners type Manager struct { Options @@ -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...) @@ -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 @@ -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 @@ -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) @@ -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) { @@ -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() { @@ -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() }) @@ -283,11 +317,12 @@ 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 } @@ -295,6 +330,14 @@ func (em *Manager) makeConsumers() { } } +// 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: @@ -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) diff --git a/manager_test.go b/manager_test.go index 7513a45..4cb168e 100644 --- a/manager_test.go +++ b/manager_test.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" "testing" + "time" "github.com/gookit/event" "github.com/stretchr/testify/assert" @@ -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()) } diff --git a/util.go b/util.go index 9540106..2f23594 100644 --- a/util.go +++ b/util.go @@ -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) {