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

View file

@ -12,6 +12,8 @@ export default class RoomStateStore {
} }
async setStateEvent(roomId, event) { 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 EventKey from "../../../room/timeline/EventKey.js";
import Platform from "../../../../Platform.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 { class Range {
constructor(only, lower, upper, lowerOpen, upperOpen) { constructor(only, lower, upper, lowerOpen, upperOpen) {
this._only = only; this._only = only;
@ -13,14 +32,14 @@ class Range {
asIDBKeyRange(roomId) { asIDBKeyRange(roomId) {
// only // only
if (this._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 // lowerBound
// also bound as we don't want to move into another roomId // also bound as we don't want to move into another roomId
if (this._lower && !this._upper) { if (this._lower && !this._upper) {
return IDBKeyRange.bound( return IDBKeyRange.bound(
[roomId, this._lower.fragmentId, this._lower.eventIndex], encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
[roomId, this._lower.fragmentId, Platform.maxStorageKey], encodeKey(roomId, this._lower.fragmentId, Platform.maxStorageKey),
this._lowerOpen, this._lowerOpen,
false false
); );
@ -29,8 +48,8 @@ class Range {
// also bound as we don't want to move into another roomId // also bound as we don't want to move into another roomId
if (!this._lower && this._upper) { if (!this._lower && this._upper) {
return IDBKeyRange.bound( return IDBKeyRange.bound(
[roomId, this._upper.fragmentId, Platform.minStorageKey], encodeKey(roomId, this._upper.fragmentId, Platform.minStorageKey),
[roomId, this._upper.fragmentId, this._upper.eventIndex], encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
false, false,
this._upperOpen this._upperOpen
); );
@ -38,8 +57,8 @@ class Range {
// bound // bound
if (this._lower && this._upper) { if (this._lower && this._upper) {
return IDBKeyRange.bound( return IDBKeyRange.bound(
[roomId, this._lower.fragmentId, this._lower.eventIndex], encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
[roomId, this._upper.fragmentId, this._upper.eventIndex], encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
this._lowerOpen, this._lowerOpen,
this._upperOpen 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. // also passing them in chronological order makes sense as that's how we'll receive them almost always.
async findFirstOccurringEventId(roomId, eventIds) { async findFirstOccurringEventId(roomId, eventIds) {
const byEventId = this._timelineStore.index("byEventId"); 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); const results = new Array(keys.length);
let firstFoundKey; let firstFoundKey;
@ -191,8 +210,7 @@ export default class TimelineEventStore {
firstFoundKey = firstFoundAndPrecedingResolved(); firstFoundKey = firstFoundAndPrecedingResolved();
return !!firstFoundKey; return !!firstFoundKey;
}); });
// key of index is [roomId, eventId], so pick out eventId return firstFoundKey && decodeEventIdKey(firstFoundKey).eventId;
return firstFoundKey && firstFoundKey[1];
} }
/** Inserts a new entry into the store. The combination of roomId and eventKey should not exist yet, or an error is thrown. /** 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} ... * @throws {StorageError} ...
*/ */
insert(entry) { 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? // TODO: map error? or in idb/store?
return this._timelineStore.add(entry); return this._timelineStore.add(entry);
} }
@ -215,7 +235,7 @@ export default class TimelineEventStore {
} }
get(roomId, eventKey) { 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) // returns the entries as well!! (or not always needed? I guess not always needed, so extra method)
removeRange(roomId, range) { removeRange(roomId, range) {
@ -224,6 +244,6 @@ export default class TimelineEventStore {
} }
getByEventId(roomId, eventId) { 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"; 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 { export default class RoomFragmentStore {
constructor(store) { constructor(store) {
this._store = store; this._store = store;
@ -7,8 +13,8 @@ export default class RoomFragmentStore {
_allRange(roomId) { _allRange(roomId) {
return IDBKeyRange.bound( return IDBKeyRange.bound(
[roomId, Platform.minStorageKey], encodeKey(roomId, Platform.minStorageKey),
[roomId, Platform.maxStorageKey] encodeKey(roomId, Platform.maxStorageKey)
); );
} }
@ -35,6 +41,7 @@ export default class RoomFragmentStore {
// depends if we want to do anything smart with fragment ids, // depends if we want to do anything smart with fragment ids,
// like give them meaning depending on range. not for now probably ... // like give them meaning depending on range. not for now probably ...
add(fragment) { add(fragment) {
fragment.key = encodeKey(fragment.roomId, fragment.id);
return this._store.add(fragment); return this._store.add(fragment);
} }
@ -43,6 +50,6 @@ export default class RoomFragmentStore {
} }
get(roomId, fragmentId) { get(roomId, fragmentId) {
return this._store.get([roomId, fragmentId]); return this._store.get(encodeKey(roomId, fragmentId));
} }
} }