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/src/Watcher.js CHANGED
@@ -1,15 +1,16 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const EventEmitter = require('events');
3
+ const events = require('events');
4
4
  const { assert } = require('./util');
5
5
 
6
- const directoryWatchers = {};
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[directory]) {
24
- directoryWatchers[directory].references++;
25
- return directoryWatchers[directory];
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
- directoryWatchers[directory] = this;
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[this.directory] = null;
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
- const isDirectory = fs.statSync(fileOrDirectory).isDirectory();
61
- let directory = fileOrDirectory;
62
- if (!isDirectory) {
63
- directory = path.dirname(fileOrDirectory);
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
- if (!isDirectory) {
68
- const filename = path.basename(fileOrDirectory);
69
- fileFilter = changedFilename => changedFilename === filename;
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.watcher.on('change', this.onChange);
78
- this.watcher.on('rename', this.onRename);
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
- if (!(this.watcher instanceof EventEmitter)) {
130
- return;
131
- }
132
- this.watcher.removeListener('change', this.onChange);
133
- this.watcher.removeListener('rename', this.onRename);
134
- this.watcher.close();
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
 
@@ -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
- * @param {constructor} Base
7
- * @returns {{new(): {watchFile(): void, close(): void, open(): boolean, stopWatching(): void}, prototype: {watchFile(): void, close(): void, open(): boolean, stopWatching(): void}}}
8
- * @constructor
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 (this.fd) {
38
- return false;
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 crypto = require('crypto');
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
- * @param {string} secret The secret to use for calculating further HMACs
31
- * @returns {function(string)} A function that calculates the HMAC for a given string
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
- const createHmac = secret => string => {
34
- const hmac = crypto.createHmac('sha256', secret);
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
- * @param {object|function} matcher The matcher object or function that should be serialized.
64
- * @param {function(string)} hmac A function that calculates a HMAC of the given string.
65
- * @returns {{matcher: string|object, hmac?: string}}
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 buildMetadataForMatcher(matcher, hmac) {
68
- if (!matcher) {
69
- return undefined;
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
- const matcherString = matcher.toString();
75
- return { matcher: matcherString, hmac: hmac(matcherString) };
76
- }
52
+ let hash = 5381,
53
+ i = str.length;
77
54
 
78
- /**
79
- * @param {{matcher: string|object, hmac: string}} matcherMetadata The serialized matcher and it's HMAC
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
- return matcher;
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|boolean} The wrapped index or false if index out of bounds.
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 false;
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 false;
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
- createHmac,
177
- matches,
178
- buildMetadataForMatcher,
179
- buildMatcherFromMetadata,
180
- buildMetadataHeader
213
+ buildMetadataHeader,
214
+ alignTo,
215
+ ensureDirectory,
216
+ scanForFiles,
217
+ kWayMerge
181
218
  };