fix: no duplicate logs on repeat Enter + ESC stops the callsign lookup
Duplicate logs: `saving` is React state (async), so a burst of Enter presses / clicks fired before the re-render each ran a full AddQSO and logged the same QSO several times when a slow lookup made logging take a moment. A synchronous savingRef guard now ignores the repeat immediately. ESC: clearing the entry (ESC) left the "looking up" spinner turning and let a still in-flight lookup re-populate the field afterwards. resetEntry now cancels the pending lookup debounce, hides the spinner at once, and bumps a generation counter so a late lookup result is discarded instead of applied.
This commit is contained in:
+34
-8
@@ -605,6 +605,11 @@ export default function App() {
|
||||
QSOAudioResetClock().then((active) => { setRecording(active); setRecTick((t) => t + 1); }).catch(() => {});
|
||||
};
|
||||
const [saving, setSaving] = useState(false);
|
||||
// Synchronous re-entrancy guard: `saving` is React state (updates async), so it
|
||||
// can't stop a burst of Enter presses / clicks fired before the re-render — each
|
||||
// would run a full AddQSO and log the SAME contact several times when a slow QRZ
|
||||
// lookup made the log take seconds. This ref blocks the repeat immediately.
|
||||
const savingRef = useRef(false);
|
||||
const [filterCallsign, setFilterCallsign] = useState('');
|
||||
// Advanced filter builder (replaces the old band/mode dropdowns).
|
||||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
@@ -1153,6 +1158,10 @@ export default function App() {
|
||||
const [lookupError, setLookupError] = useState('');
|
||||
const lookupTimerRef = useRef<number | null>(null);
|
||||
const wbTimerRef = useRef<number | null>(null);
|
||||
// Bumped whenever the entry is cleared (ESC) or a new call starts, so a still
|
||||
// in-flight lookup discards its result when it finally returns instead of
|
||||
// re-populating a field the operator just cleared.
|
||||
const lookupGenRef = useRef(0);
|
||||
const [wb, setWb] = useState<WB | null>(null);
|
||||
const [wbBusy, setWbBusy] = useState(false);
|
||||
|
||||
@@ -2155,7 +2164,9 @@ export default function App() {
|
||||
}, [spots]);
|
||||
|
||||
async function save() {
|
||||
if (savingRef.current) return; // a log is already in flight — ignore the repeat
|
||||
if (!callsign.trim()) { setError('Callsign required'); return; }
|
||||
savingRef.current = true;
|
||||
setSaving(true); setError('');
|
||||
try {
|
||||
const freqHz = freqMhz.trim() ? Math.round(parseFloat(freqMhz) * 1_000_000) : undefined;
|
||||
@@ -2225,13 +2236,21 @@ export default function App() {
|
||||
await refresh();
|
||||
} catch (e: any) {
|
||||
setError(String(e?.message ?? e));
|
||||
} finally { setSaving(false); }
|
||||
} finally { setSaving(false); savingRef.current = false; }
|
||||
}
|
||||
|
||||
// resetEntry clears the form for the next QSO. Triggered after a
|
||||
// successful log AND by ESC. Locked values (band/mode/freq/start/end)
|
||||
// are preserved so backdated batches stay productive.
|
||||
function resetEntry() {
|
||||
// Stop any callsign lookup DEAD: cancel the pending debounce, hide the "looking
|
||||
// up" spinner immediately, and invalidate any request already in flight so its
|
||||
// late result can't re-fill the field we're about to clear (the "ESC clears the
|
||||
// QSO but the QRZ lookup keeps going" bug).
|
||||
if (lookupTimerRef.current) { window.clearTimeout(lookupTimerRef.current); lookupTimerRef.current = null; }
|
||||
if (wbTimerRef.current) { window.clearTimeout(wbTimerRef.current); wbTimerRef.current = null; }
|
||||
lookupGenRef.current++;
|
||||
setLookupBusy(false);
|
||||
// Discard any in-progress QSO recording (no-op if it was already saved on
|
||||
// log, or if the recorder is off).
|
||||
QSOAudioCancel(); setRecording(false); recordingCallRef.current = "";
|
||||
@@ -2436,14 +2455,15 @@ export default function App() {
|
||||
}
|
||||
async function runLookup(call: string) {
|
||||
if (call !== lastLookedUpRef.current) resetAutoFill();
|
||||
const gen = lookupGenRef.current; // invalidated by ESC / resetEntry
|
||||
setLookupBusy(true);
|
||||
try {
|
||||
const r = await LookupCallsign(call);
|
||||
// Discard a STALE result: the operator already moved to another call
|
||||
// (clicked a new spot / typed) while this lookup was in flight. Applying it
|
||||
// would clobber the current call's fields and zoom the map to the wrong
|
||||
// station — the bug where replacing a call didn't re-zoom the map.
|
||||
if (call !== callsignValRef.current.trim().toUpperCase()) return;
|
||||
// (clicked a new spot / typed) OR cleared the entry (ESC) while this lookup
|
||||
// was in flight. Applying it would clobber the current fields and zoom the
|
||||
// map to the wrong station.
|
||||
if (gen !== lookupGenRef.current || call !== callsignValRef.current.trim().toUpperCase()) return;
|
||||
lastLookedUpRef.current = call;
|
||||
// cty.dat carries ONLY DXCC-entity data (country / CQ / ITU zones / continent).
|
||||
// A QRZ/HamQTH hit is far richer (name, QTH, grid, address, image). When the
|
||||
@@ -2496,9 +2516,15 @@ export default function App() {
|
||||
QSOAudioBegin().then(setRecording).catch(() => {});
|
||||
}
|
||||
} catch (e: any) {
|
||||
setLookupResult(null);
|
||||
setLookupError(String(e?.message ?? e));
|
||||
} finally { setLookupBusy(false); }
|
||||
if (gen === lookupGenRef.current && call === callsignValRef.current.trim().toUpperCase()) {
|
||||
setLookupResult(null);
|
||||
setLookupError(String(e?.message ?? e));
|
||||
}
|
||||
} finally {
|
||||
// Only clear the spinner if we're still the current lookup — a newer one
|
||||
// (or an ESC that already reset it) owns the busy state otherwise.
|
||||
if (gen === lookupGenRef.current) setLookupBusy(false);
|
||||
}
|
||||
}
|
||||
function scheduleLookup(value: string, force?: boolean) {
|
||||
setLookupError('');
|
||||
|
||||
Reference in New Issue
Block a user