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/README.md +49 -4
- package/index.js +1 -0
- package/package.json +4 -5
- package/src/Consumer.js +16 -20
- package/src/EventStore.js +176 -118
- package/src/EventStream.js +56 -38
- package/src/Index/ReadOnlyIndex.js +1 -1
- package/src/Index/ReadableIndex.js +9 -9
- package/src/Index/WritableIndex.js +6 -10
- package/src/IndexMatcher.js +2 -2
- package/src/JoinEventStream.js +33 -59
- package/src/Partition/ReadOnlyPartition.js +1 -1
- package/src/Partition/ReadablePartition.js +158 -90
- package/src/Partition/WritablePartition.js +38 -29
- package/src/Storage/ReadOnlyStorage.js +4 -4
- package/src/Storage/ReadableStorage.js +81 -113
- package/src/Storage/WritableStorage.js +52 -37
- package/src/Watcher.js +1 -1
- package/src/utils/apiHelpers.js +123 -0
- package/src/{fsUtil.js → utils/fsUtil.js} +27 -23
- package/src/utils/jsonUtil.js +302 -0
- package/src/utils/metadataUtil.js +517 -0
- package/src/{util.js → utils/util.js} +69 -31
- package/src/metadataUtil.js +0 -126
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
|
|
235
|
-
|
|
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
|
|
260
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 } =
|
|
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
|
-
|
|
454
|
-
|
|
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
|
|
518
|
-
*
|
|
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
|
|
530
|
-
|
|
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
|
-
|
|
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
|
|
586
|
-
*
|
|
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
|
-
|
|
636
|
-
|
|
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
|
|
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}
|
|
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
|
|
763
|
+
* @returns {Consumer|null} A durable consumer, or `null` when looking up by identifier and none is registered.
|
|
739
764
|
*/
|
|
740
|
-
getConsumer(
|
|
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
|
|
748
|
-
*
|
|
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
|
|
758
|
-
scanForFiles(consumersPath, regex,
|
|
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
|
|