TeaVent v0.1.1 TeaVent View Source

TeaVent allows you to perform event-dispatching in a style that is a mixture of Event Sourcing and The ‘Elm Architecture’ (TEA).

The idea behind this is twofold:

  1. Event dispatching is separated from event handling. The event handler (the reducer) can thus be a pure function, making it very easy to reason about it, and test it in isolation.
  2. How subjects of the event handling are found and how the results are stored back to your application can be completely configured. This also means that it is easier to reason about and test in isolation.

The module strives to make it easy to work with a variety of different set-ups:

  • ‘full’ Event Sourcing where events are always made and persisted, and the state-changes are done asynchroniously (and potentially there are multiple state-representations (i.e. event-handlers) working on the same queue of events).
  • a ‘classical’ relational-database setup where this can be treated as single-source-of-truth, and events/state-changes are only persisted when they are ‘valid’.
  • a TEA-style application setup, where our business-domain model representation changes in a pure way.
  • A distributed-database setup where it is impossible to view your data-model as a ‘single-source-of-truth’ (making database-constraints impossible to work with), but the logical contexts of most events do not overlap, which allows us to handle constraints inside the application logic that is set up in TEA-style.

The main entrance point of events is TeaVent.dispatch()

Context Provider function

The configured Context Provider function receives two arguments as input: The current event, and a ‘wrapped’ reducer function.

The purpose of the Context Provider is a bit that of a ‘lens’ (or more specifically: A ‘prism’) from functional programming: It should find the precise subject (the ‘logical context’) based on the topic of the event, and after trying to combine the subject with the event, it should store the altered subject back (if successful) or do some proper error resolution (if not successful).

It should:

  1. Based on the topic in the event (and potentially other fields), find the subject of the event.
  2. Call the passed ‘wrapped’ reducer-function with this subject.
  3. pattern-match on the result of this ‘wrapped’ reducer-function:

    • {:ok, updated_event} -> The updated_event has the subject, changed_subject and changes-field filled in. The context provider.
    • {:error, some_problem} is returned whenever the event could not be applied. The ContextProvider might decide to still persist the event, or do nothing. (Or e.g. roll back the whole database-transaction, etc).

Examples for Context Providers could be:

  • a GenServer that contains a list of internal structures, which ensures that whenever an event is dispatched, this is done inside a call to the GenServer, such that it is handled synchroniously w.r.t. the GenServer’s state.
  • A (relational-)database-wrapper which fetches a representation of a database-row, and updates it based on the results.

Synchronious Callbacks

These callbacks are called in-order with the result of the ContextProvider. Each of them should return either {:ok, potentially_altered_event} or {:error, some_problem}. The next one will only be called if the previous one was successful.

Asynchronious Callbacks

These can be modeled on top of Synchronious Callbacks by sending a message with the event to wherever you’d want to handle it asynchroniously. TeaVent does not contain a built-in option for this, because there are many different approaches to do this. A couple that you could easily use are:

  • Perform a Phoenix.PubSub.broadcast! (requires the ‘phoenix_pubsub’ library). This is probably the most ‘Event-Source’-y of these solutions, because any place can subscribe to these events. Do note that using this will restrict you to certain kinds of topic-formats (i.e. ‘binary-only’)
  • Start a GenStage or Flow-stream with the event result. (requires the ‘gen_stage’/‘flow’ libraries)
  • Spawn a Task for each of the things you’d want to do asynchroniously.
  • Cast a GenServer that does something with the result.

Middleware

A middleware-function is called with one argument: The next (‘more inner’) middleware-function. The return value of a middleware-function should be a function that takes an event, and calls the next middleware-function with this event. What it does before and after calling this function is up to the middleware (it can even decide not to call the next middleware-function, for instance). The function that the middleware returns should take the event as input and return it as output (potentially adding things to e.g. the meta-field or other fields of the event). Yes, it is possible to return non-event structures from middleware, but this will make it difficult to chain middleware, unless the more ‘higher-up’ middleware also takes this other structure as return result, so it is not advised to do so.

Middleware is powerful, which means that it should be used sparingly!

Examples of potential middleware are:

  • A wrapper that wraps the event-handling in a database-transaction. (e.g. Ecto’s Repo.transaction)
  • A wrapper that measures the duration of the event-handling code or performs some other instrumentation on it.

Link to this section Summary

Functions

Creates an event based on the topic, name and data given, and dispatches it using the given configuration

Dispatches the given event (that was created before using e.g. TeaVent.Event.new) to the current TeaVent-based event-sourcing system

Link to this section Types

Link to this type context_provider() View Source
context_provider() ::
  (Event.t(), reducer() -> {:ok, Event.t()} | {:error, any()})
Link to this type middleware_function() View Source
middleware_function() ::
  (middleware_function() -> (Event.t() -> {:ok, Event.t()} | {:error, any()}))
Link to this type reducer(data_model) View Source
reducer(data_model) ::
  (data_model, Event.t() -> {:ok, data_model} | {:error, any()})
Link to this type sync_callback() View Source
sync_callback() :: (Event.t() -> {:ok, Event.t()} | {:error, any()})

Link to this section Functions

Link to this function dispatch(topic, name, data \\ %{}, configuration \\ []) View Source

Creates an event based on the topic, name and data given, and dispatches it using the given configuration.

Required Configuration

  • reducer: A function that, given an a subject and an event, returns either {:ok, changed_subject} or {:error, some_problem}.

Optional Configuration

  • context_provider: A function that receives the event and a reducer-function as input, and should fetch the context of the event and later update it back. See more information in the module documentation.
  • sync_callbacks: A list of functions that take the event as input and return {:ok, changed_event} or {:error, some_error} as output. These are called synchronious, and when the first one fails, the rest of the chain will not be called. See more info in the module documentation.
  • middleware: A list of functions that do something useful around the whole event-handling stack. See more info in the module documentation.
Link to this function dispatch_event(event, configuration \\ []) View Source

Dispatches the given event (that was created before using e.g. TeaVent.Event.new) to the current TeaVent-based event-sourcing system.

Takes the same configuration as TeaVent.dispatch.