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";
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);
}

View file

@ -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);
}
}

View file

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

View file

@ -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) {

View file

@ -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

View file

@ -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);
}
}

View file

@ -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)
);

View file

@ -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)
);

View file

@ -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;
});

View file

@ -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)
);

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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)
);

View file

@ -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;