Observers
Observers are the elements within an Arez application that react to changes. Observers are typically about
initiating effects. Each observer is associated with an observed
function. When the function executes, Arez
tracks which observable values and computable values are accessed
within the scope of the function and these elements are recorded as dependencies of the observer. Any
time a dependency is changed, Arez will invoke a hook function that will ultimately result in scheduling
the observer to re-execute the observed function. The Observer
class represents the observer within
the Arez toolkit.
The observed function can either be invoked by the Arez runtime or by the host application. Using Arez as the executor is the default choice but sometimes it is useful to designate the application as the executor. Application invocation of observed functions is useful when Arez needs to integrate into other scheduler frameworks. In most cases that use an external executor, Arez pushes change notifications to a logical queue and the external framework pulls change notifications during their own update phase. For example, the React4j library uses an external executor so that Arez can be integrated into reacts scheduler.
An application can provide an onDepsChange
hook function when creating the observer and Arez will invoke
the hook method when Arez detects that a dependency has changed. If the observer needs to support application
executor then the hook function must be provided. If the hook function is provided but the observed function
is not expected to be invoked by the application, then the application code must invoke the
Observer.schedule()
method either in the hook method or at some later time.
This will schedule the Observer
so that the observed function is invoked by the Arez runtime
next time that the Arez scheduler is triggered.
If an observer is created without a onDepsChange
hook function then Arez will automatically defines an
onDepsChange
function that immediately reschedules the observer. Other reactive frameworks often refer
to this type of an observer as an "autorun" observer.
API
There are several low-level ArezContext.observer(*)
methods that can be used to create observers, however most users will use more high-level APIs such as
the @Observe annotation.
An example of a basic observer:
Arez.context().observer( () -> {
// Interact with arez observable state (or computable values) here
// and any time these changed this function will be re-run.
...
} );
An example of an observer that is explicitly named and uses a read-write transaction:
Arez.context().observer( "MyObserver", () -> {
// Interact with arez observable state (or computable values) here
// and any time these changed this function will be re-run.
...
}, Observer.Flags.READ_WRITE );
A "tracker" observer is created with a onDepsChange
hook function but no observed
function. i.e. A
tracker observer uses an application executor. Using a tracker observer is a little more complex within Arez.
The developer must explicitly create the observer via ArezContext.tracker(*)
invocation and then explicitly observe the observed function via ArezContext.observe(*)
.
A very simple example of a tracker observer:
final Procedure observedFunction = () -> {
// Interact with arez observable state (or computable values) here
// and any time these changed the rescheduleRender function will
// be run which will somehow reschedule this function.
...
};
final Observer observer = Arez.context().tracker( () -> rescheduleRender() );
...
// The rescheduleRender should ultimately result in the following
// invocation. This call will need to be run at least once so that
// the Arez runtime can determine the dependencies and reschedule
// when the dependencies are changed.
Arez.context().observe( observer, observedFunction );
Error Handling
If an observer's observed
function is executed by the Arez runtime and throws an exception, then that exception
is caught by the Arez scheduler and passed to an error handler. The same occurs if an exception is thrown invoking
an observers onDepsChange
function. This is to make sure that an exception in one observer does not
prevent the scheduled execution of other observers. This also allows observers to recover from exceptions; throwing
an exception does not break the tracking done by Arez, so a subsequent scheduling of an observer will complete
normally again if the cause for the exception is removed.
The error handlers are added and removed from the ArezContext
by way of the ArezContext.addObserverErrorHandler(ObserverErrorHandler)
and ArezContext.removeObserverErrorHandler(ObserverErrorHandler)
methods. A simple example that emits errors to the browsers console in a browser environment follows:
Arez.context().addObserverErrorHandler( ( ( observer, error, throwable ) -> {
Console.error( error + ": Error occurred", throwable );
} ) );