Skip to content

rradczewski/simple-eventstore

Repository files navigation

CI Deps DevDeps

Simple EventStore

A simple single-aggregate eventstore without any performance optimizations or locking mechanisms but with FSA-compliant actions and a simple projection DSL

Rationale

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).

Installation

No release yet, but npm test runs the tests and npm run build runs babel to produce ES5-code without flowtype annotations.

Usage

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.

Defining events

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'}));

Projecting State

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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published