prepare storage to work with alternative idb impl

This commit is contained in:
Bruno Windels 2021-06-02 12:31:13 +02:00
parent 8dfed73524
commit edbac25613
16 changed files with 61 additions and 48 deletions

View file

@ -21,8 +21,9 @@ import { reqAsPromise } from "./utils.js";
const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey"; const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey";
export class Storage { export class Storage {
constructor(idbDatabase, hasWebkitEarlyCloseTxnBug) { constructor(idbDatabase, IDBKeyRange, hasWebkitEarlyCloseTxnBug) {
this._db = idbDatabase; this._db = idbDatabase;
this._IDBKeyRange = IDBKeyRange;
this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug; this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug;
const nameMap = STORE_NAMES.reduce((nameMap, name) => { const nameMap = STORE_NAMES.reduce((nameMap, name) => {
nameMap[name] = name; nameMap[name] = name;
@ -47,7 +48,7 @@ export class Storage {
if (this._hasWebkitEarlyCloseTxnBug) { if (this._hasWebkitEarlyCloseTxnBug) {
await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY)); await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY));
} }
return new Transaction(txn, storeNames); return new Transaction(txn, storeNames, this._IDBKeyRange);
} catch(err) { } catch(err) {
throw new StorageError("readTxn failed", err); throw new StorageError("readTxn failed", err);
} }
@ -62,7 +63,7 @@ export class Storage {
if (this._hasWebkitEarlyCloseTxnBug) { if (this._hasWebkitEarlyCloseTxnBug) {
await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY)); await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY));
} }
return new Transaction(txn, storeNames); return new Transaction(txn, storeNames, this._IDBKeyRange);
} catch(err) { } catch(err) {
throw new StorageError("readWriteTxn failed", err); throw new StorageError("readWriteTxn failed", err);
} }

View file

@ -21,14 +21,18 @@ import { schema } from "./schema.js";
import { detectWebkitEarlyCloseTxnBug } from "./quirks.js"; import { detectWebkitEarlyCloseTxnBug } from "./quirks.js";
const sessionName = sessionId => `hydrogen_session_${sessionId}`; const sessionName = sessionId => `hydrogen_session_${sessionId}`;
const openDatabaseWithSessionId = sessionId => openDatabase(sessionName(sessionId), createStores, schema.length); const openDatabaseWithSessionId = function(sessionId, idbFactory) {
return openDatabase(sessionName(sessionId), createStores, schema.length, idbFactory);
}
async function requestPersistedStorage() { async function requestPersistedStorage() {
if (navigator?.storage?.persist) { // don't assume browser so we can run in node with fake-idb
return await navigator.storage.persist(); const glob = this;
} else if (document.requestStorageAccess) { if (glob?.navigator?.storage?.persist) {
return await glob.navigator.storage.persist();
} else if (glob?.document.requestStorageAccess) {
try { try {
await document.requestStorageAccess(); await glob.document.requestStorageAccess();
return true; return true;
} catch (err) { } catch (err) {
return false; return false;
@ -39,8 +43,10 @@ async function requestPersistedStorage() {
} }
export class StorageFactory { export class StorageFactory {
constructor(serviceWorkerHandler) { constructor(serviceWorkerHandler, idbFactory = window.indexedDB, IDBKeyRange = window.IDBKeyRange) {
this._serviceWorkerHandler = serviceWorkerHandler; this._serviceWorkerHandler = serviceWorkerHandler;
this._idbFactory = idbFactory;
this._IDBKeyRange = IDBKeyRange;
} }
async create(sessionId) { async create(sessionId) {
@ -52,24 +58,24 @@ export class StorageFactory {
} }
}); });
const hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug(); const hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug(this._idbFactory);
const db = await openDatabaseWithSessionId(sessionId); const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
return new Storage(db, hasWebkitEarlyCloseTxnBug); return new Storage(db, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug);
} }
delete(sessionId) { delete(sessionId) {
const databaseName = sessionName(sessionId); const databaseName = sessionName(sessionId);
const req = indexedDB.deleteDatabase(databaseName); const req = this._idbFactory.deleteDatabase(databaseName);
return reqAsPromise(req); return reqAsPromise(req);
} }
async export(sessionId) { async export(sessionId) {
const db = await openDatabaseWithSessionId(sessionId); const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
return await exportSession(db); return await exportSession(db);
} }
async import(sessionId, data) { async import(sessionId, data) {
const db = await openDatabaseWithSessionId(sessionId); const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
return await importSession(db, data); return await importSession(db, data);
} }
} }

View file

@ -126,6 +126,10 @@ export class Store extends QueryTarget {
this._transaction = transaction; this._transaction = transaction;
} }
get IDBKeyRange() {
return this._transaction.IDBKeyRange;
}
get _idbStore() { get _idbStore() {
return this._target; return this._target;
} }

View file

@ -35,10 +35,11 @@ import {OperationStore} from "./stores/OperationStore.js";
import {AccountDataStore} from "./stores/AccountDataStore.js"; import {AccountDataStore} from "./stores/AccountDataStore.js";
export class Transaction { export class Transaction {
constructor(txn, allowedStoreNames) { constructor(txn, allowedStoreNames, IDBKeyRange) {
this._txn = txn; this._txn = txn;
this._allowedStoreNames = allowedStoreNames; this._allowedStoreNames = allowedStoreNames;
this._stores = {}; this._stores = {};
this.IDBKeyRange = IDBKeyRange;
} }
_idbStore(name) { _idbStore(name) {
@ -46,7 +47,7 @@ export class Transaction {
// more specific error? this is a bug, so maybe not ... // more specific error? this is a bug, so maybe not ...
throw new StorageError(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`); throw new StorageError(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`);
} }
return new Store(this._txn.objectStore(name)); return new Store(this._txn.objectStore(name), this);
} }
_store(name, mapStore) { _store(name, mapStore) {

View file

@ -18,12 +18,12 @@ limitations under the License.
import {openDatabase, txnAsPromise, reqAsPromise} from "./utils.js"; import {openDatabase, txnAsPromise, reqAsPromise} from "./utils.js";
// filed as https://bugs.webkit.org/show_bug.cgi?id=222746 // filed as https://bugs.webkit.org/show_bug.cgi?id=222746
export async function detectWebkitEarlyCloseTxnBug() { export async function detectWebkitEarlyCloseTxnBug(idbFactory) {
const dbName = "hydrogen_webkit_test_inactive_txn_bug"; const dbName = "hydrogen_webkit_test_inactive_txn_bug";
try { try {
const db = await openDatabase(dbName, db => { const db = await openDatabase(dbName, db => {
db.createObjectStore("test", {keyPath: "key"}); db.createObjectStore("test", {keyPath: "key"});
}, 1); }, 1, idbFactory);
const readTxn = db.transaction(["test"], "readonly"); const readTxn = db.transaction(["test"], "readonly");
await reqAsPromise(readTxn.objectStore("test").get("somekey")); await reqAsPromise(readTxn.objectStore("test").get("somekey"));
// schedule a macro task in between the two txns // schedule a macro task in between the two txns

View file

@ -31,7 +31,7 @@ export class DeviceIdentityStore {
} }
getAllForUserId(userId) { getAllForUserId(userId) {
const range = IDBKeyRange.lowerBound(encodeKey(userId, "")); const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, ""));
return this._store.selectWhile(range, device => { return this._store.selectWhile(range, device => {
return device.userId === userId; return device.userId === userId;
}); });
@ -39,7 +39,7 @@ export class DeviceIdentityStore {
async getAllDeviceIds(userId) { async getAllDeviceIds(userId) {
const deviceIds = []; const deviceIds = [];
const range = IDBKeyRange.lowerBound(encodeKey(userId, "")); const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, ""));
await this._store.iterateKeys(range, key => { await this._store.iterateKeys(range, key => {
const decodedKey = decodeKey(key); const decodedKey = decodeKey(key);
// prevent running into the next room // prevent running into the next room
@ -72,7 +72,7 @@ export class DeviceIdentityStore {
removeAllForUser(userId) { removeAllForUser(userId) {
// exclude both keys as they are theoretical min and max, // exclude both keys as they are theoretical min and max,
// but we should't have a match for just the room id, or room id with max // but we should't have a match for just the room id, or room id with max
const range = IDBKeyRange.bound(encodeKey(userId, MIN_UNICODE), encodeKey(userId, MAX_UNICODE), true, true); const range = this._store.IDBKeyRange.bound(encodeKey(userId, MIN_UNICODE), encodeKey(userId, MAX_UNICODE), true, true);
this._store.delete(range); this._store.delete(range);
} }
} }

View file

@ -35,7 +35,7 @@ export class GroupSessionDecryptionStore {
} }
removeAllForRoom(roomId) { removeAllForRoom(roomId) {
const range = IDBKeyRange.bound( const range = this._store.IDBKeyRange.bound(
encodeKey(roomId, MIN_UNICODE, MIN_UNICODE), encodeKey(roomId, MIN_UNICODE, MIN_UNICODE),
encodeKey(roomId, MAX_UNICODE, MAX_UNICODE) encodeKey(roomId, MAX_UNICODE, MAX_UNICODE)
); );

View file

@ -41,7 +41,7 @@ export class InboundGroupSessionStore {
} }
removeAllForRoom(roomId) { removeAllForRoom(roomId) {
const range = IDBKeyRange.bound( const range = this._store.IDBKeyRange.bound(
encodeKey(roomId, MIN_UNICODE, MIN_UNICODE), encodeKey(roomId, MIN_UNICODE, MIN_UNICODE),
encodeKey(roomId, MAX_UNICODE, MAX_UNICODE) encodeKey(roomId, MAX_UNICODE, MAX_UNICODE)
); );

View file

@ -30,7 +30,7 @@ export class OlmSessionStore {
async getSessionIds(senderKey) { async getSessionIds(senderKey) {
const sessionIds = []; const sessionIds = [];
const range = IDBKeyRange.lowerBound(encodeKey(senderKey, "")); const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, ""));
await this._store.iterateKeys(range, key => { await this._store.iterateKeys(range, key => {
const decodedKey = decodeKey(key); const decodedKey = decodeKey(key);
// prevent running into the next room // prevent running into the next room
@ -44,7 +44,7 @@ export class OlmSessionStore {
} }
getAll(senderKey) { getAll(senderKey) {
const range = IDBKeyRange.lowerBound(encodeKey(senderKey, "")); const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, ""));
return this._store.selectWhile(range, session => { return this._store.selectWhile(range, session => {
return session.senderKey === senderKey; return session.senderKey === senderKey;
}); });

View file

@ -55,7 +55,7 @@ export class OperationStore {
} }
async removeAllForScope(scope) { async removeAllForScope(scope) {
const range = IDBKeyRange.bound( const range = this._store.IDBKeyRange.bound(
encodeScopeTypeKey(scope, MIN_UNICODE), encodeScopeTypeKey(scope, MIN_UNICODE),
encodeScopeTypeKey(scope, MAX_UNICODE) encodeScopeTypeKey(scope, MAX_UNICODE)
); );

View file

@ -33,7 +33,7 @@ export class PendingEventStore {
} }
async getMaxQueueIndex(roomId) { async getMaxQueueIndex(roomId) {
const range = IDBKeyRange.bound( const range = this._eventStore.IDBKeyRange.bound(
encodeKey(roomId, KeyLimits.minStorageKey), encodeKey(roomId, KeyLimits.minStorageKey),
encodeKey(roomId, KeyLimits.maxStorageKey), encodeKey(roomId, KeyLimits.maxStorageKey),
false, false,
@ -46,12 +46,12 @@ export class PendingEventStore {
} }
remove(roomId, queueIndex) { remove(roomId, queueIndex) {
const keyRange = IDBKeyRange.only(encodeKey(roomId, queueIndex)); const keyRange = this._eventStore.IDBKeyRange.only(encodeKey(roomId, queueIndex));
this._eventStore.delete(keyRange); this._eventStore.delete(keyRange);
} }
async exists(roomId, queueIndex) { async exists(roomId, queueIndex) {
const keyRange = IDBKeyRange.only(encodeKey(roomId, queueIndex)); const keyRange = this._eventStore.IDBKeyRange.only(encodeKey(roomId, queueIndex));
const key = await this._eventStore.getKey(keyRange); const key = await this._eventStore.getKey(keyRange);
return !!key; return !!key;
} }
@ -72,7 +72,7 @@ export class PendingEventStore {
removeAllForRoom(roomId) { removeAllForRoom(roomId) {
const minKey = encodeKey(roomId, KeyLimits.minStorageKey); const minKey = encodeKey(roomId, KeyLimits.minStorageKey);
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey); const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey);
const range = IDBKeyRange.bound(minKey, maxKey); const range = this._eventStore.IDBKeyRange.bound(minKey, maxKey);
this._eventStore.delete(range); this._eventStore.delete(range);
} }
} }

View file

@ -42,7 +42,7 @@ export class RoomMemberStore {
} }
getAll(roomId) { getAll(roomId) {
const range = IDBKeyRange.lowerBound(encodeKey(roomId, "")); const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, ""));
return this._roomMembersStore.selectWhile(range, member => { return this._roomMembersStore.selectWhile(range, member => {
return member.roomId === roomId; return member.roomId === roomId;
}); });
@ -50,7 +50,7 @@ export class RoomMemberStore {
async getAllUserIds(roomId) { async getAllUserIds(roomId) {
const userIds = []; const userIds = [];
const range = IDBKeyRange.lowerBound(encodeKey(roomId, "")); const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, ""));
await this._roomMembersStore.iterateKeys(range, key => { await this._roomMembersStore.iterateKeys(range, key => {
const decodedKey = decodeKey(key); const decodedKey = decodeKey(key);
// prevent running into the next room // prevent running into the next room
@ -66,7 +66,7 @@ export class RoomMemberStore {
removeAllForRoom(roomId) { removeAllForRoom(roomId) {
// exclude both keys as they are theoretical min and max, // exclude both keys as they are theoretical min and max,
// but we should't have a match for just the room id, or room id with max // but we should't have a match for just the room id, or room id with max
const range = IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true); const range = this._roomMembersStore.IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);
this._roomMembersStore.delete(range); this._roomMembersStore.delete(range);
} }
} }

View file

@ -44,7 +44,7 @@ export class RoomStateStore {
removeAllForRoom(roomId) { removeAllForRoom(roomId) {
// exclude both keys as they are theoretical min and max, // exclude both keys as they are theoretical min and max,
// but we should't have a match for just the room id, or room id with max // but we should't have a match for just the room id, or room id with max
const range = IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true); const range = this._roomStateStore.IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);
this._roomStateStore.delete(range); this._roomStateStore.delete(range);
} }
} }

View file

@ -33,7 +33,8 @@ function decodeEventIdKey(eventIdKey) {
} }
class Range { class Range {
constructor(only, lower, upper, lowerOpen, upperOpen) { constructor(IDBKeyRange, only, lower, upper, lowerOpen, upperOpen) {
this._IDBKeyRange = IDBKeyRange;
this._only = only; this._only = only;
this._lower = lower; this._lower = lower;
this._upper = upper; this._upper = upper;
@ -45,12 +46,12 @@ class Range {
try { try {
// only // only
if (this._only) { if (this._only) {
return IDBKeyRange.only(encodeKey(roomId, this._only.fragmentId, this._only.eventIndex)); return this._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 this._IDBKeyRange.bound(
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex), encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
encodeKey(roomId, this._lower.fragmentId, KeyLimits.maxStorageKey), encodeKey(roomId, this._lower.fragmentId, KeyLimits.maxStorageKey),
this._lowerOpen, this._lowerOpen,
@ -60,7 +61,7 @@ class Range {
// upperBound // upperBound
// 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 this._IDBKeyRange.bound(
encodeKey(roomId, this._upper.fragmentId, KeyLimits.minStorageKey), encodeKey(roomId, this._upper.fragmentId, KeyLimits.minStorageKey),
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex), encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
false, false,
@ -69,7 +70,7 @@ class Range {
} }
// bound // bound
if (this._lower && this._upper) { if (this._lower && this._upper) {
return IDBKeyRange.bound( return this._IDBKeyRange.bound(
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex), encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex), encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
this._lowerOpen, this._lowerOpen,
@ -107,7 +108,7 @@ export class TimelineEventStore {
* @return {Range} the created range * @return {Range} the created range
*/ */
onlyRange(eventKey) { onlyRange(eventKey) {
return new Range(eventKey); return new Range(this._timelineStore.IDBKeyRange, eventKey);
} }
/** Creates a range that includes all keys before eventKey, and optionally also the key itself. /** Creates a range that includes all keys before eventKey, and optionally also the key itself.
@ -116,7 +117,7 @@ export class TimelineEventStore {
* @return {Range} the created range * @return {Range} the created range
*/ */
upperBoundRange(eventKey, open=false) { upperBoundRange(eventKey, open=false) {
return new Range(undefined, undefined, eventKey, undefined, open); return new Range(this._timelineStore.IDBKeyRange, undefined, undefined, eventKey, undefined, open);
} }
/** Creates a range that includes all keys after eventKey, and optionally also the key itself. /** Creates a range that includes all keys after eventKey, and optionally also the key itself.
@ -125,7 +126,7 @@ export class TimelineEventStore {
* @return {Range} the created range * @return {Range} the created range
*/ */
lowerBoundRange(eventKey, open=false) { lowerBoundRange(eventKey, open=false) {
return new Range(undefined, eventKey, undefined, open); return new Range(this._timelineStore.IDBKeyRange, undefined, eventKey, undefined, open);
} }
/** Creates a range that includes all keys between `lower` and `upper`, and optionally the given keys as well. /** Creates a range that includes all keys between `lower` and `upper`, and optionally the given keys as well.
@ -136,7 +137,7 @@ export class TimelineEventStore {
* @return {Range} the created range * @return {Range} the created range
*/ */
boundRange(lower, upper, lowerOpen=false, upperOpen=false) { boundRange(lower, upper, lowerOpen=false, upperOpen=false) {
return new Range(undefined, lower, upper, lowerOpen, upperOpen); return new Range(this._timelineStore.IDBKeyRange, undefined, lower, upper, lowerOpen, upperOpen);
} }
/** Looks up the last `amount` entries in the timeline for `roomId`. /** Looks up the last `amount` entries in the timeline for `roomId`.
@ -261,7 +262,7 @@ export class TimelineEventStore {
removeAllForRoom(roomId) { removeAllForRoom(roomId) {
const minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey); const minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey);
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey); const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey);
const range = IDBKeyRange.bound(minKey, maxKey); const range = this._timelineStore.IDBKeyRange.bound(minKey, maxKey);
this._timelineStore.delete(range); this._timelineStore.delete(range);
} }
} }

View file

@ -29,7 +29,7 @@ export class TimelineFragmentStore {
_allRange(roomId) { _allRange(roomId) {
try { try {
return IDBKeyRange.bound( return this._store.IDBKeyRange.bound(
encodeKey(roomId, KeyLimits.minStorageKey), encodeKey(roomId, KeyLimits.minStorageKey),
encodeKey(roomId, KeyLimits.maxStorageKey) encodeKey(roomId, KeyLimits.maxStorageKey)
); );

View file

@ -64,8 +64,8 @@ export function decodeUint32(str) {
return parseInt(str, 16); return parseInt(str, 16);
} }
export function openDatabase(name, createObjectStore, version) { export function openDatabase(name, createObjectStore, version, idbFactory = window.indexedDB) {
const req = indexedDB.open(name, version); const req = idbFactory.open(name, version);
req.onupgradeneeded = (ev) => { req.onupgradeneeded = (ev) => {
const db = ev.target.result; const db = ev.target.result;
const txn = ev.target.transaction; const txn = ev.target.transaction;