UNPKG

event-storage

Version:

An optimized embedded event store for node.js

217 lines (198 loc) 6.67 kB
const crypto = require('crypto'); const fs = require('fs'); const mkdirpSync = require('mkdirp').sync; /** * Assert that actual and expected match or throw an Error with the given message appended by information about expected and actual value. * * @param {*} actual * @param {*} expected * @param {string} message */ function assertEqual(actual, expected, message) { if (actual !== expected) { throw new Error(message + (message ? ' ' : '') + `Expected "${expected}" but got "${actual}".`); } } /** * Assert that the condition holds and if not, throw an error with the given message. * * @param {boolean} condition * @param {string} message * @param {typeof Error} ErrorType */ function assert(condition, message, ErrorType = Error) { if (!condition) { throw new ErrorType(message); } } /** * Return the amount required to align value to the given alignment. * It calculates the difference of the alignment and the modulo of value by alignment. * @param {number} value * @param {number} alignment * @returns {number} */ function alignTo(value, alignment) { return (alignment - (value % alignment)) % alignment; } /** * @param {string} secret The secret to use for calculating further HMACs * @returns {function(string)} A function that calculates the HMAC for a given string */ const createHmac = secret => string => { const hmac = crypto.createHmac('sha256', secret); hmac.update(string); return hmac.digest('hex'); }; /** * @typedef {object|function(object):boolean} Matcher */ /** * @param {object} document The document to check against the matcher. * @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) { if (typeof document === 'undefined') return false; if (typeof matcher === 'undefined') return true; if (typeof matcher === 'function') return matcher(document); for (let prop of Object.getOwnPropertyNames(matcher)) { if (typeof matcher[prop] === 'object') { if (!matches(document[prop], matcher[prop])) { return false; } } else if (typeof matcher[prop] !== 'undefined' && document[prop] !== matcher[prop]) { return false; } } return true; } /** * @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}} */ function buildMetadataForMatcher(matcher, hmac) { if (!matcher) { return undefined; } if (typeof matcher === 'object') { return { matcher }; } const matcherString = matcher.toString(); return { matcher: matcherString, hmac: hmac(matcherString) }; } /** * @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 {Matcher} The matcher object or function. */ function buildMatcherFromMetadata(matcherMetadata, hmac) { let matcher; if (typeof matcherMetadata.matcher === 'object') { matcher = matcherMetadata.matcher; } else { if (matcherMetadata.hmac !== hmac(matcherMetadata.matcher)) { throw new Error('Invalid HMAC for matcher.'); } matcher = eval('(' + matcherMetadata.matcher + ')').bind({}); // jshint ignore:line } return matcher; } /** * Build a buffer containing the file magic header and a JSON stringified metadata block, padded to be a multiple of 16 bytes long. * * @param {string} magic * @param {object} metadata * @returns {Buffer} A buffer containing the header data */ function buildMetadataHeader(magic, metadata) { assertEqual(magic.length, 8, 'The header magic bytes length is wrong.'); let metadataString = JSON.stringify(metadata); let metadataSize = Buffer.byteLength(metadataString, 'utf8'); // 8 byte MAGIC, 4 byte metadata size, 1 byte line break const pad = (16 - ((8 + 4 + metadataSize + 1) % 16)) % 16; metadataString += ' '.repeat(pad) + "\n"; metadataSize += pad + 1; const metadataBuffer = Buffer.allocUnsafe(8 + 4 + metadataSize); metadataBuffer.write(magic, 0, 8, 'utf8'); metadataBuffer.writeUInt32BE(metadataSize, 8); metadataBuffer.write(metadataString, 8 + 4, metadataSize, 'utf8'); return metadataBuffer; } /** * Do a binary search for number in the range 1-length with values retrieved via a provided getter. * * @param {number} number The value to search for * @param {number} length The upper position to search up to * @param {function(number)} get The getter function to retrieve the values at the specific position * @returns {Array<number>} An array of the low and high position that match the searched number */ function binarySearch(number, length, get) { let low = 1; let high = length; if (get(low) > number) { return [low, 0]; } if (get(high) < number) { return [0, high]; } while (low <= high) { const mid = low + ((high - low) >> 1); const value = get(mid); if (value === number) { return [mid, mid]; } if (value < number) { low = mid + 1; } else { high = mid - 1; } } return [low, high]; } /** * @param {number} index The 1-based index position to wrap around if < 0 and check against the bounds. * @param {number} length The length of the index and upper bound. * @returns {number} The wrapped index or -1 if index out of bounds. */ function wrapAndCheck(index, length) { if (typeof index !== 'number') { return -1; } if (index < 0) { index += length + 1; } if (index < 1 || index > length) { return -1; } return index; } /** * Ensure that the given directory exists. * @param {string} dirName * @return {boolean} true if the directory existed already */ function ensureDirectory(dirName) { if (!fs.existsSync(dirName)) { try { mkdirpSync(dirName); } catch (e) { } return false; } return true; } module.exports = { assert, assertEqual, wrapAndCheck, binarySearch, createHmac, matches, buildMetadataForMatcher, buildMatcherFromMetadata, buildMetadataHeader, alignTo, ensureDirectory };