iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

commit abae41d9137111cd550a76d09824b177a2110194
parent 43357e43202dd0bbbc94999b256a9bf7d1c7f292
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Sun,  9 Aug 2026 20:29:06 +0200

Harden UI polling after idle

Diffstat:
Msrc/adapters/http.rs | 15+++++++++++++--
Mwww/assets/iuna-ui.js | 60+++++++++++++++++++++++++++++++++++++++++++++++++-----------
2 files changed, 62 insertions(+), 13 deletions(-)

diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -5786,7 +5786,11 @@ mod tests { #[test] fn setup_completion_refreshes_chain_data() { let app_js = include_str!("../../www/assets/iuna-ui.js"); - assert!(app_js.contains("await this.refresh();\n this.setupFeedback = null;")); + assert!( + app_js.contains( + "await this.refresh({ force: true });\n this.setupFeedback = null;" + ) + ); assert!(!app_js.contains( "await this.refreshConfig();\n await this.resetPagedDataset(\"peer\");" )); @@ -5812,8 +5816,15 @@ mod tests { ) ); assert!(app_js.contains( - "async refresh(options = {}) {\n if (!this.canUseProtectedApi()) return;" + "async refreshNow(options = {}) {\n if (!this.canUseProtectedApi()) return;" )); + assert!(app_js.contains("refreshPromise: null")); + assert!(app_js.contains("if (this.refreshPromise)")); + assert!(app_js.contains("return this.refreshPromise;")); + assert!(app_js.contains("options.force === true")); + assert!(app_js.contains("cache: \"no-store\"")); + assert!(app_js.contains("async fetchWithTimeout(path, options = {})")); + assert!(app_js.contains("controller.abort()")); assert!(app_js.contains("async refreshPagedDataset(kind, options = {}) {\n if (!this.canUseProtectedApi()) return;")); assert!( app_js.contains( diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -89,6 +89,8 @@ window.iunaApp = function iunaApp() { showPowDifficultyInfo: false, lastUpdated: null, pollHandle: null, + refreshPromise: null, + requestTimeoutMs: 12000, hashListenerInstalled: false, newBlockHashes: new Set(), newBlockTimer: null, @@ -305,7 +307,7 @@ window.iunaApp = function iunaApp() { }, async postAuth(path, password) { - const response = await fetch(path, { + const response = await this.fetchWithTimeout(path, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ password }), @@ -346,7 +348,7 @@ window.iunaApp = function iunaApp() { old_password: this.settingsOldPassword, new_password: this.settingsNewPassword, }); - const response = await fetch("/api/auth/change-password", { + const response = await this.fetchWithTimeout("/api/auth/change-password", { method: "POST", headers: { Accept: "application/json", @@ -432,7 +434,7 @@ window.iunaApp = function iunaApp() { this.walletVerified = false; this.verifyChallenges = []; this.verifyAnswers = {}; - await this.refresh(); + await this.refresh({ force: true }); } catch (error) { this.showSetupFeedback(error.message, "error"); } @@ -496,7 +498,7 @@ window.iunaApp = function iunaApp() { this.verifyAnswers = {}; this.walletVerified = true; this.setupSeedStep = "verified"; - await this.refresh(); + await this.refresh({ force: true }); this.showSetupFeedback("Recovery phrase imported", "success"); } catch (error) { this.showSetupFeedback(error.message, "error"); @@ -508,7 +510,7 @@ window.iunaApp = function iunaApp() { for (const [key, value] of Object.entries(fields)) { body.set(key, value); } - const response = await fetch(path, { + const response = await this.fetchWithTimeout(path, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, body, @@ -529,7 +531,7 @@ window.iunaApp = function iunaApp() { throw new Error("Add a bootstrap peer before continuing"); } await this.applySetupNodeMode(); - const response = await fetch("/api/config", { + const response = await this.fetchWithTimeout("/api/config", { method: "POST", headers: { Accept: "application/json", @@ -544,7 +546,7 @@ window.iunaApp = function iunaApp() { if (!response.ok || !payload.ok) { throw new Error(payload.error || `/api/config returned ${response.status}`); } - await this.refresh(); + await this.refresh({ force: true }); this.setupFeedback = null; this.generatedSeedPhrase = ""; this.importSeedPhrase = ""; @@ -571,6 +573,24 @@ window.iunaApp = function iunaApp() { }, async refresh(options = {}) { + if (this.refreshPromise) { + if (options.force === true) { + try { + await this.refreshPromise; + } catch { + // The forced refresh below should report the current state. + } + } else { + return this.refreshPromise; + } + } + this.refreshPromise = this.refreshNow(options).finally(() => { + this.refreshPromise = null; + }); + return this.refreshPromise; + }, + + async refreshNow(options = {}) { if (!this.canUseProtectedApi()) return; try { const [config, status, blocks, p2pMetrics, blockchainMetrics, networkHealth] = await Promise.all([ @@ -627,13 +647,31 @@ window.iunaApp = function iunaApp() { }, async fetchJson(path) { - const response = await fetch(path, { headers: { Accept: "application/json" } }); + const response = await this.fetchWithTimeout(path, { + headers: { Accept: "application/json" }, + cache: "no-store", + }); if (!response.ok) { throw new Error(`${path} returned ${response.status}`); } return response.json(); }, + async fetchWithTimeout(path, options = {}) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs); + try { + return await fetch(path, { ...options, signal: controller.signal }); + } catch (error) { + if (error?.name === "AbortError") { + throw new Error(`${path} timed out`); + } + throw error; + } finally { + clearTimeout(timeout); + } + }, + datasetConfig(kind) { return { walletTx: { @@ -1069,7 +1107,7 @@ window.iunaApp = function iunaApp() { async postForm(path, fields, successMessage, method = "POST") { await this.submitForm(path, fields, method); - await this.refresh(); + await this.refresh({ force: true }); this.showFlash(successMessage, "success"); }, @@ -1082,7 +1120,7 @@ window.iunaApp = function iunaApp() { body.set(key, value); } } - const response = await fetch(path, { + const response = await this.fetchWithTimeout(path, { method, headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, body, @@ -1152,7 +1190,7 @@ window.iunaApp = function iunaApp() { try { const body = new URLSearchParams(); for (const [key, value] of Object.entries(fields)) body.set(key, value); - const response = await fetch(path, { + const response = await this.fetchWithTimeout(path, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, body,