feat(11.1-04): add the Database section and the core Services pages
- docs/database: models, migrations, queries and pagination, relations, casts and validation, attachments and transactions (lagoon.Transaction, lagoon.AfterCommit, nested savepoints, lagoon.OnDatabase) - docs/services: configuration, events, routing with auth groups, rate limiting, authentication, the OAuth server, mail and localization - runnable Examples for lagoon, attach, compass, surf, wire, bouncer, wristband, postcard, phrasebook and festival; lagoon TestDocs* regions run on the package's Postgres harness through DocsDB - 15 new required pages
This commit is contained in:
98
docs/services/events.md
Normal file
98
docs/services/events.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: Events
|
||||
description: Listen for and fire typed events on the application bus with festival, with priorities, collected results and a halting fire that stops when handled.
|
||||
section: services
|
||||
order: 20
|
||||
---
|
||||
# Events
|
||||
|
||||
`Event::listen` and `Event::fire` are how WinterCMS plugins extend each other. SummerCMS keeps the pattern with [festival](../../modules/festival/README.md): each application has one bus, `backpack.App.Events`, and plugins listen from their `Boot` step for events that other plugins fire. [Extending plugins](../plugins/extending.md) shows where events fit among the other extension points; this page covers the bus itself.
|
||||
|
||||
## Typed events
|
||||
|
||||
An event is a Go type, not a string. A listener is a function that takes a context and the event, and the bus routes by type, so a listener never receives a payload of the wrong shape and a mismatch does not compile. Name events after what happened, and keep them in the package of the plugin that fires them so listeners can import the type.
|
||||
|
||||
`festival.Bus.Listen` registers a listener at priority 0 and `festival.Bus.ListenPriority` at a given priority. Higher priorities run first, and listeners with the same priority run in registration order. The first argument is the ID of the plugin that owns the listener:
|
||||
|
||||
```go src=modules/festival/example_test.go#ExampleBus_Fire
|
||||
bus := festival.New() // in a plugin, use app.Events
|
||||
|
||||
// acme.search and acme.notify extend acme.blog from their Boot steps.
|
||||
bus.Listen("acme.search", func(ctx context.Context, e PostPublished) error {
|
||||
fmt.Println("index", e.Title)
|
||||
return nil
|
||||
})
|
||||
bus.ListenPriority("acme.notify", 10, func(ctx context.Context, e PostPublished) error {
|
||||
fmt.Println("notify subscribers of", e.Title)
|
||||
return nil
|
||||
})
|
||||
|
||||
// acme.blog fires the event; higher priorities run first.
|
||||
if err := bus.Fire(context.Background(), PostPublished{Title: "Hello"}); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
// Output:
|
||||
// notify subscribers of Hello
|
||||
// index Hello
|
||||
```
|
||||
|
||||
`festival.Bus.Fire` runs every listener, even after one fails, and returns the failures joined with `errors.Join`. A listener that panics is recovered and reported as an error that names its plugin, so one faulty plugin cannot stop the others.
|
||||
|
||||
All three dispatch methods run the listeners on the caller's goroutine, before they return. For work that should not delay the request, a listener dispatches a job; see [Queued jobs](jobs.md).
|
||||
|
||||
## Collecting contributions
|
||||
|
||||
WinterCMS events often gather something from their listeners, such as extra form fields or menu items. `festival.Bus.Collect` runs every listener and, after each one, merges the map the event returns from `festival.Collectable.Collected`. A later listener wins when two set the same key. Use a pointer event so listeners can write to it:
|
||||
|
||||
```go src=modules/festival/example_test.go#ExampleBus_Collect
|
||||
bus := festival.New()
|
||||
bus.Listen("acme.seo", func(ctx context.Context, e *PostFormExtended) error {
|
||||
e.fields = map[string]any{"meta_title": "text"}
|
||||
return nil
|
||||
})
|
||||
bus.Listen("acme.gallery", func(ctx context.Context, e *PostFormExtended) error {
|
||||
e.fields = map[string]any{"cover": "fileupload"}
|
||||
return nil
|
||||
})
|
||||
|
||||
fields, err := bus.Collect(context.Background(), &PostFormExtended{})
|
||||
fmt.Println(fields, err)
|
||||
// Output: map[cover:fileupload meta_title:text] <nil>
|
||||
```
|
||||
|
||||
`festival.Bus.Collect` returns the payload gathered so far together with the joined errors, so one failing listener does not lose the others' contributions.
|
||||
|
||||
## Stopping at the first handler
|
||||
|
||||
The WinterCMS halting fire stops at the first listener that returns a result. `festival.Bus.UntilHandled` stops as soon as the event's `festival.Handleable.IsHandled` reports true, or at the first error, and returns whether the event was handled:
|
||||
|
||||
```go src=modules/festival/example_test.go#ExampleBus_UntilHandled
|
||||
bus := festival.New()
|
||||
bus.ListenPriority("acme.pages", 10, func(ctx context.Context, e *SlugResolving) error {
|
||||
if e.Slug == "about" {
|
||||
e.Found = "page"
|
||||
}
|
||||
return nil
|
||||
})
|
||||
bus.Listen("acme.blog", func(ctx context.Context, e *SlugResolving) error {
|
||||
fmt.Println("acme.blog asked for", e.Slug)
|
||||
e.Found = "post"
|
||||
return nil
|
||||
})
|
||||
|
||||
for _, slug := range []string{"about", "hello-world"} {
|
||||
e := &SlugResolving{Slug: slug}
|
||||
handled, err := bus.UntilHandled(context.Background(), e)
|
||||
fmt.Println(slug, handled, e.Found, err)
|
||||
}
|
||||
// Output:
|
||||
// about true page <nil>
|
||||
// acme.blog asked for hello-world
|
||||
// hello-world true post <nil>
|
||||
```
|
||||
|
||||
Here `acme.pages` listens at a higher priority, so it gets the first chance to claim a slug, and `acme.blog` is asked only when no page matched.
|
||||
|
||||
## Events and transactions
|
||||
|
||||
Listeners run where the event is fired, inside any transaction the caller has open. A listener that writes to the database joins that transaction when it uses the transaction handle the event carries. A listener with a side effect outside the database, such as a mail or a broadcast, should defer it with `lagoon.AfterCommit`, so it does not announce a write that rolls back. See [Transactions](../database/transactions.md).
|
||||
Reference in New Issue
Block a user