encode idb array keys as sortable strings

that's why numeric parts of the keys have to be encoded
as a fixed length, "big-endian" ordered strings, so
string sorting will also sort the numeric keys correctly.

this also assumes room ids don't contain the "|" character,
we should probably escape the separator at some point.
This commit is contained in:
Bruno Windels 2019-06-26 21:55:33 +02:00
parent 106146660c
commit 0fd52be710
4 changed files with 54 additions and 29 deletions

View file

@ -12,18 +12,14 @@ function createStores(db) {
db.createObjectStore("roomSummary", {keyPath: "roomId"});
// need index to find live fragment? prooobably ok without for now
db.createObjectStore("timelineFragments", {keyPath: ["roomId", "id"]});
const timelineEvents = db.createObjectStore("timelineEvents", {keyPath: ["roomId", "fragmentId", "eventIndex"]});
timelineEvents.createIndex("byEventId", [
"roomId",
"event.event_id"
], {unique: true});
db.createObjectStore("roomState", {keyPath: [
"roomId",
"event.type",
"event.state_key"
]});
//key = room_id | fragment_id
db.createObjectStore("timelineFragments", {keyPath: "key"});
//key = room_id | fragment_id | event_index
const timelineEvents = db.createObjectStore("timelineEvents", {keyPath: "key"});
//eventIdKey = room_id | event_id
timelineEvents.createIndex("byEventId", "eventIdKey", {unique: true});
//key = room_id | event.type | event.state_key,
db.createObjectStore("roomState", {keyPath: "key"});
// const roomMembers = db.createObjectStore("roomMembers", {keyPath: [
// "event.room_id",

View file

@ -12,6 +12,8 @@ export default class RoomStateStore {
}
async setStateEvent(roomId, event) {
return this._roomStateStore.put({roomId, event});
const key = `${roomId}|${event.type}|${event.state_key}`;
const entry = {roomId, event, key};
return this._roomStateStore.put(entry);
}
}
}

View file

@ -1,6 +1,25 @@
import EventKey from "../../../room/timeline/EventKey.js";
import Platform from "../../../../Platform.js";
// storage keys are defined to be unsigned 32bit numbers in WebPlatform.js, which is assumed by idb
function encodeUint32(n) {
const hex = n.toString(16);
return "0".repeat(8 - hex.length) + hex;
}
function encodeKey(roomId, fragmentId, eventIndex) {
return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`;
}
function encodeEventIdKey(roomId, eventId) {
return `${roomId}|${eventId}`;
}
function decodeEventIdKey(eventIdKey) {
const [roomId, eventId] = eventIdKey.split("|");
return {roomId, eventId};
}
class Range {
constructor(only, lower, upper, lowerOpen, upperOpen) {
this._only = only;
@ -13,14 +32,14 @@ class Range {
asIDBKeyRange(roomId) {
// only
if (this._only) {
return IDBKeyRange.only([roomId, this._only.fragmentId, this._only.eventIndex]);
return IDBKeyRange.only(encodeKey(roomId, this._only.fragmentId, this._only.eventIndex));
}
// lowerBound
// also bound as we don't want to move into another roomId
if (this._lower && !this._upper) {
return IDBKeyRange.bound(
[roomId, this._lower.fragmentId, this._lower.eventIndex],
[roomId, this._lower.fragmentId, Platform.maxStorageKey],
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
encodeKey(roomId, this._lower.fragmentId, Platform.maxStorageKey),
this._lowerOpen,
false
);
@ -29,8 +48,8 @@ class Range {
// also bound as we don't want to move into another roomId
if (!this._lower && this._upper) {
return IDBKeyRange.bound(
[roomId, this._upper.fragmentId, Platform.minStorageKey],
[roomId, this._upper.fragmentId, this._upper.eventIndex],
encodeKey(roomId, this._upper.fragmentId, Platform.minStorageKey),
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
false,
this._upperOpen
);
@ -38,8 +57,8 @@ class Range {
// bound
if (this._lower && this._upper) {
return IDBKeyRange.bound(
[roomId, this._lower.fragmentId, this._lower.eventIndex],
[roomId, this._upper.fragmentId, this._upper.eventIndex],
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
this._lowerOpen,
this._upperOpen
);
@ -170,7 +189,7 @@ export default class TimelineEventStore {
// also passing them in chronological order makes sense as that's how we'll receive them almost always.
async findFirstOccurringEventId(roomId, eventIds) {
const byEventId = this._timelineStore.index("byEventId");
const keys = eventIds.map(eventId => [roomId, eventId]);
const keys = eventIds.map(eventId => encodeEventIdKey(roomId, eventId));
const results = new Array(keys.length);
let firstFoundKey;
@ -191,8 +210,7 @@ export default class TimelineEventStore {
firstFoundKey = firstFoundAndPrecedingResolved();
return !!firstFoundKey;
});
// key of index is [roomId, eventId], so pick out eventId
return firstFoundKey && firstFoundKey[1];
return firstFoundKey && decodeEventIdKey(firstFoundKey).eventId;
}
/** Inserts a new entry into the store. The combination of roomId and eventKey should not exist yet, or an error is thrown.
@ -201,6 +219,8 @@ export default class TimelineEventStore {
* @throws {StorageError} ...
*/
insert(entry) {
entry.key = encodeKey(entry.roomId, entry.fragmentId, entry.eventIndex);
entry.eventIdKey = encodeEventIdKey(entry.roomId, entry.event.event_id);
// TODO: map error? or in idb/store?
return this._timelineStore.add(entry);
}
@ -215,7 +235,7 @@ export default class TimelineEventStore {
}
get(roomId, eventKey) {
return this._timelineStore.get([roomId, eventKey.fragmentId, eventKey.eventIndex]);
return this._timelineStore.get(encodeKey(roomId, eventKey.fragmentId, eventKey.eventIndex));
}
// returns the entries as well!! (or not always needed? I guess not always needed, so extra method)
removeRange(roomId, range) {
@ -224,6 +244,6 @@ export default class TimelineEventStore {
}
getByEventId(roomId, eventId) {
return this._timelineStore.index("byEventId").get([roomId, eventId]);
return this._timelineStore.index("byEventId").get(encodeEventIdKey(roomId, eventId));
}
}

View file

@ -1,5 +1,11 @@
import Platform from "../../../../Platform.js";
function encodeKey(roomId, fragmentId) {
let fragmentIdHex = fragmentId.toString(16);
fragmentIdHex = "0".repeat(8 - fragmentIdHex.length) + fragmentIdHex;
return `${roomId}|${fragmentIdHex}`;
}
export default class RoomFragmentStore {
constructor(store) {
this._store = store;
@ -7,8 +13,8 @@ export default class RoomFragmentStore {
_allRange(roomId) {
return IDBKeyRange.bound(
[roomId, Platform.minStorageKey],
[roomId, Platform.maxStorageKey]
encodeKey(roomId, Platform.minStorageKey),
encodeKey(roomId, Platform.maxStorageKey)
);
}
@ -35,6 +41,7 @@ export default class RoomFragmentStore {
// depends if we want to do anything smart with fragment ids,
// like give them meaning depending on range. not for now probably ...
add(fragment) {
fragment.key = encodeKey(fragment.roomId, fragment.id);
return this._store.add(fragment);
}
@ -43,6 +50,6 @@ export default class RoomFragmentStore {
}
get(roomId, fragmentId) {
return this._store.get([roomId, fragmentId]);
return this._store.get(encodeKey(roomId, fragmentId));
}
}