diffusion
Version:
Diffusion JavaScript client
231 lines (196 loc) • 7.72 kB
JavaScript
/*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);
};
};