Files
summercms/modules/festival

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) 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

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.