Files
summercms/modules/festival/README.md
Jakub Zych 602e0412f3 docs: add a Role in the framework section to every module README
Each module README now says where the module sits in the dependency
graph, why it is its own module, and lists use cases for application
developers.
2026-10-11 00:58:17 +02:00

97 lines
4.7 KiB
Markdown

# festival
Typed, synchronous event bus with listener priorities, payload collection and stop-when-handled dispatch.
`import "git.golem15.com/golem15/summercms/modules/festival"`
**Name:** A summer festival announces events and the crowd reacts; it is the event bus.
## 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.
## Role in the framework
A dependency-free core primitive owned by `backpack`, so every plugin reaches the bus through the application. It is in-process and synchronous. Delivery to browsers is `lighthouse` and delivery to devices is `flare`; those depend on the database and jobs, so they cannot sit under `backpack` without an import cycle.
Use cases:
- Let one plugin react to another without importing it, for example sending a welcome mail when a user registers.
- Let plugins veto or change an action by priority, the WinterCMS "before" event style.
- Collect contributions from several listeners, such as extra fields for an API response.
## 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.