Events

Saying what happened, and listeners now or in the queue

# Events

```pascal
uses Askr.Events;

type
  TUserRegistered = class(TEvent)
  private
    FUserId: Int64;
    FEmail: string;
  published
    property UserId: Int64 read FUserId write FUserId;
    property Email: string read FEmail write FEmail;
  end;
```

```pascal
{ at startup }
Listen(TUserRegistered, @AddToNewsletter);
ListenQueued(Queue, TUserRegistered, 'welcome-mail', @SendWelcome);

{ where it happens }
E := TUserRegistered.Create;
E.UserId := U.Id;
E.Email := U.Email;
DispatchEvent(E);
```

```pascal
procedure SendWelcome(E: TEvent);
var
  R: TUserRegistered;
begin
  R := TUserRegistered(E);
  ...
end;
```

The code that registers a user says what happened. It does not need to know
that a newsletter and a welcome mail follow, and neither of them needs to be
written into it.

## Now, or in the queue

**`Listen` runs the listener now**, inside `DispatchEvent`, before it
returns, in the order the listeners were registered. An exception from a
listener comes out of `DispatchEvent`: a listener that failed quietly is a
welcome mail nobody knows was never sent.

**`ListenQueued` runs it in the queue.** The event crosses as JSON, is built
again in a worker, and gets what every job gets: retries with backoff, and
the failed table when they run out. What should not hold up a request — a
mail, a call to somebody else's API — or should survive a failure, goes
there. The name is the job's name and has to be unique.

A listener for a class hears its subclasses too. A queued listener gets the
class that was dispatched, not the one it asked for, so a subclass's own
fields arrive as well.

`DispatchEvent` owns the event and frees it when every listener has had it.

## What crosses the queue

The published properties: strings, integers, `Int64`, booleans,
enumerations, `Currency`, floats and `TDateTime`. Each arrives exactly as it
was sent — `Int64` past 2^53, `Currency` to the fourth decimal, a `Double`
to the last bit, a `TDateTime` to the millisecond, and text outside ASCII.

**`ListenQueued` refuses anything else and names the property.** An object
or a list would arrive empty, and nothing would say so. Carry an id, not an
object: a model loaded in the request is not the row the worker sees a
second later.

A property the sender did not have — an event queued before a deploy that
added it — keeps what the constructor gave it. A class the worker does not
know fails the job, with a message that says so, rather than arriving as
some other class.

**With a durable queue, register what may arrive.** A worker finds the class
by its name. The class a listener asks for, and every class dispatched in
this process, are known already; but a job in the database can be taken by
another process of the same binary that never dispatched it. Call
`RegisterEvent(TBigOrderPlaced)` at startup for a subclass that is
dispatched to a queued listener. Two event classes with the same name are
refused: the queue could not tell them apart.

## Listening is set up once

Listeners are registered at startup, before the server takes requests. The
lists are read by every worker without a lock, so registering while requests
are running is not safe. `ClearListeners` forgets them all, for a test.

It is `DispatchEvent`, not `Dispatch`: inside a class, `Dispatch` is
`TObject.Dispatch`.

## Model events are something else

A model's `BeforeSave`, `AfterInsert` and the rest are virtual methods on the
model — see [Models](models.md). They are for what the model itself has to
do. An event is for what other code wants to hear about; a model's
`AfterInsert` is a good place to dispatch one.

## What is not here

**No wildcard or string-named events.** An event is a class, so the
compiler knows which fields a listener can read.

**No subscribers** — one class registering many listeners. A procedure that
calls `Listen` a few times is the same thing.

**No stopping propagation.** Every listener hears every event it asked for.
A listener that wants the others not to run is two concerns in one event.

**No events across processes, other than through the queue.** Telling a
browser that something happened is a different thing, and not this.