Osy#betaRun it on Osyrin: osyrin.com

Reference / Function

[On] — hear an event, after the commit that raised it

[On] [Order(n)] void <Name>(<EventClass> e) { … } · [On] void <Name>(Event e) { … }

`[On]` on a function makes it a HANDLER of the event its one parameter is. Every handler of an event — in the app and in every package that sees it — runs after the commit that raised the event, with the app's authority, in one order the compiler fixes; one that throws has its writes discarded and is recorded, and the others still run. One that loses a race with another writer is run again. A handler that would run twice in one chain of events is cut, and that is recorded too. `[On] void H(Event e)` hears every event.

preview2 examples compiled by CIfunctioneventscomposition

Summary#

A handler is an ordinary function with [On], taking the event:

[On]
void WinOnAccept(QuoteAccepted e) {
  if (e.Quote.Opportunity != null) { MarkWon(e.Quote.Opportunity); }
}

It runs once the work that raised the event has committed. A kit may hear the events of a kit it depends on, and an app hears every kit's — nobody registers anything; the compiler finds every [On] in the app and its packages.

Signature#

[On]                 // hears the event its one parameter is
[Order(10)]          // optional — lower runs first; none is 100
void <Name>(<EventClass> e) { … }

[On]                 // hears EVERY event — the app's and every package's
void <Name>(Event e) { … }

Description#

In what order do handlers run?#

One order, fixed when the app compiles, never by file or load order:

  1. [Order(n)], lower first — a handler with none is at 100, so kits that leave gaps of 10 below it run first;
  2. then by package: a package before the packages that use it, the app last;
  3. then by full name.

A handler of every event#

A handler whose parameter is Event itself hears every event, of every class — what a timeline, a rule engine or webhooks need, since they work on events they have never heard of. It asks Events.* what each one is, and Events.Delivery() where it stands: the event's Id, the Root of its cascade, its Depth, when it was RaisedAt, and the Cause an action that raised it was run under. It is never cut from a cascade for having run earlier in it: it hears what it caused, by design, and is held to the depth limit like any other.

Hearing every event is not free: each one it hears is written and delivered. When it only acts on SOME of them — the ones a rule is on, the ones a subscription takes — say which with [[function-interest|[Interest]]], and an event it does not care about is not written for it at all. A read it makes for every event can be made once for all the events one commit raised with [[function-once-per-delivery|[OncePerDelivery]]].

When does it run?#

After the commit that raised the event — in process, straight after it, so a handler's work is there on the next line. A workflow's step commits more than once (a run's Start {} before its first state is entered), so an event raised in a step is delivered when the step — and every run its commit started — is done: a handler never meets a run half-way. All of one event's handlers commit together, with the event marked delivered, so their work lands once — and the events one piece of work raised (a commit, or a workflow step and every run it started) are delivered together, in the order they were raised, in one transaction: a ticket that raises three events costs one delivery, not three. A handler of the second event sees what the first event's handlers did, exactly as it would after their commit.

What if a handler fails?#

Its writes are discarded, the failure is recorded with the event — which handler, and what it said — and the handlers after it still run. One handler's fault never takes the others' work with it. It is not run again: a fault in its code would fail again, and a second run would repeat what it did before it threw.

The one exception is a LOST RACE — another writer changed a row the handler changed, first. That is not a fault, so the handler is run again, reading the row as it now stands, a few times; only one that keeps losing is recorded.

When events are delivered together and one of them cannot land — a handler loses a race, or a write is refused only at the commit — nothing of the batch is kept, and each event is delivered on its own, exactly as above. So a failing handler never takes another event's work with it, and is recorded against its own event.

What if handlers raise events that come back?#

Each event carries the handlers that ran on the way to it. A handler already on that path is not run again — that is a loop — and the cut is recorded on the event it would have run on. A chain more than eight handlers deep is cut too.

Whose authority does a handler run with?#

The APP's, as a workflow body does: it runs after the person's request has ended, so there is no caller to run as. A kit's deny still holds it — a handler is held to every package's write denies except its own package's. To act as a person, run an [[function-action|[Action]]] as them: what that action raises belongs to this event's cascade, so a rule set off by an event cannot set itself off again.

What may a handler look like?#

A top-level function with exactly one parameter, an [Event] class in sight (or Event), returning nothing — nobody waits for its answer. A package's handler may be internal: the platform calls it, and nothing else can. Anything else is refused where it is written:

'TwoParams' hears an event, so it takes exactly ONE parameter — the event: `[On] void TwoParams(QuoteAccepted e)`.
An event handler returns nothing — nobody waits for its answer, because it runs after the commit that raised the event.
`[Order]` places an event handler among the others — 'NoOn' has no `[On]`, so there is nothing for it to place.

Examples#

Three handlers, and the middle one fails#

[Order] places them; the second throws, its write is discarded, and the first and third still run.

entity Ticket {
  [Required, MaxLength(100)] string Subject;
  [MaxLength(200)] string Trail = "";
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

[Event("Ticket closed")]
public sealed class TicketClosed { public Ticket Ticket; }

void Close(Ticket t) { Raise(new TicketClosed { Ticket = t }); }

[On] [Order(10)]
void First(TicketClosed e) { e.Ticket.Trail = e.Ticket.Trail + "first;"; }

[On] [Order(20)]
void Second(TicketClosed e) {
  e.Ticket.Trail = e.Ticket.Trail + "second;";
  throw new ValidationException("the second one fails");
}

[On] [Order(30)]
void Third(TicketClosed e) { e.Ticket.Trail = e.Ticket.Trail + "third;"; }
[Test]
void A_failing_handler_leaves_the_others_their_work() {
  var t = new Ticket { Subject = "Printer jammed" };
  Close(t);
  Background.Settle();
  Assert.Equal("first;third;", Ticket.Single().Trail);
}

See also#

Related

[Event] — what happens to a record, declared once

An event is a class marked `[Event]`: something that happens to a record, declared once by the code that owns the…

Raise(e) — say that something happened

`Raise(e)` says that the event `e` happened — an instance of a class marked `[Event]`. It is written in the same commit…

try / catch / finally

Handles a fault. C#'s syntax, including typed catches, catch filters and finally. The rows written inside a try block…

[Action] — what an app offers to be done, by name

Mark a function `[Action]` and the app offers it to be done BY NAME — by a rule, a macro, a tool — with everything a…

security { }

The rules that decide who may read and write an entity's rows. A where clause filters by the row (the owner sees their…

[Interest] — the events a handler of every event cares about

`[Interest(nameof(Triggers))]` beside the `[On]` of a handler of EVERY event names a function that answers which event…

[OncePerDelivery] — a read made once for every event of a delivery

`[OncePerDelivery]` on a function that reads makes it answer once per delivery: the events one commit raised are…