# Events

Apps announce what happened through typed events, and other installed apps
react to them. Cicadel uses events where Django uses signals, but registration is
explicit: an app lists the events it emits in `App.Events` and its handlers in
`App.Subscriptions`, and `cicadel.New` checks both before the application starts.
Nothing connects at import time and there is no global registry.

The standard project subscribes in `project/events.go`. A package made with
`startpackage` declares and emits `greeted` as a template.

## Declare and emit an event

Declare each event once, as a package-level variable of the app that owns it,
with that app's label and a lowercase name. The type parameter is the payload
that handlers receive. List the declaration in the app's `Events`, then call
`Emit` with the request, command or hook context:

<!-- example: installable/apps/orders/app.go -->
```go
package orders

import (
    "context"
    "errors"
    "net/http"

    "github.com/fauzaanu/cicadel"
    "github.com/fauzaanu/cicadel/database"
    "github.com/fauzaanu/cicadel/events"
    "github.com/fauzaanu/cicadel/response"
    "github.com/fauzaanu/cicadel/urls"
    "gorm.io/gorm"
)

// Placed is emitted inside the transaction that stores a new order.
var Placed = events.New[Placement]("orders", "placed")

// Placement is the Placed payload. Handlers write through Tx so their work
// commits or rolls back with the order.
type Placement struct {
    OrderID int64
    Tx      *gorm.DB
}

// App stores orders in an orders table created by its recorded migration.
// confirm runs after commit, so a rolled-back order is never confirmed.
func App(db *gorm.DB, confirm func(context.Context, int64) error) cicadel.App {
    return cicadel.App{
        Name:   "orders",
        Events: []events.Declaration{Placed},
        Subscriptions: []events.Subscription{
            Placed.Handle(func(ctx context.Context, e Placement) error {
                id := e.OrderID
                return database.OnCommit(e.Tx, func(ctx context.Context) error { return confirm(ctx, id) })
            }),
        },
        Routes: []urls.Route{urls.Path("POST", "/orders", "orders:place", func(w http.ResponseWriter, r *http.Request) {
            err := database.Atomic(r.Context(), db, func(tx *gorm.DB) error {
                var id int64
                if err := tx.Raw("INSERT INTO orders DEFAULT VALUES RETURNING id").Scan(&id).Error; err != nil {
                    return err
                }
                return Placed.Emit(r.Context(), Placement{OrderID: id, Tx: tx})
            })
            var refused *events.HandlerError
            switch {
            case errors.As(err, &refused):
                response.Error(w, http.StatusConflict) // a handler rejected the order; nothing was stored
            case err != nil:
                response.Error(w, http.StatusInternalServerError)
            default:
                w.WriteHeader(http.StatusCreated)
            }
        })},
    }
}
```

`events.New` panics on an invalid label, like `regexp.MustCompile`, because it
runs when the package is initialized.

## Subscribe

`Event.Handle(fn)` returns a subscription; list it in any installed app's
`Subscriptions`, including the declaring app's own. The project subscribes in
`project/events.go`:

```go
func Subscriptions() []events.Subscription {
    return []events.Subscription{
        orders.Placed.Handle(func(ctx context.Context, e orders.Placement) error {
            return e.Tx.Exec("INSERT INTO order_audit(order_id) VALUES (?)", e.OrderID).Error
        }),
    }
}
```

`go run manage.go apps` lists each app's emitted (`emits`) and handled
(`handles`) events.

## What the application checks

`cicadel.New` fails, naming the app, when:

- an app declares an event whose owner label is a different app;
- the same declaration is listed twice, or two declarations share a name;
- a subscription refers to an event that no installed app declares, for example
  after its owning app was removed (the error asks whether that app is
  installed);
- a subscription has a nil handler or was not created with `Handle`.

Subscriptions refer to the declared Go value, not its name, so a handler always
receives the payload type the emitter sends.

## Delivery

- **Synchronous and ordered.** `Emit` runs handlers on the calling goroutine, in
  app dependency order (the order `manage.go apps` prints), then in the order each
  app lists them. There is no queue and no retry; enqueue a job from a handler
  for slow or retried work.
- **Errors stop delivery.** The first handler error stops the remaining handlers
  and returns a `*events.HandlerError` naming the event and the subscribing app;
  `errors.Is`/`errors.As` reach the handler's own error. The emitter decides what
  that error means. Handler panics propagate, as they would from an HTTP handler.
- **Cancellation.** `Emit` checks the context before each handler and returns
  `ctx.Err()` once it is done. Handlers should honour the context too.
- **Nesting.** A handler may emit other events. Emission nested more than
  `events.MaxDepth` (16) levels deep fails with `events.ErrDepth` instead of
  recursing forever.
- **Concurrency.** The dispatcher is built once by `cicadel.New` and never
  changes, so concurrent requests can emit freely. Handlers that share state must
  synchronize it.

## Transactions

Emit inside the transaction that made the change, with `database.Atomic`, and pass
the transaction in the payload as `Tx` when handlers may need to write. A handler
that writes through `Tx` commits or rolls back together with the change, and a
handler error rolls the change back. Use `database.OnCommit(e.Tx, fn)` for email,
cache invalidation and other effects that must happen only after commit; see
[transactions](https://cicadel.fauzaanu.com/en/0.2.5/howto/transactions/index.md). Handlers must not keep `Tx` after they return.
Emit events that do not describe a database change, such as a rejected request,
outside any transaction.

## Where events are delivered

`Emit` finds the application's dispatcher in the context. It is attached for:

- every request served by the application;
- app `Start` and cleanup hooks;
- app commands, and every command run by `manage.go`, including project commands.

For other work the application owns, such as a River worker or a goroutine
started in `Start`, attach it with `Application.Context`, which also attaches the
signing keys and static-file manifest:

```go
ctx = app.Context(ctx)
```

With no dispatcher in the context, `Emit` has no subscribers and returns nil.
This keeps services usable on their own, but events
emitted from a bare `context.Background()` are not delivered. In package tests,
build a dispatcher directly:

```go
d, err := events.NewDispatcher(
    events.App{Name: "greet", Events: []events.Declaration{greet.Greeted}},
    events.App{Name: "listener", Subscriptions: []events.Subscription{greet.Greeted.Handle(record)}},
)
ctx := events.WithDispatcher(t.Context(), d)
```

or install a listener app beside the package with `apptest.New`.

## Not covered

Model save and delete events are not emitted automatically; use GORM hooks or
emit from the code that saves. Request start and finish are middleware concerns.
Events are in-process: they do not cross processes or survive a restart. Use the
River integration in `jobs` for durable work.
