event-storage 0.9.1 → 1.1.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.
@@ -1,10 +1,11 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const WritablePartition = require('../Partition/WritablePartition');
4
- const WritableIndex = require('../Index/WritableIndex');
5
- const ReadableStorage = require('./ReadableStorage');
6
- const { assert, ensureDirectory } = require('../util');
7
- const { matches, buildMetadataForMatcher, buildMatcherFromMetadata } = require('../metadataUtil');
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import WritablePartition from '../Partition/WritablePartition.js';
4
+ import WritableIndex, { Entry as WritableIndexEntry } from '../Index/WritableIndex.js';
5
+ import ReadableStorage from './ReadableStorage.js';
6
+ import { assert } from '../util.js';
7
+ import { ensureDirectory } from '../fsUtil.js';
8
+ import { matches, buildMetadataForMatcher, buildMatcherFromMetadata } from '../metadataUtil.js';
8
9
 
9
10
  const DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024;
10
11
 
@@ -61,24 +62,30 @@ class WritableStorage extends ReadableStorage {
61
62
  super(storageName, config);
62
63
 
63
64
  this.lockFile = path.resolve(this.dataDirectory, this.storageFile + '.lock');
64
- if (config.lock === LOCK_RECLAIM) {
65
- this.unlock();
66
- }
65
+ this._lockMode = config.lock;
67
66
  this.partitioner = config.partitioner;
68
67
  }
69
68
 
70
69
  /**
71
70
  * @inheritDoc
71
+ * Acquires the write lock synchronously.
72
+ * For LOCK_RECLAIM, removes any orphaned lock before trying to acquire our own; torn-write
73
+ * repair runs after the primary index is open, before `'opened'` is emitted.
74
+ *
72
75
  * @returns {boolean}
73
76
  * @throws {StorageLockedError} If this storage is locked by another process.
74
77
  */
75
- open() {
78
+ open(callback) {
79
+ const needsRepair = this._lockMode === LOCK_RECLAIM && this.unlock();
80
+
76
81
  if (!this.lock()) {
77
82
  return true;
78
83
  }
79
- const result = super.open();
80
- this.emit('ready');
81
- return result;
84
+
85
+ const onOpen = needsRepair
86
+ ? () => { this.checkTornWrites(); callback?.(); }
87
+ : callback;
88
+ return super.open(onOpen);
82
89
  }
83
90
 
84
91
  /**
@@ -170,6 +177,8 @@ class WritableStorage extends ReadableStorage {
170
177
  }
171
178
 
172
179
  this.forEachPartition(partition => partition.close());
180
+ // Partitions were closed directly (bypassing the pool), so reset the open-handle tracking.
181
+ this.partitions.clearOpenHandles();
173
182
  }
174
183
 
175
184
  /**
@@ -196,7 +205,7 @@ class WritableStorage extends ReadableStorage {
196
205
  // Scan partitions in sequence-number order and rebuild index entries.
197
206
  // iterateDocumentsNoIndex opens any closed partitions automatically.
198
207
  for (const { document, partition, position, size } of this.iterateDocumentsNoIndex(fromSequenceNumber, Number.MAX_SAFE_INTEGER)) {
199
- const newEntry = new WritableIndex.Entry(this.index.length + 1, position, size, partition);
208
+ const newEntry = new WritableIndexEntry(this.index.length + 1, position, size, partition);
200
209
  this.index.add(newEntry);
201
210
 
202
211
  this.forEachWritableSecondaryIndex((secIndex) => {
@@ -234,19 +243,21 @@ class WritableStorage extends ReadableStorage {
234
243
  * Unlock this storage, no matter if it was previously locked by this writer.
235
244
  * Only use this if you are sure there is no other process still having a writer open.
236
245
  * Current implementation just deletes a lock file that is named like the storage.
246
+ * @returns {boolean} True if an orphaned lock from another process was removed.
237
247
  */
238
248
  unlock() {
239
- if (fs.existsSync(this.lockFile)) {
240
- if (!this.locked) {
241
- this.checkTornWrites();
242
- }
249
+ const lockExists = fs.existsSync(this.lockFile);
250
+ const orphaned = lockExists && !this.locked;
251
+ if (lockExists) {
243
252
  fs.rmdirSync(this.lockFile);
244
253
  }
245
254
  this.locked = false;
255
+ return orphaned;
246
256
  }
247
257
 
248
258
  /**
249
259
  * @inheritDoc
260
+ * Unlocks the storage, then delegates to the parent close().
250
261
  */
251
262
  close() {
252
263
  if (this.locked) {
@@ -276,7 +287,7 @@ class WritableStorage extends ReadableStorage {
276
287
  throw new Error('Corrupted index, needs to be rebuilt!');
277
288
  }*/
278
289
 
279
- const entry = new WritableIndex.Entry(this.index.length + 1, position, size, partitionId);
290
+ const entry = new WritableIndexEntry(this.index.length + 1, position, size, partitionId);
280
291
  this.index.add(entry, (indexPosition) => {
281
292
  this.emit('wrote', document, entry, indexPosition);
282
293
  /* istanbul ignore if */
@@ -305,6 +316,8 @@ class WritableStorage extends ReadableStorage {
305
316
  * If a partition with the given name does not exist, a new one will be created.
306
317
  * If a partition with the given id does not exist, an error is thrown.
307
318
  *
319
+ * Partition opening and LRU tracking are delegated to `super.getPartition()`.
320
+ *
308
321
  * @protected
309
322
  * @param {string|number} partitionIdentifier The partition name or the partition Id
310
323
  * @returns {ReadablePartition}
@@ -315,15 +328,16 @@ class WritableStorage extends ReadableStorage {
315
328
  const partitionShortName = partitionIdentifier;
316
329
  const partitionName = this.storageFile + (partitionIdentifier.length ? '.' + partitionIdentifier : '');
317
330
  partitionIdentifier = WritablePartition.idFor(partitionName);
318
- if (!this.partitions[partitionIdentifier]) {
331
+ if (!this.partitions.has(partitionIdentifier)) {
319
332
  const partitionConfig = typeof this.partitionConfig.metadata === 'function'
320
333
  ? { ...this.partitionConfig, metadata: this.partitionConfig.metadata(partitionShortName) }
321
334
  : this.partitionConfig;
322
- this.partitions[partitionIdentifier] = this.createPartition(partitionName, partitionConfig);
335
+ if (partitionName.includes('/')) {
336
+ ensureDirectory(path.join(this.dataDirectory, path.dirname(partitionName)));
337
+ }
338
+ this.partitions.add(partitionIdentifier, this.createPartition(partitionName, partitionConfig));
323
339
  this.emit('partition-created', partitionIdentifier);
324
340
  }
325
- this.partitions[partitionIdentifier].open();
326
- return this.partitions[partitionIdentifier];
327
341
  }
328
342
  return super.getPartition(partitionIdentifier);
329
343
  }
@@ -366,10 +380,11 @@ class WritableStorage extends ReadableStorage {
366
380
  * @api
367
381
  * @param {string} name The index name.
368
382
  * @param {Matcher} [matcher] An object that describes the document properties that need to match to add it this index or a function that receives a document and returns true if the document should be indexed.
383
+ * @param {boolean} [reindex=true] Whether to scan existing documents and populate the new index. Set to false when it is known that no existing documents can match the matcher.
369
384
  * @returns {ReadableIndex} The index containing all documents that match the query.
370
385
  * @throws {Error} if the index doesn't exist yet and no matcher was specified.
371
386
  */
372
- ensureIndex(name, matcher) {
387
+ ensureIndex(name, matcher, reindex = true) {
373
388
  if (name === '_all') {
374
389
  return this.index;
375
390
  }
@@ -386,18 +401,21 @@ class WritableStorage extends ReadableStorage {
386
401
 
387
402
  const metadata = buildMetadataForMatcher(matcher, this.hmac);
388
403
  const { index } = this.createIndex(indexName, Object.assign({}, this.indexOptions, { metadata }));
389
- try {
390
- this.forEachDocument((document, indexEntry) => {
391
- if (matches(document, matcher)) {
392
- index.add(indexEntry);
393
- }
394
- });
395
- } catch (e) {
396
- index.destroy();
397
- throw e;
404
+ if (reindex) {
405
+ try {
406
+ this.forEachDocument((document, indexEntry) => {
407
+ if (matches(document, matcher)) {
408
+ index.add(indexEntry);
409
+ }
410
+ });
411
+ } catch (e) {
412
+ index.destroy();
413
+ throw e;
414
+ }
398
415
  }
399
416
 
400
417
  this.secondaryIndexes[name] = { index, matcher };
418
+ this.indexMatcher.add(name, matcher);
401
419
  this.emit('index-created', name);
402
420
  return index;
403
421
  }
@@ -423,7 +441,7 @@ class WritableStorage extends ReadableStorage {
423
441
  */
424
442
  forEachDistinctPartitionOf(entries, iterationHandler) {
425
443
  const partitions = [];
426
- const numPartitions = Object.keys(this.partitions).length;
444
+ const numPartitions = this.partitions.count;
427
445
  for (let entry of entries) {
428
446
  if (partitions.indexOf(entry.partition) >= 0) {
429
447
  continue;
@@ -540,8 +558,5 @@ class WritableStorage extends ReadableStorage {
540
558
 
541
559
  }
542
560
 
543
- module.exports = WritableStorage;
544
- module.exports.StorageLockedError = StorageLockedError;
545
- module.exports.CorruptFileError = ReadableStorage.CorruptFileError;
546
- module.exports.LOCK_THROW = LOCK_THROW;
547
- module.exports.LOCK_RECLAIM = LOCK_RECLAIM;
561
+ export default WritableStorage;
562
+ export { StorageLockedError, LOCK_THROW, LOCK_RECLAIM };
package/src/Storage.js CHANGED
@@ -1,5 +1,10 @@
1
- const WritableStorage = require('./Storage/WritableStorage');
2
- const ReadOnlyStorage = require('./Storage/ReadOnlyStorage');
1
+ import WritableStorage, { StorageLockedError, LOCK_THROW, LOCK_RECLAIM } from './Storage/WritableStorage.js';
2
+ import ReadOnlyStorage from './Storage/ReadOnlyStorage.js';
3
3
 
4
- module.exports = WritableStorage;
5
- module.exports.ReadOnly = ReadOnlyStorage;
4
+ WritableStorage.ReadOnly = ReadOnlyStorage;
5
+ WritableStorage.StorageLockedError = StorageLockedError;
6
+ WritableStorage.LOCK_THROW = LOCK_THROW;
7
+ WritableStorage.LOCK_RECLAIM = LOCK_RECLAIM;
8
+
9
+ export default WritableStorage;
10
+ export { ReadOnlyStorage as ReadOnly, StorageLockedError, LOCK_THROW, LOCK_RECLAIM };
package/src/Watcher.js CHANGED
@@ -1,7 +1,7 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const events = require('events');
4
- const { assert } = require('./util');
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import events from 'events';
4
+ import { assert } from './util.js';
5
5
 
6
6
  /** @type {Map<string, DirectoryWatcher>} */
7
7
  const directoryWatchers = new Map();
@@ -147,4 +147,4 @@ class Watcher {
147
147
 
148
148
  }
149
149
 
150
- module.exports = Watcher;
150
+ export default Watcher;
@@ -1,4 +1,4 @@
1
- const Watcher = require('./Watcher');
1
+ import Watcher from './Watcher.js';
2
2
 
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.
@@ -53,4 +53,4 @@ const WatchesFile = Base => class extends Base {
53
53
 
54
54
  };
55
55
 
56
- module.exports = WatchesFile;
56
+ export default WatchesFile;
package/src/fsUtil.js ADDED
@@ -0,0 +1,123 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { mkdirpSync } from 'mkdirp';
4
+
5
+ /**
6
+ * Ensure that the given directory exists.
7
+ * @param {string} dirName
8
+ * @return {boolean} true if the directory existed already
9
+ */
10
+ function ensureDirectory(dirName) {
11
+ if (!fs.existsSync(dirName)) {
12
+ try {
13
+ mkdirpSync(dirName);
14
+ } catch (e) {
15
+ }
16
+ return false;
17
+ }
18
+ return true;
19
+ }
20
+
21
+ /**
22
+ * Invoke `onEach` if `relativePath` matches `regexPattern`, passing the first capture group or the full match.
23
+ *
24
+ * @param {string} relativePath
25
+ * @param {RegExp} regexPattern
26
+ * @param {function(string)} onEach
27
+ */
28
+ function visitMatchingPath(relativePath, regexPattern, onEach) {
29
+ const match = relativePath.match(regexPattern);
30
+ if (match !== null) {
31
+ onEach(match[1] !== undefined ? match[1] : match[0]);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Classify `entries` into matching files (visited via `onEach`) and subdirectory names (returned).
37
+ *
38
+ * @param {fs.Dirent[]} entries
39
+ * @param {string} relativePrefix
40
+ * @param {RegExp} regexPattern
41
+ * @param {function(string)} onEach
42
+ * @returns {string[]} names of subdirectory entries
43
+ */
44
+ function classifyEntries(entries, relativePrefix, regexPattern, onEach) {
45
+ const subdirs = [];
46
+ for (let entry of entries) {
47
+ if (entry.isDirectory()) {
48
+ subdirs.push(entry.name);
49
+ } else {
50
+ visitMatchingPath(relativePrefix + entry.name, regexPattern, onEach);
51
+ }
52
+ }
53
+ return subdirs;
54
+ }
55
+
56
+ /**
57
+ * Sequentially scan each name in `subdirs`, calling `done` when all are complete or on first error.
58
+ *
59
+ * @param {string[]} subdirs
60
+ * @param {string} dir
61
+ * @param {string} relativePrefix
62
+ * @param {RegExp} regexPattern
63
+ * @param {function(string)} onEach
64
+ * @param {function(Error?)} done
65
+ */
66
+ function scanSubdirs(subdirs, dir, relativePrefix, regexPattern, onEach, done) {
67
+ let i = 0;
68
+ function next() {
69
+ if (i >= subdirs.length) return done(null);
70
+ const name = subdirs[i++];
71
+ scanDir(path.join(dir, name), relativePrefix + name + '/', false, regexPattern, onEach, (err) => {
72
+ if (err) return done(err);
73
+ next();
74
+ });
75
+ }
76
+ next();
77
+ }
78
+
79
+ /**
80
+ * Asynchronously scan one directory level, then recurse into subdirectories sequentially.
81
+ *
82
+ * @param {string} dir
83
+ * @param {string} relativePrefix
84
+ * @param {boolean} isRoot
85
+ * @param {RegExp} regexPattern
86
+ * @param {function(string)} onEach
87
+ * @param {function(Error?)} done
88
+ */
89
+ function scanDir(dir, relativePrefix, isRoot, regexPattern, onEach, done) {
90
+ fs.readdir(dir, { withFileTypes: true }, (err, entries) => {
91
+ if (err) {
92
+ /* istanbul ignore next */
93
+ if (!isRoot && err.code === 'ENOENT') return done(null);
94
+ return done(err);
95
+ }
96
+ const subdirs = classifyEntries(entries, relativePrefix, regexPattern, onEach);
97
+ scanSubdirs(subdirs, dir, relativePrefix, regexPattern, onEach, done);
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Scan a directory (and its subdirectories) for files whose relative paths match a regex pattern,
103
+ * calling a callback for each match.
104
+ *
105
+ * The regex is matched against the **relative path from `directory`** (e.g. `eventstore.stream-x/foo.index`),
106
+ * so patterns that capture a path prefix work transparently for both flat and nested layouts.
107
+ *
108
+ * The `onEach` callback receives the first capturing group of the match (`match[1]`), or the full
109
+ * match (`match[0]`) when no capturing group is defined in the pattern.
110
+ *
111
+ * @param {string} directory The root directory to scan.
112
+ * @param {RegExp} regexPattern The pattern to match relative file paths against.
113
+ * @param {function(string)} onEach Called with the first capturing group (or full match) for each matching path.
114
+ * @param {function(Error?)} onDone Called when the scan is complete, or with an error if one occurred.
115
+ */
116
+ function scanForFiles(directory, regexPattern, onEach, onDone) {
117
+ scanDir(directory, '', true, regexPattern, onEach, onDone);
118
+ }
119
+
120
+ export {
121
+ ensureDirectory,
122
+ scanForFiles,
123
+ };
@@ -1,4 +1,27 @@
1
- const crypto = require('crypto');
1
+ import crypto from 'crypto';
2
+ import { assertEqual } from './util.js';
3
+
4
+ /**
5
+ * Build a buffer containing the file magic header and a JSON stringified metadata block, padded to be a multiple of 16 bytes long.
6
+ *
7
+ * @param {string} magic
8
+ * @param {object} metadata
9
+ * @returns {Buffer} A buffer containing the header data
10
+ */
11
+ function buildMetadataHeader(magic, metadata) {
12
+ assertEqual(magic.length, 8, 'The header magic bytes length is wrong.');
13
+ let metadataString = JSON.stringify(metadata);
14
+ let metadataSize = Buffer.byteLength(metadataString, 'utf8');
15
+ // 8 byte MAGIC, 4 byte metadata size, 1 byte line break
16
+ const pad = (16 - ((8 + 4 + metadataSize + 1) % 16)) % 16;
17
+ metadataString += ' '.repeat(pad) + "\n";
18
+ metadataSize += pad + 1;
19
+ const metadataBuffer = Buffer.allocUnsafe(8 + 4 + metadataSize);
20
+ metadataBuffer.write(magic, 0, 8, 'utf8');
21
+ metadataBuffer.writeUInt32BE(metadataSize, 8);
22
+ metadataBuffer.write(metadataString, 8 + 4, metadataSize, 'utf8');
23
+ return metadataBuffer;
24
+ }
2
25
 
3
26
  /**
4
27
  * @param {string} secret The secret to use for calculating further HMACs
@@ -26,7 +49,11 @@ function matches(document, matcher) {
26
49
  if (typeof matcher === 'function') return matcher(document);
27
50
 
28
51
  for (let prop of Object.getOwnPropertyNames(matcher)) {
29
- if (typeof matcher[prop] === 'object') {
52
+ if (Array.isArray(matcher[prop])) {
53
+ if (!matcher[prop].includes(document[prop])) {
54
+ return false;
55
+ }
56
+ } else if (typeof matcher[prop] === 'object') {
30
57
  if (!matches(document[prop], matcher[prop])) {
31
58
  return false;
32
59
  }
@@ -71,9 +98,29 @@ function buildMatcherFromMetadata(matcherMetadata, hmac) {
71
98
  return matcher;
72
99
  }
73
100
 
74
- module.exports = {
101
+ /**
102
+ * Builds a factory function that, given a type string, returns an object matcher for
103
+ * documents whose payload contains that type at the given dot-notation path.
104
+ *
105
+ * @param {string} payloadPath Dot-notation path relative to the event payload (e.g. `'type'`, `'meta.kind'`).
106
+ * @returns {function(string): object} A function `(typeValue) => objectMatcher`.
107
+ */
108
+ function buildTypeMatcherFn(payloadPath) {
109
+ const parts = payloadPath.split('.');
110
+ return function(typeValue) {
111
+ let obj = typeValue;
112
+ for (let i = parts.length - 1; i >= 0; i--) {
113
+ obj = { [parts[i]]: obj };
114
+ }
115
+ return { payload: obj };
116
+ };
117
+ }
118
+
119
+ export {
75
120
  createHmac,
76
121
  matches,
122
+ buildMetadataHeader,
77
123
  buildMetadataForMatcher,
78
- buildMatcherFromMetadata
124
+ buildMatcherFromMetadata,
125
+ buildTypeMatcherFn
79
126
  };
package/src/util.js CHANGED
@@ -1,6 +1,3 @@
1
- const fs = require('fs');
2
- const mkdirpSync = require('mkdirp').sync;
3
-
4
1
  /**
5
2
  * Assert that actual and expected match or throw an Error with the given message appended by information about expected and actual value.
6
3
  *
@@ -62,28 +59,6 @@ function hash(str) {
62
59
  return hash >>> 0; // jshint ignore:line
63
60
  }
64
61
 
65
- /**
66
- * Build a buffer containing the file magic header and a JSON stringified metadata block, padded to be a multiple of 16 bytes long.
67
- *
68
- * @param {string} magic
69
- * @param {object} metadata
70
- * @returns {Buffer} A buffer containing the header data
71
- */
72
- function buildMetadataHeader(magic, metadata) {
73
- assertEqual(magic.length, 8, 'The header magic bytes length is wrong.');
74
- let metadataString = JSON.stringify(metadata);
75
- let metadataSize = Buffer.byteLength(metadataString, 'utf8');
76
- // 8 byte MAGIC, 4 byte metadata size, 1 byte line break
77
- const pad = (16 - ((8 + 4 + metadataSize + 1) % 16)) % 16;
78
- metadataString += ' '.repeat(pad) + "\n";
79
- metadataSize += pad + 1;
80
- const metadataBuffer = Buffer.allocUnsafe(8 + 4 + metadataSize);
81
- metadataBuffer.write(magic, 0, 8, 'utf8');
82
- metadataBuffer.writeUInt32BE(metadataSize, 8);
83
- metadataBuffer.write(metadataString, 8 + 4, metadataSize, 'utf8');
84
- return metadataBuffer;
85
- }
86
-
87
62
  /**
88
63
  * Do a binary search for number in the range 1-length with values retrieved via a provided getter.
89
64
  *
@@ -137,22 +112,6 @@ function wrapAndCheck(index, length) {
137
112
  return index;
138
113
  }
139
114
 
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
115
  /**
157
116
  * Perform a k-way merge over multiple streams, invoking a callback for each item in ascending key order.
158
117
  * Each stream object is mutated in place by the `advance` function.
@@ -179,40 +138,30 @@ function kWayMerge(streams, getKey, advance, visit) {
179
138
  }
180
139
 
181
140
  /**
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.
141
+ * Read a scalar value at a dot-notation path from an object.
142
+ * Returns `undefined` if any path segment is absent or an intermediate value is not an object.
185
143
  *
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.
144
+ * @param {object} obj
145
+ * @param {string} dotPath Dot-separated property path, e.g. `'payload.type'`.
146
+ * @returns {*}
190
147
  */
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
- });
148
+ function getPropertyAtPath(obj, dotPath) {
149
+ let current = obj;
150
+ const parts = dotPath.split('.');
151
+ for (const part of parts) {
152
+ if (current == null || typeof current !== 'object') return undefined;
153
+ current = current[part];
154
+ }
155
+ return current;
204
156
  }
205
157
 
206
-
207
- module.exports = {
158
+ export {
208
159
  assert,
209
160
  assertEqual,
210
161
  hash,
211
162
  wrapAndCheck,
212
163
  binarySearch,
213
- buildMetadataHeader,
214
164
  alignTo,
215
- ensureDirectory,
216
- scanForFiles,
217
- kWayMerge
165
+ kWayMerge,
166
+ getPropertyAtPath
218
167
  };