UNPKG

diffusion

Version:

Diffusion JavaScript client

231 lines (196 loc) 7.72 kB
/*eslint valid-jsdoc: "off"*/ var findNextPowerOfTwo = require('util/math').findNextPowerOfTwo; var curryR = require('util/function').curryR; var ofSize = require('util/array').ofSize; var fill = require('util/array').fill; // How many bits of a timestamp to discard. The default rounds to approximately 1 second. var TIMESTAMP_ROUNDING = 10; function roundTimeStamp(timestamp) { return timestamp >> TIMESTAMP_ROUNDING; } function mask(p, capacity) { return p & capacity - 1; } /** * Port of the Java RecoveryBuffer implementation, with a bounded number of entries. * <p> * Timestamps are recorded in a timestamp index. The timestamp index also has a * fixed upper size bound. * <p> * This implementation expects timestamp parameters in milliseconds. It rounds * timestamps to reduce the required index size and the number of index updates. * * @author Peter Hughes * @since 5.8 */ module.exports = function RecoveryBuffer(minimumMessages, minimumTimeIndex) { var messagesLength = findNextPowerOfTwo(minimumMessages); var indexCapacity = findNextPowerOfTwo(minimumTimeIndex); var messagesMask = curryR(mask, messagesLength); var indexMask = curryR(mask, indexCapacity); var messages = ofSize(messagesLength); var indices = ofSize(indexCapacity, -1); var times = ofSize(indexCapacity); /** * Timestamp index. * <p> * The following state is a bounded circular buffer of (timestamp, messages index) pairs. Each entry states that * messages with earlier indexes than its index were added at its timestamp or earlier. This serves to timestamp * ranges of {@link #messages}, allowing us to flush stale entries. * <p> * Each call to markTime adds an entry, possibly evicting messages referred to by an older entry for which there is * no longer room. * * <p> * Some invariants: * <ul> * <li>Valid entries have a pair of values at the same index in the timestamps & indicies arrays. The index is in * the range [timesHead, timesTail). * <li>indices[timesHead] == -1 <=> the timestamp index is empty. * <li>timesHead == timesTail <=> either the timestamp index is empty, or indices[timesHead] >= 0 and the timestamp * index isfull. * <li>size == 0 => the timestamp index is empty. * <li>For each valid entry at index i, indices[i] >= 0 and is either equal to tail or points to a non-null entry in * messages. * <li>No entry points to the head of messages, i.e. the message at tail - size. * <li>Later entries have a messages index that is at least as great (modulo messages.length) as earlier entries. * </ul> */ var timesHead = 0; var timesTail = 0; var tail = messagesMask(0); var size = 0; /** * @returns {number} the number of messages that can be recovered */ this.size = function () { return size; }; /** * Add a message to the buffer. If the buffer is full, the oldest message is discarded * * @param message {Message} - The message */ this.put = function (message) { messages[tail] = message; tail = messagesMask(tail + 1); if (size < messagesLength) { size = size + 1; } else { while (indices[timesHead] === tail) { indices[timesHead] = -1; timesHead = indexMask(timesHead + 1); } } }; /** * If there are at least N messages in the buffer, pass the N most recent messages to the consumer function and * return true. Otherwise return false. * <P> * This is a non-destructive operation. * * @param n {Number} - The number of messages to recover * @param consumer {Function} - The consumer function to be called with recovered messages (in order) * @returns {boolean} - False if there are not N messages, in which case consumer will not be called. */ this.recover = function (n, consumer) { if (n < 0 || n > size) { return false; } // This differs from the Java implementation, since we want to return messages in the order they were added for (var i = n; i >= 1; --i) { consumer(messages[messagesMask(tail - i)]); } return true; }; /** * Clears this buffer */ this.clear = function () { fill(messages, null); fill(indices, -1); timesHead = 0; timesTail = 0; size = 0; }; function removeElements(newElementsHead) { var messagesHead = messagesMask(tail - size); var newSize; if (messagesHead < newElementsHead) { fill(messages, null, messagesHead, newElementsHead); newSize = size - newElementsHead + messagesHead; } else if (messagesHead > newElementsHead) { fill(messages, null, messagesHead, messagesLength); fill(messages, null, 0, newElementsHead); newSize = size - messagesHead + newElementsHead; } else { fill(messages, null); newSize = 0; } size = newSize; } /** * Inform this recovery buffer of the current timestamp. Subsequent messages put in the buffer are recorded as older * than this timestamp. * * @param newElementsHead {Number} - A timestamp, typically "unix time". Larger values are later. The value does not * matter, so long as the sequence is consistently used in calls to markTime and flush. */ this.markTime = function (timestamp) { if (size > 0) { var h = timesHead; var t = timesTail; var firstIndex = indices[h]; var roundedTimestamp = roundTimeStamp(timestamp); if (firstIndex >= 0) { var indexLast = indexMask(t - 1); if (indices[indexLast] === tail) { return; } if (times[indexLast] === roundedTimestamp) { indices[indexLast] = tail; return; } } timesTail = indexMask(t + 1); times[t] = roundedTimestamp; indices[t] = tail; if (h === t && firstIndex >= 0) { removeElements(firstIndex); timesHead = timesTail; } } }; /** * Remove all elements added at or before the timestamp. * * @param timestamp {Number} - Timestamp. Follows the same properties as used in RecoveryBuffer#markTime */ this.flush = function (timestamp) { // Due to time adjustments, there is no guarantee that a timestamp // supplied to markTime is later than a previous value. Treat // a more recently supplied timestamp as more accurate, iterate // from the tail of the index until we cross the threshold, and // flush everything else. if (size === 0 || indices[timesHead] < 0) { return; } var i = timesTail; var roundedTimestamp = roundTimeStamp(timestamp); do { var previousI = i; i = indexMask(i - 1); if (times[i] <= roundedTimestamp) { removeElements(indices[i]); if (timesHead <= previousI) { fill(indices, -1, timesHead, previousI); } else { fill(indices, -1, timesHead, indexCapacity); fill(indices, -1, 0, previousI); } timesHead = previousI; return; } } while (i !== timesHead); }; };