A reducer over stored events to build the current state.
A projection is built from an :initial_state, a map of :handlers keyed by
event type, and the :tags for filtering on events:
is_cancelled_projection =
Sourced.Projection.new(
initial_state: false,
tags: ["order:1"],
handlers: %{
OrderCancelled => fn _state, _event -> true end,
OrderReopened => fn _state, _event -> false end
}
)The handlers declare the event types, so the query falls out of the
definition: the projection above carries
[%{types: [OrderCancelled, OrderReopened], tags: ["order:1"]}]. Handlers
take the state and the Sourced.StoredEvent, and must be pure.
Sourced.DecisionModel pairs a projection with the store, folding it into a
state and the append condition that keeps it valid. See
dcb.events for the model itself.
Named handlers
Anything varying per decision, such as the id in a tag, belongs in a function that returns a projection. Handlers can be captures of named functions, which keeps the transitions individually testable:
defmodule MyApp.Orders do
alias Sourced.Projection
def shipped?(order_id) do
Projection.new(
initial_state: false,
tags: ["order:#{order_id}"],
handlers: %{
OrderShipped => &shipped/2,
OrderCancelled => &cancelled/2
}
)
end
defp shipped(_state, _event), do: true
defp cancelled(_state, _event), do: false
endA projection outliving a code reload — one driving a read model from a
long-lived process — wants &__MODULE__.shipped/2 instead: a local capture
is pinned to the module version that defined it and raises
BadFunctionError once that version is purged.
Composing
Rather than growing one projection until it answers every question a decision
asks, write a projection per question and compose/1 them. The composed
projection folds to a map keyed the same way, and queries the union of its
parts:
Sourced.Projection.compose(%{
placed: MyApp.Orders.placed?(1),
total: MyApp.Orders.total(1)
})Each part is only fed the events its own query matches, so a part tagged
"order:1" never sees an event that arrived because a sibling asked for
"customer:42". The result is a projection, so composites nest.
Event types
Handler keys are matched against event.type as the store returns it: the
event module under a Sourced.Middleware.Domain, the stored string
without one, and the stored string either way for an event type the domain
does not register.
A key matching none of those never fires, and nothing raises — worth checking first when a projection's state never moves.
Summary
Types
Folds one event into the state.
Handlers keyed by the event type they fold.
The key a composed projection files a part's state under.
Whatever a projection folds to. Opaque to the library.
Functions
Folds one event into state.
Combines projections into one that folds to a map keyed the same way.
Builds a projection.
Folds events into the projection's :initial_state.
Folds events into state, which a read model carries across the batches a
subscription delivers.
Types
@type handler() :: (state(), Sourced.StoredEvent.t() -> state())
Folds one event into the state.
@type handlers() :: %{required(Sourced.EventStore.Query.type()) => handler()}
Handlers keyed by the event type they fold.
@type key() :: term()
The key a composed projection files a part's state under.
@type opts() :: [ handlers: handlers(), initial_state: state(), tags: [Sourced.StoredEvent.tag()] ]
@type state() :: term()
Whatever a projection folds to. Opaque to the library.
@type t() :: %Sourced.Projection{ handlers: handlers(), initial_state: state(), query: Sourced.EventStore.Query.t() }
Functions
@spec apply(t(), state(), Sourced.StoredEvent.t()) :: state()
Folds one event into state.
An event the projection has no handler for leaves the state untouched.
Combines projections into one that folds to a map keyed the same way.
The composed query is the union of the parts', and each part is only fed the events its own query matches.
Builds a projection.
Options
:handlers— required the state transitions, keyed by event type. Their keys become the query'stypes.:initial_state— the state before any event has been folded in. Defaults tonil.:tags— restricts the projection to events carrying all of these tags. Defaults to[], matching every event of its types.
@spec project(t(), [Sourced.StoredEvent.t()]) :: state()
Folds events into the projection's :initial_state.
@spec project(t(), state(), [Sourced.StoredEvent.t()]) :: state()
Folds events into state, which a read model carries across the batches a
subscription delivers.