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/Consumer.js
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
const stream = require('stream');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const
|
|
5
|
-
const { assert } = require('./util');
|
|
4
|
+
const { assert, ensureDirectory } = require('./util');
|
|
6
5
|
|
|
7
6
|
const Storage = require('./Storage/ReadableStorage');
|
|
8
7
|
const MAX_CATCHUP_BATCH = 10;
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
10
|
+
* Safely unlink a file and ignore if it doesn't exist.
|
|
11
|
+
* @param {string} filename
|
|
12
|
+
*/
|
|
13
|
+
const safeUnlink = (filename) => {
|
|
14
|
+
/* istanbul ignore next */
|
|
15
|
+
try {
|
|
16
|
+
fs.unlinkSync(filename);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
if (e.code !== "ENOENT") {
|
|
19
|
+
throw e;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Implements an event-driven durable Consumer that provides at-least-once delivery semantics or exactly-once processing semantics if only using setState().
|
|
12
26
|
*/
|
|
13
27
|
class Consumer extends stream.Readable {
|
|
14
28
|
|
|
@@ -16,9 +30,10 @@ class Consumer extends stream.Readable {
|
|
|
16
30
|
* @param {Storage} storage The storage to create the consumer for.
|
|
17
31
|
* @param {string} indexName The name of the index to consume.
|
|
18
32
|
* @param {string} identifier The unique name to identify this consumer.
|
|
33
|
+
* @param {object} [initialState] The initial state of the consumer.
|
|
19
34
|
* @param {number} [startFrom] The revision to start from within the index to consume.
|
|
20
35
|
*/
|
|
21
|
-
constructor(storage, indexName, identifier, startFrom = 0) {
|
|
36
|
+
constructor(storage, indexName, identifier, initialState = {}, startFrom = 0) {
|
|
22
37
|
super({ objectMode: true });
|
|
23
38
|
|
|
24
39
|
assert(storage instanceof Storage, 'Must provide a storage for the consumer.');
|
|
@@ -26,7 +41,7 @@ class Consumer extends stream.Readable {
|
|
|
26
41
|
assert(typeof identifier === 'string' && identifier !== '', 'Must specify an identifier name for the consumer.');
|
|
27
42
|
|
|
28
43
|
this.initializeStorage(storage, indexName, identifier);
|
|
29
|
-
this.restoreState(startFrom);
|
|
44
|
+
this.restoreState(initialState, startFrom);
|
|
30
45
|
this.handler = this.handleNewDocument.bind(this);
|
|
31
46
|
this.on('error', () => (this.handleDocument = false));
|
|
32
47
|
}
|
|
@@ -43,10 +58,8 @@ class Consumer extends stream.Readable {
|
|
|
43
58
|
this.indexName = indexName;
|
|
44
59
|
const consumerDirectory = path.join(this.storage.indexDirectory, 'consumers');
|
|
45
60
|
this.fileName = path.join(consumerDirectory, this.storage.storageFile + '.' + indexName + '.' + identifier);
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
} else {
|
|
49
|
-
this.cleanUpFailedWrites(consumerDirectory);
|
|
61
|
+
if (ensureDirectory(consumerDirectory)) {
|
|
62
|
+
this.cleanUpFailedWrites();
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
65
|
|
|
@@ -60,28 +73,34 @@ class Consumer extends stream.Readable {
|
|
|
60
73
|
const files = fs.readdirSync(consumerDirectory);
|
|
61
74
|
for (let file of files) {
|
|
62
75
|
if (file.startsWith(consumerNamePrefix)) {
|
|
63
|
-
|
|
76
|
+
safeUnlink(path.join(consumerDirectory, file));
|
|
64
77
|
}
|
|
65
78
|
}
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
/**
|
|
69
82
|
* @private
|
|
83
|
+
* @param {object} initialState The initial state if no persisted state exists.
|
|
70
84
|
* @param {number} startFrom The revision to start from within the index to consume.
|
|
71
85
|
*/
|
|
72
|
-
restoreState(startFrom) {
|
|
86
|
+
restoreState(initialState, startFrom) {
|
|
73
87
|
/* istanbul ignore if */
|
|
74
88
|
if (!this.fileName) {
|
|
75
89
|
return;
|
|
76
90
|
}
|
|
91
|
+
if (typeof initialState === 'number') {
|
|
92
|
+
startFrom = initialState;
|
|
93
|
+
initialState = {};
|
|
94
|
+
}
|
|
77
95
|
try {
|
|
78
96
|
const consumerData = fs.readFileSync(this.fileName);
|
|
79
97
|
this.position = consumerData.readInt32LE(0);
|
|
80
98
|
this.state = JSON.parse(consumerData.toString('utf8', 4));
|
|
81
99
|
} catch (e) {
|
|
82
100
|
this.position = startFrom;
|
|
83
|
-
this.state =
|
|
101
|
+
this.state = initialState;
|
|
84
102
|
}
|
|
103
|
+
Object.freeze(this.state);
|
|
85
104
|
|
|
86
105
|
this.persisting = null;
|
|
87
106
|
this.consuming = false;
|
|
@@ -91,13 +110,18 @@ class Consumer extends stream.Readable {
|
|
|
91
110
|
* Update the state of this consumer transactionally with the position.
|
|
92
111
|
* May only be called from within the document handling callback.
|
|
93
112
|
*
|
|
94
|
-
* @param {object} newState
|
|
113
|
+
* @param {object|function(object):object} newState
|
|
114
|
+
* @param {boolean} [persist] Set to false if this state update should not be persisted yet
|
|
95
115
|
* @api
|
|
96
116
|
*/
|
|
97
|
-
setState(newState) {
|
|
117
|
+
setState(newState, persist = true) {
|
|
98
118
|
assert(this.handleDocument, 'Called setState outside of document handler!');
|
|
99
119
|
|
|
120
|
+
if (typeof newState === 'function') {
|
|
121
|
+
newState = newState(this.state);
|
|
122
|
+
}
|
|
100
123
|
this.state = Object.freeze(newState);
|
|
124
|
+
this.doPersist = persist;
|
|
101
125
|
}
|
|
102
126
|
|
|
103
127
|
/**
|
|
@@ -113,6 +137,7 @@ class Consumer extends stream.Readable {
|
|
|
113
137
|
return;
|
|
114
138
|
}
|
|
115
139
|
|
|
140
|
+
/* istanbul ignore if */
|
|
116
141
|
if (this.position !== position - 1) {
|
|
117
142
|
return;
|
|
118
143
|
}
|
|
@@ -123,7 +148,9 @@ class Consumer extends stream.Readable {
|
|
|
123
148
|
this.stop();
|
|
124
149
|
}
|
|
125
150
|
this.position = position;
|
|
126
|
-
this.
|
|
151
|
+
if (this.doPersist) {
|
|
152
|
+
this.persist();
|
|
153
|
+
}
|
|
127
154
|
}
|
|
128
155
|
|
|
129
156
|
/**
|
|
@@ -141,7 +168,7 @@ class Consumer extends stream.Readable {
|
|
|
141
168
|
const consumerData = Buffer.allocUnsafe(4 + consumerState.length);
|
|
142
169
|
consumerData.writeInt32LE(this.position, 0);
|
|
143
170
|
consumerData.write(consumerState, 4, consumerState.length, 'utf-8');
|
|
144
|
-
|
|
171
|
+
const tmpFile = this.fileName + '.' + this.position;
|
|
145
172
|
this.persisting = null;
|
|
146
173
|
/* istanbul ignore if */
|
|
147
174
|
if (fs.existsSync(tmpFile)) {
|
|
@@ -151,10 +178,10 @@ class Consumer extends stream.Readable {
|
|
|
151
178
|
fs.writeFileSync(tmpFile, consumerData);
|
|
152
179
|
// If the write fails (half-way), the consumer state file will not be corrupted
|
|
153
180
|
fs.renameSync(tmpFile, this.fileName);
|
|
154
|
-
this.emit('persisted');
|
|
181
|
+
this.emit('persisted', consumerState);
|
|
155
182
|
} catch (e) {
|
|
156
183
|
/* istanbul ignore next */
|
|
157
|
-
|
|
184
|
+
safeUnlink(tmpFile);
|
|
158
185
|
}
|
|
159
186
|
});
|
|
160
187
|
}
|
|
@@ -237,6 +264,29 @@ class Consumer extends stream.Readable {
|
|
|
237
264
|
this.handleDocument = false;
|
|
238
265
|
}
|
|
239
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Reset this projection to restart processing all documents again.
|
|
269
|
+
* 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.
|
|
272
|
+
* @api
|
|
273
|
+
*/
|
|
274
|
+
reset(initialState = {}, startFrom = 0) {
|
|
275
|
+
if (typeof initialState === 'number') {
|
|
276
|
+
startFrom = initialState;
|
|
277
|
+
initialState = {};
|
|
278
|
+
}
|
|
279
|
+
const restart = this.consuming;
|
|
280
|
+
this.stop();
|
|
281
|
+
this.state = Object.freeze(initialState);
|
|
282
|
+
this.position = startFrom;
|
|
283
|
+
this.persist();
|
|
284
|
+
if (restart) {
|
|
285
|
+
this.start();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// noinspection JSUnusedGlobalSymbols
|
|
240
290
|
/**
|
|
241
291
|
* Readable stream implementation.
|
|
242
292
|
* @private
|