[fix](trx-frontend-http): buffer decode messages until map-data plugins are ready

Eagerly load map-data plugins (AIS, APRS, VDES, HF-APRS) on startup and
buffer any decode history or live SSE messages that arrive before plugin
handlers register. Each plugin drains its pending buffer on init.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-04-04 12:08:18 +02:00
parent f978812090
commit 79068e55af
6 changed files with 65 additions and 14 deletions
@@ -563,6 +563,41 @@ const decodeHistoryTextDecoder = typeof TextDecoder === "function" ? new TextDec
let decodeHistoryReplayActive = false; let decodeHistoryReplayActive = false;
let decodeMapSyncPending = false; let decodeMapSyncPending = false;
// --- Pending decode data buffers ---
// Map-data plugins (ais.js, aprs.js, vdes.js, hf-aprs.js) are loaded eagerly
// but dynamically-inserted scripts have no guaranteed execution order. If
// decode history or live SSE messages arrive before the plugin handlers are
// registered, buffer them here and let each plugin drain on init.
const _pendingDecodeHistory = {};
const _pendingDecodeLive = {};
window._trxDrainPendingDecode = function(kind) {
const historyKey = {
ais: "restoreAisHistory",
vdes: "restoreVdesHistory",
aprs: "restoreAprsHistory",
hf_aprs: "restoreHfAprsHistory",
}[kind];
if (historyKey && _pendingDecodeHistory[kind] && window[historyKey]) {
const msgs = _pendingDecodeHistory[kind];
delete _pendingDecodeHistory[kind];
window[historyKey](msgs);
}
const liveKey = {
ais: "onServerAis",
vdes: "onServerVdes",
aprs: "onServerAprs",
hf_aprs: "onServerHfAprs",
}[kind];
if (liveKey && _pendingDecodeLive[kind] && window[liveKey]) {
const msgs = _pendingDecodeLive[kind];
delete _pendingDecodeLive[kind];
for (const msg of msgs) {
try { window[liveKey](msg); } catch (_) {}
}
}
};
function markDecodeMapSyncPending() { function markDecodeMapSyncPending() {
decodeMapSyncPending = true; decodeMapSyncPending = true;
} }
@@ -5920,10 +5955,10 @@ function updateDecodeStatus(text) {
} }
} }
function dispatchDecodeMessage(msg, skipStats) { function dispatchDecodeMessage(msg, skipStats) {
if (msg.type === "ais" && window.onServerAis) window.onServerAis(msg); if (msg.type === "ais") { if (window.onServerAis) window.onServerAis(msg); else (_pendingDecodeLive.ais = _pendingDecodeLive.ais || []).push(msg); }
if (msg.type === "vdes" && window.onServerVdes) window.onServerVdes(msg); if (msg.type === "vdes") { if (window.onServerVdes) window.onServerVdes(msg); else (_pendingDecodeLive.vdes = _pendingDecodeLive.vdes || []).push(msg); }
if (msg.type === "aprs" && window.onServerAprs) window.onServerAprs(msg); if (msg.type === "aprs") { if (window.onServerAprs) window.onServerAprs(msg); else (_pendingDecodeLive.aprs = _pendingDecodeLive.aprs || []).push(msg); }
if (msg.type === "hf_aprs" && window.onServerHfAprs) window.onServerHfAprs(msg); if (msg.type === "hf_aprs") { if (window.onServerHfAprs) window.onServerHfAprs(msg); else (_pendingDecodeLive.hf_aprs = _pendingDecodeLive.hf_aprs || []).push(msg); }
if (msg.type === "cw" && window.onServerCw) window.onServerCw(msg); if (msg.type === "cw" && window.onServerCw) window.onServerCw(msg);
if (msg.type === "ft8" && window.onServerFt8) window.onServerFt8(msg); if (msg.type === "ft8" && window.onServerFt8) window.onServerFt8(msg);
if (msg.type === "ft4" && window.onServerFt4) window.onServerFt4(msg); if (msg.type === "ft4" && window.onServerFt4) window.onServerFt4(msg);
@@ -6036,20 +6071,24 @@ function restoreDecodeHistoryGroup(kind, messages) {
} }
window.trx.map?.scheduleStatsRender(); window.trx.map?.scheduleStatsRender();
} }
if (kind === "ais" && window.restoreAisHistory) { if (kind === "ais") {
window.restoreAisHistory(messages); if (window.restoreAisHistory) { window.restoreAisHistory(messages); }
else { _pendingDecodeHistory.ais = (_pendingDecodeHistory.ais || []).concat(messages); }
return; return;
} }
if (kind === "vdes" && window.restoreVdesHistory) { if (kind === "vdes") {
window.restoreVdesHistory(messages); if (window.restoreVdesHistory) { window.restoreVdesHistory(messages); }
else { _pendingDecodeHistory.vdes = (_pendingDecodeHistory.vdes || []).concat(messages); }
return; return;
} }
if (kind === "aprs" && window.restoreAprsHistory) { if (kind === "aprs") {
window.restoreAprsHistory(messages); if (window.restoreAprsHistory) { window.restoreAprsHistory(messages); }
else { _pendingDecodeHistory.aprs = (_pendingDecodeHistory.aprs || []).concat(messages); }
return; return;
} }
if (kind === "hf_aprs" && window.restoreHfAprsHistory) { if (kind === "hf_aprs") {
window.restoreHfAprsHistory(messages); if (window.restoreHfAprsHistory) { window.restoreHfAprsHistory(messages); }
else { _pendingDecodeHistory.hf_aprs = (_pendingDecodeHistory.hf_aprs || []).concat(messages); }
return; return;
} }
if (kind === "cw" && window.restoreCwHistory) { if (kind === "cw" && window.restoreCwHistory) {
@@ -6083,6 +6122,9 @@ function connectDecode() {
terminateDecodeHistoryWorker(); terminateDecodeHistoryWorker();
decodeHistoryReplayActive = false; decodeHistoryReplayActive = false;
decodeMapSyncPending = false; decodeMapSyncPending = false;
// Clear any pending buffers from a previous connection cycle.
for (const k in _pendingDecodeHistory) delete _pendingDecodeHistory[k];
for (const k in _pendingDecodeLive) delete _pendingDecodeLive[k];
if (window.resetAisHistoryView) window.resetAisHistoryView(); if (window.resetAisHistoryView) window.resetAisHistoryView();
if (window.resetVdesHistoryView) window.resetVdesHistoryView(); if (window.resetVdesHistoryView) window.resetVdesHistoryView();
if (window.resetAprsHistoryView) window.resetAprsHistoryView(); if (window.resetAprsHistoryView) window.resetAprsHistoryView();
@@ -1620,6 +1620,7 @@
(function() { (function() {
var pluginScripts = { var pluginScripts = {
'digital-modes': ['/ft8.js', '/ft4.js', '/ft2.js', '/wspr.js', '/cw.js', '/background-decode.js', '/sat.js', '/wefax.js'], 'digital-modes': ['/ft8.js', '/ft4.js', '/ft2.js', '/wspr.js', '/cw.js', '/background-decode.js', '/sat.js', '/wefax.js'],
'map-data': ['/map-core.js', '/ais.js', '/vdes.js', '/aprs.js', '/hf-aprs.js'],
'map': ['/map-core.js', '/leaflet-ais-tracksymbol.js', '/ais.js', '/vdes.js', '/aprs.js', '/hf-aprs.js', '/sat.js', '/sat-scheduler.js'], 'map': ['/map-core.js', '/leaflet-ais-tracksymbol.js', '/ais.js', '/vdes.js', '/aprs.js', '/hf-aprs.js', '/sat.js', '/sat-scheduler.js'],
'statistics': ['/map-core.js'], 'statistics': ['/map-core.js'],
'bookmarks': ['/bookmarks.js'], 'bookmarks': ['/bookmarks.js'],
@@ -1639,8 +1640,12 @@
document.body.appendChild(s); document.body.appendChild(s);
}); });
} }
// Load core plugins immediately (needed on main tab) // Load core plugins immediately (needed on main tab).
['digital-modes', 'bookmarks', 'settings'].forEach(loadPlugins); // 'map-data' loads map-core.js + data handlers (ais/aprs/vdes/hf-aprs) eagerly
// so decode history replay can populate map data structures before the map
// tab is opened. The 'loaded' Set prevents double-loading when the map tab
// is later activated.
['digital-modes', 'map-data', 'bookmarks', 'settings'].forEach(loadPlugins);
// Load others on tab switch // Load others on tab switch
document.addEventListener('click', function(e) { document.addEventListener('click', function(e) {
var tab = e.target.closest('[data-tab]'); var tab = e.target.closest('[data-tab]');
@@ -400,3 +400,4 @@ window.onServerAis = function(msg) {
}; };
updateAisSummary(); updateAisSummary();
if (window._trxDrainPendingDecode) window._trxDrainPendingDecode("ais");
@@ -491,3 +491,4 @@ window.onServerAprs = function(pkt) {
}; };
renderAprsHistory(); renderAprsHistory();
if (window._trxDrainPendingDecode) window._trxDrainPendingDecode("aprs");
@@ -437,3 +437,4 @@ window.onServerHfAprs = function(pkt) {
}; };
renderHfAprsHistory(); renderHfAprsHistory();
if (window._trxDrainPendingDecode) window._trxDrainPendingDecode("hf_aprs");
@@ -345,3 +345,4 @@ window.pruneVdesHistoryView = function() {
}; };
updateVdesSummary(); updateVdesSummary();
if (window._trxDrainPendingDecode) window._trxDrainPendingDecode("vdes");