# 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](../backpack/README.md) 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) and `festival.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.Fire` runs every listener and returns the joined errors of all that failed (`errors.Join`). - `festival.Bus.Collect` runs every listener and, after each one, merges the event's `festival.Collectable.Collected` map into a single payload (later keys win). It returns the payload gathered so far together with the joined errors. - `festival.Bus.UntilHandled` stops at the first error or as soon as the event's `festival.Handleable.IsHandled` reports 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 `Collect` and `UntilHandled`) are usually pointers. ## Usage ```go 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 ```sh go test ./modules/festival/... ``` The tests use in-process listeners and need no external services.