A simple single-aggregate eventstore without any performance optimizations or locking mechanisms but with FSA-compliant actions and a simple projection DSL
I needed a simple persistent storage mechanism for telegram bots with low traffic, but with a fast development pace. An eventstore captures events instead of state and lets me deduce state at runtime by folding over the events it has persisted before, which in turn enables me to build features on top of things that happened before that where recorded.
Reading from the eventstore can happen asynchronously while writing only happens synchronously. Writing also takes into consideration an expected version of the store before writing (a very simple transaction mechanism).
No release yet, but npm test
runs the tests and npm run build
runs babel to produce ES5-code without flowtype annotations.
The eventStore can either be created by supplying a filename
to write to (which is a shorthand for using the JsonFileStorageBackend
)
or by supplying a custom StorageBackend
(such as InMemoryStorageBackend
, which is used for testing).
import { EventStore, InMemoryStorageBackend } from 'simple-eventstore';
const persistentEventStore = new EventStore('my-storage.json');
const inMemoryEventStorage = new EventStorage(InMemoryStorageBackend());
After that, it's mainly about defining Events and Projections.
Event Factories can be created using the event
export,
which is a function (type: string) => (payload: ?Object, meta: ?Object) => Event
import { event } from 'simple-eventstore';
// Declaring events
const UserJoined = event('USER_JOINED');
// Creating an event
eventStore.storeEvent(UserJoined({name: 'Raimo', twitter: '@rradczewski'}));
State is deduced at runtime by replaying all events in the store and folding them over a projection.
A projection is a function (events: Event[]) => S
. Using the utility functions provided as { projection, on }
,
it is very easy to write a simple projection for a specific use case:
import { projection, on } from 'simple-eventstore';
import { without } from 'ramda';
const ActiveUsers = projection(
on('USER_JOINED', (users, event) => users.concat([event.name])),
on('USER_PARTED', (users, event) => without([event.name], users))
)([]);
Later, in your application code, you can request a projection by calling EventStore#project
and supplying the projection you defined earlier:
eventStore.project(ActiveUsers)
.then(activeUsers => {
console.log(`All active users: ${activeUsers.join(', ')}`);
});
In order to project only events where the payload fulfills a precidicate, on
is overloaded as (type: String, foldOrPredicate: Fold | Predicate, fold: ?Fold)
(altough flow does not like that, see this issue for more info). In the following example, a projection can be done for an individual user
import { projection, on } from 'simple-eventstore';
import { propEq } from 'ramda';
const IsUserActive = username => projection(
on('USER_JOINED', propEq('name', username), () => true),
on('USER_PARTED', propEq('name', username), () => false)
)(false);
// SNIP
eventStore.storeEvent(UserJoined({name: 'Raimo', twitter: 'rradczewski'}));
eventStore.project(IsUserActive('Raimo'))
.then(isActive => {
if(isActive) {
console.log('Raimo is active');
} else {
console.log('Raimo is not active');
}
});
// Will print: Raimo is active