event-storage 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,15 @@
1
- const stream = require('stream');
2
- const { assert } = require('./util');
1
+ import stream from 'stream';
2
+ import { assert } from './util.js';
3
3
 
4
4
  /**
5
- * Adjusts a revision number from the EventStore/EventStream interface range into the underlying storage item number.
5
+ * Calculate the actual version number from a possibly relative (negative) version number.
6
6
  *
7
- * @param {number} rev A zero-based revision number, or a negative number to denote a "from the end" position
8
- * @returns {number} A one-based storage item number
7
+ * @param {number} version The version to normalize.
8
+ * @param {number} length The maximum version number
9
+ * @returns {number} The absolute version number.
9
10
  */
10
- function adjustedRevision(rev) {
11
- if (rev >= 0) {
12
- return rev + 1;
13
- }
14
- return rev;
11
+ function normalizeVersion(version, length) {
12
+ return version < 0 ? version + length + 1 : version;
15
13
  }
16
14
 
17
15
  /**
@@ -36,22 +34,163 @@ class EventStream extends stream.Readable {
36
34
  * @param {number} [minRevision] The minimum revision to include in the events (inclusive).
37
35
  * @param {number} [maxRevision] The maximum revision to include in the events (inclusive).
38
36
  */
39
- constructor(name, eventStore, minRevision = 0, maxRevision = -1) {
37
+ constructor(name, eventStore, minRevision = 1, maxRevision = -1) {
40
38
  super({ objectMode: true });
41
39
  assert(typeof name === 'string' && name !== '', 'Need to specify a stream name.');
42
40
  assert(typeof eventStore === 'object' && eventStore !== null, `Need to provide EventStore instance to create EventStream ${name}.`);
43
41
 
44
42
  this.name = name;
45
43
  if (eventStore.streams[name]) {
46
- const streamIndex = eventStore.streams[name].index;
47
- this.version = minVersion(streamIndex.length, maxRevision);
48
- minRevision = adjustedRevision(minRevision);
49
- maxRevision = adjustedRevision(maxRevision);
50
- this.iterator = eventStore.storage.readRange(minRevision, maxRevision, streamIndex);
44
+ this.streamIndex = eventStore.streams[name].index;
45
+ this.minRevision = normalizeVersion(minRevision, this.streamIndex.length);
46
+ this.maxRevision = normalizeVersion(maxRevision, this.streamIndex.length);
47
+ this.version = minVersion(this.streamIndex.length, maxRevision);
48
+ this._iterator = null;
49
+ this.fetch = function() {
50
+ return eventStore.storage.readRange(this.minRevision, this.maxRevision, this.streamIndex);
51
+ }
51
52
  } else {
53
+ this.streamIndex = { length: 0 };
52
54
  this.version = -1;
53
- this.iterator = { next() { return { done: true }; } };
55
+ this._iterator = { next() { return { done: true }; } };
56
+ }
57
+ }
58
+
59
+ /**
60
+ * @api
61
+ * @param {number} revision The event revision to start reading from (inclusive).
62
+ * @returns {EventStream}
63
+ */
64
+ from(revision) {
65
+ this.minRevision = normalizeVersion(revision, this.streamIndex.length);
66
+ return this;
67
+ }
68
+
69
+ /**
70
+ * @api
71
+ * @param {number} revision The event revision to read until (inclusive).
72
+ * @returns {EventStream}
73
+ */
74
+ until(revision) {
75
+ this.maxRevision = normalizeVersion(revision, this.streamIndex.length);
76
+ this.version = minVersion(this.streamIndex.length, this.maxRevision);
77
+ return this;
78
+ }
79
+
80
+ /**
81
+ * @api
82
+ * @param {number} amount The amount of events at the start of the stream to return in chronological order.
83
+ * @returns {EventStream}
84
+ */
85
+ first(amount) {
86
+ return this.fromStart().following(amount);
87
+ }
88
+
89
+ /**
90
+ * @api
91
+ * @param {number} amount The amount of events at the end of the stream to return in chronological order.
92
+ * @returns {EventStream}
93
+ */
94
+ last(amount) {
95
+ return this.fromEnd().previous(amount).forwards();
96
+ }
97
+
98
+ /**
99
+ * @api
100
+ * @returns {EventStream}
101
+ */
102
+ fromStart() {
103
+ this.minRevision = 1;
104
+ return this;
105
+ }
106
+
107
+ /**
108
+ * @api
109
+ * @returns {EventStream}
110
+ */
111
+ fromEnd() {
112
+ this.minRevision = this.streamIndex.length;
113
+ return this;
114
+ }
115
+
116
+ /**
117
+ * @param {number} amount The amount of events to return in reverse chronological order.
118
+ * @returns {EventStream}
119
+ */
120
+ previous(amount) {
121
+ this.maxRevision = Math.max(1, this.minRevision - amount + 1);
122
+ return this;
123
+ }
124
+
125
+ /**
126
+ * @param {number} amount The amount of events to return in chronological order.
127
+ * @returns {EventStream}
128
+ */
129
+ following(amount) {
130
+ this.maxRevision = Math.min(this.streamIndex.length, this.minRevision + amount - 1);
131
+ return this;
132
+ }
133
+
134
+ /**
135
+ * @api
136
+ * @returns {EventStream}
137
+ */
138
+ toEnd() {
139
+ this.maxRevision = this.version = this.streamIndex.length;
140
+ return this;
141
+ }
142
+
143
+ /**
144
+ * @api
145
+ * @returns {EventStream}
146
+ */
147
+ toStart() {
148
+ this.maxRevision = 1;
149
+ return this;
150
+ }
151
+
152
+ /**
153
+ * Reverse the current range of events, no matter which direction it currently has.
154
+ * @returns {EventStream}
155
+ */
156
+ reverse() {
157
+ let tmp = this.maxRevision;
158
+ this.maxRevision = this.minRevision;
159
+ this.minRevision = tmp;
160
+ this.version = minVersion(this.streamIndex.length, this.maxRevision);
161
+ return this;
162
+ }
163
+
164
+ /**
165
+ * Make the current range of events read in forward chronological order.
166
+ * @api
167
+ * @param {number} [amount] Amount of events to read forward. If not specified, will read forward until the previously set limit.
168
+ * @returns {EventStream}
169
+ */
170
+ forwards(amount = 0) {
171
+ if (amount > 0) {
172
+ this.following(amount);
173
+ }
174
+ if (this.maxRevision < this.minRevision) {
175
+ this.reverse();
54
176
  }
177
+ return this;
178
+ }
179
+
180
+ /**
181
+ * Make the current range of events read in backward chronological order.
182
+ * @api
183
+ * @param {number} [amount] Amount of events to read backward. If not specified, will read backward until the previously set limit.
184
+ * @returns {EventStream}
185
+ */
186
+ backwards(amount = 0) {
187
+ if (amount > 0) {
188
+ this.previous(amount);
189
+ }
190
+ if (this.maxRevision > this.minRevision) {
191
+ this.reverse();
192
+ }
193
+ return this;
55
194
  }
56
195
 
57
196
  /**
@@ -75,6 +214,7 @@ class EventStream extends stream.Readable {
75
214
  * Iterate over the events in this stream with a callback.
76
215
  * This method is useful to gain access to the event metadata.
77
216
  *
217
+ * @api
78
218
  * @param {function(object, object, string)} callback A callback function that will receive the event, the storage metadata and the original stream name for every event in this stream.
79
219
  */
80
220
  forEach(callback) {
@@ -94,13 +234,26 @@ class EventStream extends stream.Readable {
94
234
  }
95
235
  }
96
236
 
237
+ /**
238
+ * Reset this stream to the start so it can be iterated again.
239
+ * @returns {EventStream}
240
+ */
241
+ reset() {
242
+ this._iterator = null;
243
+ this._events = null;
244
+ return this;
245
+ }
246
+
97
247
  /**
98
248
  * @returns {object|boolean} The next event or false if no more events in the stream.
99
249
  */
100
250
  next() {
251
+ if (!this._iterator) {
252
+ this._iterator = this.fetch();
253
+ }
101
254
  let next;
102
255
  try {
103
- next = this.iterator.next();
256
+ next = this._iterator.next();
104
257
  } catch(e) {
105
258
  return false;
106
259
  }
@@ -119,4 +272,4 @@ class EventStream extends stream.Readable {
119
272
 
120
273
  }
121
274
 
122
- module.exports = EventStream;
275
+ export default EventStream;
@@ -1,5 +1,5 @@
1
- const ReadableIndex = require('./ReadableIndex');
2
- const watchesFile = require('../WatchesFile');
1
+ import ReadableIndex from './ReadableIndex.js';
2
+ import watchesFile from '../WatchesFile.js';
3
3
 
4
4
  /**
5
5
  * A read-only index is a readable index instance that reacts on file changes and triggers events.
@@ -45,4 +45,4 @@ class ReadOnlyIndex extends watchesFile(ReadableIndex) {
45
45
 
46
46
  }
47
47
 
48
- module.exports = ReadOnlyIndex;
48
+ export default ReadOnlyIndex;
@@ -1,8 +1,8 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const events = require('events');
4
- const Entry = require('../IndexEntry');
5
- const { assert, wrapAndCheck, binarySearch } = require('../util');
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import events from 'events';
4
+ import Entry, { assertValidEntryClass } from '../IndexEntry.js';
5
+ import { assert, wrapAndCheck, binarySearch } from '../util.js';
6
6
 
7
7
  // node-event-store-index V01
8
8
  const HEADER_MAGIC = "nesidx01";
@@ -53,7 +53,7 @@ class ReadableIndex extends events.EventEmitter {
53
53
  EntryClass: Entry
54
54
  };
55
55
  options = Object.assign(defaults, options);
56
- Entry.assertValidEntryClass(options.EntryClass);
56
+ assertValidEntryClass(options.EntryClass);
57
57
 
58
58
  this.name = name;
59
59
  this.initialize(options);
@@ -72,7 +72,7 @@ class ReadableIndex extends events.EventEmitter {
72
72
  this.EntryClass = options.EntryClass;
73
73
  this.dataDirectory = options.dataDirectory;
74
74
  this.fileName = path.resolve(options.dataDirectory, this.name);
75
- this.readBuffer = Buffer.allocUnsafe(options.EntryClass.size);
75
+ this.readBuffer = Buffer.allocUnsafe(Math.max(options.EntryClass.size, options.writeBufferSize > 0 ? options.writeBufferSize : 4096));
76
76
 
77
77
  if (options.metadata) {
78
78
  this.metadata = Object.assign({entryClass: options.EntryClass.name, entrySize: options.EntryClass.size}, options.metadata);
@@ -281,8 +281,11 @@ class ReadableIndex extends events.EventEmitter {
281
281
  const readFrom = Math.max(this.readUntil + 1, from);
282
282
  const amount = (until - readFrom + 1);
283
283
 
284
- const readBuffer = Buffer.allocUnsafe(amount * this.EntryClass.size);
285
- let readSize = fs.readSync(this.fd, readBuffer, 0, readBuffer.byteLength, this.headerSize + readFrom * this.EntryClass.size);
284
+ const bufferSize = amount * this.EntryClass.size;
285
+ const readBuffer = bufferSize <= this.readBuffer.byteLength
286
+ ? this.readBuffer
287
+ : Buffer.allocUnsafe(bufferSize);
288
+ let readSize = fs.readSync(this.fd, readBuffer, 0, bufferSize, this.headerSize + readFrom * this.EntryClass.size);
286
289
  let index = 0;
287
290
  while (index < amount && readSize > 0) {
288
291
  this.data[index + readFrom] = this.EntryClass.fromBuffer(readBuffer, index * this.EntryClass.size);
@@ -388,13 +391,14 @@ class ReadableIndex extends events.EventEmitter {
388
391
  * @returns {number} The last index entry position that is lower than or equal to the `number`. Returns 0 if no index matches.
389
392
  */
390
393
  find(number, min = false) {
394
+ if (this.length < 1) {
395
+ return 0;
396
+ }
391
397
  // We only need to search until the searched number because entry.number is always >= position
392
398
  const [low, high] = binarySearch(number, Math.min(this.length, number), index => this.get(index).number);
393
399
  return min ? low : high;
394
400
  }
395
401
  }
396
402
 
397
- module.exports = ReadableIndex;
398
- module.exports.Entry = Entry;
399
- module.exports.HEADER_MAGIC = HEADER_MAGIC;
400
- module.exports.CorruptedIndexError = CorruptedIndexError;
403
+ export default ReadableIndex;
404
+ export { Entry, HEADER_MAGIC, CorruptedIndexError };
@@ -1,6 +1,6 @@
1
- const fs = require('fs');
2
- const ReadableIndex = require('./ReadableIndex');
3
- const { assertEqual, buildMetadataHeader, ensureDirectory } = require('../util');
1
+ import fs from 'fs';
2
+ import ReadableIndex, { Entry, CorruptedIndexError, HEADER_MAGIC } from './ReadableIndex.js';
3
+ import { assertEqual, buildMetadataHeader, ensureDirectory } from '../util.js';
4
4
 
5
5
  /**
6
6
  * An index is a simple append-only file that stores an ordered list of entry elements pointing to the actual file position
@@ -71,7 +71,7 @@ class WritableIndex extends ReadableIndex {
71
71
  }
72
72
  return entries;
73
73
  } catch (e) {
74
- if (e instanceof ReadableIndex.CorruptedIndexError) {
74
+ if (e instanceof CorruptedIndexError) {
75
75
  this.truncate(e.size);
76
76
  return e.size;
77
77
  }
@@ -107,7 +107,7 @@ class WritableIndex extends ReadableIndex {
107
107
  if (!this.metadata) {
108
108
  this.metadata = {entryClass: this.EntryClass.name, entrySize: this.EntryClass.size};
109
109
  }
110
- const metadataBuffer = buildMetadataHeader(ReadableIndex.HEADER_MAGIC, this.metadata);
110
+ const metadataBuffer = buildMetadataHeader(HEADER_MAGIC, this.metadata);
111
111
  fs.writeSync(this.fd, metadataBuffer, 0, metadataBuffer.byteLength, 0);
112
112
  this.headerSize = metadataBuffer.byteLength;
113
113
  }
@@ -156,8 +156,9 @@ class WritableIndex extends ReadableIndex {
156
156
  }
157
157
 
158
158
  this.writeBufferCursor = 0;
159
- this.flushCallbacks.forEach(callback => callback());
159
+ const callbacks = this.flushCallbacks;
160
160
  this.flushCallbacks = [];
161
+ for (let i = 0; i < callbacks.length; i += 2) callbacks[i](callbacks[i + 1]);
161
162
  return true;
162
163
  }
163
164
 
@@ -172,7 +173,7 @@ class WritableIndex extends ReadableIndex {
172
173
  if (typeof callback !== 'function') {
173
174
  return;
174
175
  }
175
- this.flushCallbacks.push(() => callback(position));
176
+ this.flushCallbacks.push(callback, position);
176
177
  }
177
178
 
178
179
  /**
@@ -187,10 +188,15 @@ class WritableIndex extends ReadableIndex {
187
188
  assertEqual(entry.constructor.name, this.EntryClass.name, `Wrong entry object.`);
188
189
  assertEqual(entry.constructor.size, this.EntryClass.size, `Invalid entry size.`);
189
190
 
190
- if (this.readUntil === this.data.length - 1) {
191
+ const dataLen = this.data.length;
192
+ if (dataLen > 0 && this.data[dataLen - 1].number >= entry.number) {
193
+ throw new Error('Consistency error. Tried to add an index that should come before existing last entry.');
194
+ }
195
+
196
+ if (this.readUntil === dataLen - 1) {
191
197
  this.readUntil++;
192
198
  }
193
- this.data[this.data.length] = entry;
199
+ this.data[dataLen] = entry;
194
200
 
195
201
  if (this.writeBufferCursor === 0) {
196
202
  this.flushTimeout = setTimeout(() => this.flush(), this.flushDelay);
@@ -230,5 +236,5 @@ class WritableIndex extends ReadableIndex {
230
236
  }
231
237
  }
232
238
 
233
- module.exports = WritableIndex;
234
- module.exports.Entry = ReadableIndex.Entry;
239
+ export default WritableIndex;
240
+ export { Entry };
package/src/Index.js CHANGED
@@ -1,6 +1,8 @@
1
- const WritableIndex = require('./Index/WritableIndex');
2
- const ReadOnlyIndex = require('./Index/ReadOnlyIndex');
1
+ import WritableIndex, { Entry } from './Index/WritableIndex.js';
2
+ import ReadOnlyIndex from './Index/ReadOnlyIndex.js';
3
3
 
4
- module.exports = WritableIndex;
5
- module.exports.ReadOnly = ReadOnlyIndex;
6
- module.exports.Entry = WritableIndex.Entry;
4
+ WritableIndex.ReadOnly = ReadOnlyIndex;
5
+ WritableIndex.Entry = Entry;
6
+
7
+ export default WritableIndex;
8
+ export { ReadOnlyIndex as ReadOnly, Entry };
package/src/IndexEntry.js CHANGED
@@ -137,6 +137,5 @@ class Entry extends Array {
137
137
 
138
138
  }
139
139
 
140
- module.exports = Entry;
141
- module.exports.EntryInterface = EntryInterface;
142
- module.exports.assertValidEntryClass = assertValidEntryClass;
140
+ export default Entry;
141
+ export { EntryInterface, assertValidEntryClass };
@@ -1,18 +1,15 @@
1
- const EventStream = require('./EventStream');
1
+ import EventStream from './EventStream.js';
2
+ import { wrapAndCheck } from './util.js';
2
3
 
3
4
  /**
4
- * Translates an EventStore revision number to an index sequence number and wraps it around the given length, if it's < 0
5
+ * Calculate the actual version number from a possibly relative (negative) version number.
5
6
  *
6
- * @param {number} rev The zero-based EventStore revision
7
- * @param {number} length The length of the store
8
- * @returns {number} The 1-based index sequence number
7
+ * @param {number} version The version to normalize.
8
+ * @param {number} length The maximum version number
9
+ * @returns {number} The absolute version number.
9
10
  */
10
- function wrapRevision(rev, length) {
11
- rev++;
12
- if (rev <= 0) {
13
- rev += length;
14
- }
15
- return rev;
11
+ function normalizeVersion(version, length) {
12
+ return version < 0 ? version + length + 1 : version;
16
13
  }
17
14
 
18
15
  /**
@@ -25,30 +22,32 @@ class JoinEventStream extends EventStream {
25
22
  * @param {string} name The name of the stream.
26
23
  * @param {Array<string>} streams The name of the streams to join together.
27
24
  * @param {EventStore} eventStore The event store to get the stream from.
28
- * @param {number} [minRevision] The minimum revision to include in the events (inclusive).
29
- * @param {number} [maxRevision] The maximum revision to include in the events (inclusive).
25
+ * @param {number} [minRevision] The 1-based minimum revision to include in the events (inclusive).
26
+ * @param {number} [maxRevision] The 1-based maximum revision to include in the events (inclusive).
30
27
  */
31
- constructor(name, streams, eventStore, minRevision = 0, maxRevision = -1) {
28
+ constructor(name, streams, eventStore, minRevision = 1, maxRevision = -1) {
32
29
  super(name, eventStore, minRevision, maxRevision);
33
30
  if (!(streams instanceof Array) || streams.length === 0) {
34
31
  throw new Error(`Invalid list of streams supplied to JoinStream ${name}.`);
35
32
  }
36
- this._next = new Array(streams.length).fill(undefined);
37
33
 
34
+ this.streamIndex = eventStore.storage.index;
38
35
  // Translate revisions to index numbers (1-based) and wrap around negatives
39
- minRevision = wrapRevision(minRevision, eventStore.length);
40
- maxRevision = wrapRevision(maxRevision, eventStore.length);
41
-
42
- this.reverse = minRevision > maxRevision;
43
- this.iterator = streams.map(streamName => {
44
- if (!eventStore.streams[streamName]) {
45
- return { next() { return { done: true }; } };
46
- }
47
- const streamIndex = eventStore.streams[streamName].index;
48
- const from = streamIndex.find(minRevision, !this.reverse);
49
- const until = streamIndex.find(maxRevision, this.reverse);
50
- return eventStore.storage.readRange(from || 1, until, streamIndex);
51
- });
36
+ this.minRevision = normalizeVersion(minRevision, eventStore.length);
37
+ this.maxRevision = normalizeVersion(maxRevision, eventStore.length);
38
+ this.fetch = function() {
39
+ this._next = new Array(streams.length).fill(undefined);
40
+ return streams.map(streamName => {
41
+ if (!eventStore.streams[streamName]) {
42
+ return { next() { return { done: true }; } };
43
+ }
44
+ const streamIndex = eventStore.streams[streamName].index;
45
+ const from = streamIndex.find(this.minRevision, this.minRevision <= this.maxRevision);
46
+ const until = streamIndex.find(this.maxRevision, this.minRevision > this.maxRevision);
47
+ return eventStore.storage.readRange(from, until, streamIndex);
48
+ });
49
+ }
50
+ this._iterator = null;
52
51
  }
53
52
 
54
53
  /**
@@ -57,7 +56,7 @@ class JoinEventStream extends EventStream {
57
56
  * @returns {*}
58
57
  */
59
58
  getValue(index) {
60
- const next = this.iterator[index].next();
59
+ const next = this._iterator[index].next();
61
60
  return next.done ? false : next.value;
62
61
  }
63
62
 
@@ -65,10 +64,10 @@ class JoinEventStream extends EventStream {
65
64
  * @private
66
65
  * @param {number} first
67
66
  * @param {number} second
68
- * @returns {boolean} If the first item follows after the second in the given read order determined by this.reverse flag.
67
+ * @returns {boolean} If the first item follows after the second in the given read order determined by this.minRevision and this.maxRevision.
69
68
  */
70
69
  follows(first, second) {
71
- return (this.reverse ? first < second : first > second);
70
+ return (this.minRevision > this.maxRevision ? first < second : first > second);
72
71
  }
73
72
 
74
73
  /**
@@ -76,6 +75,9 @@ class JoinEventStream extends EventStream {
76
75
  * @returns {object|boolean} The next event or false if no more events in the stream.
77
76
  */
78
77
  next() {
78
+ if (!this._iterator) {
79
+ this._iterator = this.fetch();
80
+ }
79
81
  let nextIndex = -1;
80
82
  this._next.forEach((value, index) => {
81
83
  if (typeof value === 'undefined') {
@@ -99,4 +101,4 @@ class JoinEventStream extends EventStream {
99
101
 
100
102
  }
101
103
 
102
- module.exports = JoinEventStream;
104
+ export default JoinEventStream;
@@ -1,5 +1,5 @@
1
- const ReadablePartition = require('./ReadablePartition');
2
- const WatchesFile = require('../WatchesFile');
1
+ import ReadablePartition from './ReadablePartition.js';
2
+ import WatchesFile from '../WatchesFile.js';
3
3
 
4
4
  /**
5
5
  * A read-only partition is a readable partition that keeps it's size in sync with the underlying file.
@@ -42,4 +42,4 @@ class ReadOnlyPartition extends WatchesFile(ReadablePartition) {
42
42
 
43
43
  }
44
44
 
45
- module.exports = ReadOnlyPartition;
45
+ export default ReadOnlyPartition;