An actor model defines behavior in the system by defining how the model will respond to specific commands and which events it combines to determine its state.
Consequent loads modules ending with an _actor.js
postfix from an ./actors
path by default but allows this path to be changed during initialization. The actor module must return a function that returns a hash with the following properties:
actor
- metadata and configuration propertiesstate
- default state hash or a factory to initialize the actor instancecommands
- command handlersevents
- event handlers
Consequent will supply dependencies specified in the the actor module's exported function via fount
.
// an incomplete example
module.exports = function (dependency1, dependency2) {
return {
actor: {}, \\ metadata
state: {}, \\ initial state
commands: {}, \\ command handlers
events: {} \\ event handlers
}
}
The actor
property describes the model and provides metadata for how the storage adapters will interact with it.
namespace
type
- the model name
eventThreshold
- set the number of events that will trigger a new snapshotstoreEventPack
- store all events contributing to snapshot in a pack, default is falsesnapshotDuringPartition
- allow snapshots during partitions*snapshotOnRead
- allow snapshot creation during readaggregateFrom
- a list of actor types to aggregate events from (populated automatically)searchableBy
- a list of fields to pass on to a search adapter if one is presentidentifiedBy
- identifies a property on the model that will act as a natural key - it should be unique for across all instances of the model and will be what you use to send commands or lookup instances using. Consequent will provide an underlying unique system id in_id
- It is the model store's responsibility to determine if this is possible, in most cases, databases don't provide this capability.
Consequent will add the following fields to actor state:
_id
_vector
_version
_ancestor
_eventsApplied
_lastEventId
_lastCommandId
_lastCommandHandledOn
- ISO8601_lastEventAppliedOn
- ISO8601
None of these fields should ever be manipulated directly.
_id
will be populated by a flake id for efficient storage, guaranteed uniqueness and immutability. You are expected to specify which field in the model serve as the id that you will use to send commands and request state by. This is done to avoid circumstances where a change to this value would result in the need to update every foreign key relationship in the system and to ensure the most efficient storage implementation (most databases prefer increasing ids for primary keys vs random values)
Consequent supports two types of messages - commands and events. Commands represent a message that is processed conditionally and results in one or more events as a result. Events represent something that's already taken place and will get applied against the actor's state.
Consequent may replay the same event against an actor many times in a system before the resulting actor state is captured as a snapshot. There are no built-in mechanisms to identify or eliminate events that result from replaying an event multiple times.
The commands
and events
properties should be defined as a hash where each key is the message type/topic and the value can take one of three possible formats. Each definition has four properties that consequent uses to determine when and how to call the handler in question.
when
- a boolean value, predicate function or state that controls when the handler is calledthen
- the handler function to callexclusive
- when true, the first handler with a passing when will be the only handler calledmap
- a boolean or argument to message map that will cause consequent to map message properties to handler/predicate arguments
If the event comes from another type, you must prefix the event with the type name and a
.
:type.event
If the
when
the predicate is a string, the handler will be invoked if the actor's state has astate
property with a matching string.
Note: while the only required field is
then
, if that's all you need, just provide the handler function by itself (see handler function only).
{
when: boolean|predicate|state name (defaults to true),
then: handler function
exclusive: boolean (defaults to true),
map: argument->property map or false (defaults to true)
}
This is a short-hand form of the hash form. It's probably not worth sacrificing clarity to use it, but here it is:
[ when, then, exclusive, map ]
If the default values for when
, exclusive
and map
are what you need, just provide the function instead of a hash with only the then
property.
A command handler returns an array of events or a promise that resolves to an array of events.
An event handler mutates the actor's state directly based on the event and returns nothing.
// command handler examples
// a command handler that accepts the entire command as an argument
function handleCommand(actor, command) {
return { type: 'counterIncremented', amount: command.amount };
}
// a command that uses property-argument mapping
function handleCommand(actor, amount) {
return { type: 'counterIncremented', amount };
}
// event handler examples
// an event handler that accepts the entire event as an argument
function handleCounterIncremented(actor, event) {
actor.counter = actor.counter + event.amount;
}
// an event handler that accepts the entire event as an argument
function handleCounterIncremented(actor, amount) {
actor.counter = actor.counter + amount;
}
The when is a predicate used to determine which handler (specified by the then
property) should be called. Because the predicates are mutually exclusive, the exclusive
flag defaulting to true
prevents consequent from trying every predicate once a predicate returns true
.
var account = require( "./account" ); // model
...
commands: {
withdraw: [
{ when: account.sufficientBalance, then: account.makeWithdrawal },
{ when: account.insufficientBalance, then: account.denyWithdrawal }
]
},
events: {
withdrawn: [
{ when: account.sufficientBalance, then: account.withdraw },
{ when: account.insufficientBalance, then: account.overdraft }
]
}
Note: this is a somewhat advanced (controversial?) example where an event is handled conditionally. "Why would there be a need for conditional event handling?" This is the right question to ask - the short answer is that most systems will never need to address it. The point here is that it is possible but you should only use it if you're absolutely certain you need it and understand all the implications.
Actor Format - State as a hash of defaults
// predicates, command handlers and event handlers should be placed outside the actor defintion
// in a module that defines the model using pure functions
module.exports = function() {
return {
actor: { // defaults shown
namespace: '', // required - no default
type: '', // required - no default
eventThreshold: 100, // required - no default
snapshotDuringPartition: false,
snapshotOnRead: false
},
state: {
// *reserved fields*
id: ''
},
commands:
{
...
},
events:
{
...
}
}
};
Actor Format - State as a factory method
// a factory method is called with an id and can return a state hash or promise for one.
// the promise form is so that state can be initialized by accessing I/O - this is
// especially useful if migrating to this approach from a more traditional data access approach.
module.exports = function(oldDatabase) {
return {
actor: { // defaults shown
namespace: '', // required - no default
type: '', // required - no default
eventThreshold: 100,
snapshotDuringPartition: false,
snapshotOnRead: false,
},
state: function( id ) {
return oldDatabase.getOriginalRecord(id)
},
commands:
{
...
},
events:
{
...
}
}
};
Predicates accept the current state and either the entire command as an argument or properties from the command mapped to the remaining arguments.
// when not using a map
function hasThing (state, commmand) {
return state.collection.indexOf(command.thing) >= 0
}
// with argument mapping
function hasThing (state, thing) {
return state.collection.indexOf(thing) >= 0
}
consequent will populate aggregateFrom
automatically by looking at prefixed event types and noting any event prefix with a type name other than the current type. consequent will attempt to load events for these types but needs to be able to determine which type instances are related to the owning type in order to load the correct events.
It does this by looking at field names and trying to determine which of them may contain identities for each type. It will attempt to look at any field that begins with the type and testing to see if it ends in Id
, has an id
property, is an array ending in Ids
or is a field named for the plural of the type and contains an array of objects with id
properties.
Without the opportunity to independently process each event of interest, different models would not be possible. It's also important to not that given the complexity of various types of event processing (i.e. statisical analysis) it may be desirable to have very different kinds of snapshotting behavior per type of model.
You may be trying to find all financial transactions that belong to an indivial. If an account
type produced transaction events, you could put an accountIds
array field on the new type containing all the accounts beloning to the individual. You could also have an accounts
array containing account
objects, each with its own id
field. In each case, consequent would use these ids to load the events for those account instances and then play them against the object's event handlers allowing it to build up its own state representation based on those events.