diffusion
Version:
Diffusion JavaScript client
244 lines (243 loc) • 9.64 kB
JavaScript
"use strict";
/**
* @module MessageQueue
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.RecoveryBuffer = void 0;
var array_1 = require("./../util/array");
var function_1 = require("./../util/function");
var math_1 = require("./../util/math");
/**
* How many bits of a timestamp to discard. The default rounds to approximately 1 second.
*
* @see {@link roundTimeStamp}
*/
var TIMESTAMP_ROUNDING = 10;
/**
* Reduce the accuracy of a timestamp
*
* @param timestamp the time stamp to round
* @return the reduced accuracy time stamp
*/
function roundTimeStamp(timestamp) {
/* eslint-disable-next-line no-bitwise */
return timestamp >> TIMESTAMP_ROUNDING;
}
/**
* Apply a bitmask to a number to calculate the modulo of a power of two. This
* is used to determine the position in a circular buffer
*
* @param p the number to mask
* @param capacity the capacity, must be a power of two
* @return the reduced value
*/
function mask(p, capacity) {
/* eslint-disable-next-line no-bitwise */
return p & capacity - 1;
}
/**
* Port of the Java RecoveryBuffer implementation, with a bounded number of entries.
*
* Timestamps are recorded in a timestamp index. The timestamp index also has a
* fixed upper size bound.
*
* This implementation expects timestamp parameters in milliseconds. It rounds
* timestamps to reduce the required index size and the number of index updates.
*
* The recovery buffer is implemented as 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.
*
* Each call to markTime adds an entry, possibly evicting messages referred to
* by an older entry for which there is no longer room.
*
*
* Some invariants:
* * Valid entries have a pair of values at the same index in the timestamps &
* indicies arrays. The index is in the range `[timesHead, timesTail)`.
* * `indices[timesHead] == -1` <=> the timestamp index is empty.
* * `timesHead == timesTail` <=> either the timestamp index is empty, or
* `indices[timesHead] >= 0` and the timestamp index isfull.
* * `size == 0` => the timestamp index is empty.
* * 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.
* * No entry points to the head of messages, i.e. the message at tail - size.
* * Later entries have a messages index that is at least as great (modulo
* messages.length) as earlier entries.
*
* @author Peter Hughes
* @since 5.8
*/
var RecoveryBuffer = /** @class */ (function () {
/**
* Create a RecoveryBuffer
*
* @param minimumMessages the minimum number of messages
* @param minimumTimeIndex the minimum number of time indices
*/
function RecoveryBuffer(minimumMessages, minimumTimeIndex) {
this.messagesLength = math_1.findNextPowerOfTwo(minimumMessages);
this.indexCapacity = math_1.findNextPowerOfTwo(minimumTimeIndex);
this.messagesMask = function_1.curryR(mask, this.messagesLength);
this.indexMask = function_1.curryR(mask, this.indexCapacity);
this.messages = array_1.ofSize(this.messagesLength);
this.indices = array_1.ofSize(this.indexCapacity, -1);
this.times = array_1.ofSize(this.indexCapacity);
this.timesHead = 0;
this.timesTail = 0;
this.messagesTail = this.messagesMask(0);
this.messagesSize = 0;
}
/**
* Get the number of messages
*
* @return the number of messages that can be recovered
*/
RecoveryBuffer.prototype.size = function () {
return this.messagesSize;
};
/**
* Add a message to the buffer. If the buffer is full, the oldest message is discarded
*
* @param message the message
*/
RecoveryBuffer.prototype.put = function (message) {
this.messages[this.messagesTail] = message;
this.messagesTail = this.messagesMask(this.messagesTail + 1);
if (this.messagesSize < this.messagesLength) {
this.messagesSize = this.messagesSize + 1;
}
else {
while (this.indices[this.timesHead] === this.messagesTail) {
this.indices[this.timesHead] = -1;
this.timesHead = this.indexMask(this.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.
*
* This is a non-destructive operation.
*
* @param n the number of messages to recover
* @param consumer the consumer function to be called with recovered messages (in order)
* @returns `false` if there are not N messages, in which case consumer will not be called.
*/
RecoveryBuffer.prototype.recover = function (n, consumer) {
if (n < 0 || n > this.messagesSize) {
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(this.messages[this.messagesMask(this.messagesTail - i)]);
}
return true;
};
/**
* Clears this buffer
*/
RecoveryBuffer.prototype.clear = function () {
array_1.fill(this.messages, null);
array_1.fill(this.indices, -1);
this.timesHead = 0;
this.timesTail = 0;
this.messagesSize = 0;
};
/**
* Remove elements from the buffer
*
* @param the new index of the messages head in the circular buffer
*/
RecoveryBuffer.prototype.removeElements = function (newElementsHead) {
var messagesHead = this.messagesMask(this.messagesTail - this.messagesSize);
var newSize;
if (messagesHead < newElementsHead) {
array_1.fill(this.messages, null, messagesHead, newElementsHead);
newSize = this.messagesSize - newElementsHead + messagesHead;
}
else if (messagesHead > newElementsHead) {
array_1.fill(this.messages, null, messagesHead, this.messagesLength);
array_1.fill(this.messages, null, 0, newElementsHead);
newSize = this.messagesSize - messagesHead + newElementsHead;
}
else {
array_1.fill(this.messages, null);
newSize = 0;
}
this.messagesSize = newSize;
};
/**
* Inform this recovery buffer of the current timestamp. Subsequent messages
* put in the buffer are recorded as older than this timestamp.
*
* @param newElementsHead 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.
*/
RecoveryBuffer.prototype.markTime = function (timestamp) {
if (this.messagesSize > 0) {
var h = this.timesHead;
var t = this.timesTail;
var firstIndex = this.indices[h];
var roundedTimestamp = roundTimeStamp(timestamp);
if (firstIndex >= 0) {
var indexLast = this.indexMask(t - 1);
if (this.indices[indexLast] === this.messagesTail) {
return;
}
if (this.times[indexLast] === roundedTimestamp) {
this.indices[indexLast] = this.messagesTail;
return;
}
}
this.timesTail = this.indexMask(t + 1);
this.times[t] = roundedTimestamp;
this.indices[t] = this.messagesTail;
if (h === t && firstIndex >= 0) {
this.removeElements(firstIndex);
this.timesHead = this.timesTail;
}
}
};
/**
* Remove all elements added at or before the timestamp.
*
* @param timestamp the timestamp. Follows the same properties as used in
* RecoveryBuffer#markTime
*/
RecoveryBuffer.prototype.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 (this.messagesSize === 0 || this.indices[this.timesHead] < 0) {
return;
}
var i = this.timesTail;
var roundedTimestamp = roundTimeStamp(timestamp);
do {
var previousI = i;
i = this.indexMask(i - 1);
if (this.times[i] <= roundedTimestamp) {
this.removeElements(this.indices[i]);
if (this.timesHead <= previousI) {
array_1.fill(this.indices, -1, this.timesHead, previousI);
}
else {
array_1.fill(this.indices, -1, this.timesHead, this.indexCapacity);
array_1.fill(this.indices, -1, 0, previousI);
}
this.timesHead = previousI;
return;
}
} while (i !== this.timesHead);
};
return RecoveryBuffer;
}());
exports.RecoveryBuffer = RecoveryBuffer;