Complete TypeScript frontend migration #22
@@ -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
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -1640,6 +1640,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<script defer src="/leaflet-ais-tracksymbol.js"></script>
|
||||
<script defer src="/webgl-renderer.js"></script>
|
||||
<script defer src="/ui-core.js"></script>
|
||||
<script defer src="/plugin-runtime.js"></script>
|
||||
<script defer src="/plugin-loader.js"></script>
|
||||
<script defer src="/app.js"></script>
|
||||
<!-- Template cloning is handled by navigateToTab() in app.js -->
|
||||
|
||||
@@ -20,6 +20,7 @@ await build({
|
||||
"ui-core": path.join(sourceDir, "ui-core.ts"),
|
||||
"map-core": path.join(sourceDir, "map-core.js"),
|
||||
"plugin-loader": path.join(sourceDir, "plugin-loader.ts"),
|
||||
"plugin-runtime": path.join(sourceDir, "plugin-runtime.ts"),
|
||||
screenshot: path.join(sourceDir, "screenshot.ts"),
|
||||
"webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"),
|
||||
bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"),
|
||||
|
||||
@@ -567,10 +567,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();
|
||||
};
|
||||
@@ -615,41 +615,6 @@ const decodeHistoryTextDecoder = typeof TextDecoder === "function" ? new TextDec
|
||||
let decodeHistoryReplayActive = 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() {
|
||||
decodeMapSyncPending = true;
|
||||
}
|
||||
@@ -6250,10 +6215,9 @@ 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);
|
||||
if (msg.type === "ft4" && window.onServerFt4) window.onServerFt4(msg);
|
||||
@@ -6281,20 +6245,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) {
|
||||
@@ -6366,24 +6318,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) {
|
||||
@@ -6417,12 +6353,11 @@ function connectDecode() {
|
||||
terminateDecodeHistoryWorker();
|
||||
decodeHistoryReplayActive = 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.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();
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import type { DecoderPlugin, PluginRuntimeWindow, TrxPluginRuntime } from "./plugins/runtime-contract";
|
||||
|
||||
type QueuedAction =
|
||||
| { kind: "message"; payload: unknown }
|
||||
| { kind: "batch" | "restore"; payload: unknown[] };
|
||||
|
||||
const decoders = new Map<string, DecoderPlugin>();
|
||||
const queued = new Map<string, QueuedAction[]>();
|
||||
const MAX_QUEUED_ACTIONS_PER_DECODER = 512;
|
||||
|
||||
function enqueue(id: string, action: QueuedAction): void {
|
||||
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: DecoderPlugin, action: QueuedAction): boolean {
|
||||
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: string, action: QueuedAction): boolean {
|
||||
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: TrxPluginRuntime = {
|
||||
registerDecoder<TMessage>(plugin: DecoderPlugin<TMessage>): () => void {
|
||||
if (decoders.has(plugin.id)) throw new Error(`Decoder plugin already registered: ${plugin.id}`);
|
||||
const erased = plugin as DecoderPlugin;
|
||||
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 as unknown as PluginRuntimeWindow).trxPluginRuntime = runtime;
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
export {};
|
||||
|
||||
import type { PluginRuntimeWindow } from "./runtime-contract";
|
||||
|
||||
interface AisMessage {
|
||||
rig_id?: string | null;
|
||||
channel?: string | null;
|
||||
@@ -36,12 +38,6 @@ interface AisBridge {
|
||||
trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
|
||||
updateAisBar?: () => void;
|
||||
clearAisBar?: () => void;
|
||||
resetAisHistoryView?: () => void;
|
||||
onServerAisBatch?: (messages: AisMessage[]) => void;
|
||||
restoreAisHistory?: (messages: AisMessage[]) => void;
|
||||
pruneAisHistoryView?: () => void;
|
||||
onServerAis?: (message: AisMessage) => void;
|
||||
_trxDrainPendingDecode?: (decoder: string) => void;
|
||||
}
|
||||
const aisWindow = window as unknown as AisBridge;
|
||||
const escapeAisHtml = (input: string): string => aisWindow.escapeMapHtml?.(input) ?? input
|
||||
@@ -324,16 +320,16 @@ function updateAisBar() {
|
||||
}
|
||||
aisWindow.updateAisBar = updateAisBar;
|
||||
aisWindow.clearAisBar = function() {
|
||||
aisWindow.resetAisHistoryView?.();
|
||||
resetAisHistoryView();
|
||||
};
|
||||
|
||||
aisWindow.resetAisHistoryView = function() {
|
||||
function resetAisHistoryView(): void {
|
||||
if (aisMessagesEl) aisMessagesEl.innerHTML = "";
|
||||
aisMessageHistory = [];
|
||||
updateAisBar();
|
||||
renderAisHistory();
|
||||
aisWindow.clearMapMarkersByType?.("ais");
|
||||
};
|
||||
}
|
||||
|
||||
function renderAisHistory() {
|
||||
pruneAisMessageHistory();
|
||||
@@ -375,7 +371,7 @@ function normalizeServerAisMessage(msg: AisMessage): AisMessage {
|
||||
};
|
||||
}
|
||||
|
||||
aisWindow.onServerAisBatch = function(messages: AisMessage[]) {
|
||||
function onServerAisBatch(messages: AisMessage[]): void {
|
||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||
if (aisStatus) aisStatus.textContent = "Receiving";
|
||||
const normalized: AisMessage[] = [];
|
||||
@@ -398,23 +394,19 @@ aisWindow.onServerAisBatch = function(messages: AisMessage[]) {
|
||||
pruneAisMessageHistory();
|
||||
scheduleAisBarUpdate();
|
||||
scheduleAisHistoryRender();
|
||||
};
|
||||
}
|
||||
|
||||
aisWindow.restoreAisHistory = function(messages: AisMessage[]) {
|
||||
aisWindow.onServerAisBatch?.(messages);
|
||||
};
|
||||
|
||||
aisWindow.pruneAisHistoryView = function() {
|
||||
function pruneAisHistoryView(): void {
|
||||
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);
|
||||
}
|
||||
@@ -427,10 +419,17 @@ if (aisFilterInput) {
|
||||
});
|
||||
}
|
||||
|
||||
aisWindow.onServerAis = function(msg: AisMessage) {
|
||||
function onServerAis(msg: AisMessage): void {
|
||||
if (aisStatus) aisStatus.textContent = "Receiving";
|
||||
addAisMessage(normalizeServerAisMessage(msg));
|
||||
};
|
||||
}
|
||||
|
||||
updateAisSummary();
|
||||
aisWindow._trxDrainPendingDecode?.("ais");
|
||||
(window as unknown as PluginRuntimeWindow).trxPluginRuntime.registerDecoder<AisMessage>({
|
||||
id: "ais",
|
||||
onMessage: onServerAis,
|
||||
onBatch: onServerAisBatch,
|
||||
restore: onServerAisBatch,
|
||||
reset: resetAisHistoryView,
|
||||
prune: pruneAisHistoryView,
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type AprsPacket,
|
||||
type AprsTypeFilter,
|
||||
} from "./aprs-shared";
|
||||
import type { PluginRuntimeWindow } from "./runtime-contract";
|
||||
interface AprsBridge {
|
||||
getDecodeHistoryRetentionMs?: () => number;
|
||||
trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
|
||||
@@ -31,12 +32,6 @@ interface AprsBridge {
|
||||
updateAprsBar?: () => void;
|
||||
clearAprsBar?: () => void;
|
||||
closeAprsBar?: () => void;
|
||||
resetAprsHistoryView?: () => void;
|
||||
pruneAprsHistoryView?: () => void;
|
||||
onServerAprsBatch?: (packets: AprsPacket[]) => void;
|
||||
restoreAprsHistory?: (packets: AprsPacket[]) => void;
|
||||
onServerAprs?: (packet: AprsPacket) => void;
|
||||
_trxDrainPendingDecode?: (decoder: string) => void;
|
||||
}
|
||||
const aprsWindow = window as unknown as AprsBridge;
|
||||
const escapeAprsHtml = (input: string): string => aprsWindow.escapeMapHtml?.(input) ?? input
|
||||
@@ -288,7 +283,7 @@ function updateAprsBar() {
|
||||
}
|
||||
aprsWindow.updateAprsBar = updateAprsBar;
|
||||
aprsWindow.clearAprsBar = function() {
|
||||
aprsWindow.resetAprsHistoryView?.();
|
||||
resetAprsHistoryView();
|
||||
};
|
||||
aprsWindow.closeAprsBar = function() {
|
||||
aprsBarDismissedAtMs = Date.now();
|
||||
@@ -298,19 +293,19 @@ aprsWindow.closeAprsBar = function() {
|
||||
}
|
||||
};
|
||||
|
||||
aprsWindow.resetAprsHistoryView = function() {
|
||||
function resetAprsHistoryView(): void {
|
||||
if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
|
||||
aprsPacketHistory = [];
|
||||
updateAprsBar();
|
||||
renderAprsHistory();
|
||||
aprsWindow.clearMapMarkersByType?.("aprs");
|
||||
};
|
||||
}
|
||||
|
||||
aprsWindow.pruneAprsHistoryView = function() {
|
||||
function pruneAprsHistoryView(): void {
|
||||
pruneAprsPacketHistory();
|
||||
updateAprsBar();
|
||||
renderAprsHistory();
|
||||
};
|
||||
}
|
||||
|
||||
function addAprsPacket(pkt: AprsPacket): void {
|
||||
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
|
||||
@@ -333,7 +328,7 @@ function normalizeServerAprsPacket(pkt: AprsPacket): AprsPacket {
|
||||
return normalizeAprsPacket(pkt, aprsWindow.getDecodeRigMeta?.() ?? null);
|
||||
}
|
||||
|
||||
aprsWindow.onServerAprsBatch = function(packets: AprsPacket[]) {
|
||||
function onServerAprsBatch(packets: AprsPacket[]): void {
|
||||
if (!Array.isArray(packets) || packets.length === 0) return;
|
||||
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
||||
const normalized: AprsPacket[] = [];
|
||||
@@ -354,17 +349,13 @@ aprsWindow.onServerAprsBatch = function(packets: AprsPacket[]) {
|
||||
pruneAprsPacketHistory();
|
||||
if (hasCrcOk) scheduleAprsBarUpdate();
|
||||
scheduleAprsHistoryRender();
|
||||
};
|
||||
|
||||
aprsWindow.restoreAprsHistory = function(packets: AprsPacket[]) {
|
||||
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);
|
||||
}
|
||||
@@ -408,10 +399,17 @@ if (aprsFilterInput) {
|
||||
}
|
||||
|
||||
// --- Server-side APRS decode handler ---
|
||||
aprsWindow.onServerAprs = function(pkt: AprsPacket) {
|
||||
function onServerAprs(pkt: AprsPacket): void {
|
||||
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
||||
addAprsPacket(normalizeServerAprsPacket(pkt));
|
||||
};
|
||||
}
|
||||
|
||||
renderAprsHistory();
|
||||
aprsWindow._trxDrainPendingDecode?.("aprs");
|
||||
(window as unknown as PluginRuntimeWindow).trxPluginRuntime.registerDecoder<AprsPacket>({
|
||||
id: "aprs",
|
||||
onMessage: onServerAprs,
|
||||
onBatch: onServerAprsBatch,
|
||||
restore: onServerAprsBatch,
|
||||
reset: resetAprsHistoryView,
|
||||
prune: pruneAprsHistoryView,
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type AprsPacket,
|
||||
type AprsTypeFilter,
|
||||
} from "./aprs-shared";
|
||||
import type { PluginRuntimeWindow } from "./runtime-contract";
|
||||
|
||||
interface HfAprsBridge {
|
||||
getDecodeHistoryRetentionMs?: () => number;
|
||||
@@ -28,12 +29,6 @@ interface HfAprsBridge {
|
||||
takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<unknown>;
|
||||
postPath?: (path: string) => Promise<unknown>;
|
||||
trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
|
||||
resetHfAprsHistoryView?: () => void;
|
||||
pruneHfAprsHistoryView?: () => void;
|
||||
onServerHfAprsBatch?: (packets: AprsPacket[]) => void;
|
||||
restoreHfAprsHistory?: (packets: AprsPacket[]) => void;
|
||||
onServerHfAprs?: (packet: AprsPacket) => void;
|
||||
_trxDrainPendingDecode?: (decoder: string) => void;
|
||||
}
|
||||
const hfAprsWindow = window as unknown as HfAprsBridge;
|
||||
const escapeHfAprsHtml = (input: string): string => hfAprsWindow.escapeMapHtml?.(input) ?? input
|
||||
@@ -247,16 +242,16 @@ function renderHfAprsHistory() {
|
||||
updateHfAprsChipState();
|
||||
}
|
||||
|
||||
hfAprsWindow.resetHfAprsHistoryView = function() {
|
||||
function resetHfAprsHistoryView(): void {
|
||||
if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
|
||||
hfAprsPacketHistory = [];
|
||||
renderHfAprsHistory();
|
||||
};
|
||||
}
|
||||
|
||||
hfAprsWindow.pruneHfAprsHistoryView = function() {
|
||||
function pruneHfAprsHistoryView(): void {
|
||||
pruneHfAprsPacketHistory();
|
||||
renderHfAprsHistory();
|
||||
};
|
||||
}
|
||||
|
||||
function addHfAprsPacket(pkt: AprsPacket): void {
|
||||
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
|
||||
@@ -273,7 +268,7 @@ function normalizeServerHfAprsPacket(pkt: AprsPacket): AprsPacket {
|
||||
return normalizeAprsPacket(pkt, hfAprsWindow.getDecodeRigMeta?.() ?? null);
|
||||
}
|
||||
|
||||
hfAprsWindow.onServerHfAprsBatch = function(packets: AprsPacket[]) {
|
||||
function onServerHfAprsBatch(packets: AprsPacket[]): void {
|
||||
if (!Array.isArray(packets) || packets.length === 0) return;
|
||||
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
||||
const normalized: AprsPacket[] = [];
|
||||
@@ -288,11 +283,7 @@ hfAprsWindow.onServerHfAprsBatch = function(packets: AprsPacket[]) {
|
||||
hfAprsPacketHistory = normalized.concat(hfAprsPacketHistory);
|
||||
pruneHfAprsPacketHistory();
|
||||
scheduleHfAprsHistoryRender();
|
||||
};
|
||||
|
||||
hfAprsWindow.restoreHfAprsHistory = function(packets: AprsPacket[]) {
|
||||
hfAprsWindow.onServerHfAprsBatch?.(packets);
|
||||
};
|
||||
}
|
||||
|
||||
const hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn");
|
||||
hfAprsDecodeToggleBtn?.addEventListener("click", () => { void (async () => {
|
||||
@@ -308,7 +299,7 @@ document.getElementById("settings-clear-hf-aprs-history")?.addEventListener("cli
|
||||
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);
|
||||
}
|
||||
@@ -352,10 +343,17 @@ if (hfAprsFilterInput) {
|
||||
}
|
||||
|
||||
// --- Server-side HF APRS decode handler ---
|
||||
hfAprsWindow.onServerHfAprs = function(pkt: AprsPacket) {
|
||||
function onServerHfAprs(pkt: AprsPacket): void {
|
||||
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
||||
addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
|
||||
};
|
||||
}
|
||||
|
||||
renderHfAprsHistory();
|
||||
hfAprsWindow._trxDrainPendingDecode?.("hf_aprs");
|
||||
(window as unknown as PluginRuntimeWindow).trxPluginRuntime.registerDecoder<AprsPacket>({
|
||||
id: "hf_aprs",
|
||||
onMessage: onServerHfAprs,
|
||||
onBatch: onServerHfAprsBatch,
|
||||
restore: onServerHfAprsBatch,
|
||||
reset: resetHfAprsHistoryView,
|
||||
prune: pruneHfAprsHistoryView,
|
||||
});
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
export interface DecoderPlugin<TMessage = unknown> {
|
||||
readonly id: string;
|
||||
onMessage?(message: TMessage): void;
|
||||
onBatch?(messages: TMessage[]): void;
|
||||
restore?(messages: TMessage[]): void;
|
||||
reset?(): void;
|
||||
prune?(): void;
|
||||
}
|
||||
|
||||
export interface TrxPluginRuntime {
|
||||
registerDecoder<TMessage>(plugin: DecoderPlugin<TMessage>): () => void;
|
||||
dispatch(id: string, message: unknown): boolean;
|
||||
dispatchBatch(id: string, messages: unknown[]): boolean;
|
||||
restore(id: string, messages: unknown[]): boolean;
|
||||
reset(id: string): boolean;
|
||||
resetAll(): void;
|
||||
prune(id: string): boolean;
|
||||
clearQueued(): void;
|
||||
hasDecoder(id: string): boolean;
|
||||
}
|
||||
|
||||
export interface PluginRuntimeWindow { trxPluginRuntime: TrxPluginRuntime }
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
export {};
|
||||
|
||||
import type { PluginRuntimeWindow } from "./runtime-contract";
|
||||
|
||||
interface VdesMessage {
|
||||
rig_id?: string | null;
|
||||
message_type?: number;
|
||||
@@ -42,12 +44,6 @@ interface VdesBridge {
|
||||
vdesMapAddPoint?: (message: VdesMessage) => void;
|
||||
updateVdesBar?: () => void;
|
||||
clearVdesBar?: () => void;
|
||||
resetVdesHistoryView?: () => void;
|
||||
onServerVdesBatch?: (messages: VdesMessage[]) => void;
|
||||
restoreVdesHistory?: (messages: VdesMessage[]) => void;
|
||||
onServerVdes?: (message: VdesMessage) => void;
|
||||
pruneVdesHistoryView?: () => void;
|
||||
_trxDrainPendingDecode?: (decoder: string) => void;
|
||||
}
|
||||
|
||||
const vdesWindow = window as unknown as VdesBridge;
|
||||
@@ -267,15 +263,15 @@ function updateVdesBar() {
|
||||
}
|
||||
vdesWindow.updateVdesBar = updateVdesBar;
|
||||
vdesWindow.clearVdesBar = function() {
|
||||
vdesWindow.resetVdesHistoryView?.();
|
||||
resetVdesHistoryView();
|
||||
};
|
||||
|
||||
vdesWindow.resetVdesHistoryView = function() {
|
||||
function resetVdesHistoryView(): void {
|
||||
if (vdesMessagesEl) vdesMessagesEl.innerHTML = "";
|
||||
vdesMessageHistory = [];
|
||||
updateVdesBar();
|
||||
renderVdesHistory();
|
||||
};
|
||||
}
|
||||
|
||||
function renderVdesHistory() {
|
||||
pruneVdesMessageHistory();
|
||||
@@ -313,7 +309,7 @@ function normalizeServerVdesMessage(msg: VdesMessage): VdesMessage {
|
||||
};
|
||||
}
|
||||
|
||||
vdesWindow.onServerVdesBatch = function(messages: VdesMessage[]) {
|
||||
function onServerVdesBatch(messages: VdesMessage[]): void {
|
||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
||||
const normalized: VdesMessage[] = [];
|
||||
@@ -336,17 +332,13 @@ vdesWindow.onServerVdesBatch = function(messages: VdesMessage[]) {
|
||||
pruneVdesMessageHistory();
|
||||
scheduleVdesBarUpdate();
|
||||
scheduleVdesHistoryRender();
|
||||
};
|
||||
|
||||
vdesWindow.restoreVdesHistory = function(messages: VdesMessage[]) {
|
||||
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);
|
||||
}
|
||||
@@ -359,20 +351,27 @@ if (vdesFilterInput) {
|
||||
});
|
||||
}
|
||||
|
||||
vdesWindow.onServerVdes = function(msg: VdesMessage) {
|
||||
function onServerVdes(msg: VdesMessage): void {
|
||||
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(): void {
|
||||
pruneVdesMessageHistory();
|
||||
updateVdesBar();
|
||||
renderVdesHistory();
|
||||
};
|
||||
}
|
||||
|
||||
updateVdesSummary();
|
||||
vdesWindow._trxDrainPendingDecode?.("vdes");
|
||||
(window as unknown as PluginRuntimeWindow).trxPluginRuntime.registerDecoder<VdesMessage>({
|
||||
id: "vdes",
|
||||
onMessage: onServerVdes,
|
||||
onBatch: onServerVdesBatch,
|
||||
restore: onServerVdesBatch,
|
||||
reset: resetVdesHistoryView,
|
||||
prune: pruneVdesHistoryView,
|
||||
});
|
||||
|
||||
@@ -23,10 +23,13 @@ test("AIS entry forwards positioned vessels with normalized metadata", async ()
|
||||
Map,
|
||||
console,
|
||||
});
|
||||
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
|
||||
const source = await readFile(new URL("../../assets/web/generated/ais.js", import.meta.url), "utf8");
|
||||
new vm.Script(runtime).runInContext(context);
|
||||
new vm.Script(source).runInContext(context);
|
||||
|
||||
window.onServerAis({ mmsi: 261000001, lat: 54.5, lon: 18.5, channel: "A", ts_ms: Date.now() });
|
||||
window.trxPluginRuntime.dispatch("ais", { mmsi: 261000001, lat: 54.5, lon: 18.5, channel: "A", ts_ms: Date.now() });
|
||||
assert.equal(window.onServerAis, undefined);
|
||||
assert.equal(forwarded.length, 1);
|
||||
assert.equal(forwarded[0].mmsi, 261000001);
|
||||
assert.equal(forwarded[0].rig_id, null);
|
||||
|
||||
@@ -25,11 +25,14 @@ test("APRS entry normalizes positioned packets without remote symbol assets", as
|
||||
Reflect,
|
||||
console,
|
||||
});
|
||||
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
|
||||
const source = await readFile(new URL("../../assets/web/generated/aprs.js", import.meta.url), "utf8");
|
||||
assert.equal(source.includes("raw.githubusercontent.com"), false);
|
||||
new vm.Script(runtime).runInContext(context);
|
||||
new vm.Script(source).runInContext(context);
|
||||
|
||||
window.onServerAprs({ src_call: "SP0ABC", dest_call: "APRS", crc_ok: true, lat: 54.5, lon: 18.5, symbol_table: "/", symbol_code: ">" });
|
||||
window.trxPluginRuntime.dispatch("aprs", { src_call: "SP0ABC", dest_call: "APRS", crc_ok: true, lat: 54.5, lon: 18.5, symbol_table: "/", symbol_code: ">" });
|
||||
assert.equal(window.onServerAprs, undefined);
|
||||
assert.equal(forwarded.length, 1);
|
||||
assert.equal(forwarded[0][0], "SP0ABC");
|
||||
assert.equal(forwarded[0][1], 54.5);
|
||||
|
||||
@@ -21,11 +21,13 @@ test("HF APRS entry uses shared typed normalization and local symbols", async ()
|
||||
Reflect,
|
||||
console,
|
||||
});
|
||||
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
|
||||
const source = await readFile(new URL("../../assets/web/generated/hf-aprs.js", import.meta.url), "utf8");
|
||||
assert.equal(source.includes("raw.githubusercontent.com"), false);
|
||||
new vm.Script(runtime).runInContext(context);
|
||||
new vm.Script(source).runInContext(context);
|
||||
|
||||
window.onServerHfAprs({ src_call: "SP0ABC", dest_call: "APRS", crc_ok: true, symbol_table: "/", symbol_code: ">" });
|
||||
assert.equal(typeof window.restoreHfAprsHistory, "function");
|
||||
assert.equal(typeof window.pruneHfAprsHistoryView, "function");
|
||||
window.trxPluginRuntime.dispatch("hf_aprs", { src_call: "SP0ABC", dest_call: "APRS", crc_ok: true, symbol_table: "/", symbol_code: ">" });
|
||||
assert.equal(window.onServerHfAprs, undefined);
|
||||
assert.equal(window.trxPluginRuntime.hasDecoder("hf_aprs"), true);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
|
||||
test("plugin runtime queues lazy decoder traffic and flushes in order on registration", async () => {
|
||||
const window = {};
|
||||
const context = vm.createContext({ window, Map, Error });
|
||||
const source = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
|
||||
new vm.Script(source).runInContext(context);
|
||||
|
||||
assert.equal(window.trxPluginRuntime.dispatch("late", { sequence: 1 }), false);
|
||||
assert.equal(window.trxPluginRuntime.restore("late", [{ sequence: 2 }]), false);
|
||||
const received = [];
|
||||
window.trxPluginRuntime.registerDecoder({
|
||||
id: "late",
|
||||
onMessage: (message) => { received.push(["message", message.sequence]); },
|
||||
restore: (messages) => { received.push(["restore", messages[0].sequence]); },
|
||||
});
|
||||
assert.deepEqual(received, [["message", 1], ["restore", 2]]);
|
||||
});
|
||||
@@ -22,10 +22,13 @@ test("VDES entry normalizes and forwards positioned server messages", async () =
|
||||
Array,
|
||||
console,
|
||||
});
|
||||
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
|
||||
const source = await readFile(new URL("../../assets/web/generated/vdes.js", import.meta.url), "utf8");
|
||||
new vm.Script(runtime).runInContext(context);
|
||||
new vm.Script(source).runInContext(context);
|
||||
|
||||
window.onServerVdes({ callsign: "SP0ABC", lat: 54.5, lon: 18.5, bit_len: 120, ts_ms: Date.now() });
|
||||
window.trxPluginRuntime.dispatch("vdes", { callsign: "SP0ABC", lat: 54.5, lon: 18.5, bit_len: 120, ts_ms: Date.now() });
|
||||
assert.equal(window.onServerVdes, undefined);
|
||||
assert.equal(forwarded.length, 1);
|
||||
assert.equal(forwarded[0].callsign, "SP0ABC");
|
||||
assert.equal(forwarded[0].rig_id, null);
|
||||
|
||||
@@ -36,6 +36,11 @@ define_gz_cache!(
|
||||
status::PLUGIN_LOADER_JS,
|
||||
"plugin-loader.js"
|
||||
);
|
||||
define_gz_cache!(
|
||||
gz_plugin_runtime_js,
|
||||
status::PLUGIN_RUNTIME_JS,
|
||||
"plugin-runtime.js"
|
||||
);
|
||||
define_gz_cache!(gz_screenshot_js, status::SCREENSHOT_JS, "screenshot.js");
|
||||
define_gz_cache!(
|
||||
gz_decode_history_worker_js,
|
||||
@@ -204,6 +209,12 @@ pub(crate) async fn plugin_loader_js(req: HttpRequest) -> impl Responder {
|
||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
||||
}
|
||||
|
||||
#[get("/plugin-runtime.js")]
|
||||
pub(crate) async fn plugin_runtime_js(req: HttpRequest) -> impl Responder {
|
||||
let c = gz_plugin_runtime_js();
|
||||
static_asset_response(&req, "application/javascript; charset=utf-8", c)
|
||||
}
|
||||
|
||||
#[get("/screenshot.js")]
|
||||
pub(crate) async fn screenshot_js(req: HttpRequest) -> impl Responder {
|
||||
let c = gz_screenshot_js();
|
||||
|
||||
@@ -646,6 +646,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(assets::ui_core_js)
|
||||
.service(assets::map_core_js)
|
||||
.service(assets::plugin_loader_js)
|
||||
.service(assets::plugin_runtime_js)
|
||||
.service(assets::screenshot_js)
|
||||
.service(assets::decode_history_worker_js)
|
||||
.service(assets::webgl_renderer_js)
|
||||
|
||||
@@ -15,6 +15,7 @@ pub const APP_JS: &str = include_str!("../assets/web/generated/app.js");
|
||||
pub const UI_CORE_JS: &str = include_str!("../assets/web/generated/ui-core.js");
|
||||
pub const MAP_CORE_JS: &str = include_str!("../assets/web/generated/map-core.js");
|
||||
pub const PLUGIN_LOADER_JS: &str = include_str!("../assets/web/generated/plugin-loader.js");
|
||||
pub const PLUGIN_RUNTIME_JS: &str = include_str!("../assets/web/generated/plugin-runtime.js");
|
||||
pub const SCREENSHOT_JS: &str = include_str!("../assets/web/generated/screenshot.js");
|
||||
pub const DECODE_HISTORY_WORKER_JS: &str =
|
||||
include_str!("../assets/web/generated/decode-history-worker.js");
|
||||
@@ -94,6 +95,14 @@ mod tests {
|
||||
.find("/plugin-loader.js")
|
||||
.expect("typed plugin loader is loaded");
|
||||
assert!(plugin_loader < app, "plugin registry must load before app");
|
||||
let plugin_runtime = html
|
||||
.find("/plugin-runtime.js")
|
||||
.expect("typed plugin runtime is loaded");
|
||||
assert!(
|
||||
plugin_runtime < plugin_loader,
|
||||
"plugin runtime must load before the lazy loader"
|
||||
);
|
||||
assert!(PLUGIN_RUNTIME_JS.contains("registerDecoder"));
|
||||
assert!(PLUGIN_LOADER_JS.contains("import(path)"));
|
||||
assert!(html.contains("<summary>Scheduler controls</summary>"));
|
||||
assert!(html.contains("<summary>Audio controls</summary>"));
|
||||
|
||||
Reference in New Issue
Block a user