event-storage 1.1.0 → 1.2.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,5 +1,8 @@
1
1
  import stream from 'stream';
2
- import { assert } from './util.js';
2
+ import { assert } from './utils/util.js';
3
+ import { buildRawBufferMatcher, matches } from './utils/metadataUtil.js';
4
+
5
+ const NDJSON_NEWLINE = Buffer.from('\n');
3
6
 
4
7
  /**
5
8
  * Calculate the actual version number from a possibly relative (negative) version number.
@@ -33,25 +36,31 @@ class EventStream extends stream.Readable {
33
36
  * @param {EventStore} eventStore The event store to get the stream from.
34
37
  * @param {number} [minRevision] The minimum revision to include in the events (inclusive).
35
38
  * @param {number} [maxRevision] The maximum revision to include in the events (inclusive).
36
- * @param {function(object, object): boolean|null} [predicate] An optional filter function
37
- * `(payload, metadata) => boolean`. Only events for which this returns truthy are yielded.
39
+ * @param {function|object|null} [predicate] Optional matcher:
40
+ * - object mode: function `(payload, metadata) => boolean` or object matcher against `{ stream, payload, metadata }`
41
+ * - raw mode: function `(buffer) => boolean` or object matcher against compact NDJSON bytes.
42
+ * @param {boolean} [raw=false] If true, emit NDJSON Buffers instead of event payload objects.
38
43
  */
39
- constructor(name, eventStore, minRevision = 1, maxRevision = -1, predicate = null) {
40
- super({ objectMode: true });
44
+ constructor(name, eventStore, minRevision = 1, maxRevision = -1, predicate = null, raw = false) {
45
+ if (typeof predicate === 'boolean' && raw === false) {
46
+ raw = predicate;
47
+ predicate = null;
48
+ }
49
+ super({ objectMode: !raw });
41
50
  assert(typeof name === 'string' && name !== '', 'Need to specify a stream name.');
42
51
  assert(typeof eventStore === 'object' && eventStore !== null, `Need to provide EventStore instance to create EventStream ${name}.`);
43
52
 
44
53
  this.name = name;
54
+ this.raw = raw;
45
55
  this.predicate = predicate || null;
56
+ this.rawMatcher = null;
46
57
  if (eventStore.streams[name]) {
47
58
  this.streamIndex = eventStore.streams[name].index;
48
59
  this.minRevision = normalizeVersion(minRevision, this.streamIndex.length);
49
60
  this.maxRevision = normalizeVersion(maxRevision, this.streamIndex.length);
50
61
  this.version = minVersion(this.streamIndex.length, maxRevision);
51
62
  this._iterator = null;
52
- this.fetch = function() {
53
- return eventStore.storage.readRange(this.minRevision, this.maxRevision, this.streamIndex);
54
- }
63
+ this.fetch = () => eventStore.storage.readRange(this.minRevision, this.maxRevision, this.streamIndex, raw);
55
64
  } else {
56
65
  this.streamIndex = { length: 0 };
57
66
  this.version = -1;
@@ -233,10 +242,18 @@ class EventStream extends stream.Readable {
233
242
  *[Symbol.iterator]() {
234
243
  let next;
235
244
  while ((next = this.next()) !== false) {
236
- yield next.payload;
245
+ yield this.raw ? this.toRawBuffer(next) : next.payload;
237
246
  }
238
247
  }
239
248
 
249
+ /**
250
+ * @param {{ buffer: Buffer }} entry
251
+ * @returns {Buffer}
252
+ */
253
+ toRawBuffer(entry) {
254
+ return Buffer.concat([entry.buffer, NDJSON_NEWLINE]);
255
+ }
256
+
240
257
  /**
241
258
  * Reset this stream to the start so it can be iterated again.
242
259
  * @returns {EventStream}
@@ -259,11 +276,32 @@ class EventStream extends stream.Readable {
259
276
  */
260
277
  filter(predicate) {
261
278
  this.predicate = predicate || null;
279
+ this.rawMatcher = null;
262
280
  this._iterator = null;
263
281
  this._events = null;
264
282
  return this;
265
283
  }
266
284
 
285
+ matchesPredicate(entry) {
286
+ if (!this.predicate) {
287
+ return true;
288
+ }
289
+ if (this.raw) {
290
+ if (typeof this.predicate === 'function') {
291
+ return this.predicate(entry.buffer);
292
+ }
293
+ if (!this.rawMatcher) {
294
+ this.rawMatcher = buildRawBufferMatcher(this.predicate);
295
+ }
296
+ return this.rawMatcher(entry.buffer);
297
+ }
298
+
299
+ if (typeof this.predicate === 'function') {
300
+ return this.predicate(entry.payload, entry.metadata);
301
+ }
302
+ return matches(entry, this.predicate);
303
+ }
304
+
267
305
  /**
268
306
  * @returns {object|boolean} The next event or false if no more events in the stream.
269
307
  */
@@ -275,7 +313,7 @@ class EventStream extends stream.Readable {
275
313
  while (true) {
276
314
  const result = this._iterator.next();
277
315
  if (result.done) return false;
278
- if (!this.predicate || this.predicate(result.value.payload, result.value.metadata)) {
316
+ if (this.matchesPredicate(result.value)) {
279
317
  return result.value;
280
318
  }
281
319
  }
@@ -291,7 +329,7 @@ class EventStream extends stream.Readable {
291
329
  */
292
330
  _read() {
293
331
  const next = this.next();
294
- this.push(next ? next.payload : null);
332
+ this.push(next ? (this.raw ? this.toRawBuffer(next) : next.payload) : null);
295
333
  }
296
334
 
297
335
  }
@@ -2,7 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import events from 'events';
4
4
  import Entry, { assertValidEntryClass } from '../IndexEntry.js';
5
- import { assert, wrapAndCheck, binarySearch } from '../util.js';
5
+ import { assert, wrapAndCheck, binarySearch } from '../utils/util.js';
6
6
 
7
7
  // node-event-store-index V01
8
8
  const HEADER_MAGIC = "nesidx01";
@@ -77,6 +77,7 @@ class ReadableIndex extends events.EventEmitter {
77
77
  if (options.metadata) {
78
78
  this.metadata = Object.assign({entryClass: options.EntryClass.name, entrySize: options.EntryClass.size}, options.metadata);
79
79
  }
80
+ this.headerSize = 0;
80
81
  }
81
82
 
82
83
  /**
@@ -208,12 +209,13 @@ class ReadableIndex extends events.EventEmitter {
208
209
  * @throws {Error} if the metadata size in the header is invalid.
209
210
  */
210
211
  readMetadata() {
212
+ if (this.headerSize > 0) return this.headerSize;
211
213
  const headerBuffer = Buffer.allocUnsafe(8 + 4);
212
214
  fs.readSync(this.fd, headerBuffer, 0, 8 + 4, 0);
213
215
  const headerMagic = headerBuffer.toString('utf8', 0, 8);
214
216
 
215
- assert(headerMagic.substr(0, 6) === HEADER_MAGIC.substr(0, 6), 'Invalid file header.');
216
- assert(headerMagic === HEADER_MAGIC, `Invalid file version. The index ${this.fileName} was created with a different library version (${headerMagic.substr(6)}).`);
217
+ assert(headerMagic.substring(0, 6) === HEADER_MAGIC.substring(0, 6), 'Invalid file header.');
218
+ assert(headerMagic === HEADER_MAGIC, `Invalid file version. The index ${this.fileName} was created with a different library version (${headerMagic.substring(6)}).`);
217
219
 
218
220
  const metadataSize = headerBuffer.readUInt32BE(8);
219
221
  assert(metadataSize >= 3, 'Invalid metadata size.');
@@ -353,7 +355,7 @@ class ReadableIndex extends events.EventEmitter {
353
355
  *
354
356
  * @api
355
357
  * @param {number} from The 1-based index position from where to get entries from (inclusive). If < 0 will start at that position from end.
356
- * @param {number} [until] The 1-based index position until where to get entries to (inclusive). If < 0 will get until that position from the end. Defaults to this.length.
358
+ * @param {number} [until=-1] The 1-based index position until where to get entries to (inclusive). If < 0 will get until that position from the end. Defaults to this.length.
357
359
  * @returns {Array<Entry>|boolean} An array of entries for the given range or false on error.
358
360
  */
359
361
  range(from, until = -1) {
@@ -387,7 +389,7 @@ class ReadableIndex extends events.EventEmitter {
387
389
  *
388
390
  * @api
389
391
  * @param {number} number The sequence number to search for.
390
- * @param {boolean} [min] If set to true, will return the first entry that has a sequence number greater than or equal to `number`.
392
+ * @param {boolean} [min=false] If set to true, will return the first entry that has a sequence number greater than or equal to `number`.
391
393
  * @returns {number} The last index entry position that is lower than or equal to the `number`. Returns 0 if no index matches.
392
394
  */
393
395
  find(number, min = false) {
@@ -1,8 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import ReadableIndex, { Entry, CorruptedIndexError, HEADER_MAGIC } from './ReadableIndex.js';
3
- import { assertEqual } from '../util.js';
4
- import { buildMetadataHeader } from '../metadataUtil.js';
5
- import { ensureDirectory } from '../fsUtil.js';
3
+ import { assert, assertEqual } from '../utils/util.js';
4
+ import { buildMetadataHeader } from '../utils/metadataUtil.js';
5
+ import { ensureDirectory } from '../utils/fsUtil.js';
6
6
 
7
7
  /**
8
8
  * An index is a simple append-only file that stores an ordered list of entry elements pointing to the actual file position
@@ -191,9 +191,7 @@ class WritableIndex extends ReadableIndex {
191
191
  assertEqual(entry.constructor.size, this.EntryClass.size, `Invalid entry size.`);
192
192
 
193
193
  const dataLen = this.data.length;
194
- if (dataLen > 0 && this.data[dataLen - 1].number >= entry.number) {
195
- throw new Error('Consistency error. Tried to add an index that should come before existing last entry.');
196
- }
194
+ assert(dataLen === 0 || this.data.at(-1).number < entry.number, 'Consistency error. Tried to add an index that should come before existing last entry.');
197
195
 
198
196
  if (this.readUntil === dataLen - 1) {
199
197
  this.readUntil++;
@@ -1,5 +1,5 @@
1
- import { getPropertyAtPath } from './util.js';
2
- import { matches } from './metadataUtil.js';
1
+ import { getPropertyAtPath } from './utils/util.js';
2
+ import { matches } from './utils/metadataUtil.js';
3
3
 
4
4
  /**
5
5
  * @typedef {object|function(object):boolean} Matcher
@@ -1,5 +1,5 @@
1
1
  import EventStream from './EventStream.js';
2
- import { wrapAndCheck } from './util.js';
2
+ import { assert, kWayMerge } from './utils/util.js';
3
3
 
4
4
  /** Reusable sentinel used for missing or empty per-stream iterators. */
5
5
  const emptyIterator = Object.freeze({ next() { return { done: true }; } });
@@ -27,21 +27,18 @@ class JoinEventStream extends EventStream {
27
27
  * @param {EventStore} eventStore The event store to get the stream from.
28
28
  * @param {number} [minRevision] The 1-based minimum revision to include in the events (inclusive).
29
29
  * @param {number} [maxRevision] The 1-based maximum revision to include in the events (inclusive).
30
- * @param {function(object, object): boolean|null} [predicate] An optional filter function
31
- * `(payload, metadata) => boolean`. Only events for which this returns truthy are yielded.
30
+ * @param {function|object|null} [predicate] Optional matcher (see {@link EventStream}).
31
+ * @param {boolean} [raw=false] If true, emit NDJSON Buffers.
32
32
  */
33
- constructor(name, streams, eventStore, minRevision = 1, maxRevision = -1, predicate = null) {
34
- super(name, eventStore, minRevision, maxRevision, predicate);
35
- if (!(streams instanceof Array) || streams.length === 0) {
36
- throw new Error(`Invalid list of streams supplied to JoinStream ${name}.`);
37
- }
33
+ constructor(name, streams, eventStore, minRevision = 1, maxRevision = -1, predicate = null, raw = false) {
34
+ super(name, eventStore, minRevision, maxRevision, predicate, raw);
35
+ assert(streams instanceof Array && streams.length > 0, `Invalid list of streams supplied to JoinStream ${name}.`);
38
36
 
39
37
  this.streamIndex = eventStore.storage.index;
40
38
  // Translate revisions to index numbers (1-based) and wrap around negatives
41
39
  this.minRevision = normalizeVersion(minRevision, eventStore.length);
42
40
  this.maxRevision = normalizeVersion(maxRevision, eventStore.length);
43
41
  this.fetch = function() {
44
- this._next = new Array(streams.length).fill(undefined);
45
42
  return streams.map(streamName => {
46
43
  const streamIndex = eventStore.streams[streamName]?.index;
47
44
  if (!streamIndex || streamIndex.length === 0) {
@@ -54,60 +51,47 @@ class JoinEventStream extends EventStream {
54
51
  // (e.g. minRevision > all entries, or maxRevision < all entries).
55
52
  return emptyIterator;
56
53
  }
57
- return eventStore.storage.readRange(from, until, streamIndex);
54
+ // Raw mode: get { buffer, time64, sequenceNumber } for binary-header ordering.
55
+ // Object mode: storage deserializes for us and we order by metadata.commitId.
56
+ return eventStore.storage.readRange(from, until, streamIndex, this.raw);
58
57
  });
59
58
  }
60
59
  this._iterator = null;
61
60
  }
62
61
 
63
62
  /**
64
- * Returns the value of the iterator at position `index`
65
- * @param {number} index The iterator position for which to return the next value
66
- * @returns {*}
67
- */
68
- getValue(index) {
69
- const next = this._iterator[index].next();
70
- return next.done ? false : next.value;
71
- }
72
-
73
- /**
74
- * @private
75
- * @param {number} first
76
- * @param {number} second
77
- * @returns {boolean} If the first item follows after the second in the given read order determined by this.minRevision and this.maxRevision.
63
+ * @returns {Generator<object>}
78
64
  */
79
- follows(first, second) {
80
- return (this.minRevision > this.maxRevision ? first < second : first > second);
65
+ createMergedIterator() {
66
+ const ascending = this.minRevision <= this.maxRevision;
67
+ const raw = this.raw;
68
+ return kWayMerge(
69
+ this.fetch(),
70
+ entry => raw ? entry.sequenceNumber : entry.metadata.commitId,
71
+ ascending
72
+ );
81
73
  }
82
74
 
83
75
  /**
84
- * @private
85
- * @returns {object|boolean} The next event or false if no more events in the stream.
76
+ * Returns the next event in merge order.
77
+ *
78
+ * In raw mode: returns `{ buffer, time64, sequenceNumber }` from the binary header — no JSON
79
+ * deserialization. In object mode: returns a deserialized `{ stream, payload, metadata }` document
80
+ * produced by the storage layer.
81
+ * @returns {object|false} The next event, or `false` when the stream is exhausted.
86
82
  */
87
83
  next() {
88
84
  if (!this._iterator) {
89
- this._iterator = this.fetch();
85
+ this._iterator = this.createMergedIterator();
90
86
  }
91
87
  while (true) {
92
- let nextIndex = -1;
93
- this._next.forEach((value, index) => {
94
- if (typeof value === 'undefined') {
95
- value = this._next[index] = this.getValue(index);
96
- }
97
- if (value === false) {
98
- return;
99
- }
100
- if (nextIndex === -1 || this.follows(this._next[nextIndex].metadata.commitId, value.metadata.commitId)) {
101
- nextIndex = index;
102
- }
103
- });
104
-
105
- if (nextIndex === -1) {
88
+ const step = this._iterator.next();
89
+ if (step.done) {
106
90
  return false;
107
91
  }
108
- const next = this._next[nextIndex];
109
- this._next[nextIndex] = undefined;
110
- if (!this.predicate || this.predicate(next.payload, next.metadata)) {
92
+ const next = step.value;
93
+
94
+ if (this.matchesPredicate(next)) {
111
95
  return next;
112
96
  }
113
97
  }