From 5d64690565a39f02a27ba8ce101e4adbf1964f13 Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Tue, 16 Apr 2024 11:22:12 -0400 Subject: [PATCH] add timeouts to wait functions. fix control duplication bug. --- javascript/extraNetworks.js | 182 ++++++++++++++++++++++------------- javascript/utils.js | 87 +++++++++++++---- modules/ui_extra_networks.py | 7 +- 3 files changed, 181 insertions(+), 95 deletions(-) diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 1e462270e..d58342779 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -17,6 +17,8 @@ /*eslint no-undef: "error"*/ const SEARCH_INPUT_DEBOUNCE_TIME_MS = 250; +const EXTRA_NETWORKS_GET_PAGE_READY_MAX_ATTEMPTS = 10; +const EXTRA_NETWORKS_REQUEST_GET_TIMEOUT_MS = 1000; const re_extranet = /<([^:^>]+:[^:]+):[\d.]+>(.*)/; const re_extranet_g = /<([^:^>]+:[^:]+):[\d.]+>/g; @@ -28,8 +30,6 @@ const storedPopupIds = {}; const extraPageUserMetadataEditors = {}; const extra_networks_tabs = {}; /** Boolean flags used along with utils.js::waitForBool(). */ -// Set true when extraNetworksSetup completes. -const extra_networks_setup_complete = {state: false}; // Set true when we first load the UI options. const initialUiOptionsLoaded = {state: false}; @@ -283,85 +283,131 @@ class ExtraNetworksTab { } } - async waitForServerPageReady() { - // We need to wait for the page to be ready before we can fetch data. - // After starting the server, on the first load of the page, if the user - // immediately clicks a tab, then we will try to load the card data before - // the server has even generated it. - // We use status 503 to indicate that the page isnt ready yet. - let ready = false; - while (!ready) { - try { - await requestGetPromise( - "./sd_extra_networks/page-is-ready", - {extra_networks_tabname: this.extra_networks_tabname}, - ); - ready = true; - } catch (error) { - if (error.status === 503) { - await new Promise(resolve => setTimeout(resolve, 250)); - } else { - // We do not want to continue waiting if we get an unhandled error. - throw new Error("Error checking page readiness:", error); - } - } - } + async waitForServerPageReady( + max_attempts = EXTRA_NETWORKS_GET_PAGE_READY_MAX_ATTEMPTS, + delay_ms = EXTRA_NETWORKS_REQUEST_GET_TIMEOUT_MS, + ) { + /** Waits for a page on the server to be ready. + * + * We need to wait for the page to be ready before we can fetch any data. + * It is possible to click on a tab before the server has any data ready for us. + * Since clicking on tabs triggers a data request, there will be an error from + * the server since the data isn't ready. This function allows us to wait for + * the server to tell us that it is ready for data requests. + * + * Resolves when the response from the server is {ready: true}. + * Rejects if we exceed the max number of attempts. + * + * Args: + * max_attempts [int]: The max number of reuqests that will be attempted + * before giving up. If set to 0, will attempt forever. + * delay_ms [int]: The time between requests to the server. The server + * responds right away with its state so we need to + * slow down our request times. + */ + const err_prefix = `error waiting for server page (${this.extra_networks_tabname})`; + return new Promise((resolve, reject) => { + let attempt = 0; + const loop = () => { + setTimeout(async() => { + try { + const response = JSON.parse( + await requestGetPromise( + "./sd_extra_networks/page-is-ready", + {extra_networks_tabname: this.extra_networks_tabname}, + EXTRA_NETWORKS_REQUEST_GET_TIMEOUT_MS, + ) + ); + if (response.ready === true) { + return resolve(); + } else if (max_attempts !== 0 && attempt++ >= max_attempts) { + return reject(`${err_prefix}: max attempts exceeded`); + } else { + setTimeout(() => loop(), delay_ms); + } + } catch (error) { + return reject(`${err_prefix}: ${error}`); + } + }, 0); + }; + return loop(); + }); } async onInitCardsData() { - await this.waitForServerPageReady(); + try { + await this.waitForServerPageReady(); + } catch (error) { + console.error(error); + return {}; + } - return JSON.parse( - await requestGetPromise( - "./sd_extra_networks/init-cards-data", - { - tabname: this.tabname, - extra_networks_tabname: this.extra_networks_tabname, - }, - ) - ); + const url = "./sd_extra_networks/init-cards-data"; + const payload = {tabname: this.tabname, extra_networks_tabname: this.extra_networks_tabname}; + const timeout = EXTRA_NETWORKS_REQUEST_GET_TIMEOUT_MS; + try { + return JSON.parse(await requestGetPromise(url, payload, timeout)); + } catch (error) { + console.error(error); + return {}; + } } async onInitTreeData() { - await this.waitForServerPageReady(); + try { + await this.waitForServerPageReady(); + } catch (error) { + console.error(error); + return {}; + } - return JSON.parse( - await requestGetPromise( - "./sd_extra_networks/init-tree-data", - { - tabname: this.tabname, - extra_networks_tabname: this.extra_networks_tabname, - }, - ) - ); + const url = "./sd_extra_networks/init-tree-data"; + const payload = {tabname: this.tabname, extra_networks_tabname: this.extra_networks_tabname}; + const timeout = EXTRA_NETWORKS_REQUEST_GET_TIMEOUT_MS; + try { + return JSON.parse(await requestGetPromise(url, payload, timeout)); + } catch (error) { + console.error(error); + return {}; + } } async onFetchCardsData(div_ids) { - await this.waitForServerPageReady(); + try { + await this.waitForServerPageReady(); + } catch (error) { + console.error(error); + return {}; + } - return JSON.parse( - await requestGetPromise( - "./sd_extra_networks/fetch-cards-data", - { - extra_networks_tabname: this.extra_networks_tabname, - div_ids: div_ids, - }, - ) - ); + const url = "./sd_extra_networks/fetch-cards-data"; + const payload = {extra_networks_tabname: this.extra_networks_tabname, div_ids: div_ids}; + const timeout = EXTRA_NETWORKS_REQUEST_GET_TIMEOUT_MS; + try { + return JSON.parse(await requestGetPromise(url, payload, timeout)); + } catch (error) { + console.error(error); + return {}; + } } async onFetchTreeData(div_ids) { - await this.waitForServerPageReady(); + try { + await this.waitForServerPageReady(); + } catch (error) { + console.error(error); + return {}; + } - return JSON.parse( - await requestGetPromise( - "./sd_extra_networks/fetch-tree-data", - { - extra_networks_tabname: this.extra_networks_tabname, - div_ids: div_ids, - }, - ) - ); + const url = "./sd_extra_networks/fetch-tree-data"; + const payload = {extra_networks_tabname: this.extra_networks_tabname, div_ids: div_ids}; + const timeout = EXTRA_NETWORKS_REQUEST_GET_TIMEOUT_MS; + try { + return JSON.parse(await requestGetPromise(url, payload, timeout)); + } catch (error) { + console.error(error); + return {}; + } } updateSearch(text) { @@ -635,10 +681,11 @@ function extraNetworksUnrelatedTabSelected(tabname) { async function extraNetworksTabSelected(tabname_full, show_prompt, show_neg_prompt) { /** called from python when user selects an extra networks tab */ + await waitForKeyInObject({obj: extra_networks_tabs, k: tabname_full}); for (const [k, v] of Object.entries(extra_networks_tabs)) { if (k === tabname_full) { - await v.load(show_prompt, show_neg_prompt); + v.load(show_prompt, show_neg_prompt); } else { v.unload(); } @@ -960,7 +1007,6 @@ async function extraNetworksSetupTab(tabname) { } async function extraNetworksSetup() { - extra_networks_setup_complete.state = false; await waitForBool(initialUiOptionsLoaded); await Promise.all([ @@ -969,8 +1015,6 @@ async function extraNetworksSetup() { ]); extraNetworksSetupEventDelegators(); - - extra_networks_setup_complete.state = true; } onUiLoaded(extraNetworksSetup); diff --git a/javascript/utils.js b/javascript/utils.js index b0ff337f6..99a8b1fa8 100644 --- a/javascript/utils.js +++ b/javascript/utils.js @@ -261,9 +261,9 @@ function debounce(handler, timeout_ms) { }; } -function waitForElement(selector) { +function waitForElement(selector, timeout_ms) { /** Promise that waits for an element to exist in DOM. */ - return new Promise(resolve => { + return new Promise((resolve, reject) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } @@ -271,7 +271,7 @@ function waitForElement(selector) { const observer = new MutationObserver(mutations => { if (document.querySelector(selector)) { observer.disconnect(); - resolve(document.querySelector(selector)); + return resolve(document.querySelector(selector)); } }); @@ -279,28 +279,46 @@ function waitForElement(selector) { childList: true, subtree: true }); + + if (isNumber(timeout_ms) && timeout_ms !== 0) { + setTimeout(() => { + observer.takeRecords(); + observer.disconnect(); + return reject(`timed out waiting for element: "${selector}"`); + }, timeout_ms); + } }); } -function waitForBool(o) { +function waitForBool(o, timeout_ms) { /** Promise that waits for a boolean to be true. * * `o` must be an Object of the form: * { state: } * + * If timeout_ms is null/undefined or 0, waits forever. + * * Resolves when (state === true) + * Rejects when state is not True before timeout_ms. */ - return new Promise(resolve => { + 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); + } (function _waitForBool() { if (o.state) { return resolve(); } - setTimeout(_waitForBool, 100); + wait_timer = setTimeout(_waitForBool, 100); })(); }); } -function waitForKeyInObject(o) { +function waitForKeyInObject(o, timeout_ms) { /** Promise that waits for a key to exist in an object. * * `o` must be an Object of the form: @@ -309,19 +327,29 @@ function waitForKeyInObject(o) { * k: , * } * - * Resolves when (k in obj) + * 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. */ - return new Promise(resolve => { + 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); + } (function _waitForKeyInObject() { if (o.k in o.obj) { return resolve(); } - setTimeout(_waitForKeyInObject, 100); + wait_timer = setTimeout(_waitForKeyInObject, 100); })(); }); } -function waitForValueInObject(o) { +function waitForValueInObject(o, timeout_ms) { /** Promise that waits for a key value pair in an Object. * * `o` must be an Object of the form: @@ -331,10 +359,19 @@ function waitForValueInObject(o) { * v: * } * + * If timeout_ms is null/undefined or 0, waits forever. + * * Resolves when obj[k] == v */ - return new Promise(resolve => { - waitForKeyInObject({k: o.k, obj: o.obj}).then(() => { + 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); + } + waitForKeyInObject({k: o.k, obj: o.obj}, timeout_ms).then(() => { (function _waitForValueInObject() { if (o.k in o.obj && o.obj[o.k] == o.v) { @@ -342,6 +379,8 @@ function waitForValueInObject(o) { } setTimeout(_waitForValueInObject, 100); })(); + }).catch((error) => { + return reject(error); }); }); } @@ -374,26 +413,32 @@ function requestGet(url, data, handler, errorHandler) { xhr.send(js); } -function requestGetPromise(url, data) { +function requestGetPromise(url, data, timeout_ms) { return new Promise((resolve, reject) => { - let xhr = new XMLHttpRequest(); - let args = Object.keys(data).map(k => { - return encodeURIComponent(k) + "=" + encodeURIComponent(data[k]); + const xhr = new XMLHttpRequest(); + const args = Object.entries(data).map(([k, v]) => { + return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`; }).join("&"); - xhr.open("GET", url + "?" + args, true); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { - resolve(xhr.responseText); + return resolve(xhr.responseText); } else { - reject({status: xhr.status, response: xhr.responseText}); + return reject({status: xhr.status, response: xhr.responseText}); } }; xhr.onerror = () => { - reject({status: xhr.status, response: xhr.responseText}); + return reject({status: xhr.status, response: xhr.responseText}); }; + + xhr.ontimeout = () => { + return reject(`Request for ${url} timed out.`); + }; + const payload = JSON.stringify(data); + xhr.open("GET", `${url}?${args}`, true); + xhr.timeout = timeout_ms; xhr.send(payload); }); } diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index ab432d212..80e0c38d2 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -323,11 +323,8 @@ def page_is_ready(extra_networks_tabname: str = "") -> JSONResponse: page = get_page_by_name(extra_networks_tabname) try: - items_list = list(page.list_items()) - if len(page.items) == len(items_list): - return JSONResponse({}, status_code=200) - - return JSONResponse({"error": "page not ready"}, status_code=503) + ready = len(page.items) == len(list(page.list_items())) + return JSONResponse({"ready": ready}, status_code=200) except Exception as exc: return JSONResponse({"error": str(exc)}, status_code=500)