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/Watcher.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
3
|
+
const events = require('events');
|
|
4
4
|
const { assert } = require('./util');
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** @type {Map<string, DirectoryWatcher>} */
|
|
7
|
+
const directoryWatchers = new Map();
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* A reference counting singleton nodejs watcher for directories.
|
|
10
11
|
* Emits events 'change' and 'rename' with the file name as argument.
|
|
11
12
|
*/
|
|
12
|
-
class DirectoryWatcher extends EventEmitter {
|
|
13
|
+
class DirectoryWatcher extends events.EventEmitter {
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* @param {string} directory
|
|
@@ -18,14 +19,17 @@ class DirectoryWatcher extends EventEmitter {
|
|
|
18
19
|
*/
|
|
19
20
|
constructor(directory, options = {}) {
|
|
20
21
|
directory = path.normalize(directory);
|
|
21
|
-
assert(fs.existsSync(directory), 'Can not watch a non-existing directory.');
|
|
22
22
|
|
|
23
|
-
if (directoryWatchers
|
|
24
|
-
directoryWatchers
|
|
25
|
-
|
|
23
|
+
if (directoryWatchers.has(directory)) {
|
|
24
|
+
const watcher = directoryWatchers.get(directory);
|
|
25
|
+
watcher.references++;
|
|
26
|
+
return watcher;
|
|
26
27
|
}
|
|
28
|
+
assert(fs.existsSync(directory), `Can not watch a non-existing directory "${directory}".`);
|
|
29
|
+
assert(fs.statSync(directory).isDirectory(), `Can only watch directories, but "${directory}" is none.`);
|
|
27
30
|
super();
|
|
28
|
-
|
|
31
|
+
this.setMaxListeners(1000);
|
|
32
|
+
directoryWatchers.set(directory, this);
|
|
29
33
|
this.directory = directory;
|
|
30
34
|
this.watcher = fs.watch(directory, Object.assign({ persistent: false }, options), this.emit.bind(this));
|
|
31
35
|
this.references = 1;
|
|
@@ -40,7 +44,7 @@ class DirectoryWatcher extends EventEmitter {
|
|
|
40
44
|
if (this.references === 0 && this.watcher) {
|
|
41
45
|
this.watcher.close();
|
|
42
46
|
this.watcher = null;
|
|
43
|
-
directoryWatchers
|
|
47
|
+
directoryWatchers.delete(this.directory);
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
|
|
@@ -52,30 +56,36 @@ class DirectoryWatcher extends EventEmitter {
|
|
|
52
56
|
class Watcher {
|
|
53
57
|
|
|
54
58
|
/**
|
|
55
|
-
* @param {string} fileOrDirectory
|
|
56
|
-
* @param {function(string): boolean} [fileFilter] A filter that will receive a filename and needs to return true if this watcher should be invoked.
|
|
59
|
+
* @param {string|string[]} fileOrDirectory The filename or directory or list of directories to watch
|
|
60
|
+
* @param {function(string): boolean} [fileFilter] A filter that will receive a filename and needs to return true if this watcher should be invoked. Will be ignored if the first argument is a file.
|
|
57
61
|
* @returns {Watcher}
|
|
58
62
|
*/
|
|
59
63
|
constructor(fileOrDirectory, fileFilter = null) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
let directories;
|
|
65
|
+
if (typeof fileOrDirectory === 'string') {
|
|
66
|
+
directories = [fileOrDirectory];
|
|
67
|
+
if (!fs.statSync(fileOrDirectory).isDirectory()) {
|
|
68
|
+
directories = [path.dirname(fileOrDirectory)];
|
|
69
|
+
const filename = path.basename(fileOrDirectory);
|
|
70
|
+
fileFilter = changedFilename => changedFilename === filename;
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
directories = [...new Set(fileOrDirectory.map(path.normalize))];
|
|
64
74
|
}
|
|
65
|
-
this.watcher = new DirectoryWatcher(directory);
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
} else if (fileFilter === null) {
|
|
76
|
+
this.watchers = directories.map(dir => new DirectoryWatcher(dir));
|
|
77
|
+
|
|
78
|
+
if (fileFilter === null) {
|
|
71
79
|
fileFilter = () => true;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
this.fileFilter = fileFilter;
|
|
75
83
|
this.onChange = this.onChange.bind(this);
|
|
76
84
|
this.onRename = this.onRename.bind(this);
|
|
77
|
-
this.
|
|
78
|
-
|
|
85
|
+
this.watchers.forEach(watcher => {
|
|
86
|
+
watcher.on('change', this.onChange);
|
|
87
|
+
watcher.on('rename', this.onRename);
|
|
88
|
+
});
|
|
79
89
|
this.handlers = { change: [], rename: [] };
|
|
80
90
|
}
|
|
81
91
|
|
|
@@ -126,13 +136,12 @@ class Watcher {
|
|
|
126
136
|
* @api
|
|
127
137
|
*/
|
|
128
138
|
close() {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
this.
|
|
135
|
-
this.watcher = null;
|
|
139
|
+
this.watchers.forEach(watcher => {
|
|
140
|
+
watcher.removeListener('change', this.onChange);
|
|
141
|
+
watcher.removeListener('rename', this.onRename);
|
|
142
|
+
watcher.close();
|
|
143
|
+
});
|
|
144
|
+
this.watchers = [];
|
|
136
145
|
this.handlers = { change: [], rename: [] };
|
|
137
146
|
}
|
|
138
147
|
|
package/src/WatchesFile.js
CHANGED
|
@@ -3,9 +3,9 @@ const Watcher = require('./Watcher');
|
|
|
3
3
|
/**
|
|
4
4
|
* A mixin that provides a file watcher for this.fileName which triggers a method `onChange` on the class, that needs to be implemented.
|
|
5
5
|
*
|
|
6
|
-
* @
|
|
7
|
-
* @
|
|
8
|
-
* @
|
|
6
|
+
* @template T
|
|
7
|
+
* @param {T} Base
|
|
8
|
+
* @returns {T} An anonymous class that extends Base
|
|
9
9
|
*/
|
|
10
10
|
const WatchesFile = Base => class extends Base {
|
|
11
11
|
|
|
@@ -34,12 +34,13 @@ const WatchesFile = Base => class extends Base {
|
|
|
34
34
|
* @returns {boolean}
|
|
35
35
|
*/
|
|
36
36
|
open() {
|
|
37
|
-
if (
|
|
38
|
-
|
|
37
|
+
if (super.open()) {
|
|
38
|
+
if (!this.watcher) {
|
|
39
|
+
this.watchFile();
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
this.watchFile();
|
|
42
|
-
return super.open();
|
|
43
|
+
return false;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} secret The secret to use for calculating further HMACs
|
|
5
|
+
* @returns {function(string)} A function that calculates the HMAC for a given string
|
|
6
|
+
*/
|
|
7
|
+
const createHmac = secret => string => {
|
|
8
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
9
|
+
hmac.update(string);
|
|
10
|
+
return hmac.digest('hex');
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object|function(object):boolean} Matcher
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} document The document to check against the matcher.
|
|
19
|
+
* @param {Matcher} matcher An object of properties and their values that need to match in the object or a function that checks if the document matches.
|
|
20
|
+
* @returns {boolean} True if the document matches the matcher or false otherwise.
|
|
21
|
+
*/
|
|
22
|
+
function matches(document, matcher) {
|
|
23
|
+
if (typeof document === 'undefined') return false;
|
|
24
|
+
if (typeof matcher === 'undefined') return true;
|
|
25
|
+
|
|
26
|
+
if (typeof matcher === 'function') return matcher(document);
|
|
27
|
+
|
|
28
|
+
for (let prop of Object.getOwnPropertyNames(matcher)) {
|
|
29
|
+
if (typeof matcher[prop] === 'object') {
|
|
30
|
+
if (!matches(document[prop], matcher[prop])) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
} else if (typeof matcher[prop] !== 'undefined' && document[prop] !== matcher[prop]) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {Matcher} matcher The matcher object or function that should be serialized.
|
|
42
|
+
* @param {function(string)} hmac A function that calculates a HMAC of the given string.
|
|
43
|
+
* @returns {{matcher: string|object, hmac?: string}}
|
|
44
|
+
*/
|
|
45
|
+
function buildMetadataForMatcher(matcher, hmac) {
|
|
46
|
+
if (!matcher) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
if (typeof matcher === 'object') {
|
|
50
|
+
return { matcher };
|
|
51
|
+
}
|
|
52
|
+
const matcherString = matcher.toString();
|
|
53
|
+
return { matcher: matcherString, hmac: hmac(matcherString) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {{matcher: string|object, hmac: string}} matcherMetadata The serialized matcher and its HMAC
|
|
58
|
+
* @param {function(string)} hmac A function that calculates a HMAC of the given string.
|
|
59
|
+
* @returns {Matcher} The matcher object or function.
|
|
60
|
+
*/
|
|
61
|
+
function buildMatcherFromMetadata(matcherMetadata, hmac) {
|
|
62
|
+
let matcher;
|
|
63
|
+
if (typeof matcherMetadata.matcher === 'object') {
|
|
64
|
+
matcher = matcherMetadata.matcher;
|
|
65
|
+
} else {
|
|
66
|
+
if (matcherMetadata.hmac !== hmac(matcherMetadata.matcher)) {
|
|
67
|
+
throw new Error('Invalid HMAC for matcher.');
|
|
68
|
+
}
|
|
69
|
+
matcher = eval('(' + matcherMetadata.matcher + ')').bind({}); // jshint ignore:line
|
|
70
|
+
}
|
|
71
|
+
return matcher;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
createHmac,
|
|
76
|
+
matches,
|
|
77
|
+
buildMetadataForMatcher,
|
|
78
|
+
buildMatcherFromMetadata
|
|
79
|
+
};
|
package/src/util.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const mkdirpSync = require('mkdirp').sync;
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Assert that actual and expected match or throw an Error with the given message appended by information about expected and actual value.
|
|
@@ -27,70 +28,38 @@ function assert(condition, message, ErrorType = Error) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
31
|
+
* Return the amount required to align value to the given alignment.
|
|
32
|
+
* It calculates the difference of the alignment and the modulo of value by alignment.
|
|
33
|
+
* @param {number} value
|
|
34
|
+
* @param {number} alignment
|
|
35
|
+
* @returns {number}
|
|
32
36
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
hmac.update(string);
|
|
36
|
-
return hmac.digest('hex');
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @param {object} document The document to check against the matcher.
|
|
41
|
-
* @param {object|function} matcher An object of properties and their values that need to match in the object or a function that checks if the document matches.
|
|
42
|
-
* @returns {boolean} True if the document matches the matcher or false otherwise.
|
|
43
|
-
*/
|
|
44
|
-
function matches(document, matcher) {
|
|
45
|
-
if (typeof document === 'undefined') return false;
|
|
46
|
-
if (typeof matcher === 'undefined') return true;
|
|
47
|
-
|
|
48
|
-
if (typeof matcher === 'function') return matcher(document);
|
|
49
|
-
|
|
50
|
-
for (let prop of Object.getOwnPropertyNames(matcher)) {
|
|
51
|
-
if (typeof matcher[prop] === 'object') {
|
|
52
|
-
if (!matches(document[prop], matcher[prop])) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
} else if (document[prop] !== matcher[prop]) {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return true;
|
|
37
|
+
function alignTo(value, alignment) {
|
|
38
|
+
return (alignment - (value % alignment)) % alignment;
|
|
60
39
|
}
|
|
61
40
|
|
|
62
41
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* @
|
|
42
|
+
* Method for hashing a string (e.g. a partition name) to a 32-bit unsigned integer.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} str
|
|
45
|
+
* @returns {number}
|
|
66
46
|
*/
|
|
67
|
-
function
|
|
68
|
-
if
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (typeof matcher === 'object') {
|
|
72
|
-
return { matcher };
|
|
47
|
+
function hash(str) {
|
|
48
|
+
/* istanbul ignore if */
|
|
49
|
+
if (str.length === 0) {
|
|
50
|
+
return 0;
|
|
73
51
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
52
|
+
let hash = 5381,
|
|
53
|
+
i = str.length;
|
|
77
54
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
* @param {function(string)} hmac A function that calculates a HMAC of the given string.
|
|
81
|
-
* @returns {object|function} The matcher object or function.
|
|
82
|
-
*/
|
|
83
|
-
function buildMatcherFromMetadata(matcherMetadata, hmac) {
|
|
84
|
-
let matcher;
|
|
85
|
-
if (typeof matcherMetadata.matcher === 'object') {
|
|
86
|
-
matcher = matcherMetadata.matcher;
|
|
87
|
-
} else {
|
|
88
|
-
if (matcherMetadata.hmac !== hmac(matcherMetadata.matcher)) {
|
|
89
|
-
throw new Error('Invalid HMAC for matcher.');
|
|
90
|
-
}
|
|
91
|
-
matcher = eval('(' + matcherMetadata.matcher + ')').bind({}); // jshint ignore:line
|
|
55
|
+
while(i) {
|
|
56
|
+
hash = ((hash << 5) + hash) ^ str.charCodeAt(--i); // jshint ignore:line
|
|
92
57
|
}
|
|
93
|
-
|
|
58
|
+
|
|
59
|
+
/* JavaScript does bitwise operations (like XOR, above) on 32-bit signed
|
|
60
|
+
* integers. Since we want the results to be always positive, convert the
|
|
61
|
+
* signed int to an unsigned by doing an unsigned bitshift. */
|
|
62
|
+
return hash >>> 0; // jshint ignore:line
|
|
94
63
|
}
|
|
95
64
|
|
|
96
65
|
/**
|
|
@@ -152,30 +121,98 @@ function binarySearch(number, length, get) {
|
|
|
152
121
|
/**
|
|
153
122
|
* @param {number} index The 1-based index position to wrap around if < 0 and check against the bounds.
|
|
154
123
|
* @param {number} length The length of the index and upper bound.
|
|
155
|
-
* @returns {number
|
|
124
|
+
* @returns {number} The wrapped index position or -1 if index out of bounds.
|
|
156
125
|
*/
|
|
157
126
|
function wrapAndCheck(index, length) {
|
|
158
127
|
if (typeof index !== 'number') {
|
|
159
|
-
return
|
|
128
|
+
return -1;
|
|
160
129
|
}
|
|
161
130
|
|
|
162
131
|
if (index < 0) {
|
|
163
132
|
index += length + 1;
|
|
164
133
|
}
|
|
165
134
|
if (index < 1 || index > length) {
|
|
166
|
-
return
|
|
135
|
+
return -1;
|
|
167
136
|
}
|
|
168
137
|
return index;
|
|
169
138
|
}
|
|
170
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Ensure that the given directory exists.
|
|
142
|
+
* @param {string} dirName
|
|
143
|
+
* @return {boolean} true if the directory existed already
|
|
144
|
+
*/
|
|
145
|
+
function ensureDirectory(dirName) {
|
|
146
|
+
if (!fs.existsSync(dirName)) {
|
|
147
|
+
try {
|
|
148
|
+
mkdirpSync(dirName);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Perform a k-way merge over multiple streams, invoking a callback for each item in ascending key order.
|
|
158
|
+
* Each stream object is mutated in place by the `advance` function.
|
|
159
|
+
*
|
|
160
|
+
* @param {object[]} streams Array of stream state objects; entries are removed when exhausted.
|
|
161
|
+
* @param {function(object): number} getKey Returns the current sort key for a stream state.
|
|
162
|
+
* @param {function(object): boolean} advance Advances the stream to its next item.
|
|
163
|
+
* Returns true if the stream has more items within range, false if exhausted.
|
|
164
|
+
* @param {function(object): void} visit Called for each stream state in merged order.
|
|
165
|
+
*/
|
|
166
|
+
function kWayMerge(streams, getKey, advance, visit) {
|
|
167
|
+
while (streams.length > 0) {
|
|
168
|
+
let minIdx = 0;
|
|
169
|
+
for (let i = 1; i < streams.length; i++) {
|
|
170
|
+
if (getKey(streams[i]) < getKey(streams[minIdx])) {
|
|
171
|
+
minIdx = i;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
visit(streams[minIdx]);
|
|
175
|
+
if (!advance(streams[minIdx])) {
|
|
176
|
+
streams.splice(minIdx, 1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Scan a directory for files whose names match a regex pattern, calling a callback for each match.
|
|
183
|
+
* The `onEach` callback receives the first capturing group of the match (`match[1]`), or the full
|
|
184
|
+
* match (`match[0]`) when no capturing group is defined in the pattern.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} directory The directory to scan.
|
|
187
|
+
* @param {RegExp} regexPattern The pattern to match file names against.
|
|
188
|
+
* @param {function(string)} onEach Called with the first capturing group (or full match) for each matching file name.
|
|
189
|
+
* @param {function(Error?)} onDone Called when the scan is complete, or with an error if one occurred.
|
|
190
|
+
*/
|
|
191
|
+
function scanForFiles(directory, regexPattern, onEach, onDone) {
|
|
192
|
+
fs.readdir(directory, (err, files) => {
|
|
193
|
+
if (err) {
|
|
194
|
+
return onDone(err);
|
|
195
|
+
}
|
|
196
|
+
let match;
|
|
197
|
+
for (let file of files) {
|
|
198
|
+
if ((match = file.match(regexPattern)) !== null) {
|
|
199
|
+
onEach(match[1] !== undefined ? match[1] : match[0]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
onDone(null);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
171
207
|
module.exports = {
|
|
172
208
|
assert,
|
|
173
209
|
assertEqual,
|
|
210
|
+
hash,
|
|
174
211
|
wrapAndCheck,
|
|
175
212
|
binarySearch,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
213
|
+
buildMetadataHeader,
|
|
214
|
+
alignTo,
|
|
215
|
+
ensureDirectory,
|
|
216
|
+
scanForFiles,
|
|
217
|
+
kWayMerge
|
|
181
218
|
};
|