navree
Documentation

Events and observers

Model events from retrieved to restored: observers, closure listeners, typed application events and real-time broadcasts, when each hears an event inside a unit of work, canceling writes, quiet operations, custom events and testing with ModelEventFake.

On this page

Every save, delete and read of a record goes through a fixed series of steps, and each step is a model event your code can hear: creating before a new record is inserted, created after, saving around both inserts and updates, and so on. Listeners use them to keep data tidy, refuse a write, notify someone after a deal is won, or push the change to open browsers.

Rust

The events

EventFired byCan cancel
retrievedevery record a query reads
saving, savedevery save (also of a record with no change), create, update, touch, restoresaving
creating, createdinserting a new recordcreating
updating, updatedupdating a changed record, increment, decrementupdating
deleting, deleteddelete, force_delete, destroydeleting
trasheda soft delete
force_deleting, force_deletedforce_delete of a soft-deleting modelforce_deleting
restoring, restoredrestorerestoring
replicatingreplicate_on

A new record runs saving, creating, the INSERT, created, saved; a changed one saving, updating, the UPDATE, updated, saved. A soft delete runs deleting, trashed, deleted.

The -ing events run before the database is touched. Their listeners may change the record (fill a column, normalize a value) or cancel the operation by returning Flow::Cancel: save and delete then return false and write nothing. An error from an -ing listener stops the operation with that error. To refuse a write with an error of your own, return OrmError::refused(model, error). The error stays the source of the OrmError::Refused the operation returns, so the error mapping registered for it answers when a handler's ? passes it on:

Rust

Inside a unit of work

Most writes happen inside a unit of work, the request's or one you open. There, the -ing events run at once, in the transaction, so a listener can still change or refuse the write. Listeners hear the write events (created, updated, saved, deleted, trashed, force_deleted, restored) after the unit of work commits, on a snapshot of the record as it was when the event happened, so get_changes, was_changed and get_original describe that write. A write that rolls back is never announced: a "deal won" mail can't go out for a deal that wasn't won.

An after-commit listener's error is logged and reported to the app's telemetry; it can't undo the commit. Outside a unit of work, every listener hears every event at once. Listeners run with the operation's unit of work as Uow::current(), so a job or event they dispatch joins it.

Observers

An observer implements Observer<M>: one async method per event, all optional (retrieved, saving, creating, created, updating, updated, saved, deleting, deleted, trashed, force_deleting, force_deleted, restoring, restored, replicating, and custom for custom events). The -ing methods return Result<Flow>; the others Result<()>.

A stateless observer can be declared on the model, where it runs on every pool, even one opened without an app:

Rust

A module registers observers too. observe takes a value; observer lets the container build it, so it can inject services:

Rust

An observer whose "-ed" methods must write in the same transaction as the change, such as a history table that commits or rolls back with it, sets const IN_TRANSACTION: bool = true: its methods then run at once, inside the unit of work.

Closure listeners

For one event, a closure is enough:

Rust

The second type parameter is what the closure returns: (), or Flow to cancel. Closure listeners follow the same timing as observers.

Application events

A model can turn its events into typed application events, which navree-events delivers to their listeners (queued ones included):

Rust

dispatches(..) maps any model event to an event type built From<&Record<M>>. "-ed" events are emitted after the commit; an -ing event at once, and a failing listener then refuses the write. An app without EventsModule dispatches nothing, and logs a warning saying so once.

Broadcasting changes

A model marked broadcasts pushes its changes to real-time channels after each commit, on created, updated and deleted (and trashed and restored for soft-deleting models):

Rust
DefaultChange it with
Channelsthe record's private channel, private-deal.{key}broadcast_on (no channels: no broadcast)
Event namedeal.created, deal.updated, ...broadcast_as
Data{"deal": <the record's JSON>}broadcast_with
Deliveryright after the commitbroadcast_queue: a job on that queue

Every method has a default, so impl Broadcasts for Deal {} broadcasts on the record's channel. The app needs RealtimeModule (a dyn Broadcaster). without_broadcasting(future) silences broadcasts while the rest of the listeners run.

Quiet operations

A data fix or an import often must not notify anyone. without_events runs a future with every listener silenced:

Rust

Observers, closure listeners, application events and broadcasts hear nothing; the write still records its change event for the audit log. The silence belongs to the task, so concurrent requests are unaffected. The _quietly methods do the same for one call: save_quietly, update_quietly, delete_quietly, touch_quietly, increment_quietly, decrement_quietly, create_quietly, restore_quietly, force_delete_quietly, and the relations' create_quietly and save_quietly.

Mass updates and deletes never fire events: they write rows without reading them.

Behaviors

A type that implements ModelHook<M> directly, with before and after methods taking the event, is a behavior: it runs at once for every event, in quiet operations too. The ORM's own features are behaviors (touching parents, filling a workspace column), so they hold however a write happens, and so is the guard of shared libraries. Write one when a rule must never be skipped; write an observer otherwise. A behavior learns the operation's current workspace from navree::orm::workspace::current_in(ctx).

Custom events

A record can fire events of its own, which observers hear in custom and closure listeners under ModelEvent::Custom(name):

Rust

Custom events run where they are fired, at once; a listener may cancel. Built-in names are refused. record.replicate_on(&uow) copies a record (like replicate) after its listeners heard replicating.

Testing

ModelEventFake stands in for every listener: put it in the test app, and the events listeners would have heard are recorded instead, at the moment they would have heard them. Behaviors still run.

Rust

fired() lists every recorded event with its model and key, keys::<M>(event) the keys an event fired for, assert_nothing_fired() checks silence, and clear() starts over.