2024-04-09 21:26:56 +00:00
|
|
|
/** Collators used for sorting. */
|
2024-04-26 18:16:31 +00:00
|
|
|
const INT_COLLATOR = new Intl.Collator([], {numeric: true});
|
|
|
|
const STR_COLLATOR = new Intl.Collator("en", {numeric: true, sensitivity: "base"});
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
/** Helper functions for checking types and simplifying logging/error handling. */
|
2024-04-14 18:52:17 +00:00
|
|
|
function isNumber(x) {
|
|
|
|
return typeof x === "number" && isFinite(x);
|
|
|
|
}
|
2024-04-09 21:26:56 +00:00
|
|
|
function isNumberLogError(x) {
|
2024-04-05 12:40:49 +00:00
|
|
|
if (isNumber(x)) {
|
|
|
|
return true;
|
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
console.error(`expected number, got: ${typeof x}`);
|
2024-04-05 12:40:49 +00:00
|
|
|
return false;
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
|
|
|
function isNumberThrowError(x) {
|
2024-04-05 12:40:49 +00:00
|
|
|
if (isNumber(x)) {
|
|
|
|
return;
|
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
throw new Error(`expected number, got: ${typeof x}`);
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-14 18:52:17 +00:00
|
|
|
function isString(x) {
|
|
|
|
return typeof x === "string" || x instanceof String;
|
|
|
|
}
|
2024-04-09 21:26:56 +00:00
|
|
|
function isStringLogError(x) {
|
2024-04-05 12:40:49 +00:00
|
|
|
if (isString(x)) {
|
|
|
|
return true;
|
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
console.error(`expected string, got: ${typeof x}`);
|
2024-04-05 12:40:49 +00:00
|
|
|
return false;
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
|
|
|
function isStringThrowError(x) {
|
2024-04-05 12:40:49 +00:00
|
|
|
if (isString(x)) {
|
|
|
|
return;
|
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
throw new Error(`expected string, got: ${typeof x}`);
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-14 18:52:17 +00:00
|
|
|
function isNull(x) {
|
|
|
|
return x === null;
|
|
|
|
}
|
|
|
|
function isUndefined(x) {
|
|
|
|
return typeof x === "undefined" || x === undefined;
|
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
// checks both null and undefined for simplicity sake.
|
2024-04-14 18:52:17 +00:00
|
|
|
function isNullOrUndefined(x) {
|
|
|
|
return isNull(x) || isUndefined(x);
|
|
|
|
}
|
2024-04-09 21:26:56 +00:00
|
|
|
function isNullOrUndefinedLogError(x) {
|
2024-04-05 12:40:49 +00:00
|
|
|
if (isNullOrUndefined(x)) {
|
|
|
|
console.error("Variable is null/undefined.");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
|
|
|
function isNullOrUndefinedThrowError(x) {
|
2024-04-05 12:40:49 +00:00
|
|
|
if (!isNullOrUndefined(x)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw new Error("Variable is null/undefined.");
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-14 18:52:17 +00:00
|
|
|
function isElement(x) {
|
|
|
|
return x instanceof Element;
|
|
|
|
}
|
2024-04-09 21:26:56 +00:00
|
|
|
function isElementLogError(x) {
|
2024-04-05 12:40:49 +00:00
|
|
|
if (isElement(x)) {
|
|
|
|
return true;
|
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
console.error(`expected element type, got: ${typeof x}`);
|
2024-04-05 12:40:49 +00:00
|
|
|
return false;
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
|
|
|
function isElementThrowError(x) {
|
2024-04-05 12:40:49 +00:00
|
|
|
if (isElement(x)) {
|
|
|
|
return;
|
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
throw new Error(`expected element type, got: ${typeof x}`);
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-14 18:52:17 +00:00
|
|
|
function isFunction(x) {
|
|
|
|
return typeof x === "function";
|
|
|
|
}
|
2024-04-09 21:26:56 +00:00
|
|
|
function isFunctionLogError(x) {
|
2024-04-05 12:40:49 +00:00
|
|
|
if (isFunction(x)) {
|
|
|
|
return true;
|
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
console.error(`expected function type, got: ${typeof x}`);
|
2024-04-05 12:40:49 +00:00
|
|
|
return false;
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
|
|
|
function isFunctionThrowError(x) {
|
2024-04-05 12:40:49 +00:00
|
|
|
if (isFunction(x)) {
|
|
|
|
return;
|
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
throw new Error(`expected function type, got: ${typeof x}`);
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
|
2024-04-14 18:52:17 +00:00
|
|
|
function isObject(x) {
|
|
|
|
return typeof x === "object" && !Array.isArray(x);
|
|
|
|
}
|
2024-04-09 21:26:56 +00:00
|
|
|
function isObjectLogError(x) {
|
2024-04-09 11:19:07 +00:00
|
|
|
if (isObject(x)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
console.error(`expected object type, got: ${typeof x}`);
|
|
|
|
return false;
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
|
|
|
function isObjectThrowError(x) {
|
2024-04-09 11:19:07 +00:00
|
|
|
if (isObject(x)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw new Error(`expected object type, got: ${typeof x}`);
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
function keyExists(obj, k) {
|
|
|
|
return isObject(obj) && isString(k) && k in obj;
|
|
|
|
}
|
|
|
|
function keyExistsLogError(obj, k) {
|
2024-04-09 11:19:07 +00:00
|
|
|
if (keyExists(obj, k)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
console.error(`key does not exist in object: ${k}`);
|
|
|
|
return false;
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
|
|
|
function keyExistsThrowError(obj, k) {
|
2024-04-09 11:19:07 +00:00
|
|
|
if (keyExists(obj, k)) {
|
|
|
|
return;
|
|
|
|
}
|
2024-04-14 18:52:17 +00:00
|
|
|
throw new Error(`key does not exist in object: ${k}`);
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
function getValue(obj, k) {
|
2024-04-09 11:19:07 +00:00
|
|
|
/** Returns value of object for given key if it exists, otherwise returns null. */
|
|
|
|
if (keyExists(obj, k)) {
|
|
|
|
return obj[k];
|
|
|
|
}
|
|
|
|
return null;
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
|
|
|
function getValueLogError(obj, k) {
|
2024-04-09 11:19:07 +00:00
|
|
|
if (keyExistsLogError(obj, k)) {
|
|
|
|
return obj[k];
|
|
|
|
}
|
|
|
|
return null;
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
|
|
|
function getValueThrowError(obj, k) {
|
2024-04-09 11:19:07 +00:00
|
|
|
keyExistsThrowError(obj, k);
|
|
|
|
return obj[k];
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
function getElementByIdLogError(selector) {
|
2024-04-05 12:40:49 +00:00
|
|
|
const elem = gradioApp().getElementById(selector);
|
|
|
|
isElementLogError(elem);
|
|
|
|
return elem;
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
|
|
|
function getElementByIdThrowError(selector) {
|
2024-04-05 12:40:49 +00:00
|
|
|
const elem = gradioApp().getElementById(selector);
|
|
|
|
isElementThrowError(elem);
|
|
|
|
return elem;
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
function querySelectorLogError(selector) {
|
2024-04-05 12:40:49 +00:00
|
|
|
const elem = gradioApp().querySelector(selector);
|
|
|
|
isElementLogError(elem);
|
|
|
|
return elem;
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
|
|
|
function querySelectorThrowError(selector) {
|
2024-04-05 12:40:49 +00:00
|
|
|
const elem = gradioApp().querySelector(selector);
|
|
|
|
isElementThrowError(elem);
|
|
|
|
return elem;
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
|
|
|
/** Functions for getting dimensions of elements. */
|
2024-04-12 16:40:20 +00:00
|
|
|
function getStyle(elem) {
|
|
|
|
return window.getComputedStyle ? window.getComputedStyle(elem) : elem.currentStyle;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getComputedProperty(elem, prop) {
|
|
|
|
return getStyle(elem)[prop];
|
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
function getComputedPropertyDims(elem, prop) {
|
2024-04-05 12:40:49 +00:00
|
|
|
/** Returns the top/left/bottom/right float dimensions of an element for the specified property. */
|
2024-04-12 16:40:20 +00:00
|
|
|
const style = getStyle(elem);
|
2024-04-05 12:40:49 +00:00
|
|
|
return {
|
|
|
|
top: parseFloat(style.getPropertyValue(`${prop}-top`)),
|
|
|
|
left: parseFloat(style.getPropertyValue(`${prop}-left`)),
|
|
|
|
bottom: parseFloat(style.getPropertyValue(`${prop}-bottom`)),
|
|
|
|
right: parseFloat(style.getPropertyValue(`${prop}-right`)),
|
|
|
|
};
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
function getComputedMarginDims(elem) {
|
2024-04-05 12:40:49 +00:00
|
|
|
/** Returns the width/height of the computed margin of an element. */
|
|
|
|
const dims = getComputedPropertyDims(elem, "margin");
|
|
|
|
return {
|
|
|
|
width: dims.left + dims.right,
|
|
|
|
height: dims.top + dims.bottom,
|
|
|
|
};
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
function getComputedPaddingDims(elem) {
|
2024-04-05 12:40:49 +00:00
|
|
|
/** Returns the width/height of the computed padding of an element. */
|
|
|
|
const dims = getComputedPropertyDims(elem, "padding");
|
|
|
|
return {
|
|
|
|
width: dims.left + dims.right,
|
|
|
|
height: dims.top + dims.bottom,
|
|
|
|
};
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
function getComputedBorderDims(elem) {
|
2024-04-05 12:40:49 +00:00
|
|
|
/** Returns the width/height of the computed border of an element. */
|
|
|
|
// computed border will always start with the pixel width so thankfully
|
|
|
|
// the parseFloat() conversion will just give us the width and ignore the rest.
|
|
|
|
// Otherwise we'd have to use border-<pos>-width instead.
|
|
|
|
const dims = getComputedPropertyDims(elem, "border");
|
|
|
|
return {
|
|
|
|
width: dims.left + dims.right,
|
|
|
|
height: dims.top + dims.bottom,
|
|
|
|
};
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
function getComputedDims(elem) {
|
2024-04-05 12:40:49 +00:00
|
|
|
/** Returns the full width and height of an element including its margin, padding, and border. */
|
|
|
|
const width = elem.scrollWidth;
|
|
|
|
const height = elem.scrollHeight;
|
|
|
|
const margin = getComputedMarginDims(elem);
|
|
|
|
const padding = getComputedPaddingDims(elem);
|
|
|
|
const border = getComputedBorderDims(elem);
|
|
|
|
return {
|
|
|
|
width: width + margin.width + padding.width + border.width,
|
|
|
|
height: height + margin.height + padding.height + border.height,
|
|
|
|
};
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
|
|
|
/** Functions for asynchronous operations. */
|
|
|
|
|
2024-04-16 15:22:12 +00:00
|
|
|
function waitForElement(selector, timeout_ms) {
|
2024-04-05 12:40:49 +00:00
|
|
|
/** Promise that waits for an element to exist in DOM. */
|
2024-04-16 15:22:12 +00:00
|
|
|
return new Promise((resolve, reject) => {
|
2024-04-05 12:40:49 +00:00
|
|
|
if (document.querySelector(selector)) {
|
|
|
|
return resolve(document.querySelector(selector));
|
|
|
|
}
|
|
|
|
|
|
|
|
const observer = new MutationObserver(mutations => {
|
|
|
|
if (document.querySelector(selector)) {
|
|
|
|
observer.disconnect();
|
2024-04-16 15:22:12 +00:00
|
|
|
return resolve(document.querySelector(selector));
|
2024-04-05 12:40:49 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
observer.observe(document.documentElement, {
|
|
|
|
childList: true,
|
|
|
|
subtree: true
|
|
|
|
});
|
2024-04-16 15:22:12 +00:00
|
|
|
|
|
|
|
if (isNumber(timeout_ms) && timeout_ms !== 0) {
|
|
|
|
setTimeout(() => {
|
|
|
|
observer.takeRecords();
|
|
|
|
observer.disconnect();
|
|
|
|
return reject(`timed out waiting for element: "${selector}"`);
|
|
|
|
}, timeout_ms);
|
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
});
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-16 15:22:12 +00:00
|
|
|
function waitForBool(o, timeout_ms) {
|
2024-04-05 12:40:49 +00:00
|
|
|
/** Promise that waits for a boolean to be true.
|
|
|
|
*
|
|
|
|
* `o` must be an Object of the form:
|
|
|
|
* { state: <bool value> }
|
|
|
|
*
|
2024-04-16 15:22:12 +00:00
|
|
|
* If timeout_ms is null/undefined or 0, waits forever.
|
|
|
|
*
|
2024-04-05 12:40:49 +00:00
|
|
|
* Resolves when (state === true)
|
2024-04-16 15:22:12 +00:00
|
|
|
* Rejects when state is not True before timeout_ms.
|
2024-04-05 12:40:49 +00:00
|
|
|
*/
|
2024-04-16 15:22:12 +00:00
|
|
|
let wait_timer;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
if (isNumber(timeout_ms) && timeout_ms !== 0) {
|
|
|
|
setTimeout(() => {
|
|
|
|
clearTimeout(wait_timer);
|
|
|
|
return reject("timed out waiting for bool");
|
|
|
|
}, timeout_ms);
|
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
(function _waitForBool() {
|
|
|
|
if (o.state) {
|
|
|
|
return resolve();
|
|
|
|
}
|
2024-04-16 15:22:12 +00:00
|
|
|
wait_timer = setTimeout(_waitForBool, 100);
|
2024-04-05 12:40:49 +00:00
|
|
|
})();
|
|
|
|
});
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-16 15:22:12 +00:00
|
|
|
function waitForKeyInObject(o, timeout_ms) {
|
2024-04-05 12:40:49 +00:00
|
|
|
/** Promise that waits for a key to exist in an object.
|
|
|
|
*
|
|
|
|
* `o` must be an Object of the form:
|
|
|
|
* {
|
|
|
|
* obj: <object to watch for key>,
|
|
|
|
* k: <key to watch for>,
|
|
|
|
* }
|
|
|
|
*
|
2024-04-16 15:22:12 +00:00
|
|
|
* If timeout_ms is null/undefined or 0, waits forever.
|
|
|
|
*
|
|
|
|
* Resolves when (k in obj).
|
|
|
|
* Rejects when k is not found in obj before timeout_ms.
|
2024-04-05 12:40:49 +00:00
|
|
|
*/
|
2024-04-16 15:22:12 +00:00
|
|
|
let wait_timer;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
if (isNumber(timeout_ms) && timeout_ms !== 0) {
|
|
|
|
setTimeout(() => {
|
|
|
|
clearTimeout(wait_timer);
|
|
|
|
return reject(`timed out waiting for key: ${o.k}`);
|
|
|
|
}, timeout_ms);
|
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
(function _waitForKeyInObject() {
|
|
|
|
if (o.k in o.obj) {
|
|
|
|
return resolve();
|
|
|
|
}
|
2024-04-16 15:22:12 +00:00
|
|
|
wait_timer = setTimeout(_waitForKeyInObject, 100);
|
2024-04-05 12:40:49 +00:00
|
|
|
})();
|
|
|
|
});
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-16 15:22:12 +00:00
|
|
|
function waitForValueInObject(o, timeout_ms) {
|
2024-04-05 12:40:49 +00:00
|
|
|
/** Promise that waits for a key value pair in an Object.
|
|
|
|
*
|
|
|
|
* `o` must be an Object of the form:
|
|
|
|
* {
|
|
|
|
* obj: <object containing value>,
|
|
|
|
* k: <key in object>,
|
|
|
|
* v: <value at key for comparison>
|
|
|
|
* }
|
|
|
|
*
|
2024-04-16 15:22:12 +00:00
|
|
|
* If timeout_ms is null/undefined or 0, waits forever.
|
|
|
|
*
|
2024-04-05 12:40:49 +00:00
|
|
|
* Resolves when obj[k] == v
|
|
|
|
*/
|
2024-04-16 15:22:12 +00:00
|
|
|
let wait_timer;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
if (isNumber(timeout_ms) && timeout_ms !== 0) {
|
|
|
|
setTimeout(() => {
|
|
|
|
clearTimeout(wait_timer);
|
|
|
|
return reject(`timed out waiting for value: ${o.k}: ${o.v}`);
|
|
|
|
}, timeout_ms);
|
|
|
|
}
|
2024-04-26 18:16:31 +00:00
|
|
|
waitForKeyInObject({k: o.k, obj: o.obj}, timeout_ms).then(() => {
|
2024-04-05 12:40:49 +00:00
|
|
|
(function _waitForValueInObject() {
|
|
|
|
|
|
|
|
if (o.k in o.obj && o.obj[o.k] == o.v) {
|
|
|
|
return resolve();
|
|
|
|
}
|
|
|
|
setTimeout(_waitForValueInObject, 100);
|
|
|
|
})();
|
2024-04-16 15:22:12 +00:00
|
|
|
}).catch((error) => {
|
|
|
|
return reject(error);
|
2024-04-05 12:40:49 +00:00
|
|
|
});
|
|
|
|
});
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-09 11:19:07 +00:00
|
|
|
/** Requests */
|
|
|
|
|
2024-04-26 18:00:00 +00:00
|
|
|
class FetchError extends Error {
|
|
|
|
constructor(...args) {
|
|
|
|
super(...args);
|
|
|
|
this.name = this.constructor.name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Fetch4xxError extends FetchError {
|
2024-04-26 18:16:31 +00:00
|
|
|
constructor(...args) {
|
|
|
|
super(...args);
|
|
|
|
}
|
2024-04-26 18:00:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class Fetch5xxError extends FetchError {
|
2024-04-26 18:16:31 +00:00
|
|
|
constructor(...args) {
|
|
|
|
super(...args);
|
|
|
|
}
|
2024-04-26 18:00:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class FetchRetryLimitError extends FetchError {
|
2024-04-26 18:16:31 +00:00
|
|
|
constructor(...args) {
|
|
|
|
super(...args);
|
|
|
|
}
|
2024-04-26 18:00:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class FetchTimeoutError extends FetchError {
|
2024-04-26 18:16:31 +00:00
|
|
|
constructor(...args) {
|
|
|
|
super(...args);
|
|
|
|
}
|
2024-04-26 18:00:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class FetchWithRetryAndBackoffTimeoutError extends FetchError {
|
2024-04-26 18:16:31 +00:00
|
|
|
constructor(...args) {
|
|
|
|
super(...args);
|
|
|
|
}
|
2024-04-26 18:00:00 +00:00
|
|
|
}
|
|
|
|
|
2024-05-07 11:42:17 +00:00
|
|
|
async function fetchWithRetryAndBackoff(url, data, args = {}) {
|
2024-04-26 18:00:00 +00:00
|
|
|
/** Wrapper around `fetch` with retries, backoff, and timeout.
|
|
|
|
*
|
|
|
|
* Uses a Decorrelated jitter backoff strategy.
|
|
|
|
* https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
|
|
|
|
*
|
|
|
|
* Args:
|
|
|
|
* url: Primary URL to fetch.
|
|
|
|
* data: Data to append to the URL when making the request.
|
|
|
|
* opts:
|
|
|
|
* method: The HTTP request method to use.
|
|
|
|
* timeout_ms: Max allowed time before this function fails.
|
|
|
|
* fetch_timeout_ms: Max allowed time for individual `fetch` calls.
|
|
|
|
* min_delay_ms: Min time that a delay between requests can be.
|
|
|
|
* max_delay_ms: Max time that a delay between reqeusts can be.
|
|
|
|
* response_handler: A callback function that returns a promise.
|
|
|
|
* This function is sent the response from the `fetch` call.
|
|
|
|
* If not specified, all status codes >= 400 are handled as errors.
|
|
|
|
* This is useful for handling requests whose responses from the server
|
|
|
|
* are erronious but the HTTP status is 200.
|
|
|
|
*/
|
2024-05-07 11:42:17 +00:00
|
|
|
args.method = args.method || "GET";
|
|
|
|
args.timeout_ms = args.timeout_ms || 30000;
|
|
|
|
args.min_delay_ms = args.min_delay_ms || 100;
|
|
|
|
args.max_delay_ms = args.max_delay_ms || 3000;
|
|
|
|
args.fetch_timeout_ms = args.fetch_timeout_ms || 10000;
|
2024-04-26 18:00:00 +00:00
|
|
|
// The default response handler function for `fetch` call responses.
|
2024-04-26 18:16:31 +00:00
|
|
|
const response_handler = (response) => new Promise((resolve, reject) => {
|
2024-04-26 18:00:00 +00:00
|
|
|
if (response.ok) {
|
2024-04-26 18:16:31 +00:00
|
|
|
return response.json().then(json => {
|
|
|
|
return resolve(json);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
if (response.status >= 400 && response.status < 500) {
|
|
|
|
throw new Fetch4xxError("client error:", response);
|
|
|
|
}
|
|
|
|
if (response.status >= 500 && response.status < 600) {
|
|
|
|
throw new Fetch5xxError("server error:", response);
|
|
|
|
}
|
|
|
|
return reject(response);
|
2024-04-26 18:00:00 +00:00
|
|
|
}
|
|
|
|
});
|
2024-05-07 11:42:17 +00:00
|
|
|
args.response_handler = args.response_handler || response_handler;
|
2024-04-26 18:00:00 +00:00
|
|
|
|
2024-05-07 11:42:17 +00:00
|
|
|
const url_args = Object.entries(data).map(([k, v]) => {
|
2024-04-26 18:00:00 +00:00
|
|
|
return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`;
|
|
|
|
}).join("&");
|
2024-05-07 11:42:17 +00:00
|
|
|
url = `${url}?${url_args}`;
|
2024-04-26 18:00:00 +00:00
|
|
|
|
|
|
|
let controller;
|
|
|
|
let retry = true;
|
|
|
|
|
|
|
|
const randrange = (min, max) => {
|
|
|
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
2024-04-26 18:16:31 +00:00
|
|
|
};
|
2024-04-26 18:00:00 +00:00
|
|
|
const get_jitter = (base, max, prev) => {
|
|
|
|
return Math.min(max, randrange(base, prev * 3));
|
2024-04-26 18:16:31 +00:00
|
|
|
};
|
2024-04-26 18:00:00 +00:00
|
|
|
|
|
|
|
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
|
|
|
|
// Timeout for individual `fetch` calls.
|
|
|
|
const fetch_timeout = (ms, promise) => {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
setTimeout(() => {
|
|
|
|
return reject(new FetchTimeoutError("Fetch timed out."));
|
|
|
|
}, ms);
|
|
|
|
return promise.then(resolve, reject);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// Timeout for all retries.
|
|
|
|
const run_timeout = (ms, promise) => {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
setTimeout(() => {
|
|
|
|
retry = false;
|
|
|
|
controller.abort();
|
|
|
|
return reject(new FetchWithRetryAndBackoffTimeoutError("Request timed out."));
|
|
|
|
}, ms);
|
|
|
|
return promise.then(resolve, reject);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2024-04-26 18:16:31 +00:00
|
|
|
const run = async(delay_ms) => {
|
2024-04-26 18:00:00 +00:00
|
|
|
if (!retry) {
|
|
|
|
// Retry is controlled externally via `run_timeout`. This function's promise
|
|
|
|
// is also handled via that timeout so we can just return here.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
controller = new AbortController();
|
2024-05-07 11:42:17 +00:00
|
|
|
const fetch_opts = {method: args.method, signal: controller.signal};
|
|
|
|
const response = await fetch_timeout(args.fetch_timeout_ms, fetch(url, fetch_opts));
|
|
|
|
return await args.response_handler(response);
|
2024-04-26 18:00:00 +00:00
|
|
|
} catch (error) {
|
|
|
|
controller.abort();
|
|
|
|
// dont bother with anything else if told to not retry.
|
|
|
|
if (!retry) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (error instanceof Fetch4xxError) {
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
if (error instanceof Fetch5xxError) {
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
// Any other errors mean we need to retry the request.
|
2024-05-07 11:42:17 +00:00
|
|
|
delay_ms = get_jitter(args.min_delay_ms, args.max_delay_ms, delay_ms);
|
2024-04-26 18:00:00 +00:00
|
|
|
await delay(delay_ms);
|
|
|
|
return await run(delay_ms);
|
|
|
|
}
|
2024-04-26 18:16:31 +00:00
|
|
|
};
|
2024-05-07 11:42:17 +00:00
|
|
|
return await run_timeout(args.timeout_ms, run(args.min_delay_ms));
|
2024-04-26 18:00:00 +00:00
|
|
|
}
|
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
function requestGet(url, data, handler, errorHandler) {
|
2024-04-09 11:19:07 +00:00
|
|
|
var xhr = new XMLHttpRequest();
|
2024-04-26 18:16:31 +00:00
|
|
|
var args = Object.keys(data).map(function(k) {
|
2024-04-09 11:19:07 +00:00
|
|
|
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]);
|
|
|
|
}).join('&');
|
|
|
|
xhr.open("GET", url + "?" + args, true);
|
|
|
|
|
2024-04-26 18:16:31 +00:00
|
|
|
xhr.onreadystatechange = function() {
|
2024-04-09 11:19:07 +00:00
|
|
|
if (xhr.readyState === 4) {
|
|
|
|
if (xhr.status === 200) {
|
|
|
|
try {
|
|
|
|
var js = JSON.parse(xhr.responseText);
|
|
|
|
handler(js);
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
errorHandler();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
errorHandler();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
var js = JSON.stringify(data);
|
|
|
|
xhr.send(js);
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
|
2024-04-16 15:22:12 +00:00
|
|
|
function requestGetPromise(url, data, timeout_ms) {
|
2024-04-18 17:09:58 +00:00
|
|
|
/**Asynchronous `GET` request that returns a promise.
|
|
|
|
*
|
|
|
|
* The result will be of the format {status: int, response: JSON object}.
|
|
|
|
* Thus, the xhr.responseText that we receive is expected to be a JSON string.
|
|
|
|
* Acceptable status codes for successful requests are 200 <= status < 300.
|
|
|
|
*/
|
|
|
|
if (!isNumber(timeout_ms)) {
|
|
|
|
timeout_ms = 1000;
|
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
return new Promise((resolve, reject) => {
|
2024-04-16 15:22:12 +00:00
|
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
const args = Object.entries(data).map(([k, v]) => {
|
|
|
|
return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`;
|
2024-04-09 11:19:07 +00:00
|
|
|
}).join("&");
|
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
xhr.onload = () => {
|
|
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
2024-04-26 18:16:31 +00:00
|
|
|
return resolve({status: xhr.status, response: JSON.parse(xhr.responseText)});
|
2024-04-09 21:26:56 +00:00
|
|
|
} else {
|
2024-04-26 18:16:31 +00:00
|
|
|
return reject({status: xhr.status, response: JSON.parse(xhr.responseText)});
|
2024-04-09 11:19:07 +00:00
|
|
|
}
|
|
|
|
};
|
2024-04-09 21:26:56 +00:00
|
|
|
|
|
|
|
xhr.onerror = () => {
|
2024-04-26 18:16:31 +00:00
|
|
|
return reject({status: xhr.status, response: JSON.parse(xhr.responseText)});
|
2024-04-16 15:22:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
xhr.ontimeout = () => {
|
2024-04-26 18:16:31 +00:00
|
|
|
return reject({status: 408, response: {detail: `Request timeout: ${url}`}});
|
2024-04-09 21:26:56 +00:00
|
|
|
};
|
2024-04-16 15:22:12 +00:00
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
const payload = JSON.stringify(data);
|
2024-04-16 15:22:12 +00:00
|
|
|
xhr.open("GET", `${url}?${args}`, true);
|
|
|
|
xhr.timeout = timeout_ms;
|
2024-04-09 21:26:56 +00:00
|
|
|
xhr.send(payload);
|
2024-04-09 11:19:07 +00:00
|
|
|
});
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
|
2024-04-05 12:40:49 +00:00
|
|
|
/** Misc helper functions. */
|
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
function clamp(x, min, max) {
|
|
|
|
return Math.max(min, Math.min(x, max));
|
|
|
|
}
|
2024-04-05 12:40:49 +00:00
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
function htmlStringToElement(s) {
|
2024-04-05 12:40:49 +00:00
|
|
|
/** Converts an HTML string into an Element type. */
|
|
|
|
let parser = new DOMParser();
|
2024-04-09 21:26:56 +00:00
|
|
|
let tmp = parser.parseFromString(s, "text/html");
|
2024-04-05 12:40:49 +00:00
|
|
|
return tmp.body.firstElementChild;
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
|
2024-04-15 13:31:30 +00:00
|
|
|
function htmlStringToFragment(s) {
|
|
|
|
/** Converts an HTML string into a DocumentFragment. */
|
|
|
|
return document.createRange().createContextualFragment(s);
|
|
|
|
}
|
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
function toggleCss(key, css, enable) {
|
2024-04-09 11:19:07 +00:00
|
|
|
var style = document.getElementById(key);
|
|
|
|
if (enable && !style) {
|
|
|
|
style = document.createElement('style');
|
|
|
|
style.id = key;
|
|
|
|
style.type = 'text/css';
|
|
|
|
document.head.appendChild(style);
|
|
|
|
}
|
|
|
|
if (style && !enable) {
|
|
|
|
document.head.removeChild(style);
|
|
|
|
}
|
|
|
|
if (style) {
|
|
|
|
style.innerHTML == '';
|
|
|
|
style.appendChild(document.createTextNode(css));
|
|
|
|
}
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-04-09 11:19:07 +00:00
|
|
|
|
2024-04-09 21:26:56 +00:00
|
|
|
function copyToClipboard(s) {
|
2024-04-09 11:19:07 +00:00
|
|
|
/** Copies the passed string to the clipboard. */
|
|
|
|
isStringThrowError(s);
|
|
|
|
navigator.clipboard.writeText(s);
|
2024-04-09 21:26:56 +00:00
|
|
|
}
|
2024-05-12 20:43:57 +00:00
|
|
|
|
|
|
|
function attrPromise({elem, attr, timeout_ms} = {}) {
|
|
|
|
timeout_ms = timeout_ms || 0;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
let res = false;
|
|
|
|
const observer_config = {attributes: true, attributeOldValue: true};
|
|
|
|
const observer = new MutationObserver(mutations => {
|
|
|
|
mutations.forEach(mutation => {
|
|
|
|
if (isString(attr) && mutation.attributeName === attr) {
|
|
|
|
res = true;
|
|
|
|
observer.disconnect();
|
|
|
|
resolve(elem, elem.getAttribute(attr));
|
|
|
|
}
|
|
|
|
if (!isString(attr)) {
|
|
|
|
res = true;
|
|
|
|
observer.disconnect();
|
|
|
|
resolve(elem);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
if (timeout_ms > 0) {
|
|
|
|
setTimeout(() => {
|
|
|
|
if (!res) {
|
|
|
|
reject(elem);
|
|
|
|
}
|
|
|
|
}, timeout_ms);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isString(attr)) {
|
|
|
|
observer_config.attributeFilter = [attr];
|
|
|
|
}
|
|
|
|
observer.observe(elem, observer_config);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function waitForVisible(elem, callback) {
|
|
|
|
new IntersectionObserver((entries, observer) => {
|
|
|
|
entries.forEach(entry => {
|
|
|
|
if (entry.intersectionRatio > 0) {
|
|
|
|
callback(elem);
|
|
|
|
observer.disconnect();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}).observe(elem);
|
|
|
|
if (!callback) return new Promise(resolve => callback = resolve);
|
|
|
|
}
|
2024-05-20 20:34:53 +00:00
|
|
|
|
|
|
|
function cssRelativeUnitToPx(css_value, target) {
|
|
|
|
// https://stackoverflow.com/a/66569574
|
|
|
|
// doesnt work on `%` unit.
|
|
|
|
target = target || document.body;
|
|
|
|
const units = {
|
|
|
|
px: x => x, // no conversion needed here
|
|
|
|
rem: x => x * parseFloat(getComputedStyle(document.documentElement).fontSize),
|
|
|
|
em: x => x * parseFloat(getComputedStyle(target).fontSize),
|
|
|
|
vw: x => x / 100 * window.innerWidth,
|
|
|
|
vh: x => x / 100 * window.innerHeight,
|
|
|
|
};
|
|
|
|
|
|
|
|
const re = new RegExp(`^([-+]?(?:\\d+(?:\\.\\d+)?))(${Object.keys(units).join('|')})$`, 'i');
|
|
|
|
const matches = css_value.toString().trim().match(re);
|
|
|
|
if (matches) {
|
|
|
|
const value = Number(matches[1]);
|
|
|
|
const unit = matches[2].toLocaleLowerCase();
|
|
|
|
if (unit in units) {
|
|
|
|
return units[unit](value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return css_value;
|
|
|
|
}
|