Observers

Observers are decoupled from the events they observe. This is because we're not always observing events! We may be observing a route like '/:id' or Route[':id'] or a status code like Status[404], and we can trigger events for these objects too. Think of events as the building blocks/structure, and observers as the wiring/glue that connects them together.

Nodes include observers by default, but you can include them into any class:

class MySubscriber
  include Observers
end

Observing

Subscriber to Publisher

class MySubscriber
  include Observers
  observe MyPublisher

  def self.handle
    # This class method will be called upon trigger.
  end
end

Publisher to Subscriber

Add observers from the object being observed with:

class MyPublisher
  include Observers
  observers << MySubscriber
end

Anywhere to Subscriber

Reference an object other than self to observe:

observers(my_object) << my_observer

Ordering

Observers are ordered, called in the order that they are defined but can be reordered via an array-like interface.

MyEvent.define do |observers|
  observers # Access existing observers array.
  observers << MyObserver # Add an observer at the end.
  observers = [MyObserver, *observers] # Add an observer at the start.
end

By defining observers on events this way we are effectively able to replicate the traditional tracing of execution from the call site, but from an event.

For observing objects that aren't events such as routes you can do:

Observers['/:user_id' => :get].define |observers|
  observerse << my_observer
end

Tip

You can add observers on either side; from the object being observed or the observer. When ordering is important, add observers from the observed side.

Actions

Note

Events define their own actions and observers CANNOT change the action. Events retain control over which actions are called. This allows for a predictable one-directional flow between events. Complex workflows can be reasoned about more easily without worrying that an observer somewhere is overriding every action on an event and executing unrelated behaviour and output.

Call the my_action method on all observers of MyPublisher with:

class MyPublisher
  include Observers
  trigger action: :my_action
end

Events

Trigger events on observers with the event keyword argument:

class MyPublisher
  include Observers
  trigger event: MyEvent.new(my_data)
end

Events define their own actions, or you can override them when triggering:

class MyPublisher
  include Observers
  trigger event: MyEvent.new(my_data), action: :my_action
end

ℹ️ All observers to MyPublisher will have their my_action(event:) method called with the event passed in as a keyword argument.

With Keys

Call actions on all observers of a differeent object/key with a key: keyword argument:

trigger key: OtherPublisher, action: :my_action
trigger key: OtherPublisher, action: :my_action
trigger key: OtherPublisher, action: :my_action, event: MyEvent.new(event_data)

Action Accepting [UNRELEASED]

In this example we're observing the route for all types of HTTP requests, but only accepting the QUERY HTTP verb as our action/method:

observe Route['/:question'] => :query

Action Forwarding [UNRELEASED]

You can redirect an action to call a method of a different name:

observe Route[QUERY => '/:question'] => { query: :answer }

This forwards the query: action to the :answer action/method.

Action Types