Package emitter implements channel based pubsub pattern. The design goals are use Golang concurrency model instead of flat callbacks and the simplest API to understand and use.
Go has expressive concurrency model but nobody doesn't use it properly for pubsub, as I see(at the end of 2015). I had implemented my own as I didn't find any acceptable. Please, read this article for more information.
- sync/async event emitting
- predicates/middlewares
- bi-directional wildcard
- discard emitting if needed
- merge events from different channels
- shallow on demand type casting
- work with callbacks(traditional way)
e := &emitter.Emitter{}
go func(){
<-e.Emit("change", 42) // wait for event sent successfully
<-e.Emit("change", 37)
e.Off("*") // unsubscribe any listeners
}()
for event := range e.On("change") {
// do something with event.Args
plintln(event.Int(0)) // cast first argument to int
}
// listener channel was closed
emitter.New
takes a uint
as first argument to indicate what buffer size should be used for listeners. Also possible to change capacity at runtime: e.Cap = 10
.
By default emitter use goroutine per listener to send an event. You may want to change it via e.Use("*", emitter.Sync)
. I recommend to specify middlewares(see below) for the emitter at start.
The package allows publications and subscriptions with wildcard. This feature based on path.Match
function.
Example:
go e.Emit("something:special", 42)
event := <-e.Once("*") // grub any events
println(event.Int(0)) // will print 42
// or emit event with wildcard path
go e.Emit("*", 37) // emmit for everyone
event := <-e.Once("something:special")
println(event.Int(0)) // will print 37
Note that wildcard uses path.Match
, but the lib is not return errors related for parsing. As this is not main feature. Please check the topic explicitly via emitter.Test()
function.
Important part of pubsub package is predicates. It should be allow to skip some event. Middlewares solve this problem. Middleware is a function that takes a pointer to the Event as first argument. All that middlewares can do is just modify the event. It allows to skip sending it needed or modify event's agruments. Or specify the mode to describe how exactly event should be emitted(see below).
There are two ways to add middleware into emitting flow:
- via .On("event", middlewares...)
- via .Use("event", middlewares...)
The first add middlewares ony for this listener, but the second add middlewares for all events with given topic.
For example:
// use synchronous mode for all events, it also depends
// on emitter capacity(buffered/unbuffered channels)
e.Use("*", emitter.Sync)
go e.Emit("something:special", 42)
// define predicate
event := <-e.Once("*", func(ev *emitter.Event){
if ev.Int(0) == 42 {
// skip sending
ev.Flags = ev.Flags | emitter.FlagVoid
}
})
panic("will never happen")
Flags needs to describe how exactly the event should be emitted. Available options are listed here.
Every event(emitter.Event
) has field .Flags
that contains flags as binary mask.
Flags can be set only via middlewares(see above).
There are several predefined middlewares to set needed flags:
You can combine it as a chain:
e.Use("*", emitter.Void) // skip sending for any events
go e.Emit("surprise", 65536)
event := <-e.On("*", emitter.Reset, emitter.Sync, emitter.Once) // set custom flags for this listener
pintln(event.Int(0)) // prints 65536
Golang give as more control for asynchronous flow. We may know it the channel is blocked and we may discard sending as well. So, emitter allows to discard emitting based on this language feature. It's good practice to design your application with timeouts and cancellation possibilities.
Assume you have time out to emit the events:
done := e.Emit("broadcast", "the", "event", "with", "timeout")
select {
case <-done:
// so the sending is done
case <-time.After(timeout):
// time is out, let's discard emitting
close(done)
}
It's pretty useful to control any goroutines inside th emitter instance.
Use emitter in traditional way is also possible. If you don't need async mode or you very attentive to application resources. The recipe is use emitter with zero capacity, define FlagVoid
to skip sending into the listener channel and use middleware as callback:
e := &emitter.Emitter{}
e.Use("*", emitter.Void)
go e.Emit("change", "field", "value")
e.On("change", func(event *Event){
// handle changes here
field := event.String(0)
value := event.String(1)
// ...and so on
})
Group merges different listeners into one channel. Example:
e1 := &emitter.Emitter{}
e2 := &emitter.Emitter{}
e3 := &emitter.Emitter{}
g := &emitter.Group{Cap: 1}
g.Add(e1.On("first"), e2.On("second"), e3.On("third"))
for event := g.On() {
// handle the event
// event has field OriginalTopic and Topic
}
Also you can combine several groups into one.
See the api here.
Event is struct that contain event info. Also Event has some helpers to cast arguments into bool
, string
, float64
, int
by given argiment index with optional default value.
Example:
go e.Emit("*", "some string", 42, 37.0, true)
event := <-e.Once("*")
first := event.String(0)
second := event.Int(1)
third := event.Float(2)
fourth := event.Bool(3)
// use default value if not exists
dontExists := event.Int(10, 64)
// or use dafault value if type don't match
def := event.Int(0, 128)
// .. and so on
MIT