flush promises manually in idb event handler
This commit is contained in:
parent
becdf656a4
commit
64290d5ae6
3 changed files with 411 additions and 6 deletions
|
@ -15,6 +15,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// polyfills needed for IE11
|
// polyfills needed for IE11
|
||||||
|
|
||||||
|
const hasNativePromise = typeof window.Promise === "function";
|
||||||
|
|
||||||
|
// TODO: don't include a polyfill for promises as we already provide one
|
||||||
import "core-js/stable";
|
import "core-js/stable";
|
||||||
import "regenerator-runtime/runtime";
|
import "regenerator-runtime/runtime";
|
||||||
import "mdn-polyfills/Element.prototype.closest";
|
import "mdn-polyfills/Element.prototype.closest";
|
||||||
|
@ -24,6 +28,12 @@ import "mdn-polyfills/Element.prototype.closest";
|
||||||
// it will also include the file supporting *all* the encodings,
|
// it will also include the file supporting *all* the encodings,
|
||||||
// weighing a good extra 500kb :-(
|
// weighing a good extra 500kb :-(
|
||||||
import "text-encoding";
|
import "text-encoding";
|
||||||
|
import {Promifill} from "./utils/Promifill.js";
|
||||||
|
|
||||||
|
// console.log("hasNativePromise", hasNativePromise);
|
||||||
|
// if (!hasNativePromise) {
|
||||||
|
window.Promise = Promifill;
|
||||||
|
// }
|
||||||
|
|
||||||
// TODO: contribute this to mdn-polyfills
|
// TODO: contribute this to mdn-polyfills
|
||||||
if (!Element.prototype.remove) {
|
if (!Element.prototype.remove) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import { StorageError } from "../common.js";
|
import { StorageError } from "../common.js";
|
||||||
|
|
||||||
class WrappedDOMException extends StorageError {
|
class IDBRequestError extends StorageError {
|
||||||
constructor(request) {
|
constructor(request) {
|
||||||
const source = request?.source;
|
const source = request?.source;
|
||||||
const storeName = source?.name || "<unknown store>";
|
const storeName = source?.name || "<unknown store>";
|
||||||
|
@ -50,15 +50,27 @@ export function openDatabase(name, createObjectStore, version) {
|
||||||
|
|
||||||
export function reqAsPromise(req) {
|
export function reqAsPromise(req) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
req.addEventListener("success", event => resolve(event.target.result));
|
req.addEventListener("success", event => {
|
||||||
req.addEventListener("error", event => reject(new WrappedDOMException(event.target)));
|
resolve(event.target.result);
|
||||||
|
Promise.flushQueue && Promise.flushQueue();
|
||||||
|
});
|
||||||
|
req.addEventListener("error", () => {
|
||||||
|
reject(new IDBRequestError(req));
|
||||||
|
Promise.flushQueue && Promise.flushQueue();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function txnAsPromise(txn) {
|
export function txnAsPromise(txn) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
txn.addEventListener("complete", resolve);
|
txn.addEventListener("complete", () => {
|
||||||
txn.addEventListener("abort", event => reject(new WrappedDOMException(event.target)));
|
resolve();
|
||||||
|
Promise.flushQueue && Promise.flushQueue();
|
||||||
|
});
|
||||||
|
txn.addEventListener("abort", () => {
|
||||||
|
reject(new IDBRequestError(txn));
|
||||||
|
Promise.flushQueue && Promise.flushQueue();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,13 +78,15 @@ export function iterateCursor(cursorRequest, processValue) {
|
||||||
// TODO: does cursor already have a value here??
|
// TODO: does cursor already have a value here??
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
cursorRequest.onerror = () => {
|
cursorRequest.onerror = () => {
|
||||||
reject(new StorageError("Query failed", cursorRequest.error));
|
reject(new IDBRequestError(cursorRequest));
|
||||||
|
Promise.flushQueue && Promise.flushQueue();
|
||||||
};
|
};
|
||||||
// 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.flushQueue && Promise.flushQueue();
|
||||||
return; // end of results
|
return; // end of results
|
||||||
}
|
}
|
||||||
const result = processValue(cursor.value, cursor.key);
|
const result = processValue(cursor.value, cursor.key);
|
||||||
|
@ -81,6 +95,7 @@ export function iterateCursor(cursorRequest, processValue) {
|
||||||
|
|
||||||
if (done) {
|
if (done) {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
|
Promise.flushQueue && Promise.flushQueue();
|
||||||
} else if(jumpTo) {
|
} else if(jumpTo) {
|
||||||
cursor.continue(jumpTo);
|
cursor.continue(jumpTo);
|
||||||
} else {
|
} else {
|
||||||
|
|
380
src/utils/Promifill.js
Normal file
380
src/utils/Promifill.js
Normal file
|
@ -0,0 +1,380 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const [PENDING, FULFILLED, REJECTED] =
|
||||||
|
[void 0, true, false];
|
||||||
|
|
||||||
|
export class Promifill {
|
||||||
|
get state () {
|
||||||
|
return PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
get value () {
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get settled () {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (executor) {
|
||||||
|
if (typeof executor != "function") {
|
||||||
|
throw new TypeError(`Promise resolver ${Object.prototype.toString.call(executor)} is not a function`);
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProperty(this, "chain", []);
|
||||||
|
defineProperty(this, "observers", []);
|
||||||
|
|
||||||
|
const secret = [];
|
||||||
|
|
||||||
|
const resolve =
|
||||||
|
(value, bypassKey) => {
|
||||||
|
if (this.settled && bypassKey !== secret) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProperty(this, "settled", true);
|
||||||
|
|
||||||
|
const then_ = value && value.then;
|
||||||
|
const thenable = typeof then_ == "function";
|
||||||
|
|
||||||
|
if (thenable) {
|
||||||
|
defineProperty(value, "preventThrow", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thenable && value.state === PENDING) {
|
||||||
|
then_.call(
|
||||||
|
value,
|
||||||
|
(v) =>
|
||||||
|
resolve(v, secret),
|
||||||
|
(r) =>
|
||||||
|
reject(r, secret)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
defineProperty(this, "value",
|
||||||
|
thenable
|
||||||
|
? value.value
|
||||||
|
: value);
|
||||||
|
defineProperty(this, "state",
|
||||||
|
thenable
|
||||||
|
? value.state
|
||||||
|
: FULFILLED);
|
||||||
|
|
||||||
|
schedule(
|
||||||
|
this.observers.map((observer) => (
|
||||||
|
{
|
||||||
|
handler: this.state === FULFILLED
|
||||||
|
? observer.onfulfill
|
||||||
|
: observer.onreject,
|
||||||
|
value: this.value
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.state === REJECTED) {
|
||||||
|
raiseUnhandledPromiseRejectionException(this.value, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reject =
|
||||||
|
(reason, bypassKey) => {
|
||||||
|
if (this.settled && bypassKey !== secret) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProperty(this, "settled", true);
|
||||||
|
|
||||||
|
defineProperty(this, "value", reason);
|
||||||
|
defineProperty(this, "state", REJECTED);
|
||||||
|
|
||||||
|
schedule(
|
||||||
|
this.observers.map((observer) => (
|
||||||
|
{
|
||||||
|
handler: observer.onreject,
|
||||||
|
value: this.value
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
raiseUnhandledPromiseRejectionException(this.value, this);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
executor(resolve, reject);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
then (onfulfill, onreject) {
|
||||||
|
const chainedPromise = new this.constructor((resolve, reject) => {
|
||||||
|
const internalOnfulfill =
|
||||||
|
(value) => {
|
||||||
|
try {
|
||||||
|
resolve(
|
||||||
|
typeof onfulfill == "function"
|
||||||
|
? onfulfill(value)
|
||||||
|
: value
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const internalOnreject =
|
||||||
|
(reason) => {
|
||||||
|
try {
|
||||||
|
if (typeof onreject == "function") {
|
||||||
|
resolve(onreject(reason));
|
||||||
|
} else {
|
||||||
|
reject(reason);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.state === PENDING) {
|
||||||
|
this.observers.push({ onfulfill: internalOnfulfill, onreject: internalOnreject });
|
||||||
|
} else {
|
||||||
|
schedule(
|
||||||
|
[{
|
||||||
|
handler: this.state === FULFILLED
|
||||||
|
? internalOnfulfill
|
||||||
|
: internalOnreject,
|
||||||
|
value: this.value
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.chain.push(chainedPromise);
|
||||||
|
return chainedPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (onreject) {
|
||||||
|
return this.then(null, onreject);
|
||||||
|
}
|
||||||
|
|
||||||
|
finally (oncomplete) {
|
||||||
|
const chainedPromise = new this.constructor((resolve, reject) => {
|
||||||
|
const internalOncomplete =
|
||||||
|
() => {
|
||||||
|
try {
|
||||||
|
oncomplete();
|
||||||
|
if (this.state === FULFILLED) {
|
||||||
|
resolve(this.value);
|
||||||
|
} else {
|
||||||
|
reject(this.value);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.state === PENDING) {
|
||||||
|
this.observers.push({ onfulfill: internalOncomplete, onreject: internalOncomplete });
|
||||||
|
} else {
|
||||||
|
schedule([{
|
||||||
|
handler: internalOncomplete
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.chain.push(chainedPromise);
|
||||||
|
return chainedPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
static resolve (value) {
|
||||||
|
return value && value.constructor === Promifill
|
||||||
|
? value
|
||||||
|
: new Promifill((resolve) => {
|
||||||
|
resolve(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static reject (reason) {
|
||||||
|
return new Promifill((_, reject) => {
|
||||||
|
reject(reason);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static all (iterable) {
|
||||||
|
return new Promifill((resolve, reject) => {
|
||||||
|
validateIterable(iterable);
|
||||||
|
|
||||||
|
let iterableSize = 0;
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
if (isEmptyIterable(iterable)) {
|
||||||
|
return resolve(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
const add =
|
||||||
|
(value, index) => {
|
||||||
|
values[index] = value;
|
||||||
|
if (values.filter(() => true).length === iterableSize) {
|
||||||
|
resolve(values);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let item of iterable) {
|
||||||
|
((entry, index) => {
|
||||||
|
Promifill.resolve(entry)
|
||||||
|
.then(
|
||||||
|
(value) =>
|
||||||
|
add(value, index),
|
||||||
|
reject
|
||||||
|
);
|
||||||
|
})(item, iterableSize++);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static race (iterable) {
|
||||||
|
return new Promifill((resolve, reject) => {
|
||||||
|
validateIterable(iterable);
|
||||||
|
|
||||||
|
if (isEmptyIterable(iterable)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let entry of iterable) {
|
||||||
|
Promifill.resolve(entry)
|
||||||
|
.then(resolve, reject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static flushQueue() {
|
||||||
|
console.log("flushing promise queue sync");
|
||||||
|
schedule.flushQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defineProperty =
|
||||||
|
(obj, propName, propValue) => {
|
||||||
|
Object.defineProperty(obj, propName, { value: propValue });
|
||||||
|
};
|
||||||
|
|
||||||
|
const defer =
|
||||||
|
(handler) =>
|
||||||
|
(...args) => {
|
||||||
|
setTimeout(handler, 0, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
const thrower =
|
||||||
|
(error) => {
|
||||||
|
throw error instanceof Error
|
||||||
|
? error
|
||||||
|
: new Error(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const raiseUnhandledPromiseRejectionException =
|
||||||
|
defer((error, promise) => {
|
||||||
|
if (promise.preventThrow || promise.chain.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thrower(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
class MutationObserverStrategy {
|
||||||
|
constructor (handler) {
|
||||||
|
const observer = new MutationObserver(handler);
|
||||||
|
const node = this.node =
|
||||||
|
document.createTextNode("");
|
||||||
|
observer.observe(node, { characterData: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger () {
|
||||||
|
this.node.data = this.node.data === 1
|
||||||
|
? 0
|
||||||
|
: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NextTickStrategy {
|
||||||
|
constructor (handler) {
|
||||||
|
this.scheduleNextTick =
|
||||||
|
() => process.nextTick(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger () {
|
||||||
|
this.scheduleNextTick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BetterThanNothingStrategy {
|
||||||
|
constructor (handler) {
|
||||||
|
this.scheduleAsap =
|
||||||
|
() => setTimeout(handler, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger () {
|
||||||
|
this.scheduleAsap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStrategy =
|
||||||
|
() => {
|
||||||
|
if (typeof window != "undefined" && typeof window.MutationObserver == "function") {
|
||||||
|
return MutationObserverStrategy;
|
||||||
|
}
|
||||||
|
if (typeof global != "undefined" && typeof process != "undefined" && typeof process.nextTick == "function") {
|
||||||
|
return NextTickStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BetterThanNothingStrategy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const schedule =
|
||||||
|
(() => {
|
||||||
|
let microtasks = [];
|
||||||
|
|
||||||
|
const run =
|
||||||
|
() => {
|
||||||
|
let handler, value;
|
||||||
|
while (microtasks.length > 0 && ({ handler, value } = microtasks.shift())) {
|
||||||
|
handler(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Strategy = getStrategy();
|
||||||
|
const ctrl = new Strategy(run);
|
||||||
|
|
||||||
|
const scheduleFn = (observers) => {
|
||||||
|
if (observers.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
microtasks = microtasks.concat(observers);
|
||||||
|
observers.length = 0;
|
||||||
|
|
||||||
|
ctrl.trigger();
|
||||||
|
};
|
||||||
|
|
||||||
|
scheduleFn.flushQueue = function() {
|
||||||
|
run();
|
||||||
|
};
|
||||||
|
|
||||||
|
return scheduleFn;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const isIterable =
|
||||||
|
(subject) => subject != null && typeof subject[Symbol.iterator] == "function";
|
||||||
|
|
||||||
|
const validateIterable =
|
||||||
|
(subject) => {
|
||||||
|
if (isIterable(subject)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError(`Cannot read property 'Symbol(Symbol.iterator)' of ${Object.prototype.toString.call(subject)}.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEmptyIterable =
|
||||||
|
(subject) => {
|
||||||
|
for (let _ of subject) { // eslint-disable-line no-unused-vars
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
Reference in a new issue