first go at a timeline memory store

first to use in unit tests for persister
later also to use in production when idb is not available
This commit is contained in:
Bruno Windels 2019-03-29 23:00:22 +01:00
parent b1e382d7c9
commit 7d91b2dde3

View file

@ -0,0 +1,220 @@
import SortKey from "../sortkey.js";
import sortedIndex from "../../../utils/sortedIndex.js";
function compareKeys(key, entry) {
if (key.roomId === entry.roomId) {
return key.sortKey.compare(entry.sortKey);
} else {
return key.roomId < entry.roomId ? -1 : 1;
}
}
class Range {
constructor(timeline, lower, upper, lowerOpen, upperOpen) {
this._timeline = timeline;
this._lower = lower;
this._upper = upper;
this._lowerOpen = lowerOpen;
this._upperOpen = upperOpen;
}
/** projects the range onto the timeline array */
project(roomId, maxCount = Number.MAX_SAFE_INTEGER) {
// determine lowest and highest allowed index.
// Important not to bleed into other roomIds here.
const lowerKey = {roomId, sortKey: this._lower || SortKey.minKey };
// apply lower key being open (excludes given key)
let minIndex = sortedIndex(this._timeline, lowerKey, compareKeys);
if (this._lowerOpen && minIndex < this._timeline.length && compareKeys(lowerKey, this._timeline[minIndex]) === 0) {
minIndex += 1;
}
const upperKey = {roomId, sortKey: this._upper || SortKey.maxKey };
// apply upper key being open (excludes given key)
let maxIndex = sortedIndex(this._timeline, upperKey, compareKeys);
if (this._upperOpen && maxIndex < this._timeline.length && compareKeys(upperKey, this._timeline[maxIndex]) === 0) {
maxIndex -= 1;
}
// find out from which edge we should grow
// if upper or lower bound
// again, important not to go below minIndex or above maxIndex
// to avoid bleeding into other rooms
let startIndex, endIndex;
if (!this._lower && this._upper) {
startIndex = Math.max(minIndex, maxIndex - maxCount);
endIndex = maxIndex;
} else if (this._lower && !this._upper) {
startIndex = minIndex;
endIndex = Math.min(maxIndex, minIndex + maxCount);
} else {
startIndex = minIndex;
endIndex = maxIndex;
}
// if startIndex is out of range, make range empty
if (startIndex === this._timeline.length) {
startIndex = endIndex = 0;
}
const count = endIndex - startIndex;
return {startIndex, count};
}
select(roomId, maxCount) {
const {startIndex, count} = this.project(roomId, this._timeline, maxCount);
return this._timeline.slice(startIndex, startIndex + count);
}
}
export default class RoomTimelineStore {
constructor(initialTimeline = []) {
this._timeline = initialTimeline;
}
/** Creates a range that only includes the given key
* @param {SortKey} sortKey the key
* @return {Range} the created range
*/
onlyRange(sortKey) {
return new Range(this._timeline, sortKey, sortKey);
}
/** Creates a range that includes all keys before sortKey, and optionally also the key itself.
* @param {SortKey} sortKey the key
* @param {boolean} [open=false] whether the key is included (false) or excluded (true) from the range at the upper end.
* @return {Range} the created range
*/
upperBoundRange(sortKey, open=false) {
return new Range(this._timeline, undefined, sortKey, undefined, open);
}
/** Creates a range that includes all keys after sortKey, and optionally also the key itself.
* @param {SortKey} sortKey the key
* @param {boolean} [open=false] whether the key is included (false) or excluded (true) from the range at the lower end.
* @return {Range} the created range
*/
lowerBoundRange(sortKey, open=false) {
return new Range(this._timeline, sortKey, undefined, open);
}
/** Creates a range that includes all keys between `lower` and `upper`, and optionally the given keys as well.
* @param {SortKey} lower the lower key
* @param {SortKey} upper the upper key
* @param {boolean} [lowerOpen=false] whether the lower key is included (false) or excluded (true) from the range.
* @param {boolean} [upperOpen=false] whether the upper key is included (false) or excluded (true) from the range.
* @return {Range} the created range
*/
boundRange(lower, upper, lowerOpen=false, upperOpen=false) {
return new Range(this._timeline, lower, upper, lowerOpen, upperOpen);
}
/** Looks up the last `amount` entries in the timeline for `roomId`.
* @param {string} roomId
* @param {number} amount
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order.
*/
lastEvents(roomId, amount) {
return this.eventsBefore(roomId, SortKey.maxKey, amount);
}
/** Looks up the first `amount` entries in the timeline for `roomId`.
* @param {string} roomId
* @param {number} amount
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order.
*/
firstEvents(roomId, amount) {
return this.eventsAfter(roomId, SortKey.minKey, amount);
}
/** Looks up `amount` entries after `sortKey` in the timeline for `roomId`.
* The entry for `sortKey` is not included.
* @param {string} roomId
* @param {SortKey} sortKey
* @param {number} amount
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order.
*/
eventsAfter(roomId, sortKey, amount) {
const events = this.lowerBoundRange(sortKey, true).select(roomId, amount);
return Promise.resolve(events);
}
/** Looks up `amount` entries before `sortKey` in the timeline for `roomId`.
* The entry for `sortKey` is not included.
* @param {string} roomId
* @param {SortKey} sortKey
* @param {number} amount
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order.
*/
eventsBefore(roomId, sortKey, amount) {
const events = this.upperBoundRange(sortKey, true).select(roomId, amount);
return Promise.resolve(events);
}
/** Looks up the first, if any, event entry (so excluding gap entries) after `sortKey`.
* @param {string} roomId
* @param {SortKey} sortKey
* @return {Promise<(?Entry)>} a promise resolving to entry, if any.
*/
nextEvent(roomId, sortKey) {
const searchSpace = this.lowerBoundRange(sortKey, true).select(roomId);
const event = searchSpace.find(entry => !!entry.event);
return Promise.resolve(event);
}
/** Looks up the first, if any, event entry (so excluding gap entries) before `sortKey`.
* @param {string} roomId
* @param {SortKey} sortKey
* @return {Promise<(?Entry)>} a promise resolving to entry, if any.
*/
previousEvent(roomId, sortKey) {
const searchSpace = this.upperBoundRange(sortKey, true).select(roomId);
const event = searchSpace.reverse().find(entry => !!entry.event);
return Promise.resolve(event);
}
/** Inserts a new entry into the store. The combination of roomId and sortKey should not exist yet, or an error is thrown.
* @param {Entry} entry the entry to insert
* @return {Promise<>} a promise resolving to undefined if the operation was successful, or a StorageError if not.
* @throws {StorageError} ...
*/
insert(entry) {
const insertIndex = sortedIndex(this._timeline, entry, compareKeys);
if (insertIndex < this._timeline.length) {
const existingEntry = this._timeline[insertIndex];
if (compareKeys(entry, existingEntry) === 0) {
return Promise.reject(new Error("entry already exists"));
}
}
this._timeline.splice(insertIndex, 0, entry);
return Promise.resolve();
}
/** Updates the entry into the store with the given [roomId, sortKey] combination.
* If not yet present, will insert. Might be slower than add.
* @param {Entry} entry the entry to update.
* @return {Promise<>} a promise resolving to undefined if the operation was successful, or a StorageError if not.
*/
update(entry) {
let update = false;
const updateIndex = sortedIndex(this._timeline, entry, compareKeys);
if (updateIndex < this._timeline.length) {
const existingEntry = this._timeline[updateIndex];
if (compareKeys(entry, existingEntry) === 0) {
update = true;
}
}
this._timeline.splice(updateIndex, update ? 1 : 0, entry);
return Promise.resolve();
}
get(roomId, sortKey) {
const range = this.onlyRange(sortKey);
const {startIndex, count} = range.project(roomId);
const event = count ? this._timeline[startIndex] : undefined;
return Promise.resolve(event);
}
removeRange(roomId, range) {
const {startIndex, count} = range.project(roomId);
const removedEntries = this._timeline.splice(startIndex, count);
return Promise.resolve(removedEntries);
}
}