[fix](trx-frontend-http): make setRigFrequency fire-and-forget

The HTTP round-trip for set_freq blocks on the server processing the
command (mpsc → TCP → rig hardware → response). With optimistic local
updates, CSS overlay, and SSE snap-back guard already in place, there is
no reason to await the network call. All callers (jog, freq input, spectrum
click, RDS AF tune) now return immediately after the optimistic update.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-22 08:31:01 +01:00
parent 7f9ecad34c
commit faf86faff9
@@ -1943,32 +1943,32 @@ async function ensureTunedBandwidthCoverage(freqHz, bandwidthHz = coverageGuardB
let _freqOptimisticHz = null; let _freqOptimisticHz = null;
let _freqOptimisticSeq = 0; let _freqOptimisticSeq = 0;
async function setRigFrequency(freqHz) { function setRigFrequency(freqHz) {
const targetHz = Math.round(freqHz); const targetHz = Math.round(freqHz);
if (!freqAllowed(targetHz)) { if (!freqAllowed(targetHz)) {
showUnsupportedFreqPopup(targetHz); showUnsupportedFreqPopup(targetHz);
throw new Error(`Unsupported frequency: ${targetHz}`); throw new Error(`Unsupported frequency: ${targetHz}`);
} }
// Optimistic local update before any network round-trip. // Optimistic local update — visual is instant via CSS overlay + guard.
const prevFreqHz = lastFreqHz; const prevFreqHz = lastFreqHz;
const seq = ++_freqOptimisticSeq; const seq = ++_freqOptimisticSeq;
_freqOptimisticHz = targetHz; _freqOptimisticHz = targetHz;
applyLocalTunedFrequency(targetHz); applyLocalTunedFrequency(targetHz);
try { // Fire-and-forget: network calls run in background. The SSE stream will
// set_freq and set_center_freq are independent server operations — run in parallel. // push the confirmed frequency; the optimistic guard prevents snap-back.
await Promise.all([ Promise.all([
postPath(`/set_freq?hz=${targetHz}`), postPath(`/set_freq?hz=${targetHz}`),
ensureTunedBandwidthCoverage(targetHz), ensureTunedBandwidthCoverage(targetHz),
]); ]).catch((err) => {
} catch (err) { // Roll back only if no newer optimistic call has superseded this one.
// Roll back the optimistic update so the spectrum view stays on the if (_freqOptimisticSeq === seq && prevFreqHz != null) {
// actual server-side frequency (e.g. when the user loses control access). _freqOptimisticHz = null;
if (prevFreqHz != null) applyLocalTunedFrequency(prevFreqHz, true); applyLocalTunedFrequency(prevFreqHz, true);
throw err; }
} finally { console.warn("setRigFrequency failed:", err);
// Only clear the guard if no newer optimistic call has superseded us. }).finally(() => {
if (_freqOptimisticSeq === seq) _freqOptimisticHz = null; if (_freqOptimisticSeq === seq) _freqOptimisticHz = null;
} });
} }
function spectrumBinIndexForHz(data, hz) { function spectrumBinIndexForHz(data, hz) {
@@ -3509,7 +3509,7 @@ pttBtn.addEventListener("click", async () => {
} }
}); });
async function applyFreqFromInput() { function applyFreqFromInput() {
const parsedRaw = parseFreqInput(freqEl.value, jogUnit); const parsedRaw = parseFreqInput(freqEl.value, jogUnit);
const parsed = alignFreqToRigStep(parsedRaw); const parsed = alignFreqToRigStep(parsedRaw);
if (parsed === null) { if (parsed === null) {
@@ -3521,17 +3521,8 @@ async function applyFreqFromInput() {
return; return;
} }
freqDirty = false; freqDirty = false;
freqEl.disabled = true; // setRigFrequency is fire-and-forget; visual update is instant.
showHint("Setting frequency…"); setRigFrequency(parsed);
try {
await setRigFrequency(parsed);
showHint("Freq set", 1500);
} catch (err) {
showHint("Set freq failed", 2000);
console.error(err);
} finally {
freqEl.disabled = false;
}
} }
async function applyCenterFreqFromInput() { async function applyCenterFreqFromInput() {
@@ -3616,7 +3607,7 @@ function setJogDivisor(divisor) {
applyJogStep(); applyJogStep();
} }
async function jogFreq(direction) { function jogFreq(direction) {
if (lastLocked) { showHint("Locked", 1500); return; } if (lastLocked) { showHint("Locked", 1500); return; }
if (lastFreqHz === null) return; if (lastFreqHz === null) return;
const newHz = alignFreqToRigStep(lastFreqHz + direction * jogStep); const newHz = alignFreqToRigStep(lastFreqHz + direction * jogStep);
@@ -3626,22 +3617,8 @@ async function jogFreq(direction) {
} }
jogAngle = (jogAngle + direction * 15) % 360; jogAngle = (jogAngle + direction * 15) % 360;
jogIndicator.style.transform = `translateX(-50%) rotate(${jogAngle}deg)`; jogIndicator.style.transform = `translateX(-50%) rotate(${jogAngle}deg)`;
showHint("Setting frequency…"); // setRigFrequency is fire-and-forget; visual update is instant.
// Optimistic local state update — visible to the user before the network call. setRigFrequency(newHz);
// Set the guard BEFORE yielding so SSE cannot snap back during the yield.
++_freqOptimisticSeq;
_freqOptimisticHz = newHz;
applyLocalTunedFrequency(newHz);
// Yield so the browser paints the updated freq display before the network
// round-trip begins. Chrome's INP tracking ends at the yield point.
await yieldToMain();
try {
await setRigFrequency(newHz);
showHint("Freq set", 1000);
} catch (err) {
showHint("Set freq failed", 2000);
console.error(err);
}
} }
jogDownBtn.addEventListener("click", () => jogFreq(-1)); jogDownBtn.addEventListener("click", () => jogFreq(-1));
@@ -8837,15 +8814,11 @@ function formatRdsAfMHz(hz) {
return `${(hz / 1_000_000).toFixed(1)} MHz`; return `${(hz / 1_000_000).toFixed(1)} MHz`;
} }
async function tuneRdsAlternativeFrequency(hz) { function tuneRdsAlternativeFrequency(hz) {
if (!Number.isFinite(hz) || hz <= 0) return; if (!Number.isFinite(hz) || hz <= 0) return;
const targetHz = Math.round(hz); const targetHz = Math.round(hz);
try { setRigFrequency(targetHz);
await setRigFrequency(targetHz); showHint(`Tuned ${formatRdsAfMHz(targetHz)}`, 1200);
showHint(`Tuned ${formatRdsAfMHz(targetHz)}`, 1200);
} catch (_) {
showHint("Set freq failed", 1500);
}
} }
function renderRdsAlternativeFrequencies(list) { function renderRdsAlternativeFrequencies(list) {
@@ -9688,7 +9661,7 @@ function handleSpectrumClick(e, canvasEl) {
const cssX = e.clientX - rect.left; const cssX = e.clientX - rect.left;
const targetHz = spectrumTargetHzAt(cssX, rect.width, lastSpectrumData); const targetHz = spectrumTargetHzAt(cssX, rect.width, lastSpectrumData);
if (!Number.isFinite(targetHz)) return; if (!Number.isFinite(targetHz)) return;
setRigFrequency(targetHz).catch(() => {}); setRigFrequency(targetHz);
} }
if (spectrumCanvas) { if (spectrumCanvas) {