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.
package/src/EventStore.js CHANGED
@@ -2,11 +2,10 @@ const EventStream = require('./EventStream');
2
2
  const JoinEventStream = require('./JoinEventStream');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const EventEmitter = require('events');
5
+ const events = require('events');
6
6
  const Storage = require('./Storage');
7
7
  const Consumer = require('./Consumer');
8
- const stream = require('stream');
9
- const { assert } = require('./util');
8
+ const { assert, scanForFiles } = require('./util');
10
9
 
11
10
  const ExpectedVersion = {
12
11
  Any: -1,
@@ -15,30 +14,12 @@ const ExpectedVersion = {
15
14
 
16
15
  class OptimisticConcurrencyError extends Error {}
17
16
 
18
- class EventUnwrapper extends stream.Transform {
19
-
20
- constructor() {
21
- super({ objectMode: true });
22
- }
23
-
24
- _transform(data, encoding, callback) {
25
- /* istanbul ignore else */
26
- if (data.stream && data.payload) {
27
- this.push(data.payload);
28
- } else {
29
- this.push(data);
30
- }
31
- callback();
32
- }
33
-
34
- }
35
-
36
17
  /**
37
18
  * An event store optimized for working with many streams.
38
19
  * An event stream is implemented as an iterator over an index on the storage, therefore indexes need to be lightweight
39
20
  * and highly performant in read-only mode.
40
21
  */
41
- class EventStore extends EventEmitter {
22
+ class EventStore extends events.EventEmitter {
42
23
 
43
24
  /**
44
25
  * @param {string} [storeName] The name of the store which will be used as storage prefix. Default 'eventstore'.
@@ -47,6 +28,10 @@ class EventStore extends EventEmitter {
47
28
  * @param {string} [config.streamsDirectory] The directory where the streams should be stored. Default '{storageDirectory}/streams'.
48
29
  * @param {object} [config.storageConfig] Additional config options given to the storage backend. See `Storage`.
49
30
  * @param {boolean} [config.readOnly] If the storage should be mounted in read-only mode.
31
+ * @param {object|function(string): object} [config.streamMetadata] A metadata object or a function `(streamName) => object`
32
+ * that is called whenever a new stream partition is created. The returned object is stored once in the partition
33
+ * file header and surfaced to `preCommit` / `preRead` hooks. Takes precedence only when
34
+ * `config.storageConfig.metadata` is not also set.
50
35
  */
51
36
  constructor(storeName = 'eventstore', config = {}) {
52
37
  super();
@@ -55,7 +40,7 @@ class EventStore extends EventEmitter {
55
40
  storeName = 'eventstore';
56
41
  }
57
42
 
58
- this.storageDirectory = path.resolve(config.storageDirectory || './data');
43
+ this.storageDirectory = path.resolve(config.storageDirectory || /* istanbul ignore next */ './data');
59
44
  let defaults = {
60
45
  dataDirectory: this.storageDirectory,
61
46
  indexDirectory: config.streamsDirectory || path.join(this.storageDirectory, 'streams'),
@@ -63,32 +48,76 @@ class EventStore extends EventEmitter {
63
48
  readOnly: config.readOnly || false
64
49
  };
65
50
  const storageConfig = Object.assign(defaults, config.storageConfig);
51
+
52
+ // Translate the high-level streamMetadata option into the storage-level metadata function,
53
+ // but only when the caller has not already provided a lower-level storageConfig.metadata.
54
+ if (config.streamMetadata !== undefined && storageConfig.metadata === undefined) {
55
+ if (typeof config.streamMetadata === 'function') {
56
+ storageConfig.metadata = config.streamMetadata;
57
+ } else {
58
+ storageConfig.metadata = (streamName) => config.streamMetadata[streamName] || {};
59
+ }
60
+ }
61
+
62
+ this.initialize(storeName, storageConfig);
63
+ }
64
+
65
+ /**
66
+ * @private
67
+ * @param {string} storeName
68
+ * @param {object} storageConfig
69
+ */
70
+ initialize(storeName, storageConfig) {
66
71
  this.streamsDirectory = path.resolve(storageConfig.indexDirectory);
67
72
 
68
73
  this.storeName = storeName;
69
- this.storage = this.createStorage(this.storeName, storageConfig);
74
+ this.storage = (storageConfig.readOnly === true) ?
75
+ new Storage.ReadOnly(storeName, storageConfig)
76
+ : new Storage(storeName, storageConfig);
70
77
  this.storage.open();
71
- this.streams = { _all: { index: this.storage.index } };
78
+ this.streams = Object.create(null);
79
+ this.streams._all = { index: this.storage.index };
72
80
 
73
81
  this.scanStreams((err) => {
74
82
  if (err) {
75
83
  this.storage.close();
76
84
  throw err;
77
85
  }
86
+ this.checkUnfinishedCommits();
78
87
  this.emit('ready');
79
88
  });
80
89
  }
81
90
 
82
91
  /**
83
- * @param {string} name
84
- * @param {object} config
85
- * @returns {ReadableStorage|WritableStorage}
92
+ * Check if the last commit in the store was unfinished, which is the case if not all events of the commit have been written.
93
+ * Torn writes are handled at the storage level, so this method only deals with unfinished commits.
94
+ * @private
86
95
  */
87
- createStorage(name, config) {
88
- if (config.readOnly === true) {
89
- return new Storage.ReadOnly(name, config);
96
+ checkUnfinishedCommits() {
97
+ let position = this.storage.length;
98
+ let lastEvent;
99
+ let truncateIndex = false;
100
+ while (position > 0) {
101
+ try {
102
+ lastEvent = this.storage.read(position);
103
+ } catch (e) {
104
+ // A preRead hook may throw (e.g. access control). Stop repair check.
105
+ return;
106
+ }
107
+ if (lastEvent !== false) break;
108
+ truncateIndex = true;
109
+ position--;
110
+ }
111
+
112
+ if (lastEvent && lastEvent.metadata.commitSize && lastEvent.metadata.commitVersion !== lastEvent.metadata.commitSize - 1) {
113
+ this.emit('unfinished-commit', lastEvent);
114
+ // commitId = global sequence number at which the commit started
115
+ this.storage.truncate(lastEvent.metadata.commitId);
116
+ } else if (truncateIndex) {
117
+ // The index contained items that are not in the storage file; truncate everything
118
+ // after `position`, the last sequence number that was successfully read.
119
+ this.storage.truncate(position);
90
120
  }
91
- return new Storage(name, config);
92
121
  }
93
122
 
94
123
  /**
@@ -103,21 +132,45 @@ class EventStore extends EventEmitter {
103
132
  callback = () => {};
104
133
  }
105
134
  // Find existing streams by scanning dir for filenames starting with 'stream-'
106
- fs.readdir(this.streamsDirectory, (err, files) => {
107
- if (err) {
108
- return callback(err);
109
- }
110
- let matches;
111
- for (let file of files) {
112
- if ((matches = file.match(/(stream-(.*))\.index$/)) !== null) {
113
- const streamName = matches[2];
114
- const index = this.storage.openIndex(matches[1]);
115
- this.streams[streamName] = { index };
116
- this.emit('stream-available', streamName);
117
- }
135
+ scanForFiles(this.streamsDirectory, /(stream-.*)\.index$/, this.registerStream.bind(this), callback);
136
+ this.storage.on('index-created', this.registerStream.bind(this));
137
+ }
138
+
139
+ /**
140
+ * @private
141
+ * @param {string} name The full stream name, including the `stream-` prefix (and optional `.closed` suffix).
142
+ */
143
+ registerStream(name) {
144
+ /* istanbul ignore if */
145
+ if (!name.startsWith('stream-')) {
146
+ return;
147
+ }
148
+ let streamName = name.slice(7);
149
+ // Detect the `.closed` suffix — present both in the initial scan and when the directory
150
+ // watcher emits 'index-created' after a writer renames the file (e.g. 'stream-foo-bar.closed').
151
+ let isClosed = false;
152
+ if (streamName.endsWith('.closed')) {
153
+ streamName = streamName.slice(0, -7);
154
+ isClosed = true;
155
+ }
156
+ if (streamName in this.streams) {
157
+ if (isClosed && !this.streams[streamName].closed) {
158
+ // The stream was renamed to .closed while this instance had it open.
159
+ // The old ReadOnlyIndex was already closed via onRename, so we open the new one.
160
+ const closedIndexName = 'stream-' + streamName + '.closed';
161
+ const closedIndex = this.storage.openReadonlyIndex(closedIndexName);
162
+ // deepcode ignore PrototypePollutionFunctionParams: streams is a Map
163
+ this.streams[streamName] = { index: closedIndex, closed: true };
164
+ this.emit('stream-closed', streamName);
118
165
  }
119
- callback();
120
- });
166
+ return;
167
+ }
168
+ const index = isClosed
169
+ ? this.storage.openReadonlyIndex(name)
170
+ : this.storage.openIndex(name);
171
+ // deepcode ignore PrototypePollutionFunctionParams: streams is a Map
172
+ this.streams[streamName] = { index, closed: isClosed };
173
+ this.emit('stream-available', streamName);
121
174
  }
122
175
 
123
176
  /**
@@ -129,6 +182,103 @@ class EventStore extends EventEmitter {
129
182
  this.storage.close();
130
183
  }
131
184
 
185
+ /**
186
+ * Override EventEmitter.on() to delegate 'preCommit' and 'preRead' event registrations
187
+ * to the underlying storage, so that `eventstore.on('preCommit', handler)` works naturally.
188
+ * All other events are handled by the default EventEmitter.
189
+ *
190
+ * @param {string} event
191
+ * @param {function} listener
192
+ * @returns {this}
193
+ */
194
+ on(event, listener) {
195
+ if (event === 'preCommit' || event === 'preRead') {
196
+ if (event === 'preCommit') {
197
+ assert(!(this.storage instanceof Storage.ReadOnly), 'The storage was opened in read-only mode. Can not register a preCommit handler on it.');
198
+ }
199
+ this.storage.on(event, listener);
200
+ return this;
201
+ }
202
+ return super.on(event, listener);
203
+ }
204
+
205
+ /**
206
+ * @inheritDoc
207
+ */
208
+ addListener(event, listener) {
209
+ return this.on(event, listener);
210
+ }
211
+
212
+ /**
213
+ * Override EventEmitter.once() to delegate 'preCommit' and 'preRead' to the underlying storage.
214
+ *
215
+ * @param {string} event
216
+ * @param {function} listener
217
+ * @returns {this}
218
+ */
219
+ once(event, listener) {
220
+ if (event === 'preCommit' || event === 'preRead') {
221
+ if (event === 'preCommit') {
222
+ assert(!(this.storage instanceof Storage.ReadOnly), 'The storage was opened in read-only mode. Can not register a preCommit handler on it.');
223
+ }
224
+ this.storage.once(event, listener);
225
+ return this;
226
+ }
227
+ return super.once(event, listener);
228
+ }
229
+
230
+ /**
231
+ * Override EventEmitter.off() / removeListener() to delegate 'preCommit' and 'preRead'
232
+ * to the underlying storage.
233
+ *
234
+ * @param {string} event
235
+ * @param {function} listener
236
+ * @returns {this}
237
+ */
238
+ off(event, listener) {
239
+ if (event === 'preCommit' || event === 'preRead') {
240
+ this.storage.off(event, listener);
241
+ return this;
242
+ }
243
+ return super.off(event, listener);
244
+ }
245
+
246
+ /**
247
+ * @inheritDoc
248
+ */
249
+ removeListener(event, listener) {
250
+ return this.off(event, listener);
251
+ }
252
+
253
+ /**
254
+ * Convenience method to register a handler called before an event is committed to storage.
255
+ * Equivalent to `eventstore.on('preCommit', hook)`.
256
+ * The handler receives `(event, partitionMetadata)` and may throw to abort the write.
257
+ * Multiple handlers can be registered; all run on every write in registration order.
258
+ * The handler is invoked on every write, so its logic should be cheap, fast, and synchronous.
259
+ *
260
+ * @api
261
+ * @param {function(object, object): void} hook A function receiving (event, partitionMetadata).
262
+ * @throws {Error} If the storage was opened in read-only mode.
263
+ */
264
+ preCommit(hook) {
265
+ this.on('preCommit', hook);
266
+ }
267
+
268
+ /**
269
+ * Convenience method to register a handler called before an event is read from storage.
270
+ * Equivalent to `eventstore.on('preRead', hook)`.
271
+ * The handler receives `(position, partitionMetadata)` and may throw to abort the read.
272
+ * Multiple handlers can be registered; all run on every read in registration order.
273
+ * The handler is invoked on every read, so its logic should be cheap, fast, and synchronous.
274
+ *
275
+ * @api
276
+ * @param {function(number, object): void} hook A function receiving (position, partitionMetadata).
277
+ */
278
+ preRead(hook) {
279
+ this.on('preRead', hook);
280
+ }
281
+
132
282
  /**
133
283
  * Get the number of events stored.
134
284
  *
@@ -149,7 +299,7 @@ class EventStore extends EventEmitter {
149
299
  * @private
150
300
  * @param {Array<object>|object} events
151
301
  * @param {number} [expectedVersion]
152
- * @param {object} [metadata]
302
+ * @param {object|function} [metadata]
153
303
  * @param {function} [callback]
154
304
  * @returns {{events: Array<object>, metadata: object, callback: function, expectedVersion: number}}
155
305
  */
@@ -195,13 +345,19 @@ class EventStore extends EventEmitter {
195
345
  if (!(streamName in this.streams)) {
196
346
  this.createEventStream(streamName, { stream: streamName });
197
347
  }
348
+ assert(!this.streams[streamName].closed, `Stream "${streamName}" is closed and cannot be written to.`);
198
349
  let streamVersion = this.streams[streamName].index.length;
199
350
  if (expectedVersion !== ExpectedVersion.Any && streamVersion !== expectedVersion) {
200
351
  throw new OptimisticConcurrencyError(`Optimistic Concurrency error. Expected stream "${streamName}" at version ${expectedVersion} but is at version ${streamVersion}.`);
201
352
  }
202
353
 
354
+ if (events.length > 1) {
355
+ delete metadata.commitVersion;
356
+ }
357
+
203
358
  const commitId = this.length;
204
359
  let commitVersion = 0;
360
+ const commitSize = events.length;
205
361
  const committedAt = Date.now();
206
362
  const commit = Object.assign({
207
363
  commitId,
@@ -216,7 +372,7 @@ class EventStore extends EventEmitter {
216
372
  callback(commit);
217
373
  };
218
374
  for (let event of events) {
219
- const eventMetadata = Object.assign({ commitId, committedAt }, metadata, { commitVersion, streamVersion });
375
+ const eventMetadata = Object.assign({ commitId, committedAt, commitVersion, commitSize }, metadata, { streamVersion });
220
376
  const storedEvent = { stream: streamName, payload: event, metadata: eventMetadata };
221
377
  commitVersion++;
222
378
  streamVersion++;
@@ -242,11 +398,11 @@ class EventStore extends EventEmitter {
242
398
  *
243
399
  * @api
244
400
  * @param {string} streamName The name of the stream to get.
245
- * @param {number} [minRevision] The minimum revision to include in the events (inclusive).
246
- * @param {number} [maxRevision] The maximum revision to include in the events (inclusive).
401
+ * @param {number} [minRevision] The 1-based minimum revision to include in the events (inclusive).
402
+ * @param {number} [maxRevision] The 1-based maximum revision to include in the events (inclusive).
247
403
  * @returns {EventStream|boolean} The event stream or false if a stream with the name doesn't exist.
248
404
  */
249
- getEventStream(streamName, minRevision = 0, maxRevision = -1) {
405
+ getEventStream(streamName, minRevision = 1, maxRevision = -1) {
250
406
  if (!(streamName in this.streams)) {
251
407
  return false;
252
408
  }
@@ -258,25 +414,25 @@ class EventStore extends EventEmitter {
258
414
  * This is the same as `getEventStream('_all', ...)`.
259
415
  *
260
416
  * @api
261
- * @param {number} [minRevision] The minimum revision to include in the events (inclusive).
262
- * @param {number} [maxRevision] The maximum revision to include in the events (inclusive).
417
+ * @param {number} [minRevision] The 1-based minimum revision to include in the events (inclusive).
418
+ * @param {number} [maxRevision] The 1-based maximum revision to include in the events (inclusive).
263
419
  * @returns {EventStream} The event stream.
264
420
  */
265
- getAllEvents(minRevision = 0, maxRevision = -1) {
421
+ getAllEvents(minRevision = 1, maxRevision = -1) {
266
422
  return this.getEventStream('_all', minRevision, maxRevision);
267
423
  }
268
424
 
269
425
  /**
270
- * Create a new event stream from existing streams by joining them.
426
+ * Create a virtual event stream from existing streams by joining them.
271
427
  *
272
428
  * @param {string} streamName The (transient) name of the joined stream.
273
429
  * @param {Array<string>} streamNames An array of the stream names to join.
274
- * @param {number} [minRevision] The minimum revision to include in the events (inclusive).
275
- * @param {number} [maxRevision] The maximum revision to include in the events (inclusive).
430
+ * @param {number} [minRevision] The 1-based minimum revision to include in the events (inclusive).
431
+ * @param {number} [maxRevision] The 1-based maximum revision to include in the events (inclusive).
276
432
  * @returns {EventStream} The joined event stream.
277
433
  * @throws {Error} if any of the streams doesn't exist.
278
434
  */
279
- fromStreams(streamName, streamNames, minRevision = 0, maxRevision = -1) {
435
+ fromStreams(streamName, streamNames, minRevision = 1, maxRevision = -1) {
280
436
  assert(streamNames instanceof Array, 'Must specify an array of stream names.');
281
437
 
282
438
  for (let stream of streamNames) {
@@ -285,6 +441,33 @@ class EventStore extends EventEmitter {
285
441
  return new JoinEventStream(streamName, streamNames, this, minRevision, maxRevision);
286
442
  }
287
443
 
444
+ /**
445
+ * Get a stream for a category of streams. This will effectively return a joined stream of all streams that start
446
+ * with the given `categoryName` followed by a dash.
447
+ * If you frequently use this for a category consisting of a lot of streams (e.g. `users`), consider creating a
448
+ * dedicated physical stream for the category:
449
+ *
450
+ * `eventstore.createEventStream('users', e => e.stream.startsWith('users-'))`
451
+ *
452
+ * @api
453
+ * @param {string} categoryName The name of the category to get a stream for. A category is a stream name prefix.
454
+ * @param {number} [minRevision] The 1-based minimum revision to include in the events (inclusive).
455
+ * @param {number} [maxRevision] The 1-based maximum revision to include in the events (inclusive).
456
+ * @returns {EventStream} The joined event stream for all streams of the given category.
457
+ * @throws {Error} If no stream for this category exists.
458
+ */
459
+ getEventStreamForCategory(categoryName, minRevision = 1, maxRevision = -1) {
460
+ if (categoryName in this.streams) {
461
+ return this.getEventStream(categoryName, minRevision, maxRevision);
462
+ }
463
+ const categoryStreams = Object.keys(this.streams).filter(streamName => streamName.startsWith(categoryName + '-'));
464
+
465
+ if (categoryStreams.length === 0) {
466
+ throw new Error(`No streams for category '${categoryName}' exist.`);
467
+ }
468
+ return this.fromStreams(categoryName, categoryStreams, minRevision, maxRevision);
469
+ }
470
+
288
471
  /**
289
472
  * Create a new stream with the given matcher.
290
473
  *
@@ -303,6 +486,7 @@ class EventStore extends EventEmitter {
303
486
  const index = this.storage.ensureIndex(streamIndexName, matcher);
304
487
  assert(index !== null, `Error creating stream index ${streamName}.`);
305
488
 
489
+ // deepcode ignore PrototypePollutionFunctionParams: streams is a Map
306
490
  this.streams[streamName] = { index, matcher };
307
491
  this.emit('stream-created', streamName);
308
492
  return new EventStream(streamName, this);
@@ -329,57 +513,84 @@ class EventStore extends EventEmitter {
329
513
  this.emit('stream-deleted', streamName);
330
514
  }
331
515
 
516
+ /**
517
+ * Close a stream so that no new events are indexed into it.
518
+ * The stream will still be readable, but any attempt to write to it will throw an error.
519
+ * A closed stream is persisted by renaming its index file to include a `.closed` marker
520
+ * (e.g. `stream-X.closed.index`), so it will be recognized as closed when the store is reopened.
521
+ *
522
+ * @api
523
+ * @param {string} streamName The name of the stream to close.
524
+ * @returns void
525
+ * @throws {Error} If the storage is read-only.
526
+ * @throws {Error} If the stream does not exist.
527
+ * @throws {Error} If the stream is already closed.
528
+ */
529
+ closeEventStream(streamName) {
530
+ assert(!(this.storage instanceof Storage.ReadOnly), 'The storage was opened in read-only mode. Can not close a stream on it.');
531
+ assert(streamName in this.streams, `Stream "${streamName}" does not exist.`);
532
+ assert(!this.streams[streamName].closed, `Stream "${streamName}" is already closed.`);
533
+
534
+ const indexName = 'stream-' + streamName;
535
+ const { index } = this.streams[streamName];
536
+
537
+ // Flush and close the index before renaming the file
538
+ index.close();
539
+
540
+ // Rename the index file to mark it as closed (e.g. stream-foo.index -> stream-foo.closed.index)
541
+ const closedFileName = index.fileName.replace(/\.index$/, '.closed.index');
542
+ fs.renameSync(index.fileName, closedFileName);
543
+
544
+ // Remove from secondary indexes so that new writes are no longer indexed into this stream
545
+ delete this.storage.secondaryIndexes[indexName];
546
+
547
+ // Reopen the renamed index for read access, outside the secondary indexes write path
548
+ const closedIndexName = indexName + '.closed';
549
+ const closedIndex = this.storage.openReadonlyIndex(closedIndexName);
550
+
551
+ // deepcode ignore PrototypePollutionFunctionParams: streams is a Map
552
+ this.streams[streamName] = { index: closedIndex, closed: true };
553
+ this.emit('stream-closed', streamName);
554
+ }
555
+
332
556
  /**
333
557
  * Get a durable consumer for the given stream that will keep receiving events from the last position.
334
558
  *
335
559
  * @param {string} streamName The name of the stream to consume.
336
560
  * @param {string} identifier The unique identifying name of this consumer.
561
+ * @param {object} [initialState] The initial state of the consumer.
337
562
  * @param {number} [since] The stream revision to start consuming from.
338
563
  * @returns {Consumer} A durable consumer for the given stream.
339
564
  */
340
- getConsumer(streamName, identifier, since = 0) {
341
- const consumer = new Consumer(this.storage, 'stream-' + streamName, identifier, since);
342
- return consumer.pipe(new EventUnwrapper());
565
+ getConsumer(streamName, identifier, initialState = {}, since = 0) {
566
+ const consumer = new Consumer(this.storage, streamName === '_all' ? '_all' : 'stream-' + streamName, identifier, initialState, since);
567
+ consumer.streamName = streamName;
568
+ return consumer;
343
569
  }
344
570
 
345
571
  /**
346
- * Get all commits that happened since the given store revision.
347
- *
348
- * @param {number} [since] The event revision since when to return commits (inclusive). If since is within a commit, the full commit will be returned.
349
- * @returns {Generator<object>} A generator of commit objects, each containing the commit metadata and the array of events.
572
+ * Scan the existing consumers on this EventStore and asynchronously return a list of their names.
573
+ * @param {function(error: Error, consumers: array)} callback A callback that will receive an error as first and the list of consumers as second argument.
350
574
  */
351
- *getCommits(since = 0) {
352
- let commit;
353
- let eventStream = this.getAllEvents(since);
354
- let storedEvent;
355
- while ((storedEvent = eventStream.next()) !== false) {
356
- const { metadata, stream, payload } = storedEvent;
357
-
358
- if (!commit && metadata.commitVersion > 0) {
359
- eventStream = this.getAllEvents(since - metadata.commitVersion);
360
- continue;
361
- }
362
-
363
- if (!commit || commit.commitId !== metadata.commitId) {
364
- if (commit) {
365
- yield commit;
366
- }
367
- commit = {
368
- commitId: metadata.commitId,
369
- committedAt: metadata.committedAt,
370
- streamName: stream,
371
- streamVersion: metadata.streamVersion,
372
- events: []
373
- };
374
- }
375
- commit.events.push(payload);
376
- }
377
- if (commit) {
378
- yield commit;
575
+ scanConsumers(callback) {
576
+ const consumersPath = path.join(this.storage.indexDirectory, 'consumers');
577
+ if (!fs.existsSync(consumersPath)) {
578
+ callback(null, []);
579
+ return;
379
580
  }
581
+ const regex = new RegExp(`^${this.storage.storageFile}\\.([^.]*\\..*)$`);
582
+ const consumers = [];
583
+ scanForFiles(consumersPath, regex, consumers.push.bind(consumers), /* istanbul ignore next */ (err) => {
584
+ if (err) {
585
+ return callback(err, []);
586
+ }
587
+ callback(null, consumers);
588
+ });
380
589
  }
381
590
  }
382
591
 
383
592
  module.exports = EventStore;
384
593
  module.exports.ExpectedVersion = ExpectedVersion;
385
- module.exports.OptimisticConcurrencyError = OptimisticConcurrencyError;
594
+ module.exports.OptimisticConcurrencyError = OptimisticConcurrencyError;
595
+ module.exports.LOCK_THROW = Storage.LOCK_THROW;
596
+ module.exports.LOCK_RECLAIM = Storage.LOCK_RECLAIM;