From 98a6d82820413b75306246cdc92e86ab208583aa Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 28 Sep 2020 14:51:41 +0200 Subject: [PATCH] detect when sync promise hack is needed --- src/legacy-polyfill.js | 5 ++++ src/matrix/storage/idb/utils.js | 47 +++++++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/legacy-polyfill.js b/src/legacy-polyfill.js index 2a62d228..1bd7b793 100644 --- a/src/legacy-polyfill.js +++ b/src/legacy-polyfill.js @@ -27,6 +27,7 @@ import "mdn-polyfills/Element.prototype.closest"; // weighing a good extra 500kb :-( import "text-encoding"; import Promise from "../lib/es6-promise/lib/es6-promise/promise.js"; +import {checkNeedsSyncPromise} from "./matrix/storage/idb/utils.js"; const flush = Promise._flush; Promise._flush = function() { @@ -35,6 +36,8 @@ Promise._flush = function() { } window.Promise = Promise; +// TODO: should be awaited before opening any session in the picker +checkNeedsSyncPromise(); // TODO: contribute this to mdn-polyfills if (!Element.prototype.remove) { @@ -42,3 +45,5 @@ if (!Element.prototype.remove) { this.parentNode.removeChild(this); }; } + + diff --git a/src/matrix/storage/idb/utils.js b/src/matrix/storage/idb/utils.js index 8f140d52..70196884 100644 --- a/src/matrix/storage/idb/utils.js +++ b/src/matrix/storage/idb/utils.js @@ -16,6 +16,36 @@ limitations under the License. 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; +} + class IDBRequestError extends StorageError { constructor(request) { const source = request?.source; @@ -38,7 +68,7 @@ export function decodeUint32(str) { } export function openDatabase(name, createObjectStore, version) { - const req = window.indexedDB.open(name, version); + const req = indexedDB.open(name, version); req.onupgradeneeded = (ev) => { const db = ev.target.result; const txn = ev.target.transaction; @@ -52,11 +82,11 @@ export function reqAsPromise(req) { return new Promise((resolve, reject) => { req.addEventListener("success", event => { resolve(event.target.result); - Promise._flush && Promise._flush(); + needsSyncPromise && Promise._flush && Promise._flush(); }); req.addEventListener("error", () => { reject(new IDBRequestError(req)); - Promise._flush && Promise._flush(); + needsSyncPromise && Promise._flush && Promise._flush(); }); }); } @@ -65,11 +95,11 @@ export function txnAsPromise(txn) { return new Promise((resolve, reject) => { txn.addEventListener("complete", () => { resolve(); - Promise._flush && Promise._flush(); + needsSyncPromise && Promise._flush && Promise._flush(); }); txn.addEventListener("abort", () => { reject(new IDBRequestError(txn)); - Promise._flush && Promise._flush(); + needsSyncPromise && Promise._flush && Promise._flush(); }); }); } @@ -79,23 +109,24 @@ export function iterateCursor(cursorRequest, processValue) { return new Promise((resolve, reject) => { cursorRequest.onerror = () => { reject(new IDBRequestError(cursorRequest)); - Promise._flush && Promise._flush(); + needsSyncPromise && Promise._flush && Promise._flush(); }; // collect results cursorRequest.onsuccess = (event) => { const cursor = event.target.result; if (!cursor) { resolve(false); - Promise._flush && Promise._flush(); + needsSyncPromise && Promise._flush && Promise._flush(); return; // end of results } const result = processValue(cursor.value, cursor.key); + // TODO: don't use object for result and assume it's jumpTo when not === true/false or undefined const done = result?.done; const jumpTo = result?.jumpTo; if (done) { resolve(true); - Promise._flush && Promise._flush(); + needsSyncPromise && Promise._flush && Promise._flush(); } else if(jumpTo) { cursor.continue(jumpTo); } else {