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:
- 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. - 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:
- Based on the
topic
in the event (and potentially other fields), find thesubject
of the event. - Call the passed ‘wrapped’
reducer
-function with thissubject
. pattern-match on the result of this ‘wrapped’
reducer
-function:{:ok, updated_event}
-> Theupdated_event
has thesubject
,changed_subject
andchanges
-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 oftopic
-formats (i.e. ‘binary-only’) - Start a
GenStage
orFlow
-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
middleware_function() :: (middleware_function() -> (Event.t() -> {:ok, Event.t()} | {:error, any()}))
reducer(data_model) :: (data_model, Event.t() -> {:ok, data_model} | {:error, any()})
sync_callback() :: (Event.t() -> {:ok, Event.t()} | {:error, any()})
Link to this section Functions
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.
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
.