event-storage
Version:
An optimized embedded event store for node.js
633 lines (514 loc) • 21.4 kB
JavaScript
const expect = require('expect.js');
const fs = require('fs-extra');
const path = require('path');
const EventStore = require('../src/EventStore');
const storageDirectory = __dirname + '/data';
describe('EventStore', function() {
let eventstore;
beforeEach(function () {
fs.emptyDirSync(storageDirectory);
});
afterEach(function () {
if (eventstore) {
eventstore.close();
}
eventstore = null;
});
it('basically works', function(done) {
eventstore = new EventStore({
storageDirectory
});
let events = [{foo: 'bar'}, {foo: 'baz'}, {foo: 'quux'}];
eventstore.on('ready', () => {
eventstore.commit('foo-bar', events, () => {
const stream = eventstore.getEventStream('foo-bar');
let i = 0;
for (let event of stream) {
expect(event).to.eql(events[i++]);
}
done();
});
});
});
it('can be created with custom name', function(done) {
eventstore = new EventStore('custom-store', {
storageDirectory
});
eventstore.commit('foo-bar', [{ type: 'foo'}], () => {
expect(fs.existsSync(path.join(storageDirectory, 'custom-store.foo-bar'))).to.be(true);
done();
});
});
it('throws when scanning of stream directory fails', function() {
const fs = require('fs');
const originalReaddir = fs.readdir;
fs.readdir = (dir, callback) => callback(new Error('Something went wrong!'), null);
expect(() => new EventStore({
storageDirectory
})).to.throwError(/Something went wrong!/);
fs.readdir = originalReaddir;
});
it('repairs torn writes', function(done) {
eventstore = new EventStore({
storageDirectory
});
const events = [{foo: 'bar'.repeat(500)}];
eventstore.on('ready', () => {
eventstore.commit('foo-bar', events, () => {
// Simulate a torn write (but indexes are still written)
fs.truncateSync(eventstore.storage.getPartition('foo-bar').fileName, 512);
// The previous instance was not closed, so the lock still exists
eventstore = new EventStore({
storageDirectory,
storageConfig: {
lock: EventStore.LOCK_RECLAIM
}
});
eventstore.on('ready', () => {
expect(eventstore.length).to.be(0);
expect(eventstore.getStreamVersion('foo-bar')).to.be(0);
done();
});
});
});
});
it('throws when trying to open non-existing store read-only', function() {
expect(() => new EventStore({
storageDirectory,
readOnly: true
})).to.throwError();
});
it('can open read-only', function(done) {
eventstore = new EventStore({
storageDirectory
});
let events = [{foo: 'bar'}, {foo: 'baz'}, {foo: 'quux'}];
eventstore.on('ready', () => {
eventstore.commit('foo-bar', events, () => {
let readstore = new EventStore({
storageDirectory,
readOnly: true
});
readstore.on('ready', () => {
const stream = readstore.getEventStream('foo-bar');
let i = 0;
for (let event of stream) {
expect(event).to.eql(events[i++]);
}
readstore.close();
done();
});
});
});
});
describe('commit', function() {
it('throws when no stream name specified', function() {
eventstore = new EventStore({
storageDirectory
});
expect(() => eventstore.commit({ foo: 'bar' })).to.throwError();
});
it('throws when no events specified', function() {
eventstore = new EventStore({
storageDirectory
});
expect(() => eventstore.commit('foo-bar')).to.throwError();
});
it('throws when opened in read-only mode', function() {
eventstore = new EventStore({
storageDirectory
});
eventstore.close();
eventstore = new EventStore({
storageDirectory,
readOnly: true
});
expect(() => eventstore.commit('foo-bar', { foo: 'bar' })).to.throwError();
});
it('can commit a single event', function() {
eventstore = new EventStore({
storageDirectory
});
eventstore.commit('foo-bar', { foo: 'bar' });
expect(eventstore.length).to.be(1);
});
it('can commit multiple events at once', function() {
eventstore = new EventStore({
storageDirectory
});
eventstore.commit('foo-bar', [{ foo: 'bar' }, { bar: 'baz' }, { baz: 'quux' }]);
expect(eventstore.length).to.be(3);
});
it('invokes callback when finished', function(done) {
eventstore = new EventStore({
storageDirectory
});
eventstore.commit('foo-bar', [{ foo: 'bar' }], (commit) => {
expect(eventstore.length).to.be(1);
expect(commit.streamName).to.be('foo-bar');
expect(commit.streamVersion).to.be(0);
expect(commit.events).to.eql([{ foo: 'bar' }]);
done();
});
});
it('invokes callback when finished with optimistic concurrency check', function(done) {
eventstore = new EventStore({
storageDirectory
});
eventstore.commit('foo-bar', [{ foo: 'bar' }], EventStore.ExpectedVersion.EmptyStream, (commit) => {
expect(eventstore.length).to.be(1);
expect(commit.streamName).to.be('foo-bar');
expect(commit.streamVersion).to.be(0);
expect(commit.events).to.eql([{ foo: 'bar' }]);
done();
});
});
it('invokes callback when finished with optimistic concurrency check and metdata', function(done) {
eventstore = new EventStore({
storageDirectory
});
eventstore.commit('foo-bar', [{ foo: 'bar' }], EventStore.ExpectedVersion.EmptyStream, {}, (commit) => {
expect(eventstore.length).to.be(1);
expect(commit.streamName).to.be('foo-bar');
expect(commit.streamVersion).to.be(0);
expect(commit.events).to.eql([{ foo: 'bar' }]);
done();
});
});
it('invokes "commit" event when finished', function(done) {
eventstore = new EventStore({
storageDirectory
});
eventstore.on('commit', (commit) => {
expect(eventstore.length).to.be(1);
expect(commit.streamName).to.be('foo-bar');
expect(commit.streamVersion).to.be(0);
expect(commit.events).to.eql([{ foo: 'bar' }]);
done();
});
eventstore.commit('foo-bar', [{ foo: 'bar' }]);
});
it('throws an optimistic concurrency error if stream version does not match', function(done) {
eventstore = new EventStore({
storageDirectory
});
expect(() => eventstore.commit('foo-bar', { foo: 'bar' }, 1)).to.throwError(
e => expect(e).to.be.a(EventStore.OptimisticConcurrencyError)
);
eventstore.commit('foo-bar', { foo: 'bar' }, () => {
expect(() => eventstore.commit('foo-bar', { foo: 'baz' }, EventStore.ExpectedVersion.EmptyStream)).to.throwError(
e => expect(e).to.be.a(EventStore.OptimisticConcurrencyError)
);
expect(() => eventstore.commit('foo-bar', { foo: 'baz' }, 2)).to.throwError(
e => expect(e).to.be.a(EventStore.OptimisticConcurrencyError)
);
done();
});
});
it('does not throw an optimistic concurrency error if stream version matches', function(done) {
eventstore = new EventStore({
storageDirectory
});
expect(() => eventstore.commit('foo-bar', { foo: 'bar' }, EventStore.ExpectedVersion.EmptyStream)).to.not.throwError();
eventstore.commit('foo-bar', { foo: 'bar' }, () => {
expect(() => eventstore.commit('foo-bar', {foo: 'baz'}, 2)).to.not.throwError();
expect(() => eventstore.commit('foo-bar', {foo: 'baz'}, EventStore.ExpectedVersion.Any)).to.not.throwError();
done();
});
});
it('uses metadata from argument for commit', function(done) {
eventstore = new EventStore({
storageDirectory
});
eventstore.commit('foo-bar', [{ foo: 'bar' }], { commitId: 1, committedAt: 12345, quux: 'quux' }, (commit) => {
expect(commit.commitId).to.be(1);
expect(commit.committedAt).to.be(12345);
expect(commit.quux).to.be('quux');
const stream = eventstore.getEventStream('foo-bar');
const storedEvent = stream.next();
expect(storedEvent.metadata.commitId).to.be(1);
expect(storedEvent.metadata.committedAt).to.be(12345);
expect(storedEvent.metadata.quux).to.be('quux');
done();
});
});
});
describe('createEventStream', function() {
it('throws when trying to recreate existing stream', function(done) {
eventstore = new EventStore({
storageDirectory
});
eventstore.commit('foo-bar', [{ type: 'foo' }], () => {
expect(() => eventstore.createEventStream('foo-bar', event => event.payload.type === 'foo')).to.throwError();
done();
});
});
it('can create new streams on existing events', function(done) {
eventstore = new EventStore({
storageDirectory
});
eventstore.commit('foo-bar', [{ type: 'foo' }], () => {
const stream = eventstore.createEventStream('my-foo-bar', event => event.payload.type === 'foo');
expect(stream.events.length).to.be(1);
expect(stream.events[0]).to.eql({ type: 'foo' });
done();
});
});
});
describe('getStreamVersion', function() {
it('returns -1 if the stream does not exist', function() {
eventstore = new EventStore({
storageDirectory
});
expect(eventstore.getStreamVersion('foo')).to.be(-1);
});
it('returns 0 if the stream is empty', function() {
eventstore = new EventStore({
storageDirectory
});
eventstore.createEventStream('foo', () => true);
expect(eventstore.getStreamVersion('foo')).to.be(0);
});
it('returns the version of the stream', function(done) {
eventstore = new EventStore({
storageDirectory
});
eventstore.commit('foo', [{ type: 'foo' }], () => {
expect(eventstore.getStreamVersion('foo')).to.be(1);
done();
});
});
});
describe('getEventStream', function() {
it('can open existing streams', function(done) {
eventstore = new EventStore({
storageDirectory
});
eventstore.commit('foo-bar', [{ foo: 'bar' }]);
eventstore.close();
eventstore = new EventStore({
storageDirectory
});
eventstore.on('ready', () => {
const stream = eventstore.getEventStream('foo-bar');
expect(stream.events.length).to.be(1);
done();
});
});
it('can iterate events in reverse order', function() {
eventstore = new EventStore({
storageDirectory
});
for (let i=1; i<=20; i++) {
eventstore.commit('foo-bar', [{key: i}]);
}
let reverseStream = eventstore.getEventStream('foo-bar', -1, 0);
let i = 20;
for (let event of reverseStream) {
expect(event).to.eql({ key: i-- });
}
});
it('can open streams created in writer', function(done) {
eventstore = new EventStore({
storageDirectory
});
const readstore = new EventStore({
storageDirectory,
readOnly: true
});
expect(readstore.getStreamVersion('foo')).to.be(-1);
readstore.on('stream-available', (streamName) => {
if (streamName === 'foo') {
expect(readstore.getStreamVersion('foo')).to.be(0);
readstore.close();
done();
}
});
eventstore.createEventStream('foo', { type: 'foo' });
});
it('needs to be tested further.');
});
describe('getAllEvents', function() {
it('returns stream for all events', function (done) {
eventstore = new EventStore({
storageDirectory
});
for (let i=1; i<=20; i++) {
eventstore.commit('foo-bar', [{key: i}]);
}
eventstore.on('ready', () => {
const stream = eventstore.getAllEvents();
expect(stream.events.length).to.be(20);
done();
});
});
});
describe('fromStreams', function() {
it('throws when not specifying a join stream name', function() {
eventstore = new EventStore({
storageDirectory
});
expect(() => eventstore.fromStreams()).to.throwError();
});
it('throws when not specifying an array of stream names to join', function() {
eventstore = new EventStore({
storageDirectory
});
expect(() => eventstore.fromStreams('join-foo-bar')).to.throwError();
});
it('throws when specifying a non-existing stream to join', function() {
eventstore = new EventStore({
storageDirectory
});
expect(() => eventstore.fromStreams('join-foo-bar', ['foo-bar', 'baz'])).to.throwError(/does not exist/);
});
it('iterates events from multiple streams in correct order', function(done) {
eventstore = new EventStore({
storageDirectory
});
eventstore.commit('foo', { key: 1 }, () => {
eventstore.commit('bar', { key: 2}, () => {
eventstore.commit('foo', { key: 3 }, () => {
eventstore.commit('bar', { key: 4}, () => {
let joinStream = eventstore.fromStreams('foobar', ['foo','bar']);
let key = 1;
for (let event of joinStream) {
expect(event.key).to.be(key);
key++;
}
expect(key).to.be(5);
done();
});
});
});
});
});
it('iterates events from multiple streams in reverse order', function() {
eventstore = new EventStore({
storageDirectory
});
for (let i=1; i<=20; i++) {
eventstore.commit(i % 2 ? 'foo' : 'bar', [{key: i}]);
}
let reverseStream = eventstore.fromStreams('foo-bar', ['foo', 'bar'],-1, 0);
let i = 20;
for (let event of reverseStream) {
expect(event).to.eql({ key: i-- });
}
expect(i).to.be(0);
});
});
describe('getEventStreamForCategory', function() {
it('throws when not specifying category without streams', function () {
eventstore = new EventStore({
storageDirectory
});
expect(() => eventstore.getEventStreamForCategory('non-existing-category')).to.throwError();
});
it('iterates events for all streams with a given category prefix', function () {
eventstore = new EventStore({
storageDirectory
});
eventstore.commit('bar', [{key: 0}]);
for (let i=1; i<=20; i++) {
eventstore.commit('foo-' + i, [{key: i}]);
}
eventstore.commit('foobar', [{key: 21}]);
let categoryStream = eventstore.getEventStreamForCategory('foo');
let i = 1;
for (let event of categoryStream) {
expect(event).to.eql({ key: i++ });
}
expect(i).to.be(21);
});
it('works with a dedicated stream for the category', function () {
eventstore = new EventStore({
storageDirectory
});
eventstore.createEventStream('foo', e => e.stream.startsWith('foo-'));
for (let i=1; i<=20; i++) {
eventstore.commit('foo-' + i, [{key: i}]);
}
let categoryStream = eventstore.getEventStreamForCategory('foo');
let i = 1;
for (let event of categoryStream) {
expect(event).to.eql({ key: i++ });
}
expect(i).to.be(21);
});
});
describe('createEventStream', function() {
it('throws in read-only mode', function () {
eventstore = new EventStore({
storageDirectory
});
let readstore = new EventStore({
storageDirectory,
readOnly: true
});
expect(() => readstore.createEventStream('foo-bar', () => true)).to.throwError();
readstore.close();
});
it('throws when trying to re-create stream', function () {
eventstore = new EventStore({
storageDirectory
});
eventstore.createEventStream('foo-bar', () => true)
expect(() => eventstore.createEventStream('foo-bar', () => true)).to.throwError();
});
});
describe('deleteEventStream', function() {
it('throws in read-only mode', function(done) {
eventstore = new EventStore({
storageDirectory
});
eventstore.createEventStream('foo-bar', () => true);
let readstore = new EventStore({
storageDirectory,
readOnly: true
});
readstore.on('ready', () => {
expect(() => readstore.deleteEventStream('foo-bar')).to.throwError();
readstore.close();
done();
});
});
it('removes the stream persistently', function(done) {
eventstore = new EventStore({
storageDirectory
});
eventstore.commit('foo-bar', [{ foo: 'bar' }], () => {
expect(fs.existsSync(storageDirectory + '/streams/eventstore.stream-foo-bar.index')).to.be(true);
eventstore.deleteEventStream('foo-bar');
expect(eventstore.getEventStream('foo-bar')).to.be(false);
expect(fs.existsSync(storageDirectory + '/streams/eventstore.stream-foo-bar.index')).to.be(false);
done();
});
});
it('is noop for non-existing stream', function(done) {
eventstore = new EventStore({
storageDirectory
});
eventstore.commit('foo-bar', [{ foo: 'bar' }], () => {
eventstore.deleteEventStream('bar');
expect(eventstore.getEventStream('foo-bar')).to.not.be(false);
done();
});
});
});
describe('getConsumer', function() {
it('returns a consumer for the given stream', function(done) {
eventstore = new EventStore({
storageDirectory
});
eventstore.createEventStream('foo-bar', event => event.payload.foo === 'bar');
const consumer = eventstore.getConsumer('foo-bar', 'consumer1');
consumer.on('data', event => {
expect(event.id).to.be(2);
done();
});
eventstore.commit('foo', { foo: 'baz', id: 1 });
eventstore.commit('foo', { foo: 'bar', id: 2 });
});
});
});