event-storage 1.1.0 → 1.3.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,26 +1,9 @@
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
+ import { normalizeRevision, normalizeMaxRevision } from './utils/apiHelpers.js';
3
5
 
4
- /**
5
- * Calculate the actual version number from a possibly relative (negative) version number.
6
- *
7
- * @param {number} version The version to normalize.
8
- * @param {number} length The maximum version number
9
- * @returns {number} The absolute version number.
10
- */
11
- function normalizeVersion(version, length) {
12
- return version < 0 ? version + length + 1 : version;
13
- }
14
-
15
- /**
16
- * Return the lower absolute version given a version and a maxVersion constraint.
17
- * @param {number} version
18
- * @param {number} maxVersion
19
- * @returns {number}
20
- */
21
- function minVersion(version, maxVersion) {
22
- return Math.min(version, maxVersion < 0 ? version + maxVersion + 1 : maxVersion);
23
- }
6
+ const NDJSON_NEWLINE = Buffer.from('\n');
24
7
 
25
8
  /**
26
9
  * An event stream is a simple wrapper around an iterator over storage documents.
@@ -33,25 +16,31 @@ class EventStream extends stream.Readable {
33
16
  * @param {EventStore} eventStore The event store to get the stream from.
34
17
  * @param {number} [minRevision] The minimum revision to include in the events (inclusive).
35
18
  * @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.
19
+ * @param {function|object|null} [predicate] Optional matcher:
20
+ * - object mode: function `(payload, metadata) => boolean` or object matcher against `{ stream, payload, metadata }`
21
+ * - raw mode: function `(buffer) => boolean` or object matcher against compact NDJSON bytes.
22
+ * @param {boolean} [raw=false] If true, emit NDJSON Buffers instead of event payload objects.
38
23
  */
39
- constructor(name, eventStore, minRevision = 1, maxRevision = -1, predicate = null) {
40
- super({ objectMode: true });
24
+ constructor(name, eventStore, minRevision = 1, maxRevision = -1, predicate = null, raw = false) {
25
+ if (typeof predicate === 'boolean' && raw === false) {
26
+ raw = predicate;
27
+ predicate = null;
28
+ }
29
+ super({ objectMode: !raw });
41
30
  assert(typeof name === 'string' && name !== '', 'Need to specify a stream name.');
42
31
  assert(typeof eventStore === 'object' && eventStore !== null, `Need to provide EventStore instance to create EventStream ${name}.`);
43
32
 
44
33
  this.name = name;
34
+ this.raw = raw;
45
35
  this.predicate = predicate || null;
36
+ this.rawMatcher = null;
46
37
  if (eventStore.streams[name]) {
47
38
  this.streamIndex = eventStore.streams[name].index;
48
- this.minRevision = normalizeVersion(minRevision, this.streamIndex.length);
49
- this.maxRevision = normalizeVersion(maxRevision, this.streamIndex.length);
50
- this.version = minVersion(this.streamIndex.length, maxRevision);
39
+ this.minRevision = normalizeRevision(minRevision, this.streamIndex.length);
40
+ this.maxRevision = normalizeRevision(maxRevision, this.streamIndex.length);
41
+ this.version = normalizeMaxRevision(this.streamIndex.length, maxRevision);
51
42
  this._iterator = null;
52
- this.fetch = function() {
53
- return eventStore.storage.readRange(this.minRevision, this.maxRevision, this.streamIndex);
54
- }
43
+ this.fetch = () => eventStore.storage.readRange(this.minRevision, this.maxRevision, this.streamIndex, raw);
55
44
  } else {
56
45
  this.streamIndex = { length: 0 };
57
46
  this.version = -1;
@@ -65,7 +54,7 @@ class EventStream extends stream.Readable {
65
54
  * @returns {EventStream}
66
55
  */
67
56
  from(revision) {
68
- this.minRevision = normalizeVersion(revision, this.streamIndex.length);
57
+ this.minRevision = normalizeRevision(revision, this.streamIndex.length);
69
58
  return this;
70
59
  }
71
60
 
@@ -75,8 +64,8 @@ class EventStream extends stream.Readable {
75
64
  * @returns {EventStream}
76
65
  */
77
66
  until(revision) {
78
- this.maxRevision = normalizeVersion(revision, this.streamIndex.length);
79
- this.version = minVersion(this.streamIndex.length, this.maxRevision);
67
+ this.maxRevision = normalizeRevision(revision, this.streamIndex.length);
68
+ this.version = normalizeMaxRevision(this.streamIndex.length, this.maxRevision);
80
69
  return this;
81
70
  }
82
71
 
@@ -160,7 +149,7 @@ class EventStream extends stream.Readable {
160
149
  let tmp = this.maxRevision;
161
150
  this.maxRevision = this.minRevision;
162
151
  this.minRevision = tmp;
163
- this.version = minVersion(this.streamIndex.length, this.maxRevision);
152
+ this.version = normalizeMaxRevision(this.streamIndex.length, this.maxRevision);
164
153
  return this;
165
154
  }
166
155
 
@@ -233,10 +222,18 @@ class EventStream extends stream.Readable {
233
222
  *[Symbol.iterator]() {
234
223
  let next;
235
224
  while ((next = this.next()) !== false) {
236
- yield next.payload;
225
+ yield this.raw ? this.toRawBuffer(next) : next.payload;
237
226
  }
238
227
  }
239
228
 
229
+ /**
230
+ * @param {{ buffer: Buffer }} entry
231
+ * @returns {Buffer}
232
+ */
233
+ toRawBuffer(entry) {
234
+ return Buffer.concat([entry.buffer, NDJSON_NEWLINE]);
235
+ }
236
+
240
237
  /**
241
238
  * Reset this stream to the start so it can be iterated again.
242
239
  * @returns {EventStream}
@@ -259,11 +256,32 @@ class EventStream extends stream.Readable {
259
256
  */
260
257
  filter(predicate) {
261
258
  this.predicate = predicate || null;
259
+ this.rawMatcher = null;
262
260
  this._iterator = null;
263
261
  this._events = null;
264
262
  return this;
265
263
  }
266
264
 
265
+ matchesPredicate(entry) {
266
+ if (!this.predicate) {
267
+ return true;
268
+ }
269
+ if (this.raw) {
270
+ if (typeof this.predicate === 'function') {
271
+ return this.predicate(entry.buffer);
272
+ }
273
+ if (!this.rawMatcher) {
274
+ this.rawMatcher = buildRawBufferMatcher(this.predicate);
275
+ }
276
+ return this.rawMatcher(entry.buffer);
277
+ }
278
+
279
+ if (typeof this.predicate === 'function') {
280
+ return this.predicate(entry.payload, entry.metadata);
281
+ }
282
+ return matches(entry, this.predicate);
283
+ }
284
+
267
285
  /**
268
286
  * @returns {object|boolean} The next event or false if no more events in the stream.
269
287
  */
@@ -275,7 +293,7 @@ class EventStream extends stream.Readable {
275
293
  while (true) {
276
294
  const result = this._iterator.next();
277
295
  if (result.done) return false;
278
- if (!this.predicate || this.predicate(result.value.payload, result.value.metadata)) {
296
+ if (this.matchesPredicate(result.value)) {
279
297
  return result.value;
280
298
  }
281
299
  }
@@ -291,7 +309,7 @@ class EventStream extends stream.Readable {
291
309
  */
292
310
  _read() {
293
311
  const next = this.next();
294
- this.push(next ? next.payload : null);
312
+ this.push(next ? (this.raw ? this.toRawBuffer(next) : next.payload) : null);
295
313
  }
296
314
 
297
315
  }
@@ -20,7 +20,7 @@ class ReadOnlyIndex extends watchesFile(ReadableIndex) {
20
20
  * @param {string} filename
21
21
  */
22
22
  onChange(filename) {
23
- /* istanbul ignore if */
23
+ /* c8 ignore next 3 */
24
24
  if (!this.fd) {
25
25
  return;
26
26
  }
@@ -2,7 +2,8 @@ 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
+ import { normalizeNamedCtorArgs } from '../utils/apiHelpers.js';
6
7
 
7
8
  // node-event-store-index V01
8
9
  const HEADER_MAGIC = "nesidx01";
@@ -44,10 +45,7 @@ class ReadableIndex extends events.EventEmitter {
44
45
  */
45
46
  constructor(name = '.index', options = {}) {
46
47
  super();
47
- if (typeof name !== 'string') {
48
- options = name;
49
- name = '.index';
50
- }
48
+ ({ name, options } = normalizeNamedCtorArgs(name, options, '.index'));
51
49
  let defaults = {
52
50
  dataDirectory: '.',
53
51
  EntryClass: Entry
@@ -77,6 +75,7 @@ class ReadableIndex extends events.EventEmitter {
77
75
  if (options.metadata) {
78
76
  this.metadata = Object.assign({entryClass: options.EntryClass.name, entrySize: options.EntryClass.size}, options.metadata);
79
77
  }
78
+ this.headerSize = 0;
80
79
  }
81
80
 
82
81
  /**
@@ -208,12 +207,13 @@ class ReadableIndex extends events.EventEmitter {
208
207
  * @throws {Error} if the metadata size in the header is invalid.
209
208
  */
210
209
  readMetadata() {
210
+ if (this.headerSize > 0) return this.headerSize;
211
211
  const headerBuffer = Buffer.allocUnsafe(8 + 4);
212
212
  fs.readSync(this.fd, headerBuffer, 0, 8 + 4, 0);
213
213
  const headerMagic = headerBuffer.toString('utf8', 0, 8);
214
214
 
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)}).`);
215
+ assert(headerMagic.substring(0, 6) === HEADER_MAGIC.substring(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.substring(6)}).`);
217
217
 
218
218
  const metadataSize = headerBuffer.readUInt32BE(8);
219
219
  assert(metadataSize >= 3, 'Invalid metadata size.');
@@ -353,7 +353,7 @@ class ReadableIndex extends events.EventEmitter {
353
353
  *
354
354
  * @api
355
355
  * @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.
356
+ * @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
357
  * @returns {Array<Entry>|boolean} An array of entries for the given range or false on error.
358
358
  */
359
359
  range(from, until = -1) {
@@ -387,7 +387,7 @@ class ReadableIndex extends events.EventEmitter {
387
387
  *
388
388
  * @api
389
389
  * @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`.
390
+ * @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
391
  * @returns {number} The last index entry position that is lower than or equal to the `number`. Returns 0 if no index matches.
392
392
  */
393
393
  find(number, min = false) {
@@ -1,8 +1,9 @@
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
+ import { normalizeNamedCtorArgs } from '../utils/apiHelpers.js';
6
7
 
7
8
  /**
8
9
  * An index is a simple append-only file that stores an ordered list of entry elements pointing to the actual file position
@@ -27,10 +28,7 @@ class WritableIndex extends ReadableIndex {
27
28
  * @param {object} [options.metadata] An object containing the metadata information for this index. Will be written on initial creation and checked on subsequent openings.
28
29
  */
29
30
  constructor(name = '.index', options = {}) {
30
- if (typeof name !== 'string') {
31
- options = name;
32
- name = '.index';
33
- }
31
+ ({ name, options } = normalizeNamedCtorArgs(name, options, '.index'));
34
32
  let defaults = {
35
33
  writeBufferSize: 4096,
36
34
  flushDelay: 100,
@@ -191,9 +189,7 @@ class WritableIndex extends ReadableIndex {
191
189
  assertEqual(entry.constructor.size, this.EntryClass.size, `Invalid entry size.`);
192
190
 
193
191
  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
- }
192
+ 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
193
 
198
194
  if (this.readUntil === dataLen - 1) {
199
195
  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,20 +1,10 @@
1
1
  import EventStream from './EventStream.js';
2
- import { wrapAndCheck } from './util.js';
2
+ import { assert, kWayMerge } from './utils/util.js';
3
+ import { normalizeRevision } from './utils/apiHelpers.js';
3
4
 
4
5
  /** Reusable sentinel used for missing or empty per-stream iterators. */
5
6
  const emptyIterator = Object.freeze({ next() { return { done: true }; } });
6
7
 
7
- /**
8
- * Calculate the actual version number from a possibly relative (negative) version number.
9
- *
10
- * @param {number} version The version to normalize.
11
- * @param {number} length The maximum version number
12
- * @returns {number} The absolute version number.
13
- */
14
- function normalizeVersion(version, length) {
15
- return version < 0 ? version + length + 1 : version;
16
- }
17
-
18
8
  /**
19
9
  * An event stream is a simple wrapper around an iterator over storage documents.
20
10
  * It implements a node readable stream interface.
@@ -27,21 +17,18 @@ class JoinEventStream extends EventStream {
27
17
  * @param {EventStore} eventStore The event store to get the stream from.
28
18
  * @param {number} [minRevision] The 1-based minimum revision to include in the events (inclusive).
29
19
  * @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.
20
+ * @param {function|object|null} [predicate] Optional matcher (see {@link EventStream}).
21
+ * @param {boolean} [raw=false] If true, emit NDJSON Buffers.
32
22
  */
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
- }
23
+ constructor(name, streams, eventStore, minRevision = 1, maxRevision = -1, predicate = null, raw = false) {
24
+ super(name, eventStore, minRevision, maxRevision, predicate, raw);
25
+ assert(streams instanceof Array && streams.length > 0, `Invalid list of streams supplied to JoinStream ${name}.`);
38
26
 
39
27
  this.streamIndex = eventStore.storage.index;
40
28
  // Translate revisions to index numbers (1-based) and wrap around negatives
41
- this.minRevision = normalizeVersion(minRevision, eventStore.length);
42
- this.maxRevision = normalizeVersion(maxRevision, eventStore.length);
29
+ this.minRevision = normalizeRevision(minRevision, eventStore.length);
30
+ this.maxRevision = normalizeRevision(maxRevision, eventStore.length);
43
31
  this.fetch = function() {
44
- this._next = new Array(streams.length).fill(undefined);
45
32
  return streams.map(streamName => {
46
33
  const streamIndex = eventStore.streams[streamName]?.index;
47
34
  if (!streamIndex || streamIndex.length === 0) {
@@ -54,60 +41,47 @@ class JoinEventStream extends EventStream {
54
41
  // (e.g. minRevision > all entries, or maxRevision < all entries).
55
42
  return emptyIterator;
56
43
  }
57
- return eventStore.storage.readRange(from, until, streamIndex);
44
+ // Raw mode: get { buffer, time64, sequenceNumber } for binary-header ordering.
45
+ // Object mode: storage deserializes for us and we order by metadata.commitId.
46
+ return eventStore.storage.readRange(from, until, streamIndex, this.raw);
58
47
  });
59
48
  }
60
49
  this._iterator = null;
61
50
  }
62
51
 
63
52
  /**
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 {*}
53
+ * @returns {Generator<object>}
67
54
  */
68
- getValue(index) {
69
- const next = this._iterator[index].next();
70
- return next.done ? false : next.value;
55
+ createMergedIterator() {
56
+ const ascending = this.minRevision <= this.maxRevision;
57
+ const raw = this.raw;
58
+ return kWayMerge(
59
+ this.fetch(),
60
+ entry => raw ? entry.sequenceNumber : entry.metadata.commitId,
61
+ ascending
62
+ );
71
63
  }
72
64
 
73
65
  /**
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.
78
- */
79
- follows(first, second) {
80
- return (this.minRevision > this.maxRevision ? first < second : first > second);
81
- }
82
-
83
- /**
84
- * @private
85
- * @returns {object|boolean} The next event or false if no more events in the stream.
66
+ * Returns the next event in merge order.
67
+ *
68
+ * In raw mode: returns `{ buffer, time64, sequenceNumber }` from the binary header — no JSON
69
+ * deserialization. In object mode: returns a deserialized `{ stream, payload, metadata }` document
70
+ * produced by the storage layer.
71
+ * @returns {object|false} The next event, or `false` when the stream is exhausted.
86
72
  */
87
73
  next() {
88
74
  if (!this._iterator) {
89
- this._iterator = this.fetch();
75
+ this._iterator = this.createMergedIterator();
90
76
  }
91
77
  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) {
78
+ const step = this._iterator.next();
79
+ if (step.done) {
106
80
  return false;
107
81
  }
108
- const next = this._next[nextIndex];
109
- this._next[nextIndex] = undefined;
110
- if (!this.predicate || this.predicate(next.payload, next.metadata)) {
82
+ const next = step.value;
83
+
84
+ if (this.matchesPredicate(next)) {
111
85
  return next;
112
86
  }
113
87
  }
@@ -18,7 +18,7 @@ class ReadOnlyPartition extends WatchesFile(ReadablePartition) {
18
18
  * @param {string} filename
19
19
  */
20
20
  onChange(filename) {
21
- /* istanbul ignore if */
21
+ /* c8 ignore next 3 */
22
22
  if (!this.fd) {
23
23
  return;
24
24
  }