event-storage 0.7.2 → 0.9.1

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.
@@ -2,16 +2,14 @@ const stream = require('stream');
2
2
  const { assert } = require('./util');
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,7 +214,8 @@ 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
  *
78
- * @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.
217
+ * @api
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) {
81
221
  let next;
@@ -94,19 +234,33 @@ 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
  }
107
260
  return next.done ? false : next.value;
108
261
  }
109
262
 
263
+ // noinspection JSUnusedGlobalSymbols
110
264
  /**
111
265
  * Readable stream implementation.
112
266
  * @private
@@ -1,12 +1,25 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const EventEmitter = require('events');
3
+ const events = require('events');
4
4
  const Entry = require('../IndexEntry');
5
5
  const { assert, wrapAndCheck, binarySearch } = require('../util');
6
6
 
7
7
  // node-event-store-index V01
8
8
  const HEADER_MAGIC = "nesidx01";
9
9
 
10
+ class CorruptedIndexError extends Error {}
11
+
12
+ /**
13
+ * Returns a constructor for a CorruptedIndexError with the given size property.
14
+ */
15
+ function CorruptedIndexErrorFactory(size) {
16
+ return function (...args) {
17
+ let error = new CorruptedIndexError(...args);
18
+ error.size = size;
19
+ return error;
20
+ };
21
+ }
22
+
10
23
  /**
11
24
  * An index is a simple append-only file that stores an ordered list of entry elements pointing to the actual file position
12
25
  * where the matching document is found in the storage file.
@@ -18,7 +31,7 @@ const HEADER_MAGIC = "nesidx01";
18
31
  *
19
32
  * The index basically functions like a simplified LSM list.
20
33
  */
21
- class ReadableIndex extends EventEmitter {
34
+ class ReadableIndex extends events.EventEmitter {
22
35
 
23
36
  /**
24
37
  * @param {string} [name] The name of the file to use for storing the index.
@@ -52,13 +65,14 @@ class ReadableIndex extends EventEmitter {
52
65
  * @param {object} options
53
66
  */
54
67
  initialize(options) {
68
+ /* @type Array<Entry> */
55
69
  this.data = [];
56
70
  this.fd = null;
57
71
  this.fileMode = 'r';
58
72
  this.EntryClass = options.EntryClass;
59
73
  this.dataDirectory = options.dataDirectory;
60
74
  this.fileName = path.resolve(options.dataDirectory, this.name);
61
- this.readBuffer = Buffer.allocUnsafe(options.EntryClass.size);
75
+ this.readBuffer = Buffer.allocUnsafe(Math.max(options.EntryClass.size, options.writeBufferSize > 0 ? options.writeBufferSize : 4096));
62
76
 
63
77
  if (options.metadata) {
64
78
  this.metadata = Object.assign({entryClass: options.EntryClass.name, entrySize: options.EntryClass.size}, options.metadata);
@@ -110,14 +124,13 @@ class ReadableIndex extends EventEmitter {
110
124
  const stat = fs.fstatSync(this.fd);
111
125
  if (stat.size === 0) {
112
126
  return -1;
113
- } else {
114
- stat.size -= this.readMetadata();
115
- assert(stat.size >= 0, 'Invalid index file!');
116
127
  }
117
128
 
129
+ stat.size -= this.readMetadata();
130
+ assert(stat.size >= 0, 'Invalid index file!');
131
+
118
132
  const length = Math.floor(stat.size / this.EntryClass.size);
119
- // Corrupt index file
120
- assert(stat.size === length * this.EntryClass.size, 'Index file is corrupt!');
133
+ assert(stat.size === length * this.EntryClass.size, 'Index file is corrupt!', CorruptedIndexErrorFactory(length));
121
134
 
122
135
  return length;
123
136
  }
@@ -237,7 +250,7 @@ class ReadableIndex extends EventEmitter {
237
250
  * @returns {Entry} The index entry at the given position.
238
251
  */
239
252
  read(index) {
240
- index--;
253
+ index = Number(index) - 1;
241
254
 
242
255
  fs.readSync(this.fd, this.readBuffer, 0, this.EntryClass.size, this.headerSize + index * this.EntryClass.size);
243
256
  if (index === this.readUntil + 1) {
@@ -268,8 +281,11 @@ class ReadableIndex extends EventEmitter {
268
281
  const readFrom = Math.max(this.readUntil + 1, from);
269
282
  const amount = (until - readFrom + 1);
270
283
 
271
- const readBuffer = Buffer.allocUnsafe(amount * this.EntryClass.size);
272
- 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);
273
289
  let index = 0;
274
290
  while (index < amount && readSize > 0) {
275
291
  this.data[index + readFrom] = this.EntryClass.fromBuffer(readBuffer, index * this.EntryClass.size);
@@ -306,7 +322,7 @@ class ReadableIndex extends EventEmitter {
306
322
  */
307
323
  get(index) {
308
324
  index = wrapAndCheck(index, this.length);
309
- if (index === false) {
325
+ if (index <= 0) {
310
326
  return false;
311
327
  }
312
328
 
@@ -344,7 +360,7 @@ class ReadableIndex extends EventEmitter {
344
360
  from = wrapAndCheck(from, this.length);
345
361
  until = wrapAndCheck(until, this.length);
346
362
 
347
- if (from === false || until < from) {
363
+ if (from <= 0 || until < from) {
348
364
  return false;
349
365
  }
350
366
 
@@ -375,6 +391,9 @@ class ReadableIndex extends EventEmitter {
375
391
  * @returns {number} The last index entry position that is lower than or equal to the `number`. Returns 0 if no index matches.
376
392
  */
377
393
  find(number, min = false) {
394
+ if (this.length < 1) {
395
+ return 0;
396
+ }
378
397
  // We only need to search until the searched number because entry.number is always >= position
379
398
  const [low, high] = binarySearch(number, Math.min(this.length, number), index => this.get(index).number);
380
399
  return min ? low : high;
@@ -384,3 +403,4 @@ class ReadableIndex extends EventEmitter {
384
403
  module.exports = ReadableIndex;
385
404
  module.exports.Entry = Entry;
386
405
  module.exports.HEADER_MAGIC = HEADER_MAGIC;
406
+ module.exports.CorruptedIndexError = CorruptedIndexError;
@@ -1,7 +1,6 @@
1
1
  const fs = require('fs');
2
- const mkdirpSync = require('mkdirp').sync;
3
2
  const ReadableIndex = require('./ReadableIndex');
4
- const { assertEqual, buildMetadataHeader } = require('../util');
3
+ const { assertEqual, buildMetadataHeader, ensureDirectory } = require('../util');
5
4
 
6
5
  /**
7
6
  * An index is a simple append-only file that stores an ordered list of entry elements pointing to the actual file position
@@ -45,9 +44,7 @@ class WritableIndex extends ReadableIndex {
45
44
  */
46
45
  initialize(options) {
47
46
  super.initialize(options);
48
- if (!fs.existsSync(options.dataDirectory)) {
49
- mkdirpSync(options.dataDirectory);
50
- }
47
+ ensureDirectory(options.dataDirectory);
51
48
 
52
49
  this.fileMode = 'a+';
53
50
  this.writeBuffer = Buffer.allocUnsafe(options.writeBufferSize >>> 0); // jshint ignore:line
@@ -65,13 +62,21 @@ class WritableIndex extends ReadableIndex {
65
62
  * @throws {Error} If the file is corrupt or can not be read correctly.
66
63
  */
67
64
  checkFile() {
68
- const entries = super.checkFile();
69
- if (entries < 0) {
70
- // Freshly created index... write metadata initially.
71
- this.writeMetadata();
72
- return 0;
65
+ try {
66
+ const entries = super.checkFile();
67
+ if (entries < 0) {
68
+ // Freshly created index... write metadata initially.
69
+ this.writeMetadata();
70
+ return 0;
71
+ }
72
+ return entries;
73
+ } catch (e) {
74
+ if (e instanceof ReadableIndex.CorruptedIndexError) {
75
+ this.truncate(e.size);
76
+ return e.size;
77
+ }
78
+ throw e;
73
79
  }
74
- return entries;
75
80
  }
76
81
 
77
82
  /**
@@ -151,8 +156,9 @@ class WritableIndex extends ReadableIndex {
151
156
  }
152
157
 
153
158
  this.writeBufferCursor = 0;
154
- this.flushCallbacks.forEach(callback => callback());
159
+ const callbacks = this.flushCallbacks;
155
160
  this.flushCallbacks = [];
161
+ for (let i = 0; i < callbacks.length; i += 2) callbacks[i](callbacks[i + 1]);
156
162
  return true;
157
163
  }
158
164
 
@@ -167,7 +173,7 @@ class WritableIndex extends ReadableIndex {
167
173
  if (typeof callback !== 'function') {
168
174
  return;
169
175
  }
170
- this.flushCallbacks.push(() => callback(position));
176
+ this.flushCallbacks.push(callback, position);
171
177
  }
172
178
 
173
179
  /**
@@ -182,10 +188,15 @@ class WritableIndex extends ReadableIndex {
182
188
  assertEqual(entry.constructor.name, this.EntryClass.name, `Wrong entry object.`);
183
189
  assertEqual(entry.constructor.size, this.EntryClass.size, `Invalid entry size.`);
184
190
 
185
- 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) {
186
197
  this.readUntil++;
187
198
  }
188
- this.data[this.data.length] = entry;
199
+ this.data[dataLen] = entry;
189
200
 
190
201
  if (this.writeBufferCursor === 0) {
191
202
  this.flushTimeout = setTimeout(() => this.flush(), this.flushDelay);
@@ -206,7 +217,7 @@ class WritableIndex extends ReadableIndex {
206
217
  * @param {number} after The index entry number to truncate after.
207
218
  */
208
219
  truncate(after) {
209
- if (after > this.length) {
220
+ if (!this.fd) {
210
221
  return;
211
222
  }
212
223
  if (after < 0) {
@@ -214,7 +225,12 @@ class WritableIndex extends ReadableIndex {
214
225
  }
215
226
  this.flush();
216
227
 
217
- fs.truncateSync(this.fileName, this.headerSize + after * this.EntryClass.size);
228
+ const stat = fs.statSync(this.fileName);
229
+ const truncatePosition = this.headerSize + after * this.EntryClass.size;
230
+ if (truncatePosition >= stat.size) {
231
+ return;
232
+ }
233
+ fs.truncateSync(this.fileName, truncatePosition);
218
234
  this.data.splice(after);
219
235
  this.readUntil = Math.min(this.readUntil, after);
220
236
  }
package/src/IndexEntry.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  class EntryInterface {
7
7
  /**
8
+ * @abstract
8
9
  * @returns {number} The byte size of this Entry.
9
10
  */
10
11
  static get size() {}
@@ -12,6 +13,7 @@ class EntryInterface {
12
13
  /**
13
14
  * Read a new Entry from a Buffer object at the given offset.
14
15
  *
16
+ * @abstract
15
17
  * @param {Buffer} buffer The buffer object to read the index data from.
16
18
  * @param {number} [offset] The buffer offset to start reading from. Default 0.
17
19
  * @returns {EntryInterface} A new entry matching the values from the Buffer.
@@ -21,6 +23,7 @@ class EntryInterface {
21
23
  /**
22
24
  * Write this Entry into a Buffer object at the given offset.
23
25
  *
26
+ * @abstract
24
27
  * @param {Buffer} buffer The buffer object to write the index entry data to.
25
28
  * @param {number} offset The offset to start writing into the buffer.
26
29
  * @returns {number} The size of the data written.
@@ -48,7 +51,8 @@ function assertValidEntryClass(EntryClass) {
48
51
 
49
52
  /**
50
53
  * Default Entry item contains information about the sequence number, the file position, the document size and the partition number.
51
- * @type Array<number>
54
+ * @extends Array<number>
55
+ * @implements EntryInterface
52
56
  */
53
57
  class Entry extends Array {
54
58
 
@@ -1,18 +1,15 @@
1
1
  const EventStream = require('./EventStream');
2
+ const { wrapAndCheck } = require('./util');
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') {
@@ -18,6 +18,7 @@ class ReadOnlyPartition extends WatchesFile(ReadablePartition) {
18
18
  * @param {string} filename
19
19
  */
20
20
  onChange(filename) {
21
+ /* istanbul ignore if */
21
22
  if (!this.fd) {
22
23
  return;
23
24
  }