Documentation ¶
Overview ¶
Package ed implements an event dispatcher that is meant to allow codebases to organize concerns around events. Instead of placing all concerns about a particular business logic event in one function, this package allows you to declare events by type and dispatach those events to other code so that other concerns may be maintained in separate areas of the codebase.
Stability ¶
This module is tagged as v0, thus complies with Go's definition and rules about v0 modules (https://go.dev/doc/modules/version-numbers#v0-number). In short, it means that the API of this module may change without incrementing the major version number. Each releasable version will simply increment the patch number.
Given the surface area of this module is quite small, this should not be a huge issue if used in production code.
Register/Dispatch events ¶
ed uses Go's own type system to dispatch events to the correct event handlers.
By simply registering an event handler that accepts an event of a particular type:
ed.Register(func(ctx context.Context, event MyEvent) error { fmt.Println("neat! got an event!") return nil })
That handler will be triggered when an event of the same type is dispatched from somewhere else in the program:
err := ed.Dispatch(ctx, MyEvent{Handy: "Info"}) // neat! got an event!
Because it is Go's type system, event handlers can also be registered against interfaces instead of just concrete types. Thus, creating an event handler that will be triggered for all events is as simple as:
ed.Register(func(ctx context.Context, event any) error { fmt.Println("give me all the events!") return nil }) type CreateEvent interface { IsCreatedEvent() } ed.Register(func(ctx context.Context, createEvent CreateEvent) error { fmt.Println("give me all events about stuff getting created.") return nil })
Goals ¶
Overall, this package's goal is to mostly be an aid in allowing application business logic to remain clear of ancillary conerns/activities that might otherwise pile up in certain code areas.
For example, as a SaaS company grows, a lot of cross-cutting concerns can build up around a customer signing up for a service. In a monolithic codebase/service, the code that handles a new customer signup would become full of "do this check", "send this data to marketing systems", "screen this signup for abusive behavior" logic.
This is where ed is meant to be: taking all of those concerns/side-effects that are ancillary to the core act of signing up a user, and placing them in there own code areas and allowing them to be communicated with via events.
Non-goals ¶
This package is not trying to be a job queue, or a messaging system. The dispatching of events is done synchronously and the events are designed to allow errors to be returned so that use-cases like synchronous validation are supported.
That being said, a common use-case might be to use this package to allow the publishing of events/messages to a Kafka/SQS-like system, an event handler:
ed.Register(func(ctx context.Context, event UserSignupEvent) error { return kafka.Send(ctx, KafkaMessage{Topic: "send_welcome_email", Body: json.MustEncode(event)}) })
Then in your main business logic for user signups:
func UserSignupEndpoint(ctx context.Context, request UserSignupRequest) (UserSignupResponse, error) { // check email not in use // check password strength err := db.SaveUser(...) if err != nil { /* ... */ } if err := ed.Dispatch(ctx, UserSignupEvent{/* ... */}); err != nil { return UserSignupResponse{}, err } // The welcome email was sent! // ... }
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Dispatch ¶
Dispatch will send the provided event to all registered handlers and wrappers and allow them to return an error if necessary.
func Register ¶
Register binds a Handler to a particular event by it's event type (E). The registered event type may also be an interface, to allow for capturing multiple types in one handler.
func Using ¶
func Using[E any](r *Dispatcher) interface { // Wrap does the equivalent of [Wrap] on an explicit [Dispatcher]. Wrap(wrapper Wrapper[E]) // Register does the equivalent of [Register] on an explicit [Dispatcher]. Register(handler Handler[E]) // Dispatch does the equivalent of [Dispatch] on an explict [Dispatcher]. Dispatch(ctx context.Context, event E) error }
Using allows an instance of Dispatcher to be used with a specific type. The returned value of this is not meant to be "long-lived" or stored anywhere; rather used ephemerally and called as a chain.
func Wrap ¶
Wrap will allow a Wrapper function to be called before any Handler of a matching event. The use-case for wrapping tends to be things like observability (logging, metrics, tracing, etc.). Wrapper functions that match a particular Dispatch will all be called serially in the order they were setup.
Types ¶
type Dispatcher ¶
type Dispatcher struct {
// contains filtered or unexported fields
}
Dispatcher is the object that represents how events of particular types are routed to registered Handler and Wrapper.
type Handler ¶
Handler is a function responsible for handling an event. Returning an error from a Handler function will cause the entire dispatch operation for a Dispatch() to be canceled and will return the error.
type Wrapper ¶
Wrapper is a function that will be called before an Handler is called for a particular Dispatch call. With each Dispatch call, zero or many Wrapper functions might be called, but they will all be guaranteed to be called serially. Once all wrapper functions have invoked their next(), the actual Handler functions will be invoked.