diff --git a/src/matrix/storage/idb/Storage.js b/src/matrix/storage/idb/Storage.js index 93d55f34..1f96e347 100644 --- a/src/matrix/storage/idb/Storage.js +++ b/src/matrix/storage/idb/Storage.js @@ -21,8 +21,9 @@ import { reqAsPromise } from "./utils.js"; const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey"; export class Storage { - constructor(idbDatabase, hasWebkitEarlyCloseTxnBug) { + constructor(idbDatabase, IDBKeyRange, hasWebkitEarlyCloseTxnBug) { this._db = idbDatabase; + this._IDBKeyRange = IDBKeyRange; this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug; const nameMap = STORE_NAMES.reduce((nameMap, name) => { nameMap[name] = name; @@ -47,7 +48,7 @@ export class Storage { if (this._hasWebkitEarlyCloseTxnBug) { await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY)); } - return new Transaction(txn, storeNames); + return new Transaction(txn, storeNames, this._IDBKeyRange); } catch(err) { throw new StorageError("readTxn failed", err); } @@ -62,7 +63,7 @@ export class Storage { if (this._hasWebkitEarlyCloseTxnBug) { await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY)); } - return new Transaction(txn, storeNames); + return new Transaction(txn, storeNames, this._IDBKeyRange); } catch(err) { throw new StorageError("readWriteTxn failed", err); } diff --git a/src/matrix/storage/idb/StorageFactory.js b/src/matrix/storage/idb/StorageFactory.js index 13860f67..719d2672 100644 --- a/src/matrix/storage/idb/StorageFactory.js +++ b/src/matrix/storage/idb/StorageFactory.js @@ -21,14 +21,18 @@ import { schema } from "./schema.js"; import { detectWebkitEarlyCloseTxnBug } from "./quirks.js"; 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() { - if (navigator?.storage?.persist) { - return await navigator.storage.persist(); - } else if (document.requestStorageAccess) { + // don't assume browser so we can run in node with fake-idb + const glob = this; + if (glob?.navigator?.storage?.persist) { + return await glob.navigator.storage.persist(); + } else if (glob?.document.requestStorageAccess) { try { - await document.requestStorageAccess(); + await glob.document.requestStorageAccess(); return true; } catch (err) { return false; @@ -39,8 +43,10 @@ async function requestPersistedStorage() { } export class StorageFactory { - constructor(serviceWorkerHandler) { + constructor(serviceWorkerHandler, idbFactory = window.indexedDB, IDBKeyRange = window.IDBKeyRange) { this._serviceWorkerHandler = serviceWorkerHandler; + this._idbFactory = idbFactory; + this._IDBKeyRange = IDBKeyRange; } async create(sessionId) { @@ -52,24 +58,24 @@ export class StorageFactory { } }); - const hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug(); - const db = await openDatabaseWithSessionId(sessionId); - return new Storage(db, hasWebkitEarlyCloseTxnBug); + const hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug(this._idbFactory); + const db = await openDatabaseWithSessionId(sessionId, this._idbFactory); + return new Storage(db, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug); } delete(sessionId) { const databaseName = sessionName(sessionId); - const req = indexedDB.deleteDatabase(databaseName); + const req = this._idbFactory.deleteDatabase(databaseName); return reqAsPromise(req); } async export(sessionId) { - const db = await openDatabaseWithSessionId(sessionId); + const db = await openDatabaseWithSessionId(sessionId, this._idbFactory); return await exportSession(db); } async import(sessionId, data) { - const db = await openDatabaseWithSessionId(sessionId); + const db = await openDatabaseWithSessionId(sessionId, this._idbFactory); return await importSession(db, data); } } diff --git a/src/matrix/storage/idb/Store.js b/src/matrix/storage/idb/Store.js index 22851af0..2cafe500 100644 --- a/src/matrix/storage/idb/Store.js +++ b/src/matrix/storage/idb/Store.js @@ -126,6 +126,10 @@ export class Store extends QueryTarget { this._transaction = transaction; } + get IDBKeyRange() { + return this._transaction.IDBKeyRange; + } + get _idbStore() { return this._target; } diff --git a/src/matrix/storage/idb/Transaction.js b/src/matrix/storage/idb/Transaction.js index d497077b..a2041d31 100644 --- a/src/matrix/storage/idb/Transaction.js +++ b/src/matrix/storage/idb/Transaction.js @@ -35,10 +35,11 @@ import {OperationStore} from "./stores/OperationStore.js"; import {AccountDataStore} from "./stores/AccountDataStore.js"; export class Transaction { - constructor(txn, allowedStoreNames) { + constructor(txn, allowedStoreNames, IDBKeyRange) { this._txn = txn; this._allowedStoreNames = allowedStoreNames; this._stores = {}; + this.IDBKeyRange = IDBKeyRange; } _idbStore(name) { @@ -46,7 +47,7 @@ export class Transaction { // more specific error? this is a bug, so maybe not ... 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) { diff --git a/src/matrix/storage/idb/quirks.js b/src/matrix/storage/idb/quirks.js index 8f4c1826..9739b7da 100644 --- a/src/matrix/storage/idb/quirks.js +++ b/src/matrix/storage/idb/quirks.js @@ -18,12 +18,12 @@ limitations under the License. import {openDatabase, txnAsPromise, reqAsPromise} from "./utils.js"; // 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"; try { const db = await openDatabase(dbName, db => { db.createObjectStore("test", {keyPath: "key"}); - }, 1); + }, 1, idbFactory); const readTxn = db.transaction(["test"], "readonly"); await reqAsPromise(readTxn.objectStore("test").get("somekey")); // schedule a macro task in between the two txns diff --git a/src/matrix/storage/idb/stores/DeviceIdentityStore.js b/src/matrix/storage/idb/stores/DeviceIdentityStore.js index fed8878b..bfc5b30b 100644 --- a/src/matrix/storage/idb/stores/DeviceIdentityStore.js +++ b/src/matrix/storage/idb/stores/DeviceIdentityStore.js @@ -31,7 +31,7 @@ export class DeviceIdentityStore { } getAllForUserId(userId) { - const range = IDBKeyRange.lowerBound(encodeKey(userId, "")); + const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, "")); return this._store.selectWhile(range, device => { return device.userId === userId; }); @@ -39,7 +39,7 @@ export class DeviceIdentityStore { async getAllDeviceIds(userId) { const deviceIds = []; - const range = IDBKeyRange.lowerBound(encodeKey(userId, "")); + const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, "")); await this._store.iterateKeys(range, key => { const decodedKey = decodeKey(key); // prevent running into the next room @@ -72,7 +72,7 @@ export class DeviceIdentityStore { removeAllForUser(userId) { // 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 - 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); } } diff --git a/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.js b/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.js index df3c6fbe..f3a721f1 100644 --- a/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.js +++ b/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.js @@ -35,7 +35,7 @@ export class GroupSessionDecryptionStore { } removeAllForRoom(roomId) { - const range = IDBKeyRange.bound( + const range = this._store.IDBKeyRange.bound( encodeKey(roomId, MIN_UNICODE, MIN_UNICODE), encodeKey(roomId, MAX_UNICODE, MAX_UNICODE) ); diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.js b/src/matrix/storage/idb/stores/InboundGroupSessionStore.js index 928f7572..f5dcdc39 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.js +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.js @@ -41,7 +41,7 @@ export class InboundGroupSessionStore { } removeAllForRoom(roomId) { - const range = IDBKeyRange.bound( + const range = this._store.IDBKeyRange.bound( encodeKey(roomId, MIN_UNICODE, MIN_UNICODE), encodeKey(roomId, MAX_UNICODE, MAX_UNICODE) ); diff --git a/src/matrix/storage/idb/stores/OlmSessionStore.js b/src/matrix/storage/idb/stores/OlmSessionStore.js index 4648f09c..d81cc048 100644 --- a/src/matrix/storage/idb/stores/OlmSessionStore.js +++ b/src/matrix/storage/idb/stores/OlmSessionStore.js @@ -30,7 +30,7 @@ export class OlmSessionStore { async getSessionIds(senderKey) { const sessionIds = []; - const range = IDBKeyRange.lowerBound(encodeKey(senderKey, "")); + const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, "")); await this._store.iterateKeys(range, key => { const decodedKey = decodeKey(key); // prevent running into the next room @@ -44,7 +44,7 @@ export class OlmSessionStore { } getAll(senderKey) { - const range = IDBKeyRange.lowerBound(encodeKey(senderKey, "")); + const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, "")); return this._store.selectWhile(range, session => { return session.senderKey === senderKey; }); diff --git a/src/matrix/storage/idb/stores/OperationStore.js b/src/matrix/storage/idb/stores/OperationStore.js index 09e4552f..e2040e30 100644 --- a/src/matrix/storage/idb/stores/OperationStore.js +++ b/src/matrix/storage/idb/stores/OperationStore.js @@ -55,7 +55,7 @@ export class OperationStore { } async removeAllForScope(scope) { - const range = IDBKeyRange.bound( + const range = this._store.IDBKeyRange.bound( encodeScopeTypeKey(scope, MIN_UNICODE), encodeScopeTypeKey(scope, MAX_UNICODE) ); diff --git a/src/matrix/storage/idb/stores/PendingEventStore.js b/src/matrix/storage/idb/stores/PendingEventStore.js index a6727ec5..455cad2c 100644 --- a/src/matrix/storage/idb/stores/PendingEventStore.js +++ b/src/matrix/storage/idb/stores/PendingEventStore.js @@ -33,7 +33,7 @@ export class PendingEventStore { } async getMaxQueueIndex(roomId) { - const range = IDBKeyRange.bound( + const range = this._eventStore.IDBKeyRange.bound( encodeKey(roomId, KeyLimits.minStorageKey), encodeKey(roomId, KeyLimits.maxStorageKey), false, @@ -46,12 +46,12 @@ export class PendingEventStore { } remove(roomId, queueIndex) { - const keyRange = IDBKeyRange.only(encodeKey(roomId, queueIndex)); + const keyRange = this._eventStore.IDBKeyRange.only(encodeKey(roomId, queueIndex)); this._eventStore.delete(keyRange); } 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); return !!key; } @@ -72,7 +72,7 @@ export class PendingEventStore { removeAllForRoom(roomId) { const minKey = encodeKey(roomId, KeyLimits.minStorageKey); const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey); - const range = IDBKeyRange.bound(minKey, maxKey); + const range = this._eventStore.IDBKeyRange.bound(minKey, maxKey); this._eventStore.delete(range); } } diff --git a/src/matrix/storage/idb/stores/RoomMemberStore.js b/src/matrix/storage/idb/stores/RoomMemberStore.js index 340e48a1..f900ff0b 100644 --- a/src/matrix/storage/idb/stores/RoomMemberStore.js +++ b/src/matrix/storage/idb/stores/RoomMemberStore.js @@ -42,7 +42,7 @@ export class RoomMemberStore { } getAll(roomId) { - const range = IDBKeyRange.lowerBound(encodeKey(roomId, "")); + const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, "")); return this._roomMembersStore.selectWhile(range, member => { return member.roomId === roomId; }); @@ -50,7 +50,7 @@ export class RoomMemberStore { async getAllUserIds(roomId) { const userIds = []; - const range = IDBKeyRange.lowerBound(encodeKey(roomId, "")); + const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, "")); await this._roomMembersStore.iterateKeys(range, key => { const decodedKey = decodeKey(key); // prevent running into the next room @@ -66,7 +66,7 @@ export class RoomMemberStore { removeAllForRoom(roomId) { // 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 - 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); } } diff --git a/src/matrix/storage/idb/stores/RoomStateStore.js b/src/matrix/storage/idb/stores/RoomStateStore.js index 84dd4ec8..73bde1ec 100644 --- a/src/matrix/storage/idb/stores/RoomStateStore.js +++ b/src/matrix/storage/idb/stores/RoomStateStore.js @@ -44,7 +44,7 @@ export class RoomStateStore { removeAllForRoom(roomId) { // 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 - 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); } } diff --git a/src/matrix/storage/idb/stores/TimelineEventStore.js b/src/matrix/storage/idb/stores/TimelineEventStore.js index 9c40aad2..b4b204a8 100644 --- a/src/matrix/storage/idb/stores/TimelineEventStore.js +++ b/src/matrix/storage/idb/stores/TimelineEventStore.js @@ -33,7 +33,8 @@ function decodeEventIdKey(eventIdKey) { } class Range { - constructor(only, lower, upper, lowerOpen, upperOpen) { + constructor(IDBKeyRange, only, lower, upper, lowerOpen, upperOpen) { + this._IDBKeyRange = IDBKeyRange; this._only = only; this._lower = lower; this._upper = upper; @@ -45,12 +46,12 @@ class Range { try { // 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 // also bound as we don't want to move into another roomId 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, KeyLimits.maxStorageKey), this._lowerOpen, @@ -60,7 +61,7 @@ class Range { // upperBound // also bound as we don't want to move into another roomId if (!this._lower && this._upper) { - return IDBKeyRange.bound( + return this._IDBKeyRange.bound( encodeKey(roomId, this._upper.fragmentId, KeyLimits.minStorageKey), encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex), false, @@ -69,7 +70,7 @@ class Range { } // bound if (this._lower && this._upper) { - return IDBKeyRange.bound( + return this._IDBKeyRange.bound( encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex), encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex), this._lowerOpen, @@ -107,7 +108,7 @@ export class TimelineEventStore { * @return {Range} the created range */ 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. @@ -116,7 +117,7 @@ export class TimelineEventStore { * @return {Range} the created range */ 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. @@ -125,7 +126,7 @@ export class TimelineEventStore { * @return {Range} the created range */ 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. @@ -136,7 +137,7 @@ export class TimelineEventStore { * @return {Range} the created range */ 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`. @@ -261,7 +262,7 @@ export class TimelineEventStore { removeAllForRoom(roomId) { const minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey); 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); } } diff --git a/src/matrix/storage/idb/stores/TimelineFragmentStore.js b/src/matrix/storage/idb/stores/TimelineFragmentStore.js index 10eaede1..96ff441e 100644 --- a/src/matrix/storage/idb/stores/TimelineFragmentStore.js +++ b/src/matrix/storage/idb/stores/TimelineFragmentStore.js @@ -29,7 +29,7 @@ export class TimelineFragmentStore { _allRange(roomId) { try { - return IDBKeyRange.bound( + return this._store.IDBKeyRange.bound( encodeKey(roomId, KeyLimits.minStorageKey), encodeKey(roomId, KeyLimits.maxStorageKey) ); diff --git a/src/matrix/storage/idb/utils.js b/src/matrix/storage/idb/utils.js index 2e6c2152..fbb82fc5 100644 --- a/src/matrix/storage/idb/utils.js +++ b/src/matrix/storage/idb/utils.js @@ -64,8 +64,8 @@ export function decodeUint32(str) { return parseInt(str, 16); } -export function openDatabase(name, createObjectStore, version) { - const req = indexedDB.open(name, version); +export function openDatabase(name, createObjectStore, version, idbFactory = window.indexedDB) { + const req = idbFactory.open(name, version); req.onupgradeneeded = (ev) => { const db = ev.target.result; const txn = ev.target.transaction;