- architecture: introduction, Go modules and workspaces, application lifecycle, request lifecycle - plugins: registration, scheduling, extending, testing - verified Examples for backpack services, towel request context, pact schedules and festival events - TestDocsRequiredPages lists the eight new pages
festival
Typed, synchronous event bus with listener priorities, payload collection and stop-when-handled dispatch.
import "git.golem15.com/golem15/summercms/modules/festival"
Overview
festival is the SummerCMS counterpart of WinterCMS's Event::listen and Event::fire, the mechanism plugins use to extend each other without direct calls. Events are routed by their Go type rather than by a string name, so a listener registered for *PostPublished receives exactly that type, and a payload mismatch is a compile error. Each application owns one bus: backpack creates it in backpack.New and exposes it as backpack.App.Events, and plugins register listeners from their Boot step.
Features
- Listener registration with
festival.Bus.Listen(priority 0) andfestival.Bus.ListenPriority. Higher priorities run first; listeners with equal priority run in registration order. Every listener carries the ID of the plugin that owns it. - Three dispatch modes, all synchronous on the caller's goroutine:
festival.Bus.Fireruns every listener and returns the joined errors of all that failed (errors.Join).festival.Bus.Collectruns every listener and, after each one, merges the event'sfestival.Collectable.Collectedmap into a single payload (later keys win). It returns the payload gathered so far together with the joined errors.festival.Bus.UntilHandledstops at the first error or as soon as the event'sfestival.Handleable.IsHandledreports true, and returns whether the event was handled (WinterCMS's halting fire).
- Panic isolation: a panicking listener is recovered and reported as an error that names its owner plugin, so one faulty plugin cannot take down the dispatch.
- Safe for concurrent use: registration is locked and dispatch works on a snapshot of the listener list.
- Value and pointer types are distinct event types; events that listeners modify (for
CollectandUntilHandled) are usually pointers.
Usage
package blog
import (
"context"
"git.golem15.com/golem15/summercms/modules/festival"
)
// PostPublished is fired after a post goes live. Listeners add payload
// entries and may mark the event handled.
type PostPublished struct {
PostID uint
payload map[string]any
handled bool
}
func (e *PostPublished) Collected() map[string]any { return e.payload }
func (e *PostPublished) IsHandled() bool { return e.handled }
func publish(ctx context.Context, bus *festival.Bus) (map[string]any, error) {
bus.ListenPriority("acme.search", 10, func(ctx context.Context, e *PostPublished) error {
if e.payload == nil {
e.payload = map[string]any{}
}
e.payload["indexed"] = true
return nil
})
return bus.Collect(ctx, &PostPublished{PostID: 42})
}
The event type is inferred from the listener's parameter, so this listener only receives *PostPublished events. In a plugin, the bus is app.Events on the backpack.App passed to Boot; festival.New is for tests and standalone use.
API reference
| Identifier | Description |
|---|---|
festival.Bus |
Application-owned, type-keyed event dispatcher. |
festival.New |
Returns an empty bus. |
festival.Bus.Listen |
Registers a listener for event type T at priority 0. |
festival.Bus.ListenPriority |
Registers a listener for event type T at an explicit priority. |
festival.Bus.Fire |
Runs every listener and joins their errors. |
festival.Bus.Collect |
Runs every listener and merges the event's collected payload. |
festival.Bus.UntilHandled |
Runs listeners until one handles the event or fails. |
festival.Collectable |
Implemented by events that expose a mergeable payload for Collect. |
festival.Handleable |
Implemented by events that can stop UntilHandled. |
Dependencies
- SummerCMS modules: none.
- Third-party: none.
- Standard library:
context,errors,fmt,reflect,sort,sync.
Testing
go test ./modules/festival/...
The tests use in-process listeners and need no external services.