2020-08-05 18:38:55 +02:00
|
|
|
/*
|
|
|
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
2020-09-28 15:28:51 +02:00
|
|
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
2020-08-05 18:38:55 +02:00
|
|
|
|
|
|
|
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 09:17:03 +02:00
|
|
|
import { IDBRequestError } from "./error.js";
|
2021-08-09 22:44:07 +02:00
|
|
|
import { StorageError } from "../common";
|
2019-06-26 22:00:50 +02:00
|
|
|
|
2020-09-28 14:51:41 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-10-26 10:34:35 +01:00
|
|
|
// storage keys are defined to be unsigned 32bit numbers in KeyLimits, which is assumed by idb
|
2019-07-01 10:00:29 +02:00
|
|
|
export function encodeUint32(n) {
|
|
|
|
const hex = n.toString(16);
|
|
|
|
return "0".repeat(8 - hex.length) + hex;
|
|
|
|
}
|
|
|
|
|
2021-02-12 13:04:05 +01:00
|
|
|
// 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 10:00:29 +02:00
|
|
|
export function decodeUint32(str) {
|
|
|
|
return parseInt(str, 16);
|
|
|
|
}
|
|
|
|
|
2021-06-02 12:31:13 +02:00
|
|
|
export function openDatabase(name, createObjectStore, version, idbFactory = window.indexedDB) {
|
|
|
|
const req = idbFactory.open(name, version);
|
2019-02-06 23:06:56 +01:00
|
|
|
req.onupgradeneeded = (ev) => {
|
|
|
|
const db = ev.target.result;
|
2020-06-26 23:26:24 +02:00
|
|
|
const txn = ev.target.transaction;
|
2019-02-06 23:06:56 +01:00
|
|
|
const oldVersion = ev.oldVersion;
|
2020-06-26 23:26:24 +02:00
|
|
|
createObjectStore(db, txn, oldVersion, version);
|
2019-02-06 23:06:56 +01:00
|
|
|
};
|
|
|
|
return reqAsPromise(req);
|
2019-01-09 11:06:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export function reqAsPromise(req) {
|
|
|
|
return new Promise((resolve, reject) => {
|
2020-09-25 16:53:19 +02:00
|
|
|
req.addEventListener("success", event => {
|
|
|
|
resolve(event.target.result);
|
2020-09-28 14:51:41 +02:00
|
|
|
needsSyncPromise && Promise._flush && Promise._flush();
|
2020-09-25 16:53:19 +02:00
|
|
|
});
|
2021-05-05 14:36:43 +02:00
|
|
|
req.addEventListener("error", event => {
|
|
|
|
const error = new IDBRequestError(event.target);
|
|
|
|
reject(error);
|
2020-09-28 14:51:41 +02:00
|
|
|
needsSyncPromise && Promise._flush && Promise._flush();
|
2020-09-25 16:53:19 +02:00
|
|
|
});
|
2019-01-09 11:06:09 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
export function txnAsPromise(txn) {
|
2021-05-05 16:02:39 +02:00
|
|
|
let error;
|
2019-01-09 11:06:09 +01:00
|
|
|
return new Promise((resolve, reject) => {
|
2020-09-25 16:53:19 +02:00
|
|
|
txn.addEventListener("complete", () => {
|
|
|
|
resolve();
|
2020-09-28 14:51:41 +02:00
|
|
|
needsSyncPromise && Promise._flush && Promise._flush();
|
2020-09-25 16:53:19 +02:00
|
|
|
});
|
2021-05-05 16:02:39 +02:00
|
|
|
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);
|
2020-09-28 14:51:41 +02:00
|
|
|
needsSyncPromise && Promise._flush && Promise._flush();
|
2020-09-25 16:53:19 +02:00
|
|
|
});
|
2019-01-09 11:06:09 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-09-15 12:23:54 +02:00
|
|
|
export function iterateCursor(cursorRequest, processValue) {
|
2019-02-06 23:06:56 +01:00
|
|
|
// TODO: does cursor already have a value here??
|
2019-01-09 11:06:09 +01:00
|
|
|
return new Promise((resolve, reject) => {
|
2019-09-15 12:23:54 +02:00
|
|
|
cursorRequest.onerror = () => {
|
2020-09-25 16:53:19 +02:00
|
|
|
reject(new IDBRequestError(cursorRequest));
|
2020-09-28 14:51:41 +02:00
|
|
|
needsSyncPromise && Promise._flush && Promise._flush();
|
2019-01-09 11:06:09 +01:00
|
|
|
};
|
|
|
|
// collect results
|
2019-09-15 12:23:54 +02:00
|
|
|
cursorRequest.onsuccess = (event) => {
|
2019-01-09 11:06:09 +01:00
|
|
|
const cursor = event.target.result;
|
|
|
|
if (!cursor) {
|
|
|
|
resolve(false);
|
2020-09-28 14:51:41 +02:00
|
|
|
needsSyncPromise && Promise._flush && Promise._flush();
|
2019-01-09 11:06:09 +01:00
|
|
|
return; // end of results
|
|
|
|
}
|
2021-02-11 21:07:18 +01:00
|
|
|
const result = processValue(cursor.value, cursor.key, cursor);
|
2020-09-28 14:51:41 +02:00
|
|
|
// TODO: don't use object for result and assume it's jumpTo when not === true/false or undefined
|
2020-08-19 18:25:38 +02:00
|
|
|
const done = result?.done;
|
|
|
|
const jumpTo = result?.jumpTo;
|
2020-06-26 23:26:24 +02:00
|
|
|
|
2019-05-11 13:10:31 +02:00
|
|
|
if (done) {
|
2019-02-06 23:06:56 +01:00
|
|
|
resolve(true);
|
2020-09-28 14:51:41 +02:00
|
|
|
needsSyncPromise && Promise._flush && Promise._flush();
|
2019-06-26 22:02:00 +02:00
|
|
|
} else if(jumpTo) {
|
2019-05-11 13:10:31 +02:00
|
|
|
cursor.continue(jumpTo);
|
2019-06-26 22:02:00 +02:00
|
|
|
} else {
|
|
|
|
cursor.continue();
|
2019-01-09 11:06:09 +01:00
|
|
|
}
|
|
|
|
};
|
2019-06-26 22:00:50 +02:00
|
|
|
}).catch(err => {
|
|
|
|
throw new StorageError("iterateCursor failed", err);
|
2019-01-09 11:06:09 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function fetchResults(cursor, isDone) {
|
|
|
|
const results = [];
|
|
|
|
await iterateCursor(cursor, (value) => {
|
2019-02-06 23:06:56 +01:00
|
|
|
results.push(value);
|
2019-05-11 13:10:31 +02:00
|
|
|
return {done: isDone(results)};
|
2019-01-09 11:06:09 +01:00
|
|
|
});
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function select(db, storeName, toCursor, isDone) {
|
2019-02-06 23:06:56 +01:00
|
|
|
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);
|
2019-01-09 11:06:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function findStoreValue(db, storeName, toCursor, matchesValue) {
|
2019-02-06 23:06:56 +01:00
|
|
|
if (!matchesValue) {
|
|
|
|
matchesValue = () => true;
|
|
|
|
}
|
|
|
|
if (!toCursor) {
|
|
|
|
toCursor = store => store.openCursor();
|
|
|
|
}
|
2019-01-09 11:06:09 +01:00
|
|
|
|
2019-02-06 23:06:56 +01:00
|
|
|
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) {
|
2019-06-26 22:00:50 +02:00
|
|
|
throw new StorageError("Value not found");
|
2019-02-06 23:06:56 +01:00
|
|
|
}
|
|
|
|
return match;
|
2019-05-11 13:10:31 +02:00
|
|
|
}
|