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/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,13 +75,58 @@ 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
|
+
|
|
102
|
+
## HTTP API
|
|
103
|
+
|
|
104
|
+
To expose an event store over HTTP, see the companion package **[event-storage-http](https://github.com/albe/node-event-storage-http)**:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm install event-storage-http
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
import EventStore from 'event-storage';
|
|
112
|
+
import { createEventStoreHttpServer } from 'event-storage-http';
|
|
113
|
+
|
|
114
|
+
const eventStore = new EventStore('my-store', { storageDirectory: './data' });
|
|
115
|
+
const server = createEventStoreHttpServer(eventStore);
|
|
116
|
+
server.listen(3000);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The package exposes NDJSON stream endpoints, durable consumer management, and an `HttpEventStream` client helper for consuming event streams over fetch.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
78
123
|
## Documentation
|
|
79
124
|
|
|
80
125
|
The full documentation is hosted at **<https://node-event-storage.readthedocs.io/en/latest/>** and covers:
|
|
81
126
|
|
|
82
127
|
- [Getting Started](https://node-event-storage.readthedocs.io/en/latest/getting-started/) — installation, constructor options, basic usage.
|
|
83
128
|
- [Event Streams](https://node-event-storage.readthedocs.io/en/latest/streams/) — writing, reading, optimistic concurrency, fluent API, joining streams, categories, and event metadata.
|
|
84
|
-
- [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.
|
|
85
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.
|
|
86
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.
|
|
87
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": [
|
|
@@ -43,10 +44,8 @@
|
|
|
43
44
|
"src/Partition/*.js",
|
|
44
45
|
"src/Storage/*.js",
|
|
45
46
|
"src/WatchesFile.js",
|
|
46
|
-
"
|
|
47
|
-
"src/
|
|
48
|
-
"src/metadataUtil.js",
|
|
49
|
-
"index.js"
|
|
47
|
+
"index.js",
|
|
48
|
+
"src/utils/*.js"
|
|
50
49
|
],
|
|
51
50
|
"license": "MIT",
|
|
52
51
|
"maintainers": [
|
package/src/Consumer.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import stream from 'stream';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { assert } from './util.js';
|
|
5
|
-
import { ensureDirectory } from './fsUtil.js';
|
|
4
|
+
import { assert } from './utils/util.js';
|
|
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) {
|
|
@@ -30,8 +31,8 @@ class Consumer extends stream.Readable {
|
|
|
30
31
|
* @param {Storage} storage The storage to create the consumer for.
|
|
31
32
|
* @param {string} indexName The name of the index to consume.
|
|
32
33
|
* @param {string} identifier The unique name to identify this consumer.
|
|
33
|
-
* @param {object} [initialState] The initial state of the consumer.
|
|
34
|
-
* @param {number} [startFrom] The revision to start from within the index to consume.
|
|
34
|
+
* @param {object} [initialState={}] The initial state of the consumer.
|
|
35
|
+
* @param {number} [startFrom=0] The revision to start from within the index to consume.
|
|
35
36
|
*/
|
|
36
37
|
constructor(storage, indexName, identifier, initialState = {}, startFrom = 0) {
|
|
37
38
|
super({ objectMode: true });
|
|
@@ -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);
|
|
@@ -111,7 +109,7 @@ class Consumer extends stream.Readable {
|
|
|
111
109
|
* May only be called from within the document handling callback.
|
|
112
110
|
*
|
|
113
111
|
* @param {object|function(object):object} newState
|
|
114
|
-
* @param {boolean} [persist] Set to false if this state update should not be persisted yet
|
|
112
|
+
* @param {boolean} [persist=true] Set to false if this state update should not be persisted yet
|
|
115
113
|
* @api
|
|
116
114
|
*/
|
|
117
115
|
setState(newState, persist = true) {
|
|
@@ -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
|
}
|
|
@@ -151,6 +148,7 @@ class Consumer extends stream.Readable {
|
|
|
151
148
|
if (this.doPersist) {
|
|
152
149
|
this.persist();
|
|
153
150
|
}
|
|
151
|
+
this.emit('progress', this.position, this.state);
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
/**
|
|
@@ -170,7 +168,7 @@ class Consumer extends stream.Readable {
|
|
|
170
168
|
consumerData.write(consumerState, 4, consumerState.length, 'utf-8');
|
|
171
169
|
const tmpFile = this.fileName + '.' + this.position;
|
|
172
170
|
this.persisting = null;
|
|
173
|
-
/*
|
|
171
|
+
/* c8 ignore next 3 */
|
|
174
172
|
if (fs.existsSync(tmpFile)) {
|
|
175
173
|
throw new Error(`Trying to update consumer ${this.name} concurrently. Keep each single consumer within a single process.`);
|
|
176
174
|
}
|
|
@@ -180,7 +178,7 @@ class Consumer extends stream.Readable {
|
|
|
180
178
|
fs.renameSync(tmpFile, this.fileName);
|
|
181
179
|
this.emit('persisted', consumerState);
|
|
182
180
|
} catch (e) {
|
|
183
|
-
/*
|
|
181
|
+
/* c8 ignore next */
|
|
184
182
|
safeUnlink(tmpFile);
|
|
185
183
|
}
|
|
186
184
|
});
|
|
@@ -244,6 +242,7 @@ class Consumer extends stream.Readable {
|
|
|
244
242
|
const maxBatchPosition = Math.min(this.position + MAX_CATCHUP_BATCH + 1, this.index.length);
|
|
245
243
|
const documents = this.storage.readRange(this.position + 1, maxBatchPosition, this.index);
|
|
246
244
|
this.consumeDocuments(documents);
|
|
245
|
+
this.emit('progress', this.position, this.state);
|
|
247
246
|
this.once('persisted', () => catchUpBatch());
|
|
248
247
|
this.persist();
|
|
249
248
|
});
|
|
@@ -267,15 +266,12 @@ class Consumer extends stream.Readable {
|
|
|
267
266
|
/**
|
|
268
267
|
* Reset this projection to restart processing all documents again.
|
|
269
268
|
* NOTE: This will overwrite the current state of the projection and hence be destructive.
|
|
270
|
-
* @param {object} [initialState] The initial state of the consumer.
|
|
271
|
-
* @param {number} [startFrom] The revision to start from within the index to consume.
|
|
269
|
+
* @param {object} [initialState={}] The initial state of the consumer.
|
|
270
|
+
* @param {number} [startFrom=0] The revision to start from within the index to consume.
|
|
272
271
|
* @api
|
|
273
272
|
*/
|
|
274
273
|
reset(initialState = {}, startFrom = 0) {
|
|
275
|
-
|
|
276
|
-
startFrom = initialState;
|
|
277
|
-
initialState = {};
|
|
278
|
-
}
|
|
274
|
+
({ initialState, startFrom } = normalizeConsumerStateArgs(initialState, startFrom));
|
|
279
275
|
const restart = this.consuming;
|
|
280
276
|
this.stop();
|
|
281
277
|
this.state = Object.freeze(initialState);
|