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/README.md +51 -392
- package/index.js +2 -1
- package/package.json +28 -19
- package/src/Clock.js +20 -8
- package/src/Consumer.js +68 -18
- package/src/EventStore.js +305 -94
- package/src/EventStream.js +171 -17
- package/src/Index/ReadableIndex.js +33 -13
- package/src/Index/WritableIndex.js +33 -17
- package/src/IndexEntry.js +5 -1
- package/src/JoinEventStream.js +32 -30
- package/src/Partition/ReadOnlyPartition.js +1 -0
- package/src/Partition/ReadablePartition.js +201 -49
- package/src/Partition/WritablePartition.js +134 -61
- package/src/Storage/ReadOnlyStorage.js +6 -3
- package/src/Storage/ReadableStorage.js +147 -19
- package/src/Storage/WritableStorage.js +205 -27
- package/src/Watcher.js +38 -29
- package/src/WatchesFile.js +9 -8
- package/src/metadataUtil.js +79 -0
- package/src/util.js +102 -65
- package/test/Consumer.spec.js +0 -268
- package/test/EventStore.spec.js +0 -591
- package/test/EventStream.spec.js +0 -120
- package/test/Index.spec.js +0 -590
- package/test/JoinEventStream.spec.js +0 -113
- package/test/Partition.spec.js +0 -384
- package/test/Storage.spec.js +0 -955
- package/test/Watcher.spec.js +0 -131
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
|
|
5
|
+
const events = require('events');
|
|
6
6
|
const Storage = require('./Storage');
|
|
7
7
|
const Consumer = require('./Consumer');
|
|
8
|
-
const
|
|
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 =
|
|
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 =
|
|
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
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* @
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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, {
|
|
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 =
|
|
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 =
|
|
421
|
+
getAllEvents(minRevision = 1, maxRevision = -1) {
|
|
266
422
|
return this.getEventStream('_all', minRevision, maxRevision);
|
|
267
423
|
}
|
|
268
424
|
|
|
269
425
|
/**
|
|
270
|
-
* Create a
|
|
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 =
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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;
|