event-storage 1.2.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 +28 -4
- package/index.js +1 -0
- package/package.json +2 -1
- package/src/Consumer.js +7 -13
- package/src/EventStore.js +28 -52
- package/src/EventStream.js +8 -28
- package/src/Index/ReadOnlyIndex.js +1 -1
- package/src/Index/ReadableIndex.js +2 -4
- package/src/Index/WritableIndex.js +2 -4
- package/src/JoinEventStream.js +3 -13
- package/src/Partition/ReadOnlyPartition.js +1 -1
- package/src/Partition/ReadablePartition.js +5 -5
- package/src/Partition/WritablePartition.js +1 -1
- package/src/Storage/ReadOnlyStorage.js +1 -1
- package/src/Storage/ReadableStorage.js +9 -12
- package/src/Storage/WritableStorage.js +9 -12
- package/src/utils/apiHelpers.js +123 -0
- package/src/utils/fsUtil.js +27 -23
- package/src/utils/jsonUtil.js +257 -42
- package/src/utils/metadataUtil.js +357 -87
- package/src/utils/util.js +20 -17
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-

|
|
2
2
|
|
|
3
3
|
[](https://github.com/albe/node-event-storage/actions/workflows/build.yml)
|
|
4
4
|
[](https://badge.fury.io/js/event-storage)
|
|
@@ -62,8 +62,8 @@ eventstore.on('ready', () => {
|
|
|
62
62
|
| **Optimistic concurrency** | Pass `expectedVersion` to `commit()` to guarantee conflict-free writes. |
|
|
63
63
|
| **Flexible stream reading** | Range queries, reverse iteration, and a fluent builder API. |
|
|
64
64
|
| **Derived streams** | Filter or combine events into new read-only streams. |
|
|
65
|
-
| **
|
|
66
|
-
| **DCB
|
|
65
|
+
| **Object matchers** | Support nested equality, array values (OR semantics), and scalar operators like `$gt` / `$gte` / `$lt` / `$lte` / `$eq` / `$ne`, while still benefiting from O(1) discriminant routing on writes. |
|
|
66
|
+
| **DCB** | Configure `typeAccessor` to have per-type stream indexes maintained automatically, and use `query()` / `Condition` for fine-grained, query-scoped optimistic concurrency (Dynamic Consistency Boundaries). |
|
|
67
67
|
| **Stream categories** | Name streams `<category>-<id>` and query the whole category at once. |
|
|
68
68
|
| **Durable consumers** | At-least-once (and exactly-once with `setState`) event delivery with automatic position tracking. |
|
|
69
69
|
| **Consistency guards** | Build aggregates that enforce business invariants with built-in snapshotting. |
|
|
@@ -75,6 +75,30 @@ eventstore.on('ready', () => {
|
|
|
75
75
|
|
|
76
76
|
---
|
|
77
77
|
|
|
78
|
+
## DCB Example
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
const { stream, condition } = store.query(['OrderPlaced'], { payload: { customerId: 'cust-1' } });
|
|
82
|
+
const hasOpenOrder = stream.some((event) => event.status === 'open');
|
|
83
|
+
|
|
84
|
+
if (!hasOpenOrder) {
|
|
85
|
+
store.commit('orders-cust-1', [{ type: 'OrderPlaced', customerId: 'cust-1' }], condition);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Object Matcher Syntax
|
|
92
|
+
|
|
93
|
+
Object matchers support nested equality, array values, and scalar operators like `$gt`, `$gte`, `$lt`, `$lte`, `$eq`, and `$ne`.
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
{ payload: { type: ['OrderPlaced', 'OrderCancelled'] } }
|
|
97
|
+
{ payload: { amount: { $gte: 100, $lt: 1000 } } }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
78
102
|
## HTTP API
|
|
79
103
|
|
|
80
104
|
To expose an event store over HTTP, see the companion package **[event-storage-http](https://github.com/albe/node-event-storage-http)**:
|
|
@@ -102,7 +126,7 @@ The full documentation is hosted at **<https://node-event-storage.readthedocs.io
|
|
|
102
126
|
|
|
103
127
|
- [Getting Started](https://node-event-storage.readthedocs.io/en/latest/getting-started/) — installation, constructor options, basic usage.
|
|
104
128
|
- [Event Streams](https://node-event-storage.readthedocs.io/en/latest/streams/) — writing, reading, optimistic concurrency, fluent API, joining streams, categories, and event metadata.
|
|
105
|
-
- [Dynamic Consistency Boundaries (DCB)](https://node-event-storage.readthedocs.io/en/latest/dcb/) — `typeAccessor`,
|
|
129
|
+
- [Dynamic Consistency Boundaries (DCB)](https://node-event-storage.readthedocs.io/en/latest/dcb/) — `typeAccessor`, query matchers, consistency tokens, and the full DCB workflow.
|
|
106
130
|
- [Consumers](https://node-event-storage.readthedocs.io/en/latest/consumers/) — at-least-once and exactly-once delivery, consumer state, consistency guards, and read-only mode.
|
|
107
131
|
- [Advanced Topics](https://node-event-storage.readthedocs.io/en/latest/advanced/) — ACID properties, reliability and crash-safety guarantees, storage configuration, partitioning, custom serialization, compression, security, and access control hooks.
|
|
108
132
|
|
package/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { default as EventStream } from './src/EventStream.js';
|
|
|
3
3
|
export { default as Storage, StorageLockedError } from './src/Storage.js';
|
|
4
4
|
export { default as Index } from './src/Index.js';
|
|
5
5
|
export { default as Consumer } from './src/Consumer.js';
|
|
6
|
+
export { matches, buildRawBufferMatcher } from './src/utils/metadataUtil.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "event-storage",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "An optimized embedded event store for node.js",
|
|
6
6
|
"keywords": [
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"test": "c8 --reporter=lcov --reporter=text mocha test/*.spec.js",
|
|
29
|
+
"test:grep": "c8 --reporter=lcov --reporter=text mocha test/*.spec.js --grep",
|
|
29
30
|
"coverage": "c8 report --reporter=text-lcov | coveralls"
|
|
30
31
|
},
|
|
31
32
|
"files": [
|
package/src/Consumer.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { assert } from './utils/util.js';
|
|
5
5
|
import { ensureDirectory } from './utils/fsUtil.js';
|
|
6
|
+
import { normalizeConsumerStateArgs } from './utils/apiHelpers.js';
|
|
6
7
|
import Storage from './Storage/ReadableStorage.js';
|
|
7
8
|
const MAX_CATCHUP_BATCH = 10;
|
|
8
9
|
|
|
@@ -11,7 +12,7 @@ const MAX_CATCHUP_BATCH = 10;
|
|
|
11
12
|
* @param {string} filename
|
|
12
13
|
*/
|
|
13
14
|
const safeUnlink = (filename) => {
|
|
14
|
-
/*
|
|
15
|
+
/* c8 ignore next */
|
|
15
16
|
try {
|
|
16
17
|
fs.unlinkSync(filename);
|
|
17
18
|
} catch (e) {
|
|
@@ -84,14 +85,11 @@ class Consumer extends stream.Readable {
|
|
|
84
85
|
* @param {number} startFrom The revision to start from within the index to consume.
|
|
85
86
|
*/
|
|
86
87
|
restoreState(initialState, startFrom) {
|
|
87
|
-
/*
|
|
88
|
+
/* c8 ignore next 3 */
|
|
88
89
|
if (!this.fileName) {
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
|
-
|
|
92
|
-
startFrom = initialState;
|
|
93
|
-
initialState = {};
|
|
94
|
-
}
|
|
92
|
+
({ initialState, startFrom } = normalizeConsumerStateArgs(initialState, startFrom));
|
|
95
93
|
try {
|
|
96
94
|
const consumerData = fs.readFileSync(this.fileName);
|
|
97
95
|
this.position = consumerData.readInt32LE(0);
|
|
@@ -137,7 +135,6 @@ class Consumer extends stream.Readable {
|
|
|
137
135
|
return;
|
|
138
136
|
}
|
|
139
137
|
|
|
140
|
-
/* istanbul ignore if */
|
|
141
138
|
if (this.position !== position - 1) {
|
|
142
139
|
return;
|
|
143
140
|
}
|
|
@@ -171,7 +168,7 @@ class Consumer extends stream.Readable {
|
|
|
171
168
|
consumerData.write(consumerState, 4, consumerState.length, 'utf-8');
|
|
172
169
|
const tmpFile = this.fileName + '.' + this.position;
|
|
173
170
|
this.persisting = null;
|
|
174
|
-
/*
|
|
171
|
+
/* c8 ignore next 3 */
|
|
175
172
|
if (fs.existsSync(tmpFile)) {
|
|
176
173
|
throw new Error(`Trying to update consumer ${this.name} concurrently. Keep each single consumer within a single process.`);
|
|
177
174
|
}
|
|
@@ -181,7 +178,7 @@ class Consumer extends stream.Readable {
|
|
|
181
178
|
fs.renameSync(tmpFile, this.fileName);
|
|
182
179
|
this.emit('persisted', consumerState);
|
|
183
180
|
} catch (e) {
|
|
184
|
-
/*
|
|
181
|
+
/* c8 ignore next */
|
|
185
182
|
safeUnlink(tmpFile);
|
|
186
183
|
}
|
|
187
184
|
});
|
|
@@ -274,10 +271,7 @@ class Consumer extends stream.Readable {
|
|
|
274
271
|
* @api
|
|
275
272
|
*/
|
|
276
273
|
reset(initialState = {}, startFrom = 0) {
|
|
277
|
-
|
|
278
|
-
startFrom = initialState;
|
|
279
|
-
initialState = {};
|
|
280
|
-
}
|
|
274
|
+
({ initialState, startFrom } = normalizeConsumerStateArgs(initialState, startFrom));
|
|
281
275
|
const restart = this.consuming;
|
|
282
276
|
this.stop();
|
|
283
277
|
this.state = Object.freeze(initialState);
|
package/src/EventStore.js
CHANGED
|
@@ -9,6 +9,7 @@ import Consumer from './Consumer.js';
|
|
|
9
9
|
import { assert, getPropertyAtPath } from './utils/util.js';
|
|
10
10
|
import { ensureDirectory, scanForFiles } from './utils/fsUtil.js';
|
|
11
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,
|
|
@@ -185,7 +186,6 @@ class EventStore extends events.EventEmitter {
|
|
|
185
186
|
* @param {string} name The full stream name, including the `stream-` prefix (and optional `.closed` suffix).
|
|
186
187
|
*/
|
|
187
188
|
registerStream(name) {
|
|
188
|
-
/* istanbul ignore if */
|
|
189
189
|
if (!name.startsWith('stream-')) {
|
|
190
190
|
return;
|
|
191
191
|
}
|
|
@@ -343,39 +343,6 @@ class EventStore extends events.EventEmitter {
|
|
|
343
343
|
return this.storage.length;
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
/**
|
|
347
|
-
* This method makes it so the last three arguments can be given either as:
|
|
348
|
-
* - expectedVersion, metadata, callback
|
|
349
|
-
* - expectedVersion, callback
|
|
350
|
-
* - metadata, callback
|
|
351
|
-
* - callback
|
|
352
|
-
*
|
|
353
|
-
* @private
|
|
354
|
-
* @param {Array<object>|object} events
|
|
355
|
-
* @param {number|CommitCondition} [expectedVersion]
|
|
356
|
-
* @param {object|function} [metadata]
|
|
357
|
-
* @param {function} [callback]
|
|
358
|
-
* @returns {{events: Array<object>, metadata: object, callback: function, expectedVersion: number|CommitCondition}}
|
|
359
|
-
*/
|
|
360
|
-
static fixArgumentTypes(events, expectedVersion, metadata, callback) {
|
|
361
|
-
if (!(events instanceof Array)) {
|
|
362
|
-
events = [events];
|
|
363
|
-
}
|
|
364
|
-
if (typeof expectedVersion !== 'number' && !(expectedVersion instanceof CommitCondition)) {
|
|
365
|
-
callback = metadata;
|
|
366
|
-
metadata = expectedVersion;
|
|
367
|
-
expectedVersion = ExpectedVersion.Any;
|
|
368
|
-
}
|
|
369
|
-
if (typeof metadata !== 'object') {
|
|
370
|
-
callback = metadata;
|
|
371
|
-
metadata = {};
|
|
372
|
-
}
|
|
373
|
-
if (typeof callback !== 'function') {
|
|
374
|
-
callback = () => {};
|
|
375
|
-
}
|
|
376
|
-
return { events, expectedVersion, metadata, callback };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
346
|
/**
|
|
380
347
|
* Check a {@link CommitCondition} against the current state of the store.
|
|
381
348
|
* Iterates a join stream over all condition type streams starting from
|
|
@@ -383,6 +350,7 @@ class EventStore extends events.EventEmitter {
|
|
|
383
350
|
* {@link OptimisticConcurrencyError} when a new event of a listed type satisfies
|
|
384
351
|
* `condition.matcher(payload, metadata)` (or any such event when no matcher is provided).
|
|
385
352
|
*
|
|
353
|
+
* @private
|
|
386
354
|
* @param {CommitCondition} condition
|
|
387
355
|
* @throws {OptimisticConcurrencyError}
|
|
388
356
|
*/
|
|
@@ -410,6 +378,7 @@ class EventStore extends events.EventEmitter {
|
|
|
410
378
|
* Ensure a dedicated type stream exists for each event's type, creating it if needed.
|
|
411
379
|
* Must be called before the entity stream is created to guarantee correct index routing.
|
|
412
380
|
*
|
|
381
|
+
* @private
|
|
413
382
|
* @param {Array<object>} events The events to process.
|
|
414
383
|
*/
|
|
415
384
|
ensureTypeStreams(events) {
|
|
@@ -425,6 +394,11 @@ class EventStore extends events.EventEmitter {
|
|
|
425
394
|
}
|
|
426
395
|
}
|
|
427
396
|
|
|
397
|
+
/**
|
|
398
|
+
* @private
|
|
399
|
+
* @param {object} event
|
|
400
|
+
* @returns {string|null}
|
|
401
|
+
*/
|
|
428
402
|
resolveValidatedTypeStreamName(event) {
|
|
429
403
|
const type = this.typeAccessor(event);
|
|
430
404
|
if (type === undefined || type === null || type === '') {
|
|
@@ -435,6 +409,11 @@ class EventStore extends events.EventEmitter {
|
|
|
435
409
|
return type;
|
|
436
410
|
}
|
|
437
411
|
|
|
412
|
+
/**
|
|
413
|
+
* @private
|
|
414
|
+
* @param {string[]} types
|
|
415
|
+
* @returns {string[]}
|
|
416
|
+
*/
|
|
438
417
|
getExistingQueryTypes(types) {
|
|
439
418
|
const queryTypes = [];
|
|
440
419
|
for (const type of types) {
|
|
@@ -469,7 +448,14 @@ class EventStore extends events.EventEmitter {
|
|
|
469
448
|
assert(typeof streamName === 'string' && streamName !== '', 'Must specify a stream name for commit.');
|
|
470
449
|
assert(typeof events !== 'undefined' && events !== null, 'No events specified for commit.');
|
|
471
450
|
|
|
472
|
-
({ events, expectedVersion, metadata, callback } =
|
|
451
|
+
({ events, expectedVersion, metadata, callback } = fixCommitArgumentTypes(
|
|
452
|
+
events,
|
|
453
|
+
expectedVersion,
|
|
454
|
+
metadata,
|
|
455
|
+
callback,
|
|
456
|
+
ExpectedVersion.Any,
|
|
457
|
+
CommitCondition
|
|
458
|
+
));
|
|
473
459
|
|
|
474
460
|
// Perform DCB-style concurrency check when a CommitCondition is provided.
|
|
475
461
|
if (expectedVersion instanceof CommitCondition) {
|
|
@@ -782,7 +768,13 @@ class EventStore extends events.EventEmitter {
|
|
|
782
768
|
}
|
|
783
769
|
const streamName = streamNameOrIdentifier;
|
|
784
770
|
if (this.consumers.has(identifier)) {
|
|
785
|
-
|
|
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();
|
|
786
778
|
}
|
|
787
779
|
const consumer = new Consumer(this.storage, streamName === '_all' ? '_all' : 'stream-' + streamName, identifier, initialState, since);
|
|
788
780
|
consumer.streamName = streamName;
|
|
@@ -829,22 +821,6 @@ class EventStore extends events.EventEmitter {
|
|
|
829
821
|
}
|
|
830
822
|
}
|
|
831
823
|
|
|
832
|
-
function parseStreamFromIndexName(indexName) {
|
|
833
|
-
if (indexName === '_all') {
|
|
834
|
-
return '_all';
|
|
835
|
-
}
|
|
836
|
-
if (indexName.startsWith('stream-')) {
|
|
837
|
-
return indexName.slice(7);
|
|
838
|
-
}
|
|
839
|
-
return indexName;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function normalizePredicateRaw(predicate, raw) {
|
|
843
|
-
if (typeof predicate === 'boolean' && raw === false) {
|
|
844
|
-
return { predicate: null, raw: predicate };
|
|
845
|
-
}
|
|
846
|
-
return { predicate, raw };
|
|
847
|
-
}
|
|
848
824
|
|
|
849
825
|
EventStore.Storage = Storage;
|
|
850
826
|
EventStore.Index = Index;
|
package/src/EventStream.js
CHANGED
|
@@ -1,30 +1,10 @@
|
|
|
1
1
|
import stream from 'stream';
|
|
2
2
|
import { assert } from './utils/util.js';
|
|
3
3
|
import { buildRawBufferMatcher, matches } from './utils/metadataUtil.js';
|
|
4
|
+
import { normalizeRevision, normalizeMaxRevision } from './utils/apiHelpers.js';
|
|
4
5
|
|
|
5
6
|
const NDJSON_NEWLINE = Buffer.from('\n');
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
* Calculate the actual version number from a possibly relative (negative) version number.
|
|
9
|
-
*
|
|
10
|
-
* @param {number} version The version to normalize.
|
|
11
|
-
* @param {number} length The maximum version number
|
|
12
|
-
* @returns {number} The absolute version number.
|
|
13
|
-
*/
|
|
14
|
-
function normalizeVersion(version, length) {
|
|
15
|
-
return version < 0 ? version + length + 1 : version;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Return the lower absolute version given a version and a maxVersion constraint.
|
|
20
|
-
* @param {number} version
|
|
21
|
-
* @param {number} maxVersion
|
|
22
|
-
* @returns {number}
|
|
23
|
-
*/
|
|
24
|
-
function minVersion(version, maxVersion) {
|
|
25
|
-
return Math.min(version, maxVersion < 0 ? version + maxVersion + 1 : maxVersion);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
8
|
/**
|
|
29
9
|
* An event stream is a simple wrapper around an iterator over storage documents.
|
|
30
10
|
* It implements a node readable stream interface.
|
|
@@ -56,9 +36,9 @@ class EventStream extends stream.Readable {
|
|
|
56
36
|
this.rawMatcher = null;
|
|
57
37
|
if (eventStore.streams[name]) {
|
|
58
38
|
this.streamIndex = eventStore.streams[name].index;
|
|
59
|
-
this.minRevision =
|
|
60
|
-
this.maxRevision =
|
|
61
|
-
this.version =
|
|
39
|
+
this.minRevision = normalizeRevision(minRevision, this.streamIndex.length);
|
|
40
|
+
this.maxRevision = normalizeRevision(maxRevision, this.streamIndex.length);
|
|
41
|
+
this.version = normalizeMaxRevision(this.streamIndex.length, maxRevision);
|
|
62
42
|
this._iterator = null;
|
|
63
43
|
this.fetch = () => eventStore.storage.readRange(this.minRevision, this.maxRevision, this.streamIndex, raw);
|
|
64
44
|
} else {
|
|
@@ -74,7 +54,7 @@ class EventStream extends stream.Readable {
|
|
|
74
54
|
* @returns {EventStream}
|
|
75
55
|
*/
|
|
76
56
|
from(revision) {
|
|
77
|
-
this.minRevision =
|
|
57
|
+
this.minRevision = normalizeRevision(revision, this.streamIndex.length);
|
|
78
58
|
return this;
|
|
79
59
|
}
|
|
80
60
|
|
|
@@ -84,8 +64,8 @@ class EventStream extends stream.Readable {
|
|
|
84
64
|
* @returns {EventStream}
|
|
85
65
|
*/
|
|
86
66
|
until(revision) {
|
|
87
|
-
this.maxRevision =
|
|
88
|
-
this.version =
|
|
67
|
+
this.maxRevision = normalizeRevision(revision, this.streamIndex.length);
|
|
68
|
+
this.version = normalizeMaxRevision(this.streamIndex.length, this.maxRevision);
|
|
89
69
|
return this;
|
|
90
70
|
}
|
|
91
71
|
|
|
@@ -169,7 +149,7 @@ class EventStream extends stream.Readable {
|
|
|
169
149
|
let tmp = this.maxRevision;
|
|
170
150
|
this.maxRevision = this.minRevision;
|
|
171
151
|
this.minRevision = tmp;
|
|
172
|
-
this.version =
|
|
152
|
+
this.version = normalizeMaxRevision(this.streamIndex.length, this.maxRevision);
|
|
173
153
|
return this;
|
|
174
154
|
}
|
|
175
155
|
|
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import events from 'events';
|
|
4
4
|
import Entry, { assertValidEntryClass } from '../IndexEntry.js';
|
|
5
5
|
import { assert, wrapAndCheck, binarySearch } from '../utils/util.js';
|
|
6
|
+
import { normalizeNamedCtorArgs } from '../utils/apiHelpers.js';
|
|
6
7
|
|
|
7
8
|
// node-event-store-index V01
|
|
8
9
|
const HEADER_MAGIC = "nesidx01";
|
|
@@ -44,10 +45,7 @@ class ReadableIndex extends events.EventEmitter {
|
|
|
44
45
|
*/
|
|
45
46
|
constructor(name = '.index', options = {}) {
|
|
46
47
|
super();
|
|
47
|
-
|
|
48
|
-
options = name;
|
|
49
|
-
name = '.index';
|
|
50
|
-
}
|
|
48
|
+
({ name, options } = normalizeNamedCtorArgs(name, options, '.index'));
|
|
51
49
|
let defaults = {
|
|
52
50
|
dataDirectory: '.',
|
|
53
51
|
EntryClass: Entry
|
|
@@ -3,6 +3,7 @@ import ReadableIndex, { Entry, CorruptedIndexError, HEADER_MAGIC } from './Reada
|
|
|
3
3
|
import { assert, assertEqual } from '../utils/util.js';
|
|
4
4
|
import { buildMetadataHeader } from '../utils/metadataUtil.js';
|
|
5
5
|
import { ensureDirectory } from '../utils/fsUtil.js';
|
|
6
|
+
import { normalizeNamedCtorArgs } from '../utils/apiHelpers.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* An index is a simple append-only file that stores an ordered list of entry elements pointing to the actual file position
|
|
@@ -27,10 +28,7 @@ class WritableIndex extends ReadableIndex {
|
|
|
27
28
|
* @param {object} [options.metadata] An object containing the metadata information for this index. Will be written on initial creation and checked on subsequent openings.
|
|
28
29
|
*/
|
|
29
30
|
constructor(name = '.index', options = {}) {
|
|
30
|
-
|
|
31
|
-
options = name;
|
|
32
|
-
name = '.index';
|
|
33
|
-
}
|
|
31
|
+
({ name, options } = normalizeNamedCtorArgs(name, options, '.index'));
|
|
34
32
|
let defaults = {
|
|
35
33
|
writeBufferSize: 4096,
|
|
36
34
|
flushDelay: 100,
|
package/src/JoinEventStream.js
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
import EventStream from './EventStream.js';
|
|
2
2
|
import { assert, kWayMerge } from './utils/util.js';
|
|
3
|
+
import { normalizeRevision } from './utils/apiHelpers.js';
|
|
3
4
|
|
|
4
5
|
/** Reusable sentinel used for missing or empty per-stream iterators. */
|
|
5
6
|
const emptyIterator = Object.freeze({ next() { return { done: true }; } });
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
* Calculate the actual version number from a possibly relative (negative) version number.
|
|
9
|
-
*
|
|
10
|
-
* @param {number} version The version to normalize.
|
|
11
|
-
* @param {number} length The maximum version number
|
|
12
|
-
* @returns {number} The absolute version number.
|
|
13
|
-
*/
|
|
14
|
-
function normalizeVersion(version, length) {
|
|
15
|
-
return version < 0 ? version + length + 1 : version;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
8
|
/**
|
|
19
9
|
* An event stream is a simple wrapper around an iterator over storage documents.
|
|
20
10
|
* It implements a node readable stream interface.
|
|
@@ -36,8 +26,8 @@ class JoinEventStream extends EventStream {
|
|
|
36
26
|
|
|
37
27
|
this.streamIndex = eventStore.storage.index;
|
|
38
28
|
// Translate revisions to index numbers (1-based) and wrap around negatives
|
|
39
|
-
this.minRevision =
|
|
40
|
-
this.maxRevision =
|
|
29
|
+
this.minRevision = normalizeRevision(minRevision, eventStore.length);
|
|
30
|
+
this.maxRevision = normalizeRevision(maxRevision, eventStore.length);
|
|
41
31
|
this.fetch = function() {
|
|
42
32
|
return streams.map(streamName => {
|
|
43
33
|
const streamIndex = eventStore.streams[streamName]?.index;
|
|
@@ -130,7 +130,7 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
130
130
|
const metadata = metadataBuffer.toString('utf8').trim();
|
|
131
131
|
try {
|
|
132
132
|
this.metadata = JSON.parse(metadata);
|
|
133
|
-
this.metadata.epoch = this.metadata.epoch /*
|
|
133
|
+
this.metadata.epoch = this.metadata.epoch /* c8 ignore next */|| NES_EPOCH.getTime();
|
|
134
134
|
} catch (e) {
|
|
135
135
|
throw new Error('Invalid metadata.');
|
|
136
136
|
}
|
|
@@ -373,13 +373,13 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
373
373
|
*/
|
|
374
374
|
readDocumentBefore(position) {
|
|
375
375
|
const docPos = this.findDocumentPositionBefore(position);
|
|
376
|
-
/*
|
|
376
|
+
/* c8 ignore next */
|
|
377
377
|
if (docPos === false || docPos < 0) return null;
|
|
378
378
|
const reader = this.prepareReadBufferBackwards(Math.min(docPos + (this.readBuffer.byteLength >> 1), this.size));
|
|
379
|
-
/*
|
|
379
|
+
/* c8 ignore next */
|
|
380
380
|
if (!reader.buffer) return null;
|
|
381
381
|
const cursor = docPos - this.readBufferPos;
|
|
382
|
-
/*
|
|
382
|
+
/* c8 ignore next */
|
|
383
383
|
if (cursor < 0 || cursor + DOCUMENT_HEADER_SIZE > reader.length) return null;
|
|
384
384
|
const header = this.readDocumentHeader(reader.buffer, cursor, docPos);
|
|
385
385
|
return { header, position: docPos };
|
|
@@ -436,7 +436,7 @@ class ReadablePartition extends events.EventEmitter {
|
|
|
436
436
|
|
|
437
437
|
const header = {};
|
|
438
438
|
const data = this.readFrom(position, 0, header);
|
|
439
|
-
/*
|
|
439
|
+
/* c8 ignore next 3 */
|
|
440
440
|
if (data === false) {
|
|
441
441
|
return null;
|
|
442
442
|
}
|
|
@@ -178,7 +178,7 @@ class WritablePartition extends ReadablePartition {
|
|
|
178
178
|
*/
|
|
179
179
|
writeDocumentHeader(buffer, offset, dataSize, sequenceNumber = null, time64 = null) {
|
|
180
180
|
({ sequenceNumber, time64 } = this.normalizeWriteMetadata(sequenceNumber, time64));
|
|
181
|
-
/*
|
|
181
|
+
/* c8 ignore next */
|
|
182
182
|
assert(time64 >= 0, 'Time may not be negative!');
|
|
183
183
|
|
|
184
184
|
buffer.writeUInt32BE(dataSize, offset);
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import events from 'events';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import { ReadOnly as ReadOnlyPartition } from '../Partition.js';
|
|
5
|
+
import { ReadOnly as ReadOnlyIndex } from '../Index.js';
|
|
6
6
|
import { assert, wrapAndCheck, iterate, kWayMerge } from '../utils/util.js';
|
|
7
7
|
import { scanForFiles } from '../utils/fsUtil.js';
|
|
8
8
|
import { createHmac, matches, buildMetadataForMatcher } from '../utils/metadataUtil.js';
|
|
9
|
+
import { normalizeNamedCtorArgs } from '../utils/apiHelpers.js';
|
|
9
10
|
import IndexMatcher from '../IndexMatcher.js';
|
|
10
11
|
import PartitionPool from '../PartitionPool.js';
|
|
11
12
|
|
|
12
13
|
const DEFAULT_READ_BUFFER_SIZE = 4 * 1024;
|
|
13
|
-
const NDJSON_NEWLINE = Buffer.from('\n');
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Default ordered list of document property paths used as discriminant keys when
|
|
@@ -60,10 +60,7 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
60
60
|
*/
|
|
61
61
|
constructor(storageName = 'storage', config = {}) {
|
|
62
62
|
super();
|
|
63
|
-
|
|
64
|
-
config = storageName;
|
|
65
|
-
storageName = undefined;
|
|
66
|
-
}
|
|
63
|
+
({ name: storageName, options: config } = normalizeNamedCtorArgs(storageName, config));
|
|
67
64
|
|
|
68
65
|
this.storageFile = storageName || 'storage';
|
|
69
66
|
const defaults = {
|
|
@@ -158,7 +155,7 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
158
155
|
const partition = this.createPartition(file, this.partitionConfig);
|
|
159
156
|
this.partitions.add(partition.id, partition);
|
|
160
157
|
}, (partErr) => {
|
|
161
|
-
/*
|
|
158
|
+
/* c8 ignore next */
|
|
162
159
|
if (partErr) throw partErr;
|
|
163
160
|
|
|
164
161
|
// Scan was cancelled by close() between the two scan phases.
|
|
@@ -173,7 +170,7 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
173
170
|
this.emit('index-created', name);
|
|
174
171
|
}, (indexErr) => {
|
|
175
172
|
// The directory could disappear between existsSync and readdir (e.g. test cleanup).
|
|
176
|
-
/*
|
|
173
|
+
/* c8 ignore next */
|
|
177
174
|
if (indexErr && indexErr.code !== 'ENOENT') throw indexErr;
|
|
178
175
|
done();
|
|
179
176
|
});
|
|
@@ -481,7 +478,7 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
481
478
|
* @param {boolean} [noIndex=false] When true, bypasses the index and iterates partitions directly.
|
|
482
479
|
*/
|
|
483
480
|
forEachDocument(iterationHandler, noIndex = false) {
|
|
484
|
-
/*
|
|
481
|
+
/* c8 ignore next 3 */
|
|
485
482
|
if (typeof iterationHandler !== 'function') {
|
|
486
483
|
return;
|
|
487
484
|
}
|
|
@@ -512,7 +509,7 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
512
509
|
* @param {object} [matchDocument] If supplied, only indexes the document matches on will be iterated.
|
|
513
510
|
*/
|
|
514
511
|
forEachSecondaryIndex(iterationHandler, matchDocument) {
|
|
515
|
-
/*
|
|
512
|
+
/* c8 ignore next 3 */
|
|
516
513
|
if (typeof iterationHandler !== 'function') {
|
|
517
514
|
return;
|
|
518
515
|
}
|
|
@@ -537,7 +534,7 @@ class ReadableStorage extends events.EventEmitter {
|
|
|
537
534
|
* @param {function(ReadablePartition)} iterationHandler
|
|
538
535
|
*/
|
|
539
536
|
forEachPartition(iterationHandler) {
|
|
540
|
-
/*
|
|
537
|
+
/* c8 ignore next 3 */
|
|
541
538
|
if (typeof iterationHandler !== 'function') {
|
|
542
539
|
return;
|
|
543
540
|
}
|