event-storage
Version:
An optimized embedded event store for node.js
455 lines (410 loc) • 16.8 kB
JavaScript
const expect = require('expect.js');
const fs = require('fs-extra');
const Storage = require('../src/Storage');
const Consumer = require('../src/Consumer');
const dataDirectory = __dirname + '/data';
describe('Consumer', function() {
let consumer, storage;
beforeEach(function () {
fs.emptyDirSync(dataDirectory);
storage = new Storage({ dataDirectory });
storage.ensureIndex('foobar', (doc) => doc.type === 'Foobar');
storage.ensureIndex('bazinga', (doc) => doc.type === 'Bazinga');
});
afterEach(function () {
if (storage) {
storage.close();
}
storage = null;
consumer = null;
});
it('throws when instanciated without a storage', function() {
expect(() => new Consumer('foobar', 'consumer1')).to.throwError(/storage/);
});
it('throws when instanciated without an index name', function() {
expect(() => new Consumer(storage)).to.throwError(/index name/);
});
it('throws when instanciated without an identifier', function() {
expect(() => new Consumer(storage, 'foobar')).to.throwError(/identifier/);
});
it('creates consumer directory if not existing', function() {
consumer = new Consumer(storage, 'foobar', 'consumer1');
expect(fs.existsSync(dataDirectory + '/consumers')).to.be(true);
});
it('cleans up failed write left-overs', function() {
consumer = new Consumer(storage, 'foobar', 'consumer1');
consumer.stop();
fs.writeFileSync(consumer.fileName + '.1', 'failed write!');
consumer = new Consumer(storage, 'foobar', 'consumer1');
expect(fs.existsSync(consumer.fileName + '.1')).to.be(false);
});
it('emits event when catching up', function(done){
consumer = new Consumer(storage, 'foobar', 'consumer1');
consumer.stop();
storage.write({ type: 'Foobar', id: 1 });
consumer.on('caught-up', () => {
expect(consumer.position).to.be(1);
done();
});
consumer.start();
});
it('can start with some initial state', function(done){
consumer = new Consumer(storage, 'foobar', 'consumer1', { foos: 0, lastId: 0 });
expect(consumer.state.foos).to.be(0);
expect(consumer.state.lastId).to.be(0);
storage.write({ type: 'Foobar', id: 2 });
consumer.on('caught-up', () => {
expect(consumer.state.foos).to.be(1);
expect(consumer.state.lastId).to.be(2);
done();
});
consumer.on('data', document => {
if (document.type === 'Foobar') {
consumer.setState({foos: consumer.state.foos + 1, lastId: document.id});
}
});
});
it('continues emitting data after catching up', function(done){
consumer = new Consumer(storage, 'foobar', 'consumer1');
consumer.stop();
storage.write({ type: 'Foobar', id: 1 });
consumer.on('caught-up', () => {
expect(consumer.position).to.be(1);
storage.write({ type: 'Foobar', id: 2 });
storage.write({ type: 'Foobar', id: 3 });
});
let expected = 0;
consumer.on('data', document => {
expect(document.id).to.be(++expected);
if (document.id === 3) {
done();
}
});
consumer.start();
});
it('receives new documents as they are added', function(done){
consumer = new Consumer(storage, 'foobar', 'consumer1');
let expected = 0;
consumer.on('data', document => {
expect(document.id).to.be(++expected);
if (document.id === 3) {
done();
}
});
storage.write({ type: 'Foobar', id: 1 });
storage.write({ type: 'Foobar', id: 2 });
storage.write({ type: 'Foobar', id: 3 });
});
it('can start from arbitrary position', function(done){
consumer = new Consumer(storage, 'foobar', 'consumer1', 2);
let expected = 3;
consumer.on('data', document => {
expect(document.id).to.be(expected);
done();
});
storage.write({ type: 'Foobar', id: 1 });
storage.write({ type: 'Foobar', id: 2 });
storage.write({ type: 'Foobar', id: 3 });
});
it('stops when pushing fails', function(done){
consumer = new Consumer(storage, 'foobar', 'consumer1');
consumer.on('caught-up', () => {
storage.write({type: 'Foobar', id: 1});
storage.write({type: 'Foobar', id: 2});
storage.write({type: 'Foobar', id: 3});
});
const push = consumer.push.bind(consumer);
consumer.push = (doc) => push(doc) && false;
consumer.on('data', document => {
expect(document.id).to.be(1);
setTimeout(done, 10);
});
});
it('ignores events on other streams', function(done){
consumer = new Consumer(storage, 'foobar', 'consumer1');
consumer.on('caught-up', () => {
storage.write({type: 'Foobar', id: 1});
storage.write({type: 'Foobar', id: 2});
storage.write({type: 'Bazinga', id: 3});
});
let expected = 0;
consumer.on('data', document => {
expect(document.id).to.be(++expected);
if (document.id === 2) {
setTimeout(done, 10);
}
});
});
it('works with multiple consumers', function(done){
consumer = new Consumer(storage, 'foobar', 'consumer1');
consumer.on('caught-up', () => {
storage.write({type: 'Foobar', id: 1});
storage.write({type: 'Foobar', id: 2});
storage.write({type: 'Bazinga', id: 3});
});
const consumer2 = new Consumer(storage, 'bazinga', 'consumer2');
let expected = 0;
consumer.on('data', document => {
expect(document.id).to.be(++expected);
if (document.id === 2) {
consumer2.on('data', document => {
expect(document.id).to.be(++expected);
done();
});
}
});
});
it('continues from last position after restart', function(done){
consumer = new Consumer(storage, 'foobar', 'consumer1');
let expected = 0;
consumer.on('data', document => {
expect(document.id).to.be(++expected);
if (document.id === 3) {
consumer.stop();
storage.write({ type: 'Foobar', id: 4 });
storage.write({ type: 'Foobar', id: 5 }, () => {
expect(consumer.position).to.be(3);
consumer.start();
});
}
if (document.id === 5) {
done();
}
});
storage.write({ type: 'Foobar', id: 1 });
storage.write({ type: 'Foobar', id: 2 });
storage.write({ type: 'Foobar', id: 3 });
});
it('will automatically start consuming when registering data listener', function(done){
consumer = new Consumer(storage, 'foobar', 'consumer1');
let expected = 0;
storage.write({ type: 'Foobar', id: 1 });
storage.write({ type: 'Foobar', id: 2 });
storage.write({ type: 'Foobar', id: 3 }, () => {
expect(consumer.position).to.be(0);
consumer.on('data', document => {
expect(document.id).to.be(++expected);
if (document.id === 3) {
done();
}
});
});
});
it('can stop during catching up', function(done){
consumer = new Consumer(storage, 'foobar', 'consumer1');
storage.write({ type: 'Foobar', id: 1 });
storage.write({ type: 'Foobar', id: 2 });
storage.write({ type: 'Foobar', id: 3 }, () => {
expect(consumer.position).to.be(0);
consumer.start();
consumer.stop();
consumer.on('data', document => {
expect(this).to.be(false);
});
setTimeout(done, 10);
});
});
it('stops when push fails during catching up', function(done){
consumer = new Consumer(storage, 'foobar', 'consumer1');
const push = consumer.push.bind(consumer);
consumer.push = (doc) => push(doc) && false;
storage.write({ type: 'Foobar', id: 1 });
storage.write({ type: 'Foobar', id: 2 });
storage.write({ type: 'Foobar', id: 3 }, () => {
expect(consumer.position).to.be(0);
consumer.on('data', document => {
expect(document.id).to.be(1);
setTimeout(done, 10);
});
});
});
it('starting manually is no-op after registering data listener', function(done){
consumer = new Consumer(storage, 'foobar', 'consumer1');
let expected = 0;
consumer.on('data', document => {
expect(document.id).to.be(++expected);
if (document.id === 3) {
done();
}
});
consumer.start();
storage.write({ type: 'Foobar', id: 1 });
storage.write({ type: 'Foobar', id: 2 });
storage.write({ type: 'Foobar', id: 3 });
});
it('throws when calling setState outside of document handler', function() {
consumer = new Consumer(storage, 'foobar', 'consumer-1');
expect(() => consumer.setState({ foo: 'bar' })).to.throwError();
});
it('will persist multiple setState calls only once', function(done) {
consumer = new Consumer(storage, 'foobar', 'consumer-1');
consumer.on('data', () => {
consumer.setState({ foo: 1 });
consumer.setState({ foo: 1, bar: 2 });
consumer.once('persisted', () => {
expect(consumer.state.bar).to.be(2);
done();
});
consumer.stop();
});
storage.write({ type: 'Foobar', id: 1 });
});
it('restores state after reopening', function(done) {
const state = { foo: 0, bar: 'baz' };
consumer = new Consumer(storage, 'foobar', 'consumer-1');
consumer.on('data', (document) => {
const newState = {...state, foo: state.foo + 1, lastId: document.id};
consumer.setState(newState);
consumer.once('persisted', () => {
consumer = new Consumer(storage, 'foobar', 'consumer-1');
expect(consumer.state.foo).to.be(1);
done();
});
consumer.stop();
});
storage.write({ type: 'Foobar', id: 1 });
});
it('allows function argument in setState', function(done) {
consumer = new Consumer(storage, 'foobar', 'consumer-1');
consumer.on('data', (document) => {
consumer.setState(state => ({...state, foo: (state.foo || 0) + 1, lastId: document.id}));
consumer.once('persisted', () => {
expect(consumer.state.foo).to.be(1);
done();
});
});
storage.write({ type: 'Foobar', id: 1 });
});
it('can be reset and reprocesses all events', function(done) {
storage.write({ type: 'Foobar', id: 1 });
storage.write({ type: 'Foobar', id: 2 });
storage.write({ type: 'Foobar', id: 3 });
consumer = new Consumer(storage, 'foobar', 'consumer-1');
consumer.once('caught-up', () => {
consumer.once('caught-up', () => {
expect(consumer.position).to.be(3);
done();
});
consumer.reset();
expect(consumer.position).to.be(0);
});
consumer.start();
});
it('can be reset with new initialState and reprocesses all events', function(done) {
storage.write({ type: 'Foobar', id: 1 });
storage.write({ type: 'Foobar', id: 2 });
storage.write({ type: 'Foobar', id: 3 });
consumer = new Consumer(storage, 'foobar', 'consumer-1', { foo: 0 });
consumer.once('caught-up', () => {
consumer.once('caught-up', () => {
expect(consumer.state.foo).to.be(4);
done();
});
expect(consumer.state.foo).to.be(3);
consumer.reset({ foo: 1 });
});
consumer.on('data', document => consumer.setState(state => ({ foo: state.foo + 1, lastId: document.id })));
});
it('can be reset with new starting point and reprocesses all events', function(done) {
storage.write({ type: 'Foobar', id: 1 });
storage.write({ type: 'Foobar', id: 2 });
storage.write({ type: 'Foobar', id: 3 });
consumer = new Consumer(storage, 'foobar', 'consumer-1');
consumer.once('caught-up', () => {
consumer.once('caught-up', () => {
expect(consumer.position).to.be(3);
done();
});
consumer.reset(2);
expect(consumer.position).to.be(2);
});
consumer.start();
});
it('can be reset while running', function(done) {
consumer = new Consumer(storage, 'foobar', 'consumer-1');
consumer.once('caught-up', () => {
storage.write({ type: 'Foobar', id: 1 });
storage.write({ type: 'Foobar', id: 2 });
storage.write({ type: 'Foobar', id: 3 });
});
consumer.once('data', () => {
consumer.reset();
expect(consumer.position).to.be(0);
consumer.once('caught-up', () => {
expect(consumer.position).to.be(3);
done();
});
});
});
it('will not restart if stopped before reset', function(done) {
storage.write({ type: 'Foobar', id: 1 });
storage.write({ type: 'Foobar', id: 2 });
storage.write({ type: 'Foobar', id: 3 });
consumer = new Consumer(storage, 'foobar', 'consumer-1');
consumer.once('caught-up', () => {
consumer.stop();
expect(consumer.isPaused()).to.be(true);
consumer.reset();
expect(consumer.isPaused()).to.be(true);
done();
});
consumer.start();
});
it('persists state on every setState by default', function(done) {
consumer = new Consumer(storage, 'foobar', 'consumer-1', { foo: 0 });
let expected = 0;
consumer.on('data', document => {
consumer.setState(state => ({ foo: state.foo + 1, lastId: document.id }));
});
consumer.on('persisted', () => {
expect(consumer.state.lastId).to.be(++expected);
if (consumer.state.lastId === 3) {
done();
} else {
storage.write({type: 'Foobar', id: consumer.state.lastId + 1});
}
});
consumer.on('caught-up', () => {
storage.write({ type: 'Foobar', id: 1 });
});
});
it('allows to skip state persistence', function(done) {
consumer = new Consumer(storage, 'foobar', 'consumer-1', { foo: 0 });
consumer.on('data', document => {
consumer.setState(state => ({ foo: state.foo + 1, lastId: document.id }), false);
if (document.id === 3) {
expect(consumer.state.foo).to.be(3);
consumer.stop();
setTimeout(() => {
const consumer = new Consumer(storage, 'foobar', 'consumer-1', { foo: 0 });
expect(consumer.state.foo).to.be(0);
done();
}, 1);
}
});
consumer.on('caught-up', () => {
consumer.on('persisted', () => { throw new Error('Invoked persistence!'); });
storage.write({ type: 'Foobar', id: 1 });
storage.write({ type: 'Foobar', id: 2 });
storage.write({ type: 'Foobar', id: 3 });
});
});
it('can build consistency guards (aggregates)', function(done) {
const guard = new Consumer(storage, 'foobar', 'unique-bar-guard');
guard.apply = function(event) {
this.setState(state => ({ ...state, ...event }));
};
guard.handle = function(command) {
if (this.state.foo === 'bar') {
throw new Error('There was already a bar!');
}
return {type: 'Foobar', foo: command.foo};
};
guard.on('data', guard.apply);
storage.write(guard.handle({ foo: 'bar' }));
guard.on('persisted', () => {
expect(() => storage.write(guard.handle({foo: 'bar'}))).to.throwError(/already a bar/);
done();
});
});
});