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.
package/src/EventStore.js CHANGED
@@ -6,9 +6,10 @@ import events from 'events';
6
6
  import Storage, { ReadOnly as ReadOnlyStorage, LOCK_THROW, LOCK_RECLAIM } from './Storage.js';
7
7
  import Index from './Index.js';
8
8
  import Consumer from './Consumer.js';
9
- import { assert, getPropertyAtPath } from './util.js';
10
- import { ensureDirectory, scanForFiles } from './fsUtil.js';
11
- import { buildTypeMatcherFn } from './metadataUtil.js';
9
+ import { assert, getPropertyAtPath } from './utils/util.js';
10
+ import { ensureDirectory, scanForFiles } from './utils/fsUtil.js';
11
+ import { buildTypeMatcherFn } from './utils/metadataUtil.js';
12
+ import { fixCommitArgumentTypes, parseStreamFromIndexName, normalizePredicateRaw } from './utils/apiHelpers.js';
12
13
 
13
14
  const ExpectedVersion = {
14
15
  Any: -1,
@@ -19,6 +20,8 @@ const ExpectedVersion = {
19
20
  * Default matcher property paths mirroring the Storage default, used for index optimization.
20
21
  */
21
22
  const DEFAULT_MATCHER_PROPERTIES = ['stream', 'payload.type'];
23
+ const STREAM_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_]*(?:[\/:@~+=\-#.][A-Za-z0-9_]+)*$/;
24
+ const STORAGE_HOOK_EVENTS = new Set(['preCommit', 'preRead']);
22
25
 
23
26
  class OptimisticConcurrencyError extends Error {}
24
27
 
@@ -37,12 +40,14 @@ class OptimisticConcurrencyError extends Error {}
37
40
  class CommitCondition {
38
41
  /**
39
42
  * @param {string[]} types
40
- * @param {function(object, object): boolean|null} [matcher]
43
+ * @param {function(object, object): boolean|object|null} [matcher]
41
44
  * @param {number} noneMatchAfter
45
+ * @param {boolean} [raw=false]
42
46
  */
43
- constructor(types, matcher = null, noneMatchAfter) {
47
+ constructor(types, matcher = null, noneMatchAfter, raw = false) {
44
48
  this.types = types;
45
49
  this.matcher = matcher;
50
+ this.raw = raw;
46
51
  this.noneMatchAfter = noneMatchAfter;
47
52
  }
48
53
  }
@@ -132,6 +137,7 @@ class EventStore extends events.EventEmitter {
132
137
  : new Storage(storeName, storageConfig);
133
138
  this.streams = Object.create(null);
134
139
  this.streams._all = { index: this.storage.index };
140
+ this.consumers = new Map();
135
141
 
136
142
  this.storage.on('index-created', this.registerStream.bind(this));
137
143
 
@@ -180,7 +186,6 @@ class EventStore extends events.EventEmitter {
180
186
  * @param {string} name The full stream name, including the `stream-` prefix (and optional `.closed` suffix).
181
187
  */
182
188
  registerStream(name) {
183
- /* istanbul ignore if */
184
189
  if (!name.startsWith('stream-')) {
185
190
  return;
186
191
  }
@@ -214,10 +219,15 @@ class EventStore extends events.EventEmitter {
214
219
 
215
220
  /**
216
221
  * Close the event store and free up all resources.
222
+ * Stops all registered consumers before closing storage.
217
223
  *
218
224
  * @api
219
225
  */
220
226
  close() {
227
+ for (const consumer of this.consumers.values()) {
228
+ consumer.stop();
229
+ }
230
+ this.consumers.clear();
221
231
  this.storage.close();
222
232
  }
223
233
 
@@ -231,11 +241,8 @@ class EventStore extends events.EventEmitter {
231
241
  * @returns {this}
232
242
  */
233
243
  on(event, listener) {
234
- if (event === 'preCommit' || event === 'preRead') {
235
- if (event === 'preCommit') {
236
- assert(!(this.storage instanceof ReadOnlyStorage), 'The storage was opened in read-only mode. Can not register a preCommit handler on it.');
237
- }
238
- this.storage.on(event, listener);
244
+ if (this.isStorageHookEvent(event)) {
245
+ this.delegateStorageHookEvent('on', event, listener);
239
246
  return this;
240
247
  }
241
248
  return super.on(event, listener);
@@ -256,11 +263,8 @@ class EventStore extends events.EventEmitter {
256
263
  * @returns {this}
257
264
  */
258
265
  once(event, listener) {
259
- if (event === 'preCommit' || event === 'preRead') {
260
- if (event === 'preCommit') {
261
- assert(!(this.storage instanceof ReadOnlyStorage), 'The storage was opened in read-only mode. Can not register a preCommit handler on it.');
262
- }
263
- this.storage.once(event, listener);
266
+ if (this.isStorageHookEvent(event)) {
267
+ this.delegateStorageHookEvent('once', event, listener);
264
268
  return this;
265
269
  }
266
270
  return super.once(event, listener);
@@ -275,13 +279,24 @@ class EventStore extends events.EventEmitter {
275
279
  * @returns {this}
276
280
  */
277
281
  off(event, listener) {
278
- if (event === 'preCommit' || event === 'preRead') {
282
+ if (this.isStorageHookEvent(event)) {
279
283
  this.storage.off(event, listener);
280
284
  return this;
281
285
  }
282
286
  return super.off(event, listener);
283
287
  }
284
288
 
289
+ isStorageHookEvent(event) {
290
+ return STORAGE_HOOK_EVENTS.has(event);
291
+ }
292
+
293
+ delegateStorageHookEvent(method, event, listener) {
294
+ if (event === 'preCommit') {
295
+ assert(!(this.storage instanceof ReadOnlyStorage), 'The storage was opened in read-only mode. Can not register a preCommit handler on it.');
296
+ }
297
+ this.storage[method](event, listener);
298
+ }
299
+
285
300
  /**
286
301
  * @inheritDoc
287
302
  */
@@ -328,39 +343,6 @@ class EventStore extends events.EventEmitter {
328
343
  return this.storage.length;
329
344
  }
330
345
 
331
- /**
332
- * This method makes it so the last three arguments can be given either as:
333
- * - expectedVersion, metadata, callback
334
- * - expectedVersion, callback
335
- * - metadata, callback
336
- * - callback
337
- *
338
- * @private
339
- * @param {Array<object>|object} events
340
- * @param {number|CommitCondition} [expectedVersion]
341
- * @param {object|function} [metadata]
342
- * @param {function} [callback]
343
- * @returns {{events: Array<object>, metadata: object, callback: function, expectedVersion: number|CommitCondition}}
344
- */
345
- static fixArgumentTypes(events, expectedVersion, metadata, callback) {
346
- if (!(events instanceof Array)) {
347
- events = [events];
348
- }
349
- if (typeof expectedVersion !== 'number' && !(expectedVersion instanceof CommitCondition)) {
350
- callback = metadata;
351
- metadata = expectedVersion;
352
- expectedVersion = ExpectedVersion.Any;
353
- }
354
- if (typeof metadata !== 'object') {
355
- callback = metadata;
356
- metadata = {};
357
- }
358
- if (typeof callback !== 'function') {
359
- callback = () => {};
360
- }
361
- return { events, expectedVersion, metadata, callback };
362
- }
363
-
364
346
  /**
365
347
  * Check a {@link CommitCondition} against the current state of the store.
366
348
  * Iterates a join stream over all condition type streams starting from
@@ -368,6 +350,7 @@ class EventStore extends events.EventEmitter {
368
350
  * {@link OptimisticConcurrencyError} when a new event of a listed type satisfies
369
351
  * `condition.matcher(payload, metadata)` (or any such event when no matcher is provided).
370
352
  *
353
+ * @private
371
354
  * @param {CommitCondition} condition
372
355
  * @throws {OptimisticConcurrencyError}
373
356
  */
@@ -378,32 +361,30 @@ class EventStore extends events.EventEmitter {
378
361
  if (existingTypes.length === 0) return;
379
362
 
380
363
  // Only events after condition.noneMatchAfter can be conflicts.
364
+ // Pass the original matcher and raw flag so the stream filters at the source.
381
365
  const stream = this.fromStreams(
382
366
  '_check_' + condition.types.join('_'),
383
367
  existingTypes,
384
- condition.noneMatchAfter + 1
368
+ condition.noneMatchAfter + 1,
369
+ -1,
370
+ condition.matcher,
371
+ condition.raw
385
372
  );
386
373
 
387
- let next;
388
- while ((next = stream.next()) !== false) {
389
- if (!condition.matcher || condition.matcher(next.payload, next.metadata)) {
390
- throw new OptimisticConcurrencyError(
391
- `Optimistic Concurrency error. A conflicting event was committed since the condition was obtained.`
392
- );
393
- }
394
- }
374
+ assert(stream.next() === false, `Optimistic Concurrency error. A conflicting event was committed since the condition was obtained.`, OptimisticConcurrencyError);
395
375
  }
396
376
 
397
377
  /**
398
378
  * Ensure a dedicated type stream exists for each event's type, creating it if needed.
399
379
  * Must be called before the entity stream is created to guarantee correct index routing.
400
380
  *
381
+ * @private
401
382
  * @param {Array<object>} events The events to process.
402
383
  */
403
384
  ensureTypeStreams(events) {
404
385
  if (!this.typeAccessor) return;
405
386
  for (const event of events) {
406
- const type = this.typeAccessor(event);
387
+ const type = this.resolveValidatedTypeStreamName(event);
407
388
  if (type && !(type in this.streams)) {
408
389
  const matcher = this.typeMatcherFn
409
390
  ? this.typeMatcherFn(type)
@@ -413,6 +394,40 @@ class EventStore extends events.EventEmitter {
413
394
  }
414
395
  }
415
396
 
397
+ /**
398
+ * @private
399
+ * @param {object} event
400
+ * @returns {string|null}
401
+ */
402
+ resolveValidatedTypeStreamName(event) {
403
+ const type = this.typeAccessor(event);
404
+ if (type === undefined || type === null || type === '') {
405
+ return null;
406
+ }
407
+ assert(typeof type === 'string', 'typeAccessor must return a string.');
408
+ assert(STREAM_NAME_PATTERN.test(type), `typeAccessor must return a valid stream name. Got: "${type}"`);
409
+ return type;
410
+ }
411
+
412
+ /**
413
+ * @private
414
+ * @param {string[]} types
415
+ * @returns {string[]}
416
+ */
417
+ getExistingQueryTypes(types) {
418
+ const queryTypes = [];
419
+ for (const type of types) {
420
+ if (type in this.streams) {
421
+ queryTypes.push(type);
422
+ continue;
423
+ }
424
+ if (!this.typeAccessor) {
425
+ throw new Error(`Type stream "${type}" does not exist. Create it with createEventStream() first, or configure typeAccessor to have type streams created automatically on commit.`);
426
+ }
427
+ }
428
+ return queryTypes;
429
+ }
430
+
416
431
  /**
417
432
  * Commit a list of events for the given stream name, which is expected to be at the given version.
418
433
  * Note that the events committed may still appear in other streams too - the given stream name is only
@@ -433,7 +448,14 @@ class EventStore extends events.EventEmitter {
433
448
  assert(typeof streamName === 'string' && streamName !== '', 'Must specify a stream name for commit.');
434
449
  assert(typeof events !== 'undefined' && events !== null, 'No events specified for commit.');
435
450
 
436
- ({ events, expectedVersion, metadata, callback } = EventStore.fixArgumentTypes(events, expectedVersion, metadata, callback));
451
+ ({ events, expectedVersion, metadata, callback } = fixCommitArgumentTypes(
452
+ events,
453
+ expectedVersion,
454
+ metadata,
455
+ callback,
456
+ ExpectedVersion.Any,
457
+ CommitCondition
458
+ ));
437
459
 
438
460
  // Perform DCB-style concurrency check when a CommitCondition is provided.
439
461
  if (expectedVersion instanceof CommitCondition) {
@@ -450,9 +472,10 @@ class EventStore extends events.EventEmitter {
450
472
  }
451
473
  assert(!this.streams[streamName].closed, `Stream "${streamName}" is closed and cannot be written to.`);
452
474
  let streamVersion = this.streams[streamName].index.length;
453
- if (expectedVersion !== ExpectedVersion.Any && streamVersion !== expectedVersion) {
454
- throw new OptimisticConcurrencyError(`Optimistic Concurrency error. Expected stream "${streamName}" at version ${expectedVersion} but is at version ${streamVersion}.`);
455
- }
475
+ assert(expectedVersion === ExpectedVersion.Any || streamVersion === expectedVersion,
476
+ `Optimistic Concurrency error. Expected stream "${streamName}" at version ${expectedVersion} but is at version ${streamVersion}.`,
477
+ OptimisticConcurrencyError
478
+ );
456
479
 
457
480
  if (events.length > 1) {
458
481
  delete metadata.commitVersion;
@@ -514,35 +537,21 @@ class EventStore extends events.EventEmitter {
514
537
  *
515
538
  * @api
516
539
  * @param {string[]} types A non-empty array of event-type names to query.
517
- * @param {function(object, object): boolean|null} [matcher] An optional filter function `(payload, metadata) => boolean`
518
- * passed to the returned {@link CommitCondition}.
540
+ * @param {function|object|null} [matcher] Optional matcher used for stream pre-filtering.
541
+ * In object mode, function predicates receive `(payload, metadata)`.
519
542
  * @param {number} [minRevision=1] The 1-based minimum global revision to include in the returned stream (inclusive).
543
+ * @param {boolean} [raw=false] If true, return NDJSON buffers from the query stream.
520
544
  * @returns {{ condition: CommitCondition, stream: EventStream }} An object with:
521
545
  * - `condition` — the {@link CommitCondition} to pass to {@link EventStore#commit}.
522
546
  * - `stream` — a read-only event stream containing all matching events.
523
547
  * @throws {Error} if `types` is not a non-empty array.
524
548
  * @throws {Error} if `typeAccessor` is not configured and any of the listed type streams do not exist.
525
549
  */
526
- query(types, matcher = null, minRevision = 1) {
550
+ query(types, matcher = null, minRevision = 1, raw = false) {
527
551
  assert(Array.isArray(types) && types.length > 0, 'Must specify a non-empty array of event types for query.');
528
-
529
- const queryTypes = [];
530
- for (const type of types) {
531
- if (!(type in this.streams)) {
532
- if (this.typeAccessor) {
533
- // typeAccessor is configured: type streams are created on commit, so a missing
534
- // stream simply means no event of this type has been committed yet — treat as empty.
535
- continue;
536
- }
537
- // No typeAccessor: the stream was never created; we cannot know whether events of
538
- // this type exist in the store, so throw to avoid an unintentional full-store scan.
539
- throw new Error(`Type stream "${type}" does not exist. Create it with createEventStream() first, or configure typeAccessor to have type streams created automatically on commit.`);
540
- }
541
- queryTypes.push(type);
542
- }
543
-
544
- const condition = new CommitCondition(types, matcher, this.storage.length);
545
- const stream = this.fromStreams('_query_' + types.join('_'), queryTypes, minRevision, -1, matcher);
552
+ const queryTypes = this.getExistingQueryTypes(types);
553
+ const condition = new CommitCondition(types, matcher, this.storage.length, raw);
554
+ const stream = this.fromStreams('_query_' + types.join('_'), queryTypes, minRevision, -1, matcher, raw);
546
555
  return { stream, condition };
547
556
  }
548
557
 
@@ -551,15 +560,18 @@ class EventStore extends events.EventEmitter {
551
560
  *
552
561
  * @api
553
562
  * @param {string} streamName The name of the stream to get.
554
- * @param {number} [minRevision] The 1-based minimum revision to include in the events (inclusive).
555
- * @param {number} [maxRevision] The 1-based maximum revision to include in the events (inclusive).
563
+ * @param {number} [minRevision=1] The 1-based minimum revision to include in the events (inclusive).
564
+ * @param {number} [maxRevision=-1] The 1-based maximum revision to include in the events (inclusive).
565
+ * @param {function|object|null} [predicate] Optional matcher (see {@link EventStream}).
566
+ * @param {boolean} [raw=false] If true, return NDJSON buffers.
556
567
  * @returns {EventStream|boolean} The event stream or false if a stream with the name doesn't exist.
557
568
  */
558
- getEventStream(streamName, minRevision = 1, maxRevision = -1) {
569
+ getEventStream(streamName, minRevision = 1, maxRevision = -1, predicate = null, raw = false) {
570
+ ({ predicate, raw } = normalizePredicateRaw(predicate, raw));
559
571
  if (!(streamName in this.streams)) {
560
572
  return false;
561
573
  }
562
- return new EventStream(streamName, this, minRevision, maxRevision);
574
+ return new EventStream(streamName, this, minRevision, maxRevision, predicate, raw);
563
575
  }
564
576
 
565
577
  /**
@@ -567,12 +579,15 @@ class EventStore extends events.EventEmitter {
567
579
  * This is the same as `getEventStream('_all', ...)`.
568
580
  *
569
581
  * @api
570
- * @param {number} [minRevision] The 1-based minimum revision to include in the events (inclusive).
571
- * @param {number} [maxRevision] The 1-based maximum revision to include in the events (inclusive).
582
+ * @param {number} [minRevision=1] The 1-based minimum revision to include in the events (inclusive).
583
+ * @param {number} [maxRevision=-1] The 1-based maximum revision to include in the events (inclusive).
584
+ * @param {function|object|null} [predicate] Optional matcher (see {@link EventStream}).
585
+ * @param {boolean} [raw=false] If true, return NDJSON buffers.
572
586
  * @returns {EventStream} The event stream.
573
587
  */
574
- getAllEvents(minRevision = 1, maxRevision = -1) {
575
- return this.getEventStream('_all', minRevision, maxRevision);
588
+ getAllEvents(minRevision = 1, maxRevision = -1, predicate = null, raw = false) {
589
+ ({ predicate, raw } = normalizePredicateRaw(predicate, raw));
590
+ return this.getEventStream('_all', minRevision, maxRevision, predicate, raw);
576
591
  }
577
592
 
578
593
  /**
@@ -580,14 +595,15 @@ class EventStore extends events.EventEmitter {
580
595
  *
581
596
  * @param {string} streamName The (transient) name of the joined stream.
582
597
  * @param {Array<string>} streamNames An array of the stream names to join.
583
- * @param {number} [minRevision] The 1-based minimum revision to include in the events (inclusive).
584
- * @param {number} [maxRevision] The 1-based maximum revision to include in the events (inclusive).
585
- * @param {function(object, object): boolean|null} [predicate] An optional filter predicate
586
- * `(payload, metadata) => boolean`. Only events for which this returns truthy are yielded.
598
+ * @param {number} [minRevision=1] The 1-based minimum revision to include in the events (inclusive).
599
+ * @param {number} [maxRevision=-1] The 1-based maximum revision to include in the events (inclusive).
600
+ * @param {function|object|null} [predicate] Optional matcher (see {@link EventStream}).
601
+ * @param {boolean} [raw=false] If true, return NDJSON buffers.
587
602
  * @returns {EventStream} The joined event stream.
588
603
  * @throws {Error} if any of the streams doesn't exist.
589
604
  */
590
- fromStreams(streamName, streamNames, minRevision = 1, maxRevision = -1, predicate = null) {
605
+ fromStreams(streamName, streamNames, minRevision = 1, maxRevision = -1, predicate = null, raw = false) {
606
+ ({ predicate, raw } = normalizePredicateRaw(predicate, raw));
591
607
  assert(streamNames instanceof Array, 'Must specify an array of stream names.');
592
608
 
593
609
  if (streamNames.length === 0) {
@@ -599,12 +615,12 @@ class EventStore extends events.EventEmitter {
599
615
  }
600
616
 
601
617
  if (streamNames.length === 1) {
602
- const stream = new EventStream(streamNames[0], this, minRevision, maxRevision, predicate);
618
+ const stream = new EventStream(streamNames[0], this, minRevision, maxRevision, predicate, raw);
603
619
  stream.name = streamName;
604
620
  return stream;
605
621
  }
606
622
 
607
- return new JoinEventStream(streamName, streamNames, this, minRevision, maxRevision, predicate);
623
+ return new JoinEventStream(streamName, streamNames, this, minRevision, maxRevision, predicate, raw);
608
624
  }
609
625
 
610
626
  /**
@@ -618,24 +634,26 @@ class EventStore extends events.EventEmitter {
618
634
  *
619
635
  * @api
620
636
  * @param {string} categoryName The name of the category to get a stream for. A category is a stream name prefix.
621
- * @param {number} [minRevision] The 1-based minimum revision to include in the events (inclusive).
622
- * @param {number} [maxRevision] The 1-based maximum revision to include in the events (inclusive).
637
+ * @param {number} [minRevision=1] The 1-based minimum revision to include in the events (inclusive).
638
+ * @param {number} [maxRevision=-1] The 1-based maximum revision to include in the events (inclusive).
639
+ * @param {function|object|null} [predicate] Optional matcher (see {@link EventStream}).
640
+ * @param {boolean} [raw=false] If true, return NDJSON buffers.
623
641
  * @returns {EventStream} The joined event stream for all streams of the given category.
624
642
  * @throws {Error} If no stream for this category exists.
625
643
  */
626
- getEventStreamForCategory(categoryName, minRevision = 1, maxRevision = -1) {
644
+ getEventStreamForCategory(categoryName, minRevision = 1, maxRevision = -1, predicate = null, raw = false) {
645
+ ({ predicate, raw } = normalizePredicateRaw(predicate, raw));
627
646
  if (categoryName in this.streams) {
628
- return this.getEventStream(categoryName, minRevision, maxRevision);
647
+ return this.getEventStream(categoryName, minRevision, maxRevision, predicate, raw);
629
648
  }
630
649
  const categoryStreams = Object.keys(this.streams).filter(streamName =>
631
650
  streamName.startsWith(categoryName + '-') ||
632
651
  streamName.startsWith(categoryName + '/')
633
652
  );
634
653
 
635
- if (categoryStreams.length === 0) {
636
- throw new Error(`No streams for category '${categoryName}' exist.`);
637
- }
638
- return this.fromStreams(categoryName, categoryStreams, minRevision, maxRevision);
654
+ assert(categoryStreams.length > 0, `No streams for category '${categoryName}' exist.`);
655
+
656
+ return this.fromStreams(categoryName, categoryStreams, minRevision, maxRevision, predicate, raw);
639
657
  }
640
658
 
641
659
  /**
@@ -729,41 +747,81 @@ class EventStore extends events.EventEmitter {
729
747
  }
730
748
 
731
749
  /**
732
- * Get a durable consumer for the given stream that will keep receiving events from the last position.
750
+ * Get a durable consumer for the given stream, or look up an existing consumer by identifier.
751
+ *
752
+ * When called with a single argument, returns the running consumer registered under that
753
+ * identifier, or `null` if none is found — useful for read endpoints that need the live
754
+ * in-memory instance without creating a new one.
755
+ *
756
+ * When called with two or more arguments, creates (or re-uses) a Consumer for the given
757
+ * stream and identifier, registers it in `this.consumers`, and returns it.
733
758
  *
734
- * @param {string} streamName The name of the stream to consume.
735
- * @param {string} identifier The unique identifying name of this consumer.
759
+ * @param {string} streamNameOrIdentifier The stream name, or the consumer identifier when used as a registry lookup.
760
+ * @param {string} [identifier] The unique identifying name of this consumer. Omit for registry-only lookup.
736
761
  * @param {object} [initialState] The initial state of the consumer.
737
762
  * @param {number} [since] The stream revision to start consuming from.
738
- * @returns {Consumer} A durable consumer for the given stream.
763
+ * @returns {Consumer|null} A durable consumer, or `null` when looking up by identifier and none is registered.
739
764
  */
740
- getConsumer(streamName, identifier, initialState = {}, since = 0) {
765
+ getConsumer(streamNameOrIdentifier, identifier, initialState = {}, since = 0) {
766
+ if (identifier === undefined) {
767
+ return this.consumers.get(streamNameOrIdentifier) ?? null;
768
+ }
769
+ const streamName = streamNameOrIdentifier;
770
+ if (this.consumers.has(identifier)) {
771
+ const existingConsumer = this.consumers.get(identifier);
772
+ if (existingConsumer.streamName === streamName) {
773
+ return existingConsumer;
774
+ }
775
+ // Rebind identifier to the requested stream when a consumer with the same
776
+ // identifier already exists for another stream.
777
+ existingConsumer.stop();
778
+ }
741
779
  const consumer = new Consumer(this.storage, streamName === '_all' ? '_all' : 'stream-' + streamName, identifier, initialState, since);
742
780
  consumer.streamName = streamName;
781
+ this.consumers.set(identifier, consumer);
743
782
  return consumer;
744
783
  }
745
784
 
746
785
  /**
747
- * Scan the existing consumers on this EventStore and asynchronously return a list of their names.
748
- * @param {function(error: Error, consumers: array)} callback A callback that will receive an error as first and the list of consumers as second argument.
786
+ * Scan the existing consumers on this EventStore and asynchronously invoke a callback with the parsed list.
787
+ *
788
+ * Each consumer entry provides `{ name, stream, identifier }` parsed from the on-disk filename.
789
+ * Pass `autoStart = true` to eagerly open every discovered consumer and register it in
790
+ * `this.consumers` so that it is immediately available for registry lookups.
791
+ *
792
+ * @param {function(error: Error|null, consumers: Array<{name: string, stream: string, identifier: string}>)} callback
793
+ * @param {boolean} [autoStart=false] When true, calls `getConsumer(stream, identifier)` for each discovered consumer.
749
794
  */
750
- scanConsumers(callback) {
795
+ scanConsumers(callback, autoStart = false) {
751
796
  const consumersPath = path.join(this.storage.indexDirectory, 'consumers');
752
797
  if (!fs.existsSync(consumersPath)) {
753
798
  callback(null, []);
754
799
  return;
755
800
  }
756
801
  const regex = new RegExp(`^${this.storage.storageFile}\\.([^.]*\\..*)$`);
757
- const consumers = [];
758
- scanForFiles(consumersPath, regex, consumers.push.bind(consumers), /* istanbul ignore next */ (err) => {
802
+ const consumerNames = [];
803
+ scanForFiles(consumersPath, regex, consumerNames.push.bind(consumerNames), /* istanbul ignore next */ (err) => {
759
804
  if (err) {
760
805
  return callback(err, []);
761
806
  }
807
+ const consumers = consumerNames.map(name => {
808
+ const splitIndex = name.lastIndexOf('.');
809
+ const indexName = name.slice(0, splitIndex);
810
+ const identifier = name.slice(splitIndex + 1);
811
+ const stream = parseStreamFromIndexName(indexName);
812
+ return { name, stream, identifier };
813
+ });
814
+ if (autoStart) {
815
+ for (const { stream, identifier } of consumers) {
816
+ this.getConsumer(stream, identifier);
817
+ }
818
+ }
762
819
  callback(null, consumers);
763
820
  });
764
821
  }
765
822
  }
766
823
 
824
+
767
825
  EventStore.Storage = Storage;
768
826
  EventStore.Index = Index;
769
827