Emitter
This guide covers events in Tinka. You will learn how to:
- Define events as Go types and emit them
- Add listeners, and remove them
- Wait for listeners, run them in order, or run them in the background
- Handle errors from listeners in one place
- Check emitted events in tests without running listeners
Overview
Events let one part of an App announce that something happened, and other parts react, without knowing about each other. After a user signs up, the App sends a welcome email, adds the user to the CRM and records the sign-up for analytics. The sign-up code shouldn't know about any of that: it emits a UserRegistered event, and a listener for each concern reacts.
The events package (github.com/mucolabs/tinka/events) delivers events. An event is any Go type, usually a struct holding what listeners need. A listener is a function that receives it. Because the event's type is its name, the compiler checks that every emit and every listener agree on what the event holds.
Create an emitter
Create one Emitter for the App, as a singleton in its container, so the App waits for background listeners when it shuts down:
app.Container().Singleton(func(*di.Resolver) (*events.Emitter, error) {
return events.New(), nil
})Define and emit events
Define each event as a type. Emit it where it happens, passing the request's context:
package main
import (
"net/http"
"github.com/mucolabs/tinka"
"github.com/mucolabs/tinka/events"
)
type UserRegistered struct {
User User
}
type Accounts struct {
Events *events.Emitter
Users *UserStore
}
func (a Accounts) SignUp(c *tinka.Ctx) (User, error) {
in, err := c.Body[SignUpForm]()
if err != nil {
return User{}, err
}
user, err := a.Users.Create(c, in.Email)
if err != nil {
return User{}, err
}
if err := a.Events.Emit(c, UserRegistered{User: user}); err != nil {
return User{}, err
}
c.Status(http.StatusCreated)
return user, nil
}Add listeners
On adds a listener. Its event parameter's type says which events it receives. Register listeners while the App starts, next to your routes:
func listen(bus *events.Emitter, mailer *Mailer, crm *CRM) {
bus.On(func(ctx context.Context, e UserRegistered) error {
return mailer.SendWelcome(ctx, e.User)
})
bus.On(func(ctx context.Context, e UserRegistered) error {
return crm.AddContact(ctx, e.User.Email)
})
}A listener receives events of its exact type only: a type that embeds UserRegistered is another event. Once adds a listener for the next event only, and OnAny adds one for every event, such as one that logs them:
bus.OnAny(func(ctx context.Context, e any) error {
logger.InfoContext(ctx, "event", "type", fmt.Sprintf("%T", e))
return nil
})On and OnAny return a function that removes the listener. Clear[T] removes every listener of one event type, and ClearAll removes them all.
Wait, run in order, or run in the background
Emit runs every listener at once and waits for all of them, so the handler continues once the reactions are done. It returns their errors together.
EmitSerial runs listeners one at a time, in the order they were added, and stops at the first error: use it when a listener depends on the one before.
EmitAsync returns at once and runs the listeners in the background, for work the user shouldn't wait for, such as sending an email:
if err := a.Events.Emit(c, UserRegistered{User: user}); err != nil {
return User{}, err
}
a.Events.EmitAsync(c, UserRegistered{User: user})Background listeners keep the request's context values, such as its ID for the logs, but not its cancellation, so they finish after the response is sent. When the App shuts down, it waits for them, within its shutdown timeout.
Handle errors
A listener that returns an error or panics doesn't stop the others: a panic becomes an error instead of crashing the App. Emit and EmitSerial return the errors. Errors from EmitAsync have no caller to return to, so they're logged.
OnError sends every listener's error to one function instead, such as one that reports to your error tracker. With it set, Emit and EmitSerial return nil:
bus.OnError(func(ctx context.Context, event any, err error) {
logger.ErrorContext(ctx, "event listener failed", "event", fmt.Sprintf("%T", event), "err", err)
})Fake events in tests
To test code that emits events without running their listeners, such as ones that send real email, call Fake. Events are recorded instead of delivered, and the returned Recorder checks them. Restore the emitter when the test ends:
func TestSignUp(t *testing.T) {
bus := events.New()
rec := bus.Fake()
t.Cleanup(bus.Restore)
accounts := Accounts{Events: bus, Users: newUserStore(t)}
signUp(t, accounts, "ada@example.com")
rec.AssertEmitted(t, func(e UserRegistered) bool {
return e.User.Email == "ada@example.com"
})
rec.AssertNotEmitted[OrderShipped](t)
}Pass event types to fake some only: bus.Fake(events.TypeOf[UserRegistered]()) records sign-ups while other events reach their listeners.
See also: Application lifecycle, Dependency injection, Logger