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 CHANGED
@@ -1,4 +1,4 @@
1
- ![event-storage](logo/color.png)
1
+ ![event-storage](logo/color.svg)
2
2
 
3
3
  [![build](https://github.com/albe/node-event-storage/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/albe/node-event-storage/actions/workflows/build.yml)
4
4
  [![npm version](https://badge.fury.io/js/event-storage.svg)](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
- | **Multi-value matchers** | Object matchers support array values (OR semantics) and still benefit from O(1) discriminant routing on writes. |
66
- | **DCB / `typeAccessor`** | Configure `typeAccessor` to have per-type stream indexes maintained automatically, and use `query()` / `Condition` for fine-grained, query-scoped optimistic concurrency (Dynamic Consistency Boundaries). |
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`, multi-value matchers, consistency tokens, and the full DCB workflow.
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.1.0",
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
- "src/util.js",
47
- "src/fsUtil.js",
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
- /* istanbul ignore next */
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
- /* istanbul ignore if */
88
+ /* c8 ignore next 3 */
88
89
  if (!this.fileName) {
89
90
  return;
90
91
  }
91
- if (typeof initialState === 'number') {
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
- /* istanbul ignore if */
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
- /* istanbul ignore next */
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
- if (typeof initialState === 'number') {
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);