From 4c4f4e2c64ce503448fae6b72ece4776b66bdacc Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Thu, 20 Nov 2025 16:48:42 +0100 Subject: [PATCH] Fix for ref population --- assets/js/sections/qso.js | 122 +++++++++++++++++++++++++++----------- 1 file changed, 89 insertions(+), 33 deletions(-) diff --git a/assets/js/sections/qso.js b/assets/js/sections/qso.js index cad646487..0dc2da2d2 100644 --- a/assets/js/sections/qso.js +++ b/assets/js/sections/qso.js @@ -655,83 +655,121 @@ bc_bandmap.onmessage = function (ev) { } // Store pending references from bandmap to populate AFTER callsign lookup completes -var pendingReferences = null; +// Map structure: callsign -> {seq, refs, timestamp, populated} +var pendingReferencesMap = new Map(); +var referenceSequence = 0; // Track last lookup to prevent duplicate calls var lastLookupCallsign = null; var lookupInProgress = false; // Helper function to populate reference fields after callsign lookup completes -function populatePendingReferences(refsToPopulate) { - // Use provided references or fall back to global pendingReferences - var refs = refsToPopulate || pendingReferences; - - if (!refs) { - return; +// Uses Map-based storage with sequence validation to prevent race conditions +function populatePendingReferences(callsign, expectedSeq) { + // Handle legacy call without parameters + if (!callsign) { + callsign = $('#callsign').val(); + expectedSeq = $('#callsign').data('expected-refs-seq'); } - // POTA - uses selectize + const entry = pendingReferencesMap.get(callsign); + + if (!entry) { + console.log('No pending references for callsign:', callsign); + return; + } + + // Validate sequence to prevent stale data + if (expectedSeq && entry.seq !== expectedSeq) { + console.warn('Sequence mismatch - ignoring stale references', { + callsign: callsign, + expected: expectedSeq, + actual: entry.seq + }); + return; + } + + // Mark as populated to prevent double-population + if (entry.populated) { + console.warn('References already populated for', callsign); + return; + } + entry.populated = true; + + const refs = entry.refs; + + // POTA - set without triggering change initially (silent = true) if (refs.pota_ref && $('#pota_ref').length) { try { var $select = $('#pota_ref').selectize(); if ($select.length && $select[0].selectize) { var selectize = $select[0].selectize; selectize.addOption({name: refs.pota_ref}); - selectize.setValue(refs.pota_ref, false); - $('#pota_ref').trigger('change'); + selectize.setValue(refs.pota_ref, true); // Silent = true } } catch (e) { console.warn('Could not set POTA reference:', e); } } - // SOTA - uses selectize + // SOTA - set without triggering change initially (silent = true) if (refs.sota_ref && $('#sota_ref').length) { try { var $select = $('#sota_ref').selectize(); if ($select.length && $select[0].selectize) { var selectize = $select[0].selectize; selectize.addOption({name: refs.sota_ref}); - selectize.setValue(refs.sota_ref, false); - $('#sota_ref').trigger('change'); + selectize.setValue(refs.sota_ref, true); // Silent = true } } catch (e) { console.warn('Could not set SOTA reference:', e); } } - // WWFF - uses selectize + // WWFF - set without triggering change initially (silent = true) if (refs.wwff_ref && $('#wwff_ref').length) { try { var $select = $('#wwff_ref').selectize(); if ($select.length && $select[0].selectize) { var selectize = $select[0].selectize; selectize.addOption({name: refs.wwff_ref}); - selectize.setValue(refs.wwff_ref, false); - $('#wwff_ref').trigger('change'); + selectize.setValue(refs.wwff_ref, true); // Silent = true } } catch (e) { console.warn('Could not set WWFF reference:', e); } } - // IOTA - regular select dropdown (not selectize) + // IOTA - set silently (no change trigger yet) if (refs.iota_ref && $('#iota_ref').length) { try { let $iotaSelect = $('#iota_ref'); if ($iotaSelect.find('option[value="' + refs.iota_ref + '"]').length === 0) { $iotaSelect.append(new Option(refs.iota_ref, refs.iota_ref)); } - $iotaSelect.val(refs.iota_ref).trigger('change'); + $iotaSelect.val(refs.iota_ref); // Don't trigger change yet } catch (e) { console.warn('Could not set IOTA reference:', e); } } - // Only clear global pendingReferences if we used it (not a captured copy) - if (!refsToPopulate && pendingReferences) { - pendingReferences = null; - } + // NOW trigger gridsquare lookup ONLY ONCE for the highest priority reference + // Priority: POTA > SOTA > WWFF (most commonly used) + // This prevents multiple simultaneous AJAX gridsquare lookups that can race + setTimeout(function() { + if (refs.pota_ref && $('#pota_ref').length) { + $('#pota_ref').trigger('change'); + } else if (refs.sota_ref && $('#sota_ref').length) { + $('#sota_ref').trigger('change'); + } else if (refs.wwff_ref && $('#wwff_ref').length) { + $('#wwff_ref').trigger('change'); + } + }, 100); // Small delay to let form settle + + // Cleanup after successful population + setTimeout(function() { + pendingReferencesMap.delete(callsign); + }, 5000); // Keep for 5 seconds in case of retry } var bc = new BroadcastChannel('qso_wish'); @@ -746,21 +784,35 @@ bc.onmessage = function (ev) { } else { // Always process frequency, callsign, and reference data from bandmap // (regardless of manual mode - bandmap should control the form) + const callsign = ev.data.call; + const seq = ++referenceSequence; let delay = 0; // Only reset if callsign is different from what we're about to set - if ($("#callsign").val() != "" && $("#callsign").val() != ev.data.call) { + if ($("#callsign").val() != "" && $("#callsign").val() != callsign) { reset_fields(); delay = 600; } - // Store references for later population (after callsign lookup completes) - pendingReferences = { - pota_ref: ev.data.pota_ref, - sota_ref: ev.data.sota_ref, - wwff_ref: ev.data.wwff_ref, - iota_ref: ev.data.iota_ref - }; + // Store references with metadata in Map (prevents race conditions) + pendingReferencesMap.set(callsign, { + seq: seq, + refs: { + pota_ref: ev.data.pota_ref, + sota_ref: ev.data.sota_ref, + wwff_ref: ev.data.wwff_ref, + iota_ref: ev.data.iota_ref + }, + timestamp: Date.now(), + populated: false + }); + + // Cleanup old entries (> 30 seconds) + for (let [key, value] of pendingReferencesMap) { + if (Date.now() - value.timestamp > 30000) { + pendingReferencesMap.delete(key); + } + } setTimeout(() => { if (ev.data.frequency != null) { @@ -775,7 +827,11 @@ bc.onmessage = function (ev) { if (ev.data.mode) { $("#mode").val(ev.data.mode); } - $("#callsign").val(ev.data.call); + + // Store sequence for validation in populatePendingReferences + $("#callsign").data('expected-refs-seq', seq); + + $("#callsign").val(callsign); $("#callsign").focusout(); $("#callsign").blur(); }, delay); @@ -1057,9 +1113,9 @@ function reset_to_default() { /* Function: reset_fields is used to reset the fields on the QSO page */ function reset_fields() { - // we set the pendingReferences to null to avoid they get prefilled in the next QSO after clear + // Clear all pending references to avoid they get prefilled in the next QSO after clear // we do this first to avoid race conditions for slow javascript - pendingReferences = null; + pendingReferencesMap.clear(); $('#locator_info').text(""); $('#comment').val("");