hydrogen-web/src/matrix/storage/idb/utils.js

199 lines
6.8 KiB
JavaScript
Raw Normal View History

2020-08-05 22:08:55 +05:30
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
2020-09-28 18:58:51 +05:30
Copyright 2020 The Matrix.org Foundation C.I.C.
2020-08-05 22:08:55 +05:30
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
2020-09-29 12:47:03 +05:30
import { IDBRequestError } from "./error.js";
import { StorageError } from "../common.js";
let needsSyncPromise = false;
/* should be called on legacy platforms to see
if transactions close before draining the microtask queue (IE11 on Windows 7).
If this is the case, promises need to be resolved
synchronously from the idb request handler to prevent the transaction from closing prematurely.
*/
export async function checkNeedsSyncPromise() {
// important to have it turned off while doing the test,
// otherwise reqAsPromise would not fail
needsSyncPromise = false;
const NAME = "test-idb-needs-sync-promise";
const db = await openDatabase(NAME, db => {
db.createObjectStore("test", {keyPath: "key"});
}, 1);
const txn = db.transaction("test", "readonly");
try {
await reqAsPromise(txn.objectStore("test").get(1));
await reqAsPromise(txn.objectStore("test").get(2));
} catch (err) {
// err.name would be either TransactionInactiveError or InvalidStateError,
// but let's not exclude any other failure modes
needsSyncPromise = true;
}
// we could delete the store here,
// but let's not create it on every page load on legacy platforms,
// and just keep it around
return needsSyncPromise;
}
// storage keys are defined to be unsigned 32bit numbers in KeyLimits, which is assumed by idb
2019-07-01 13:30:29 +05:30
export function encodeUint32(n) {
const hex = n.toString(16);
return "0".repeat(8 - hex.length) + hex;
}
2021-02-12 17:34:05 +05:30
// used for logs where timestamp is part of key, which is larger than 32 bit
export function encodeUint64(n) {
const hex = n.toString(16);
return "0".repeat(16 - hex.length) + hex;
}
2019-07-01 13:30:29 +05:30
export function decodeUint32(str) {
return parseInt(str, 16);
}
export function openDatabase(name, createObjectStore, version, idbFactory = window.indexedDB) {
const req = idbFactory.open(name, version);
2019-02-07 03:36:56 +05:30
req.onupgradeneeded = (ev) => {
const db = ev.target.result;
2020-06-27 02:56:24 +05:30
const txn = ev.target.transaction;
2019-02-07 03:36:56 +05:30
const oldVersion = ev.oldVersion;
2020-06-27 02:56:24 +05:30
createObjectStore(db, txn, oldVersion, version);
2019-02-07 03:36:56 +05:30
};
return reqAsPromise(req);
}
export function reqAsPromise(req) {
return new Promise((resolve, reject) => {
req.addEventListener("success", event => {
resolve(event.target.result);
needsSyncPromise && Promise._flush && Promise._flush();
});
2021-05-05 18:06:43 +05:30
req.addEventListener("error", event => {
const error = new IDBRequestError(event.target);
reject(error);
needsSyncPromise && Promise._flush && Promise._flush();
});
});
}
export function txnAsPromise(txn) {
2021-05-05 19:32:39 +05:30
let error;
return new Promise((resolve, reject) => {
txn.addEventListener("complete", () => {
resolve();
needsSyncPromise && Promise._flush && Promise._flush();
});
2021-05-05 19:32:39 +05:30
txn.addEventListener("error", event => {
const request = event.target;
// catch first error here, but don't reject yet,
// as we don't have access to the failed request in the abort event handler
if (!error && request) {
error = new IDBRequestError(request);
}
});
txn.addEventListener("abort", event => {
if (!error) {
const txn = event.target;
const dbName = txn.db.name;
const storeNames = Array.from(txn.objectStoreNames).join(", ")
error = new StorageError(`Transaction on ${dbName} with stores ${storeNames} was aborted.`);
}
reject(error);
needsSyncPromise && Promise._flush && Promise._flush();
});
});
}
export function iterateCursor(cursorRequest, processValue) {
2019-02-07 03:36:56 +05:30
// TODO: does cursor already have a value here??
return new Promise((resolve, reject) => {
cursorRequest.onerror = () => {
reject(new IDBRequestError(cursorRequest));
needsSyncPromise && Promise._flush && Promise._flush();
};
// collect results
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
resolve(false);
needsSyncPromise && Promise._flush && Promise._flush();
return; // end of results
}
2021-02-12 01:37:18 +05:30
const result = processValue(cursor.value, cursor.key, cursor);
// TODO: don't use object for result and assume it's jumpTo when not === true/false or undefined
2020-08-19 21:55:38 +05:30
const done = result?.done;
const jumpTo = result?.jumpTo;
2020-06-27 02:56:24 +05:30
if (done) {
2019-02-07 03:36:56 +05:30
resolve(true);
needsSyncPromise && Promise._flush && Promise._flush();
} else if(jumpTo) {
cursor.continue(jumpTo);
} else {
cursor.continue();
}
};
}).catch(err => {
throw new StorageError("iterateCursor failed", err);
});
}
export async function fetchResults(cursor, isDone) {
const results = [];
await iterateCursor(cursor, (value) => {
2019-02-07 03:36:56 +05:30
results.push(value);
return {done: isDone(results)};
});
return results;
}
export async function select(db, storeName, toCursor, isDone) {
2019-02-07 03:36:56 +05:30
if (!isDone) {
isDone = () => false;
}
if (!toCursor) {
toCursor = store => store.openCursor();
}
const tx = db.transaction([storeName], "readonly");
const store = tx.objectStore(storeName);
const cursor = toCursor(store);
return await fetchResults(cursor, isDone);
}
export async function findStoreValue(db, storeName, toCursor, matchesValue) {
2019-02-07 03:36:56 +05:30
if (!matchesValue) {
matchesValue = () => true;
}
if (!toCursor) {
toCursor = store => store.openCursor();
}
2019-02-07 03:36:56 +05:30
const tx = db.transaction([storeName], "readwrite");
const store = tx.objectStore(storeName);
const cursor = await reqAsPromise(toCursor(store));
let match;
const matched = await iterateCursor(cursor, (value) => {
if (matchesValue(value)) {
match = value;
return true;
}
});
if (!matched) {
throw new StorageError("Value not found");
2019-02-07 03:36:56 +05:30
}
return match;
}