From 5596129a74d8600d69b8a11343e17e2e0711c61b Mon Sep 17 00:00:00 2001 From: Alexander Berl Date: Sun, 7 Feb 2021 17:05:20 +0100 Subject: [PATCH 1/9] Add indexers that allow to create ad-hoc indexes --- src/Storage/WritableStorage.js | 56 ++++++++++++++++++++++++++++++---- test/Storage.spec.js | 21 +++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/Storage/WritableStorage.js b/src/Storage/WritableStorage.js index 57c982e..579ee32 100644 --- a/src/Storage/WritableStorage.js +++ b/src/Storage/WritableStorage.js @@ -57,6 +57,7 @@ class WritableStorage extends ReadableStorage { } super(storageName, config); this.partitioner = config.partitioner; + this.indexers = []; } /** @@ -188,6 +189,7 @@ class WritableStorage extends ReadableStorage { assert(position !== false, 'Error writing document.'); const indexEntry = this.addIndex(partition.id, position, dataSize, document); + this.runIndexers(document); this.forEachSecondaryIndex((index, name) => { if (!index.isOpen()) { index.open(); @@ -199,6 +201,16 @@ class WritableStorage extends ReadableStorage { return this.index.length; } + /** + * Add an indexer, which will be invoked for every document and ensures an index exists with the returned name and matcher. + * @param {function(document): {name:string, matcher:object|function}|null} indexer The indexer function, which returns an object containing the index name and matcher. + */ + addIndexer(indexer) { + if (typeof indexer === 'function') { + this.indexers.push(indexer); + } + } + /** * Ensure that an index with the given name and document matcher exists. * Will create the index if it doesn't exist, otherwise return the existing index. @@ -206,10 +218,11 @@ class WritableStorage extends ReadableStorage { * @api * @param {string} name The index name. * @param {object|function} [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. - * @returns {ReadableIndex} The index containing all documents that match the query. + * @param {boolean} [updateIndex] If set to false the index will not be matched against all existing documents. + * @returns {ReadableIndex|WritableIndex} The index containing all documents that match the query. * @throws {Error} if the index doesn't exist yet and no matcher was specified. */ - ensureIndex(name, matcher) { + ensureIndex(name, matcher, updateIndex = true) { if (name in this.secondaryIndexes) { return this.secondaryIndexes[name].index; } @@ -223,6 +236,24 @@ class WritableStorage extends ReadableStorage { const metadata = buildMetadataForMatcher(matcher, this.hmac); const { index } = this.createIndex(indexName, Object.assign({}, this.indexOptions, { metadata })); + if (updateIndex) { + this.updateIndex(index, matcher); + } + + this.secondaryIndexes[name] = { index, matcher }; + this.emit('index-created', name); + return index; + } + + /** + * Run the given matcher through all existing documents and add them to the index on match. + * If an error occurs during indexing, the index will be destroyed. + * + * @private + * @param {WritableIndex} index + * @param {object|function} matcher + */ + updateIndex(index, matcher) { try { this.forEachDocument((document, indexEntry) => { if (matches(document, matcher)) { @@ -233,10 +264,6 @@ class WritableStorage extends ReadableStorage { index.destroy(); throw e; } - - this.secondaryIndexes[name] = { index, matcher }; - this.emit('index-created', name); - return index; } /** @@ -273,6 +300,23 @@ class WritableStorage extends ReadableStorage { } } + /** + * Run all indexers against the given document and ensure according indexes. + * Note: This will not cause newly created indexes to index all existing documents. + * + * @private + * @param {object} document + */ + runIndexers(document) { + for (let indexer of this.indexers) { + const index = indexer(document); + if (!index || !index.name) { + continue; + } + this.ensureIndex(index.name, index.matcher, false); + } + } + /** * Truncate all partitions after the given (global) sequence number. * diff --git a/test/Storage.spec.js b/test/Storage.spec.js index d449599..0845ad4 100644 --- a/test/Storage.spec.js +++ b/test/Storage.spec.js @@ -121,6 +121,27 @@ describe('Storage', function() { expect(index.isOpen()).to.be(true); }); + it('invokes indexers and correctly indexes documents', function() { + const sampleIndexer = (document) => { + const name = 'type-' + document.type; + const matcher = { type: document.type }; + return {name, matcher}; + }; + storage = createStorage(); + storage.addIndexer(sampleIndexer); + + expect(() => storage.openIndex('type-bar')).to.throwError(); + expect(() => storage.openIndex('type-foo')).to.throwError(); + for (let i = 1; i <= 10; i++) { + storage.write({ type: (i % 3) ? 'bar' : 'foo', id: i }); + } + const barIndex = storage.openIndex('type-bar'); + const fooIndex = storage.openIndex('type-foo'); + + expect(barIndex.length).to.be(7); + expect(fooIndex.length).to.be(3); + }); + }); describe('length', function() { From e0f0cda6c148a9344ac17096d603c8679a925e5d Mon Sep 17 00:00:00 2001 From: Alexander Berl Date: Sun, 7 Feb 2021 22:03:23 +0100 Subject: [PATCH 2/9] Increase test coverage --- src/Storage/WritableStorage.js | 3 ++- test/Storage.spec.js | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Storage/WritableStorage.js b/src/Storage/WritableStorage.js index 579ee32..4d460a4 100644 --- a/src/Storage/WritableStorage.js +++ b/src/Storage/WritableStorage.js @@ -203,7 +203,8 @@ class WritableStorage extends ReadableStorage { /** * Add an indexer, which will be invoked for every document and ensures an index exists with the returned name and matcher. - * @param {function(document): {name:string, matcher:object|function}|null} indexer The indexer function, which returns an object containing the index name and matcher. + * @typedef {{name:string|null, matcher:object|function}|null} IndexDefinition + * @param {function(document): IndexDefinition} indexer The indexer function, which returns an object containing the index name and matcher or null if the document should not be indexed. Alternatively the index name may be null. */ addIndexer(indexer) { if (typeof indexer === 'function') { diff --git a/test/Storage.spec.js b/test/Storage.spec.js index 0845ad4..e55231f 100644 --- a/test/Storage.spec.js +++ b/test/Storage.spec.js @@ -129,12 +129,15 @@ describe('Storage', function() { }; storage = createStorage(); storage.addIndexer(sampleIndexer); + storage.addIndexer((document) => null); + storage.addIndexer((document) => ({ name: null, matcher: null })); expect(() => storage.openIndex('type-bar')).to.throwError(); expect(() => storage.openIndex('type-foo')).to.throwError(); for (let i = 1; i <= 10; i++) { storage.write({ type: (i % 3) ? 'bar' : 'foo', id: i }); } + expect(Object.keys(storage.secondaryIndexes)).to.eql(['type-bar', 'type-foo']); const barIndex = storage.openIndex('type-bar'); const fooIndex = storage.openIndex('type-foo'); From 48f23cf9fd57d484dd9ab3b453420a2ccacca9f0 Mon Sep 17 00:00:00 2001 From: Alexander Berl Date: Sun, 7 Feb 2021 22:08:47 +0100 Subject: [PATCH 3/9] Ignore safety check for coverage --- src/Storage/WritableStorage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Storage/WritableStorage.js b/src/Storage/WritableStorage.js index 4d460a4..adc5124 100644 --- a/src/Storage/WritableStorage.js +++ b/src/Storage/WritableStorage.js @@ -207,6 +207,7 @@ class WritableStorage extends ReadableStorage { * @param {function(document): IndexDefinition} indexer The indexer function, which returns an object containing the index name and matcher or null if the document should not be indexed. Alternatively the index name may be null. */ addIndexer(indexer) { + /* istanbul ignore else */ if (typeof indexer === 'function') { this.indexers.push(indexer); } From 8b7835dcd88f4422b13c6a916a1769cb346795ee Mon Sep 17 00:00:00 2001 From: Alexander Berl Date: Tue, 9 Feb 2021 22:58:36 +0100 Subject: [PATCH 4/9] Correctly handle new streams in read-only --- src/EventStore.js | 12 ++++++++++++ test/EventStore.spec.js | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/EventStore.js b/src/EventStore.js index d66dbf2..d60d7e7 100644 --- a/src/EventStore.js +++ b/src/EventStore.js @@ -119,6 +119,18 @@ class EventStore extends events.EventEmitter { } callback(); }); + this.storage.on('index-created', name => { + if (!name.startsWith('stream-')) { + return; + } + const streamName = name.substr(7, name.length - 7); + if (streamName in this.streams) { + return; + } + const index = this.storage.openIndex('stream-'+streamName); + this.streams[streamName] = { index }; + this.emit('stream-available', streamName); + }); } /** diff --git a/test/EventStore.spec.js b/test/EventStore.spec.js index 41a546b..5335aa8 100644 --- a/test/EventStore.spec.js +++ b/test/EventStore.spec.js @@ -348,6 +348,29 @@ describe('EventStore', function() { } }); + it('can open streams created in writer', function(done) { + eventstore = new EventStore({ + storageDirectory + }); + + const readstore = new EventStore({ + storageDirectory, + readOnly: true + }); + + expect(readstore.getStreamVersion('foo')).to.be(-1); + + readstore.on('stream-available', (streamName) => { + if (streamName === 'foo') { + expect(readstore.getStreamVersion('foo')).to.be(0); + readstore.close(); + done(); + } + }); + + eventstore.createEventStream('foo', { type: 'foo' }); + }); + it('needs to be tested further.'); }); From ac3ae5e983dc58b0f8301a3a2235d9d8af211748 Mon Sep 17 00:00:00 2001 From: Alexander Berl Date: Tue, 9 Feb 2021 23:09:39 +0100 Subject: [PATCH 5/9] Refactor scanStreams --- src/EventStore.js | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/EventStore.js b/src/EventStore.js index d60d7e7..06fc69f 100644 --- a/src/EventStore.js +++ b/src/EventStore.js @@ -109,28 +109,31 @@ class EventStore extends events.EventEmitter { } let matches; for (let file of files) { - if ((matches = file.match(/(stream-(.*))\.index$/)) !== null) { - const streamName = matches[2]; - const index = this.storage.openIndex(matches[1]); - // deepcode ignore PrototypePollution: streams is a Map - this.streams[streamName] = { index }; - this.emit('stream-available', streamName); + if ((matches = file.match(/(stream-.*)\.index$/)) !== null) { + this.registerStream(matches[1]); } } callback(); }); - this.storage.on('index-created', name => { - if (!name.startsWith('stream-')) { - return; - } - const streamName = name.substr(7, name.length - 7); - if (streamName in this.streams) { - return; - } - const index = this.storage.openIndex('stream-'+streamName); - this.streams[streamName] = { index }; - this.emit('stream-available', streamName); - }); + this.storage.on('index-created', this.registerStream.bind(this)); + } + + /** + * @private + * @param {string} name + */ + registerStream(name) { + if (!name.startsWith('stream-')) { + return; + } + const streamName = name.substr(7, name.length - 7); + if (streamName in this.streams) { + return; + } + const index = this.storage.openIndex('stream-'+streamName); + // deepcode ignore PrototypePollution: streams is a Map + this.streams[streamName] = { index }; + this.emit('stream-available', streamName); } /** From 5a5df4a0398d2e92f8b74a530a96d8fa87c274af Mon Sep 17 00:00:00 2001 From: Alexander Berl Date: Wed, 10 Feb 2021 00:11:33 +0100 Subject: [PATCH 6/9] Ignore deepcode PrototypePollutionFunctionParams --- src/EventStore.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EventStore.js b/src/EventStore.js index 06fc69f..7fa3951 100644 --- a/src/EventStore.js +++ b/src/EventStore.js @@ -131,7 +131,7 @@ class EventStore extends events.EventEmitter { return; } const index = this.storage.openIndex('stream-'+streamName); - // deepcode ignore PrototypePollution: streams is a Map + // deepcode ignore PrototypePollutionFunctionParams: streams is a Map this.streams[streamName] = { index }; this.emit('stream-available', streamName); } @@ -350,6 +350,7 @@ class EventStore extends events.EventEmitter { const index = this.storage.ensureIndex(streamIndexName, matcher); assert(index !== null, `Error creating stream index ${streamName}.`); + // deepcode ignore PrototypePollutionFunctionParams: streams is a Map this.streams[streamName] = { index, matcher }; this.emit('stream-created', streamName); return new EventStream(streamName, this); From 359da419c85da3957e0a6e0032a69901e8e518cb Mon Sep 17 00:00:00 2001 From: Alexander Berl Date: Wed, 10 Feb 2021 15:00:36 +0100 Subject: [PATCH 7/9] Ignore safety-checks in coverage --- src/EventStore.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/EventStore.js b/src/EventStore.js index 7fa3951..2fd12be 100644 --- a/src/EventStore.js +++ b/src/EventStore.js @@ -120,13 +120,15 @@ class EventStore extends events.EventEmitter { /** * @private - * @param {string} name + * @param {string} name The full stream name, including the `stream-` prefix. */ registerStream(name) { + /* istanbul ignore if */ if (!name.startsWith('stream-')) { return; } const streamName = name.substr(7, name.length - 7); + /* istanbul ignore if */ if (streamName in this.streams) { return; } From 1ca9b6b31b8dbdd557bcb1c9f21a3a22393e9d0e Mon Sep 17 00:00:00 2001 From: Alexander Berl Date: Thu, 11 Feb 2021 23:54:52 +0100 Subject: [PATCH 8/9] Add dynamic stream definition --- README.md | 22 ++++++++++++++++++++++ src/EventStore.js | 31 ++++++++++++++++++++++++++++++- test/EventStore.spec.js | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f4c51d6..3214832 100644 --- a/README.md +++ b/README.md @@ -142,8 +142,30 @@ let myProjectionStream = eventstore.createStream('my-projection-stream', (event) for (let event of myProjectionStream) { //... } + +// This stream will include all events that have a `someProperty` with exactly the value 'equalsThisValue' +let myPropertyMatchingStream = eventstore.createStream('my-property-stream', { payload: { someProperty: 'equalsThisValue' } }); ``` +For creating streams dynamically depending on the events coming in, since version 0.8 you can define streams functionally. + +```javascript +// Will create a separate stream for every event type that occurs in the system. +eventstore.createDynamicStream((event) => 'type-' + event.payload.type); + +// Will create a separate stream for every event type and use an object matcher +// instead of executing the mapper for every subsequent event. Use this for optimization +// if the logic is relatively complex. Hint: this is a bad example! +eventstore.createDynamicStream((event) => ['type-' + event.payload.type, { payload: { type: event.type } }]); +``` + +**NOTE** +> Defining dynamic streams may lead to a lot of streams being created, which will cost a lot of performance eventually. +> Make sure to only use this when absolutely needed. For the event type case you might rather iterate all your known +> types during start-up and create streams for those. If you want to iterate events by some random property, like e.g. +> correlationId, a sane approach is to only create a stream for each prefix like the first two or three characters, then +> filter the stream events when iterating. + ### Optimistic concurrency Optimistic concurrency control is required when multiple sources generate events concurrently. diff --git a/src/EventStore.js b/src/EventStore.js index 2fd12be..8316bc3 100644 --- a/src/EventStore.js +++ b/src/EventStore.js @@ -339,7 +339,7 @@ class EventStore extends events.EventEmitter { * * @api * @param {string} streamName The name of the stream to create. - * @param {object|function(event)} matcher A matcher object, denoting the properties that need to match on an event a function that takes the event and returns true if the event should be added. + * @param {object|function(object):boolean} matcher A matcher object, denoting the properties that need to match on an event or a function that takes the event and returns true if the event should be added. * @returns {EventStream} The EventStream with all existing events matching the matcher. * @throws {Error} If a stream with that name already exists. * @throws {Error} If the stream could not be created. @@ -393,6 +393,35 @@ class EventStore extends events.EventEmitter { consumer.streamName = streamName; return consumer.pipe(new EventUnwrapper()); } + + /** + * Create a dynamic stream from a function that maps an event and it's metadata to a stream name. + * The mapping method needs to be pure and not depend on external variables. + * + * NOTE: Use this sparingly! It is costly to create streams dynamically and can lead to a lot of streams being created, + * which puts more work on the writer. + * + * @typedef {string|[string, object]|null} MappedStream + * @param {function(object):MappedStream} streamMapper A method that maps an event and it's metadata to a stream name + */ + createDynamicStream(streamMapper) { + const matcherFunc = `(storedEvent) => (${streamMapper.toString()})(storedEvent) === $streamName`; + this.storage.addIndexer((storedEvent) => { + const streamName = streamMapper(storedEvent); + if (streamName instanceof Array) { + const [name, matcher] = streamName; + if (name in this.streams) { + return null; + } + return { name: 'stream-' + name, matcher }; + } + if (!streamName || streamName in this.streams) { + return null; + } + const matcher = eval(matcherFunc.replace('$streamName', `'${streamName}'`)); // jshint ignore:line + return { name: 'stream-' + streamName, matcher }; + }); + } } module.exports = EventStore; diff --git a/test/EventStore.spec.js b/test/EventStore.spec.js index 5335aa8..371384f 100644 --- a/test/EventStore.spec.js +++ b/test/EventStore.spec.js @@ -536,6 +536,45 @@ describe('EventStore', function() { }); }); + describe('createDynamicStream', function() { + + it('creates a proper stream with matcher', function() { + eventstore = new EventStore({ + storageDirectory + }); + eventstore.createDynamicStream((event) => 'type-'+event.payload.type); + eventstore.commit('foo', [{ type: 'one' }]); + eventstore.commit('foo', [{ type: 'two' }]); + }); + + it('allows to specify an event matcher object as second return value', function(done) { + eventstore = new EventStore({ + storageDirectory + }); + eventstore.createDynamicStream(({ payload: { type }}) => ['type-'+type, { payload: { type } }]); + eventstore.commit('foo', [{ type: 'one' }]); + eventstore.commit('foo', [{ type: 'two' }], () => { + expect(eventstore.getStreamVersion('type-one')).to.be(1); + expect(eventstore.getStreamVersion('type-two')).to.be(1); + done(); + }); + }); + + it('allows to specify a metadata matcher object', function(done) { + eventstore = new EventStore({ + storageDirectory + }); + eventstore.createDynamicStream(({ metadata }) => ['correlation-'+metadata.correlationId, { metadata : { correlationId: metadata.correlationId } }]); + eventstore.commit('foo', [{ type: 'one' }], { correlationId: 1 }); + eventstore.commit('foo', [{ type: 'two' }], { correlationId: 2 }, () => { + expect(eventstore.getStreamVersion('correlation-1')).to.be(1); + expect(eventstore.getStreamVersion('correlation-2')).to.be(1); + done(); + }); + }); + + }); + describe('deleteEventStream', function() { it('throws in read-only mode', function(done) { From 35361d572feebc4e965221c7ed9e07cf763f72dd Mon Sep 17 00:00:00 2001 From: Alexander Berl Date: Thu, 11 Feb 2021 23:55:23 +0100 Subject: [PATCH 9/9] Improve matcher type hints --- src/Storage/ReadableStorage.js | 8 ++++++-- src/Storage/WritableStorage.js | 14 +++++++++----- src/util.js | 10 +++++++--- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Storage/ReadableStorage.js b/src/Storage/ReadableStorage.js index 8be0f2f..47ea87b 100644 --- a/src/Storage/ReadableStorage.js +++ b/src/Storage/ReadableStorage.js @@ -19,6 +19,10 @@ function *reverse(iterator) { } } +/** + * @typedef {object|function(object):boolean} Matcher + */ + /** * An append-only storage with highly performant positional range scans. * It's highly optimized for an event-store and hence does not support compaction or data-rewrite, nor any querying @@ -70,7 +74,7 @@ class ReadableStorage extends events.EventEmitter { * @protected * @param {string} name * @param {object} [options] - * @returns {{ index: ReadableIndex, matcher?: object|function }} + * @returns {{ index: ReadableIndex, matcher?: Matcher }} */ createIndex(name, options = {}) { /** @type ReadableIndex */ @@ -278,7 +282,7 @@ class ReadableStorage extends events.EventEmitter { * * @api * @param {string} name The index name. - * @param {object|function} [matcher] The matcher object or function that the index needs to have been defined with. If not given it will not be validated. + * @param {Matcher} [matcher] The matcher object or function that the index needs to have been defined with. If not given it will not be validated. * @returns {ReadableIndex} * @throws {Error} if the index with that name does not exist. * @throws {Error} if the HMAC for the matcher does not match. diff --git a/src/Storage/WritableStorage.js b/src/Storage/WritableStorage.js index adc5124..a7c3092 100644 --- a/src/Storage/WritableStorage.js +++ b/src/Storage/WritableStorage.js @@ -10,6 +10,10 @@ const DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024; class StorageLockedError extends Error {} +/** + * @typedef {object|function(object):boolean} Matcher + */ + /** * An append-only storage with highly performant positional range scans. * It's highly optimized for an event-store and hence does not support compaction or data-rewrite, nor any querying @@ -203,8 +207,8 @@ class WritableStorage extends ReadableStorage { /** * Add an indexer, which will be invoked for every document and ensures an index exists with the returned name and matcher. - * @typedef {{name:string|null, matcher:object|function}|null} IndexDefinition - * @param {function(document): IndexDefinition} indexer The indexer function, which returns an object containing the index name and matcher or null if the document should not be indexed. Alternatively the index name may be null. + * @typedef {{name:string|null, matcher:Matcher}|null} IndexDefinition + * @param {function(object):IndexDefinition} indexer The indexer function, which returns an object containing the index name and matcher or null if the document should not be indexed. Alternatively the index name may be null. */ addIndexer(indexer) { /* istanbul ignore else */ @@ -219,7 +223,7 @@ class WritableStorage extends ReadableStorage { * * @api * @param {string} name The index name. - * @param {object|function} [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. + * @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. * @param {boolean} [updateIndex] If set to false the index will not be matched against all existing documents. * @returns {ReadableIndex|WritableIndex} The index containing all documents that match the query. * @throws {Error} if the index doesn't exist yet and no matcher was specified. @@ -253,7 +257,7 @@ class WritableStorage extends ReadableStorage { * * @private * @param {WritableIndex} index - * @param {object|function} matcher + * @param {Matcher} matcher */ updateIndex(index, matcher) { try { @@ -379,7 +383,7 @@ class WritableStorage extends ReadableStorage { * @protected * @param {string} name * @param {object} [options] - * @returns {{ index: WritableIndex, matcher: object|function }} + * @returns {{ index: WritableIndex, matcher: Matcher }} */ createIndex(name, options = {}) { const index = new WritableIndex(name, options); diff --git a/src/util.js b/src/util.js index aad331c..3b68cfb 100644 --- a/src/util.js +++ b/src/util.js @@ -36,9 +36,13 @@ const createHmac = secret => string => { return hmac.digest('hex'); }; +/** + * @typedef {object|function(object):boolean} Matcher + */ + /** * @param {object} document The document to check against the matcher. - * @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. + * @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. * @returns {boolean} True if the document matches the matcher or false otherwise. */ function matches(document, matcher) { @@ -60,7 +64,7 @@ function matches(document, matcher) { } /** - * @param {object|function} matcher The matcher object or function that should be serialized. + * @param {Matcher} matcher The matcher object or function that should be serialized. * @param {function(string)} hmac A function that calculates a HMAC of the given string. * @returns {{matcher: string|object, hmac?: string}} */ @@ -78,7 +82,7 @@ function buildMetadataForMatcher(matcher, hmac) { /** * @param {{matcher: string|object, hmac: string}} matcherMetadata The serialized matcher and it's HMAC * @param {function(string)} hmac A function that calculates a HMAC of the given string. - * @returns {object|function} The matcher object or function. + * @returns {Matcher} The matcher object or function. */ function buildMatcherFromMetadata(matcherMetadata, hmac) { let matcher;