event-storage 0.8.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "event-storage",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "An optimized embedded event store for node.js",
5
5
  "keywords": [
6
6
  "event-storage",
@@ -15,7 +15,7 @@
15
15
  "homepage": "https://github.com/albe/node-event-storage",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "https://github.com/albe/node-event-storage"
18
+ "url": "git+https://github.com/albe/node-event-storage.git"
19
19
  },
20
20
  "bugs": {
21
21
  "url": "https://github.com/albe/node-event-storage/issues"
@@ -25,21 +25,22 @@
25
25
  "coverage": "nyc report --reporter=text-lcov | coveralls"
26
26
  },
27
27
  "files": [
28
- "*/Consumer*.js",
29
- "*/EventStore*.js",
30
- "*/EventStream*.js",
31
- "*/Index*.js",
32
- "*/IndexEntry*.js",
33
- "*/JoinEventStream*.js",
34
- "*/Partition*.js",
35
- "*/Storage*.js",
36
- "*/Watcher*.js",
37
- "*/Clock*.js",
38
- "*/Index/*.js",
39
- "*/Partition/*.js",
40
- "*/Storage/*.js",
28
+ "src/Consumer*.js",
29
+ "src/EventStore*.js",
30
+ "src/EventStream*.js",
31
+ "src/Index*.js",
32
+ "src/IndexEntry*.js",
33
+ "src/JoinEventStream*.js",
34
+ "src/Partition*.js",
35
+ "src/Storage*.js",
36
+ "src/Watcher*.js",
37
+ "src/Clock*.js",
38
+ "src/Index/*.js",
39
+ "src/Partition/*.js",
40
+ "src/Storage/*.js",
41
41
  "src/WatchesFile.js",
42
42
  "src/util.js",
43
+ "src/metadataUtil.js",
43
44
  "index.js"
44
45
  ],
45
46
  "license": "MIT",
@@ -50,10 +51,10 @@
50
51
  }
51
52
  ],
52
53
  "engines": {
53
- "node": ">=10.0"
54
+ "node": ">=18.0"
54
55
  },
55
56
  "dependencies": {
56
- "mkdirp": "^1.0.3"
57
+ "mkdirp": "^3.0.1"
57
58
  },
58
59
  "nyc": {
59
60
  "include": [
@@ -64,10 +65,10 @@
64
65
  ]
65
66
  },
66
67
  "devDependencies": {
67
- "coveralls": "^3.0.2",
68
+ "coveralls-next": "^6.0.1",
68
69
  "expect.js": "^0.3.1",
69
- "fs-extra": "^9.0.1",
70
- "mocha": "^8.0.1",
71
- "nyc": "^15.0.0"
70
+ "fs-extra": "^11.3.4",
71
+ "mocha": "^11.7.5",
72
+ "nyc": "^18.0.0"
72
73
  }
73
74
  }
package/src/Clock.js CHANGED
@@ -1,6 +1,6 @@
1
- const TIME_BASE = process.hrtime();
2
- const DATE_BASE_US = Date.now() * 1000.0 + 999.0; // DATE_BASE_US should match the TIME_BASE with lower resolution, but never be less
3
- const US_PER_SEC = 1.0e6;
1
+ const TIME_BASE = process.hrtime.bigint();
2
+ const DATE_FACTOR = 1000000n;
3
+ const DATE_BASE_NS = BigInt(Date.now() + 1) * DATE_FACTOR - 1n;
4
4
  const CLOCK_ACCURACY_US = 1; // two process.hrtime() calls take roughly this long, so this is the accuracy we can measure time
5
5
 
6
6
  /**
@@ -14,16 +14,28 @@ class Clock {
14
14
  * @param {Date|number} epoch The epoch to base this clock on, either as a Date or a number of the amount of milliseconds since the unix epoch
15
15
  */
16
16
  constructor(epoch) {
17
- this.epoch = Math.floor((epoch instanceof Date ? epoch.getTime() : Number(epoch)) * 1000.0);
17
+ this.epoch = BigInt(epoch instanceof Date ? epoch.getTime() : Number(epoch)) * DATE_FACTOR;
18
+ this.lastTime = 0;
18
19
  }
19
20
 
20
21
  /**
21
- * @returns {number} The number of microseconds since the epoch given in the constructor. The decimal part denotes the accuracy of the clock in milliseconds.
22
- * @note Needs to allow at least tens of ms accuracy, better hundreds of ms
22
+ * @returns {number} The number of microseconds since the epoch given in the constructor.
23
+ * @note Needs to allow at least tenths of ms accuracy, better hundredths of ms
23
24
  */
24
25
  time() {
25
- const delta = process.hrtime(TIME_BASE);
26
- return DATE_BASE_US - this.epoch + (delta[0] * US_PER_SEC + Math.floor(delta[1] / 1000)) + (CLOCK_ACCURACY_US / 1000.0);
26
+ const delta = process.hrtime.bigint() - TIME_BASE;
27
+ const timeSinceEpoch = Number((DATE_BASE_NS - this.epoch + delta) / 1000n);
28
+ return this.lastTime = Math.max(this.lastTime + 1, timeSinceEpoch);
29
+ }
30
+
31
+ /**
32
+ * Return the clock accuracy of the given timestamp. This is only useful for calculating a consistent ordering
33
+ * ala TrueTime for multi-writer scenarios.
34
+ * @param {number} time A timestamp measured by this clock.
35
+ * @returns {number} The amount of µs accuracy this timestamp has.
36
+ */
37
+ accuracy(time) {
38
+ return CLOCK_ACCURACY_US;
27
39
  }
28
40
 
29
41
  }
package/src/EventStore.js CHANGED
@@ -5,8 +5,7 @@ const path = require('path');
5
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,24 +14,6 @@ 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
@@ -47,6 +28,10 @@ class EventStore extends events.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();
@@ -63,6 +48,17 @@ class EventStore extends events.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
+
66
62
  this.initialize(storeName, storageConfig);
67
63
  }
68
64
 
@@ -87,10 +83,43 @@ class EventStore extends events.EventEmitter {
87
83
  this.storage.close();
88
84
  throw err;
89
85
  }
86
+ this.checkUnfinishedCommits();
90
87
  this.emit('ready');
91
88
  });
92
89
  }
93
90
 
91
+ /**
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
95
+ */
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);
120
+ }
121
+ }
122
+
94
123
  /**
95
124
  * Scan the streams directory for existing streams so they are ready for `getEventStream()`.
96
125
  *
@@ -103,38 +132,44 @@ class EventStore extends events.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
- this.registerStream(matches[1]);
114
- }
115
- }
116
- callback();
117
- });
135
+ scanForFiles(this.streamsDirectory, /(stream-.*)\.index$/, this.registerStream.bind(this), callback);
118
136
  this.storage.on('index-created', this.registerStream.bind(this));
119
137
  }
120
138
 
121
139
  /**
122
140
  * @private
123
- * @param {string} name The full stream name, including the `stream-` prefix.
141
+ * @param {string} name The full stream name, including the `stream-` prefix (and optional `.closed` suffix).
124
142
  */
125
143
  registerStream(name) {
126
144
  /* istanbul ignore if */
127
145
  if (!name.startsWith('stream-')) {
128
146
  return;
129
147
  }
130
- const streamName = name.substr(7, name.length - 7);
131
- /* istanbul ignore if */
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
+ }
132
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);
165
+ }
133
166
  return;
134
167
  }
135
- const index = this.storage.openIndex('stream-'+streamName);
168
+ const index = isClosed
169
+ ? this.storage.openReadonlyIndex(name)
170
+ : this.storage.openIndex(name);
136
171
  // deepcode ignore PrototypePollutionFunctionParams: streams is a Map
137
- this.streams[streamName] = { index };
172
+ this.streams[streamName] = { index, closed: isClosed };
138
173
  this.emit('stream-available', streamName);
139
174
  }
140
175
 
@@ -147,6 +182,103 @@ class EventStore extends events.EventEmitter {
147
182
  this.storage.close();
148
183
  }
149
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
+
150
282
  /**
151
283
  * Get the number of events stored.
152
284
  *
@@ -213,6 +345,7 @@ class EventStore extends events.EventEmitter {
213
345
  if (!(streamName in this.streams)) {
214
346
  this.createEventStream(streamName, { stream: streamName });
215
347
  }
348
+ assert(!this.streams[streamName].closed, `Stream "${streamName}" is closed and cannot be written to.`);
216
349
  let streamVersion = this.streams[streamName].index.length;
217
350
  if (expectedVersion !== ExpectedVersion.Any && streamVersion !== expectedVersion) {
218
351
  throw new OptimisticConcurrencyError(`Optimistic Concurrency error. Expected stream "${streamName}" at version ${expectedVersion} but is at version ${streamVersion}.`);
@@ -265,11 +398,11 @@ class EventStore extends events.EventEmitter {
265
398
  *
266
399
  * @api
267
400
  * @param {string} streamName The name of the stream to get.
268
- * @param {number} [minRevision] The minimum revision to include in the events (inclusive).
269
- * @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).
270
403
  * @returns {EventStream|boolean} The event stream or false if a stream with the name doesn't exist.
271
404
  */
272
- getEventStream(streamName, minRevision = 0, maxRevision = -1) {
405
+ getEventStream(streamName, minRevision = 1, maxRevision = -1) {
273
406
  if (!(streamName in this.streams)) {
274
407
  return false;
275
408
  }
@@ -281,11 +414,11 @@ class EventStore extends events.EventEmitter {
281
414
  * This is the same as `getEventStream('_all', ...)`.
282
415
  *
283
416
  * @api
284
- * @param {number} [minRevision] The minimum revision to include in the events (inclusive).
285
- * @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).
286
419
  * @returns {EventStream} The event stream.
287
420
  */
288
- getAllEvents(minRevision = 0, maxRevision = -1) {
421
+ getAllEvents(minRevision = 1, maxRevision = -1) {
289
422
  return this.getEventStream('_all', minRevision, maxRevision);
290
423
  }
291
424
 
@@ -294,12 +427,12 @@ class EventStore extends events.EventEmitter {
294
427
  *
295
428
  * @param {string} streamName The (transient) name of the joined stream.
296
429
  * @param {Array<string>} streamNames An array of the stream names to join.
297
- * @param {number} [minRevision] The minimum revision to include in the events (inclusive).
298
- * @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).
299
432
  * @returns {EventStream} The joined event stream.
300
433
  * @throws {Error} if any of the streams doesn't exist.
301
434
  */
302
- fromStreams(streamName, streamNames, minRevision = 0, maxRevision = -1) {
435
+ fromStreams(streamName, streamNames, minRevision = 1, maxRevision = -1) {
303
436
  assert(streamNames instanceof Array, 'Must specify an array of stream names.');
304
437
 
305
438
  for (let stream of streamNames) {
@@ -318,12 +451,12 @@ class EventStore extends events.EventEmitter {
318
451
  *
319
452
  * @api
320
453
  * @param {string} categoryName The name of the category to get a stream for. A category is a stream name prefix.
321
- * @param {number} [minRevision] The minimum revision to include in the events (inclusive).
322
- * @param {number} [maxRevision] The maximum revision to include in the events (inclusive).
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).
323
456
  * @returns {EventStream} The joined event stream for all streams of the given category.
324
457
  * @throws {Error} If no stream for this category exists.
325
458
  */
326
- getEventStreamForCategory(categoryName, minRevision = 0, maxRevision = -1) {
459
+ getEventStreamForCategory(categoryName, minRevision = 1, maxRevision = -1) {
327
460
  if (categoryName in this.streams) {
328
461
  return this.getEventStream(categoryName, minRevision, maxRevision);
329
462
  }
@@ -380,6 +513,46 @@ class EventStore extends events.EventEmitter {
380
513
  this.emit('stream-deleted', streamName);
381
514
  }
382
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
+
383
556
  /**
384
557
  * Get a durable consumer for the given stream that will keep receiving events from the last position.
385
558
  *
@@ -390,9 +563,29 @@ class EventStore extends events.EventEmitter {
390
563
  * @returns {Consumer} A durable consumer for the given stream.
391
564
  */
392
565
  getConsumer(streamName, identifier, initialState = {}, since = 0) {
393
- const consumer = new Consumer(this.storage, 'stream-' + streamName, identifier, initialState, since);
566
+ const consumer = new Consumer(this.storage, streamName === '_all' ? '_all' : 'stream-' + streamName, identifier, initialState, since);
394
567
  consumer.streamName = streamName;
395
- return consumer.pipe(new EventUnwrapper());
568
+ return consumer;
569
+ }
570
+
571
+ /**
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.
574
+ */
575
+ scanConsumers(callback) {
576
+ const consumersPath = path.join(this.storage.indexDirectory, 'consumers');
577
+ if (!fs.existsSync(consumersPath)) {
578
+ callback(null, []);
579
+ return;
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
+ });
396
589
  }
397
590
  }
398
591