refactor: add typed decoder plugin runtime

This commit is contained in:
sjg
2026-08-01 13:02:54 +02:00
parent 6a83e2e90a
commit 508cf2ba87
23 changed files with 438 additions and 324 deletions
@@ -228,15 +228,15 @@
}
aisWindow.updateAisBar = updateAisBar;
aisWindow.clearAisBar = function() {
aisWindow.resetAisHistoryView?.();
resetAisHistoryView();
};
aisWindow.resetAisHistoryView = function() {
function resetAisHistoryView() {
if (aisMessagesEl) aisMessagesEl.innerHTML = "";
aisMessageHistory = [];
updateAisBar();
renderAisHistory();
aisWindow.clearMapMarkersByType?.("ais");
};
}
function renderAisHistory() {
pruneAisMessageHistory();
if (!aisMessagesEl) {
@@ -272,7 +272,7 @@
rig_id: msg.rig_id || null
};
}
aisWindow.onServerAisBatch = function(messages) {
function onServerAisBatch(messages) {
if (!Array.isArray(messages) || messages.length === 0) return;
if (aisStatus) aisStatus.textContent = "Receiving";
const normalized = [];
@@ -295,21 +295,18 @@
pruneAisMessageHistory();
scheduleAisBarUpdate();
scheduleAisHistoryRender();
};
aisWindow.restoreAisHistory = function(messages) {
aisWindow.onServerAisBatch?.(messages);
};
aisWindow.pruneAisHistoryView = function() {
}
function pruneAisHistoryView() {
pruneAisMessageHistory();
updateAisBar();
renderAisHistory();
};
}
document.getElementById("settings-clear-ais-history")?.addEventListener("click", () => {
void (async () => {
if (!await aisWindow.trxUi.confirm({ title: "Clear AIS history?", message: "All stored AIS messages will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await aisWindow.postPath?.("/clear_ais_decode");
aisWindow.resetAisHistoryView?.();
resetAisHistoryView();
} catch (e) {
console.error("AIS history clear failed", e);
}
@@ -321,10 +318,17 @@
renderAisHistory();
});
}
aisWindow.onServerAis = function(msg) {
function onServerAis(msg) {
if (aisStatus) aisStatus.textContent = "Receiving";
addAisMessage(normalizeServerAisMessage(msg));
};
}
updateAisSummary();
aisWindow._trxDrainPendingDecode?.("ais");
window.trxPluginRuntime.registerDecoder({
id: "ais",
onMessage: onServerAis,
onBatch: onServerAisBatch,
restore: onServerAisBatch,
reset: resetAisHistoryView,
prune: pruneAisHistoryView
});
})();
@@ -488,10 +488,10 @@ function currentDecodeHistoryRetentionMs() {
}
window.getDecodeHistoryRetentionMs = currentDecodeHistoryRetentionMs;
window.applyDecodeHistoryRetention = function() {
if (typeof window.pruneAprsHistoryView === "function") window.pruneAprsHistoryView();
if (typeof window.pruneHfAprsHistoryView === "function") window.pruneHfAprsHistoryView();
if (typeof window.pruneAisHistoryView === "function") window.pruneAisHistoryView();
if (typeof window.pruneVdesHistoryView === "function") window.pruneVdesHistoryView();
window.trxPluginRuntime.prune("aprs");
window.trxPluginRuntime.prune("hf_aprs");
window.trxPluginRuntime.prune("ais");
window.trxPluginRuntime.prune("vdes");
if (typeof window.pruneFt8HistoryView === "function") window.pruneFt8HistoryView();
if (typeof window.pruneWsprHistoryView === "function") window.pruneWsprHistoryView();
};
@@ -529,37 +529,6 @@ function setConnLostOverlay(visible, title = "Connection lost", sub = "Retrying
const decodeHistoryTextDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
let decodeHistoryReplayActive = false;
let decodeMapSyncPending = false;
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() {
decodeMapSyncPending = true;
}
@@ -5888,21 +5857,8 @@ function updateDecodeStatus(text) {
}
}
function dispatchDecodeMessage(msg, skipStats) {
if (msg.type === "ais") {
if (window.onServerAis) window.onServerAis(msg);
else (_pendingDecodeLive.ais = _pendingDecodeLive.ais || []).push(msg);
}
if (msg.type === "vdes") {
if (window.onServerVdes) window.onServerVdes(msg);
else (_pendingDecodeLive.vdes = _pendingDecodeLive.vdes || []).push(msg);
}
if (msg.type === "aprs") {
if (window.onServerAprs) window.onServerAprs(msg);
else (_pendingDecodeLive.aprs = _pendingDecodeLive.aprs || []).push(msg);
}
if (msg.type === "hf_aprs") {
if (window.onServerHfAprs) window.onServerHfAprs(msg);
else (_pendingDecodeLive.hf_aprs = _pendingDecodeLive.hf_aprs || []).push(msg);
if (msg.type === "ais" || msg.type === "vdes" || msg.type === "aprs" || msg.type === "hf_aprs") {
window.trxPluginRuntime.dispatch(msg.type, msg);
}
if (msg.type === "cw" && window.onServerCw) window.onServerCw(msg);
if (msg.type === "ft8" && window.onServerFt8) window.onServerFt8(msg);
@@ -5929,20 +5885,8 @@ function dispatchDecodeBatch(batch) {
const type = String(batch[0]?.type || "");
const uniformType = batch.every((msg) => String(msg?.type || "") === type);
if (uniformType) {
if (type === "ais" && window.onServerAisBatch) {
window.onServerAisBatch(batch);
return;
}
if (type === "vdes" && window.onServerVdesBatch) {
window.onServerVdesBatch(batch);
return;
}
if (type === "aprs" && window.onServerAprsBatch) {
window.onServerAprsBatch(batch);
return;
}
if (type === "hf_aprs" && window.onServerHfAprsBatch) {
window.onServerHfAprsBatch(batch);
if (type === "ais" || type === "vdes" || type === "aprs" || type === "hf_aprs") {
window.trxPluginRuntime.dispatchBatch(type, batch);
return;
}
if (type === "ft8" && window.onServerFt8Batch) {
@@ -6010,36 +5954,8 @@ function restoreDecodeHistoryGroup(kind, messages) {
}
window.trx.modules.map?.scheduleStatsRender();
}
if (kind === "ais") {
if (window.restoreAisHistory) {
window.restoreAisHistory(messages);
} else {
_pendingDecodeHistory.ais = (_pendingDecodeHistory.ais || []).concat(messages);
}
return;
}
if (kind === "vdes") {
if (window.restoreVdesHistory) {
window.restoreVdesHistory(messages);
} else {
_pendingDecodeHistory.vdes = (_pendingDecodeHistory.vdes || []).concat(messages);
}
return;
}
if (kind === "aprs") {
if (window.restoreAprsHistory) {
window.restoreAprsHistory(messages);
} else {
_pendingDecodeHistory.aprs = (_pendingDecodeHistory.aprs || []).concat(messages);
}
return;
}
if (kind === "hf_aprs") {
if (window.restoreHfAprsHistory) {
window.restoreHfAprsHistory(messages);
} else {
_pendingDecodeHistory.hf_aprs = (_pendingDecodeHistory.hf_aprs || []).concat(messages);
}
if (kind === "ais" || kind === "vdes" || kind === "aprs" || kind === "hf_aprs") {
window.trxPluginRuntime.restore(kind, messages);
return;
}
if (kind === "cw" && window.restoreCwHistory) {
@@ -6074,11 +5990,11 @@ function connectDecode() {
terminateDecodeHistoryWorker();
decodeHistoryReplayActive = false;
decodeMapSyncPending = false;
for (const k in _pendingDecodeHistory) delete _pendingDecodeHistory[k];
for (const k in _pendingDecodeLive) delete _pendingDecodeLive[k];
if (window.resetAisHistoryView) window.resetAisHistoryView();
if (window.resetVdesHistoryView) window.resetVdesHistoryView();
if (window.resetAprsHistoryView) window.resetAprsHistoryView();
window.trxPluginRuntime.clearQueued();
window.trxPluginRuntime.reset("ais");
window.trxPluginRuntime.reset("vdes");
window.trxPluginRuntime.reset("aprs");
window.trxPluginRuntime.reset("hf_aprs");
if (window.resetCwHistoryView) window.resetCwHistoryView();
if (window.resetFt8HistoryView) window.resetFt8HistoryView();
if (window.resetFt4HistoryView) window.resetFt4HistoryView();
@@ -291,7 +291,7 @@
}
aprsWindow.updateAprsBar = updateAprsBar;
aprsWindow.clearAprsBar = function() {
aprsWindow.resetAprsHistoryView?.();
resetAprsHistoryView();
};
aprsWindow.closeAprsBar = function() {
aprsBarDismissedAtMs = Date.now();
@@ -300,18 +300,18 @@
aprsBarOverlay.innerHTML = "";
}
};
aprsWindow.resetAprsHistoryView = function() {
function resetAprsHistoryView() {
if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
aprsPacketHistory = [];
updateAprsBar();
renderAprsHistory();
aprsWindow.clearMapMarkersByType?.("aprs");
};
aprsWindow.pruneAprsHistoryView = function() {
}
function pruneAprsHistoryView() {
pruneAprsPacketHistory();
updateAprsBar();
renderAprsHistory();
};
}
function addAprsPacket(pkt) {
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
pkt._tsMs = tsMs;
@@ -327,7 +327,7 @@
function normalizeServerAprsPacket(pkt) {
return normalizeAprsPacket(pkt, aprsWindow.getDecodeRigMeta?.() ?? null);
}
aprsWindow.onServerAprsBatch = function(packets) {
function onServerAprsBatch(packets) {
if (!Array.isArray(packets) || packets.length === 0) return;
if (aprsStatus) aprsStatus.textContent = "Receiving";
const normalized = [];
@@ -348,16 +348,13 @@
pruneAprsPacketHistory();
if (hasCrcOk) scheduleAprsBarUpdate();
scheduleAprsHistoryRender();
};
aprsWindow.restoreAprsHistory = function(packets) {
aprsWindow.onServerAprsBatch?.(packets);
};
}
document.getElementById("settings-clear-aprs-history")?.addEventListener("click", () => {
void (async () => {
if (!await aprsWindow.trxUi.confirm({ title: "Clear APRS history?", message: "All stored APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await aprsWindow.postPath?.("/clear_aprs_decode");
aprsWindow.resetAprsHistoryView?.();
resetAprsHistoryView();
} catch (e) {
console.error("APRS history clear failed", e);
}
@@ -395,10 +392,17 @@
renderAprsHistory();
});
}
aprsWindow.onServerAprs = function(pkt) {
function onServerAprs(pkt) {
if (aprsStatus) aprsStatus.textContent = "Receiving";
addAprsPacket(normalizeServerAprsPacket(pkt));
};
}
renderAprsHistory();
aprsWindow._trxDrainPendingDecode?.("aprs");
window.trxPluginRuntime.registerDecoder({
id: "aprs",
onMessage: onServerAprs,
onBatch: onServerAprsBatch,
restore: onServerAprsBatch,
reset: resetAprsHistoryView,
prune: pruneAprsHistoryView
});
})();
@@ -253,15 +253,15 @@
updateHfAprsSummary();
updateHfAprsChipState();
}
hfAprsWindow.resetHfAprsHistoryView = function() {
function resetHfAprsHistoryView() {
if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
hfAprsPacketHistory = [];
renderHfAprsHistory();
};
hfAprsWindow.pruneHfAprsHistoryView = function() {
}
function pruneHfAprsHistoryView() {
pruneHfAprsPacketHistory();
renderHfAprsHistory();
};
}
function addHfAprsPacket(pkt) {
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
pkt._tsMs = tsMs;
@@ -273,7 +273,7 @@
function normalizeServerHfAprsPacket(pkt) {
return normalizeAprsPacket(pkt, hfAprsWindow.getDecodeRigMeta?.() ?? null);
}
hfAprsWindow.onServerHfAprsBatch = function(packets) {
function onServerHfAprsBatch(packets) {
if (!Array.isArray(packets) || packets.length === 0) return;
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
const normalized = [];
@@ -288,10 +288,7 @@
hfAprsPacketHistory = normalized.concat(hfAprsPacketHistory);
pruneHfAprsPacketHistory();
scheduleHfAprsHistoryRender();
};
hfAprsWindow.restoreHfAprsHistory = function(packets) {
hfAprsWindow.onServerHfAprsBatch?.(packets);
};
}
var hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn");
hfAprsDecodeToggleBtn?.addEventListener("click", () => {
void (async () => {
@@ -308,7 +305,7 @@
if (!await hfAprsWindow.trxUi.confirm({ title: "Clear HF APRS history?", message: "All stored HF APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await hfAprsWindow.postPath?.("/clear_hf_aprs_decode");
hfAprsWindow.resetHfAprsHistoryView?.();
resetHfAprsHistoryView();
} catch (e) {
console.error("HF APRS history clear failed", e);
}
@@ -346,10 +343,17 @@
renderHfAprsHistory();
});
}
hfAprsWindow.onServerHfAprs = function(pkt) {
function onServerHfAprs(pkt) {
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
};
}
renderHfAprsHistory();
hfAprsWindow._trxDrainPendingDecode?.("hf_aprs");
window.trxPluginRuntime.registerDecoder({
id: "hf_aprs",
onMessage: onServerHfAprs,
onBatch: onServerHfAprsBatch,
restore: onServerHfAprsBatch,
reset: resetHfAprsHistoryView,
prune: pruneHfAprsHistoryView
});
})();
@@ -0,0 +1,80 @@
"use strict";
const decoders = /* @__PURE__ */ new Map();
const queued = /* @__PURE__ */ new Map();
const MAX_QUEUED_ACTIONS_PER_DECODER = 512;
function enqueue(id, action) {
const actions = queued.get(id) ?? [];
actions.push(action);
if (actions.length > MAX_QUEUED_ACTIONS_PER_DECODER) actions.splice(0, actions.length - MAX_QUEUED_ACTIONS_PER_DECODER);
queued.set(id, actions);
}
function deliver(plugin, action) {
if (action.kind === "message" && plugin.onMessage) {
plugin.onMessage(action.payload);
return true;
}
if (action.kind === "batch" && plugin.onBatch) {
plugin.onBatch(action.payload);
return true;
}
if (action.kind === "restore" && plugin.restore) {
plugin.restore(action.payload);
return true;
}
return false;
}
function dispatchOrQueue(id, action) {
const plugin = decoders.get(id);
if (!plugin) {
enqueue(id, action);
return false;
}
if (!deliver(plugin, action)) {
if (action.kind === "batch" && plugin.onMessage) {
for (const message of action.payload) plugin.onMessage(message);
return true;
}
if (action.kind === "restore" && plugin.onBatch) {
plugin.onBatch(action.payload);
return true;
}
return false;
}
return true;
}
const runtime = {
registerDecoder(plugin) {
if (decoders.has(plugin.id)) throw new Error(`Decoder plugin already registered: ${plugin.id}`);
const erased = plugin;
decoders.set(plugin.id, erased);
const pending = queued.get(plugin.id) ?? [];
queued.delete(plugin.id);
for (const action of pending) deliver(erased, action);
return () => {
if (decoders.get(plugin.id) === erased) decoders.delete(plugin.id);
};
},
dispatch: (id, message) => dispatchOrQueue(id, { kind: "message", payload: message }),
dispatchBatch: (id, messages) => dispatchOrQueue(id, { kind: "batch", payload: messages }),
restore: (id, messages) => dispatchOrQueue(id, { kind: "restore", payload: messages }),
reset(id) {
const plugin = decoders.get(id);
if (!plugin?.reset) return false;
plugin.reset();
return true;
},
resetAll() {
for (const plugin of decoders.values()) plugin.reset?.();
},
prune(id) {
const plugin = decoders.get(id);
if (!plugin?.prune) return false;
plugin.prune();
return true;
},
clearQueued() {
queued.clear();
},
hasDecoder: (id) => decoders.has(id)
};
window.trxPluginRuntime = runtime;
@@ -165,14 +165,14 @@
}
vdesWindow.updateVdesBar = updateVdesBar;
vdesWindow.clearVdesBar = function() {
vdesWindow.resetVdesHistoryView?.();
resetVdesHistoryView();
};
vdesWindow.resetVdesHistoryView = function() {
function resetVdesHistoryView() {
if (vdesMessagesEl) vdesMessagesEl.innerHTML = "";
vdesMessageHistory = [];
updateVdesBar();
renderVdesHistory();
};
}
function renderVdesHistory() {
pruneVdesMessageHistory();
if (!vdesMessagesEl) {
@@ -205,7 +205,7 @@
rig_id: msg.rig_id || null
};
}
vdesWindow.onServerVdesBatch = function(messages) {
function onServerVdesBatch(messages) {
if (!Array.isArray(messages) || messages.length === 0) return;
if (vdesStatus) vdesStatus.textContent = "Receiving";
const normalized = [];
@@ -228,16 +228,13 @@
pruneVdesMessageHistory();
scheduleVdesBarUpdate();
scheduleVdesHistoryRender();
};
vdesWindow.restoreVdesHistory = function(messages) {
vdesWindow.onServerVdesBatch?.(messages);
};
}
document.getElementById("settings-clear-vdes-history")?.addEventListener("click", () => {
void (async () => {
if (!await vdesWindow.trxUi.confirm({ title: "Clear VDES history?", message: "All stored VDES decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await vdesWindow.postPath?.("/clear_vdes_decode");
vdesWindow.resetVdesHistoryView?.();
resetVdesHistoryView();
} catch (e) {
console.error("VDES history clear failed", e);
}
@@ -249,19 +246,26 @@
renderVdesHistory();
});
}
vdesWindow.onServerVdes = function(msg) {
function onServerVdes(msg) {
if (vdesStatus) vdesStatus.textContent = "Receiving";
const next = normalizeServerVdesMessage(msg);
addVdesMessage(next);
if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) {
vdesWindow.vdesMapAddPoint(next);
}
};
vdesWindow.pruneVdesHistoryView = function() {
}
function pruneVdesHistoryView() {
pruneVdesMessageHistory();
updateVdesBar();
renderVdesHistory();
};
}
updateVdesSummary();
vdesWindow._trxDrainPendingDecode?.("vdes");
window.trxPluginRuntime.registerDecoder({
id: "vdes",
onMessage: onServerVdes,
onBatch: onServerVdesBatch,
restore: onServerVdesBatch,
reset: resetVdesHistoryView,
prune: pruneVdesHistoryView
});
})();