detect when sync promise hack is needed

This commit is contained in:
Bruno Windels 2020-09-28 14:51:41 +02:00
parent 706ec97296
commit 98a6d82820
2 changed files with 44 additions and 8 deletions

View file

@ -27,6 +27,7 @@ import "mdn-polyfills/Element.prototype.closest";
// weighing a good extra 500kb :-( // weighing a good extra 500kb :-(
import "text-encoding"; import "text-encoding";
import Promise from "../lib/es6-promise/lib/es6-promise/promise.js"; import Promise from "../lib/es6-promise/lib/es6-promise/promise.js";
import {checkNeedsSyncPromise} from "./matrix/storage/idb/utils.js";
const flush = Promise._flush; const flush = Promise._flush;
Promise._flush = function() { Promise._flush = function() {
@ -35,6 +36,8 @@ Promise._flush = function() {
} }
window.Promise = Promise; window.Promise = Promise;
// TODO: should be awaited before opening any session in the picker
checkNeedsSyncPromise();
// TODO: contribute this to mdn-polyfills // TODO: contribute this to mdn-polyfills
if (!Element.prototype.remove) { if (!Element.prototype.remove) {
@ -42,3 +45,5 @@ if (!Element.prototype.remove) {
this.parentNode.removeChild(this); this.parentNode.removeChild(this);
}; };
} }

View file

@ -16,6 +16,36 @@ limitations under the License.
import { StorageError } from "../common.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;
}
class IDBRequestError extends StorageError { class IDBRequestError extends StorageError {
constructor(request) { constructor(request) {
const source = request?.source; const source = request?.source;
@ -38,7 +68,7 @@ export function decodeUint32(str) {
} }
export function openDatabase(name, createObjectStore, version) { export function openDatabase(name, createObjectStore, version) {
const req = window.indexedDB.open(name, version); const req = indexedDB.open(name, version);
req.onupgradeneeded = (ev) => { req.onupgradeneeded = (ev) => {
const db = ev.target.result; const db = ev.target.result;
const txn = ev.target.transaction; const txn = ev.target.transaction;
@ -52,11 +82,11 @@ export function reqAsPromise(req) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
req.addEventListener("success", event => { req.addEventListener("success", event => {
resolve(event.target.result); resolve(event.target.result);
Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
}); });
req.addEventListener("error", () => { req.addEventListener("error", () => {
reject(new IDBRequestError(req)); 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) => { return new Promise((resolve, reject) => {
txn.addEventListener("complete", () => { txn.addEventListener("complete", () => {
resolve(); resolve();
Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
}); });
txn.addEventListener("abort", () => { txn.addEventListener("abort", () => {
reject(new IDBRequestError(txn)); 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) => { return new Promise((resolve, reject) => {
cursorRequest.onerror = () => { cursorRequest.onerror = () => {
reject(new IDBRequestError(cursorRequest)); reject(new IDBRequestError(cursorRequest));
Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
}; };
// collect results // collect results
cursorRequest.onsuccess = (event) => { cursorRequest.onsuccess = (event) => {
const cursor = event.target.result; const cursor = event.target.result;
if (!cursor) { if (!cursor) {
resolve(false); resolve(false);
Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
return; // end of results return; // end of results
} }
const result = processValue(cursor.value, cursor.key); 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 done = result?.done;
const jumpTo = result?.jumpTo; const jumpTo = result?.jumpTo;
if (done) { if (done) {
resolve(true); resolve(true);
Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
} else if(jumpTo) { } else if(jumpTo) {
cursor.continue(jumpTo); cursor.continue(jumpTo);
} else { } else {