refactor: add typed decoder plugin runtime
This commit is contained in:
@@ -228,15 +228,15 @@
|
|||||||
}
|
}
|
||||||
aisWindow.updateAisBar = updateAisBar;
|
aisWindow.updateAisBar = updateAisBar;
|
||||||
aisWindow.clearAisBar = function() {
|
aisWindow.clearAisBar = function() {
|
||||||
aisWindow.resetAisHistoryView?.();
|
resetAisHistoryView();
|
||||||
};
|
};
|
||||||
aisWindow.resetAisHistoryView = function() {
|
function resetAisHistoryView() {
|
||||||
if (aisMessagesEl) aisMessagesEl.innerHTML = "";
|
if (aisMessagesEl) aisMessagesEl.innerHTML = "";
|
||||||
aisMessageHistory = [];
|
aisMessageHistory = [];
|
||||||
updateAisBar();
|
updateAisBar();
|
||||||
renderAisHistory();
|
renderAisHistory();
|
||||||
aisWindow.clearMapMarkersByType?.("ais");
|
aisWindow.clearMapMarkersByType?.("ais");
|
||||||
};
|
}
|
||||||
function renderAisHistory() {
|
function renderAisHistory() {
|
||||||
pruneAisMessageHistory();
|
pruneAisMessageHistory();
|
||||||
if (!aisMessagesEl) {
|
if (!aisMessagesEl) {
|
||||||
@@ -272,7 +272,7 @@
|
|||||||
rig_id: msg.rig_id || null
|
rig_id: msg.rig_id || null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
aisWindow.onServerAisBatch = function(messages) {
|
function onServerAisBatch(messages) {
|
||||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||||
if (aisStatus) aisStatus.textContent = "Receiving";
|
if (aisStatus) aisStatus.textContent = "Receiving";
|
||||||
const normalized = [];
|
const normalized = [];
|
||||||
@@ -295,21 +295,18 @@
|
|||||||
pruneAisMessageHistory();
|
pruneAisMessageHistory();
|
||||||
scheduleAisBarUpdate();
|
scheduleAisBarUpdate();
|
||||||
scheduleAisHistoryRender();
|
scheduleAisHistoryRender();
|
||||||
};
|
}
|
||||||
aisWindow.restoreAisHistory = function(messages) {
|
function pruneAisHistoryView() {
|
||||||
aisWindow.onServerAisBatch?.(messages);
|
|
||||||
};
|
|
||||||
aisWindow.pruneAisHistoryView = function() {
|
|
||||||
pruneAisMessageHistory();
|
pruneAisMessageHistory();
|
||||||
updateAisBar();
|
updateAisBar();
|
||||||
renderAisHistory();
|
renderAisHistory();
|
||||||
};
|
}
|
||||||
document.getElementById("settings-clear-ais-history")?.addEventListener("click", () => {
|
document.getElementById("settings-clear-ais-history")?.addEventListener("click", () => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (!await aisWindow.trxUi.confirm({ title: "Clear AIS history?", message: "All stored AIS messages will be permanently removed.", confirmLabel: "Clear history" })) return;
|
if (!await aisWindow.trxUi.confirm({ title: "Clear AIS history?", message: "All stored AIS messages will be permanently removed.", confirmLabel: "Clear history" })) return;
|
||||||
try {
|
try {
|
||||||
await aisWindow.postPath?.("/clear_ais_decode");
|
await aisWindow.postPath?.("/clear_ais_decode");
|
||||||
aisWindow.resetAisHistoryView?.();
|
resetAisHistoryView();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("AIS history clear failed", e);
|
console.error("AIS history clear failed", e);
|
||||||
}
|
}
|
||||||
@@ -321,10 +318,17 @@
|
|||||||
renderAisHistory();
|
renderAisHistory();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
aisWindow.onServerAis = function(msg) {
|
function onServerAis(msg) {
|
||||||
if (aisStatus) aisStatus.textContent = "Receiving";
|
if (aisStatus) aisStatus.textContent = "Receiving";
|
||||||
addAisMessage(normalizeServerAisMessage(msg));
|
addAisMessage(normalizeServerAisMessage(msg));
|
||||||
};
|
}
|
||||||
updateAisSummary();
|
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.getDecodeHistoryRetentionMs = currentDecodeHistoryRetentionMs;
|
||||||
window.applyDecodeHistoryRetention = function() {
|
window.applyDecodeHistoryRetention = function() {
|
||||||
if (typeof window.pruneAprsHistoryView === "function") window.pruneAprsHistoryView();
|
window.trxPluginRuntime.prune("aprs");
|
||||||
if (typeof window.pruneHfAprsHistoryView === "function") window.pruneHfAprsHistoryView();
|
window.trxPluginRuntime.prune("hf_aprs");
|
||||||
if (typeof window.pruneAisHistoryView === "function") window.pruneAisHistoryView();
|
window.trxPluginRuntime.prune("ais");
|
||||||
if (typeof window.pruneVdesHistoryView === "function") window.pruneVdesHistoryView();
|
window.trxPluginRuntime.prune("vdes");
|
||||||
if (typeof window.pruneFt8HistoryView === "function") window.pruneFt8HistoryView();
|
if (typeof window.pruneFt8HistoryView === "function") window.pruneFt8HistoryView();
|
||||||
if (typeof window.pruneWsprHistoryView === "function") window.pruneWsprHistoryView();
|
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;
|
const decodeHistoryTextDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
|
||||||
let decodeHistoryReplayActive = false;
|
let decodeHistoryReplayActive = false;
|
||||||
let decodeMapSyncPending = 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() {
|
function markDecodeMapSyncPending() {
|
||||||
decodeMapSyncPending = true;
|
decodeMapSyncPending = true;
|
||||||
}
|
}
|
||||||
@@ -5888,21 +5857,8 @@ function updateDecodeStatus(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function dispatchDecodeMessage(msg, skipStats) {
|
function dispatchDecodeMessage(msg, skipStats) {
|
||||||
if (msg.type === "ais") {
|
if (msg.type === "ais" || msg.type === "vdes" || msg.type === "aprs" || msg.type === "hf_aprs") {
|
||||||
if (window.onServerAis) window.onServerAis(msg);
|
window.trxPluginRuntime.dispatch(msg.type, 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 === "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);
|
||||||
@@ -5929,20 +5885,8 @@ function dispatchDecodeBatch(batch) {
|
|||||||
const type = String(batch[0]?.type || "");
|
const type = String(batch[0]?.type || "");
|
||||||
const uniformType = batch.every((msg) => String(msg?.type || "") === type);
|
const uniformType = batch.every((msg) => String(msg?.type || "") === type);
|
||||||
if (uniformType) {
|
if (uniformType) {
|
||||||
if (type === "ais" && window.onServerAisBatch) {
|
if (type === "ais" || type === "vdes" || type === "aprs" || type === "hf_aprs") {
|
||||||
window.onServerAisBatch(batch);
|
window.trxPluginRuntime.dispatchBatch(type, 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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (type === "ft8" && window.onServerFt8Batch) {
|
if (type === "ft8" && window.onServerFt8Batch) {
|
||||||
@@ -6010,36 +5954,8 @@ function restoreDecodeHistoryGroup(kind, messages) {
|
|||||||
}
|
}
|
||||||
window.trx.modules.map?.scheduleStatsRender();
|
window.trx.modules.map?.scheduleStatsRender();
|
||||||
}
|
}
|
||||||
if (kind === "ais") {
|
if (kind === "ais" || kind === "vdes" || kind === "aprs" || kind === "hf_aprs") {
|
||||||
if (window.restoreAisHistory) {
|
window.trxPluginRuntime.restore(kind, messages);
|
||||||
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);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (kind === "cw" && window.restoreCwHistory) {
|
if (kind === "cw" && window.restoreCwHistory) {
|
||||||
@@ -6074,11 +5990,11 @@ function connectDecode() {
|
|||||||
terminateDecodeHistoryWorker();
|
terminateDecodeHistoryWorker();
|
||||||
decodeHistoryReplayActive = false;
|
decodeHistoryReplayActive = false;
|
||||||
decodeMapSyncPending = false;
|
decodeMapSyncPending = false;
|
||||||
for (const k in _pendingDecodeHistory) delete _pendingDecodeHistory[k];
|
window.trxPluginRuntime.clearQueued();
|
||||||
for (const k in _pendingDecodeLive) delete _pendingDecodeLive[k];
|
window.trxPluginRuntime.reset("ais");
|
||||||
if (window.resetAisHistoryView) window.resetAisHistoryView();
|
window.trxPluginRuntime.reset("vdes");
|
||||||
if (window.resetVdesHistoryView) window.resetVdesHistoryView();
|
window.trxPluginRuntime.reset("aprs");
|
||||||
if (window.resetAprsHistoryView) window.resetAprsHistoryView();
|
window.trxPluginRuntime.reset("hf_aprs");
|
||||||
if (window.resetCwHistoryView) window.resetCwHistoryView();
|
if (window.resetCwHistoryView) window.resetCwHistoryView();
|
||||||
if (window.resetFt8HistoryView) window.resetFt8HistoryView();
|
if (window.resetFt8HistoryView) window.resetFt8HistoryView();
|
||||||
if (window.resetFt4HistoryView) window.resetFt4HistoryView();
|
if (window.resetFt4HistoryView) window.resetFt4HistoryView();
|
||||||
|
|||||||
@@ -291,7 +291,7 @@
|
|||||||
}
|
}
|
||||||
aprsWindow.updateAprsBar = updateAprsBar;
|
aprsWindow.updateAprsBar = updateAprsBar;
|
||||||
aprsWindow.clearAprsBar = function() {
|
aprsWindow.clearAprsBar = function() {
|
||||||
aprsWindow.resetAprsHistoryView?.();
|
resetAprsHistoryView();
|
||||||
};
|
};
|
||||||
aprsWindow.closeAprsBar = function() {
|
aprsWindow.closeAprsBar = function() {
|
||||||
aprsBarDismissedAtMs = Date.now();
|
aprsBarDismissedAtMs = Date.now();
|
||||||
@@ -300,18 +300,18 @@
|
|||||||
aprsBarOverlay.innerHTML = "";
|
aprsBarOverlay.innerHTML = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
aprsWindow.resetAprsHistoryView = function() {
|
function resetAprsHistoryView() {
|
||||||
if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
|
if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
|
||||||
aprsPacketHistory = [];
|
aprsPacketHistory = [];
|
||||||
updateAprsBar();
|
updateAprsBar();
|
||||||
renderAprsHistory();
|
renderAprsHistory();
|
||||||
aprsWindow.clearMapMarkersByType?.("aprs");
|
aprsWindow.clearMapMarkersByType?.("aprs");
|
||||||
};
|
}
|
||||||
aprsWindow.pruneAprsHistoryView = function() {
|
function pruneAprsHistoryView() {
|
||||||
pruneAprsPacketHistory();
|
pruneAprsPacketHistory();
|
||||||
updateAprsBar();
|
updateAprsBar();
|
||||||
renderAprsHistory();
|
renderAprsHistory();
|
||||||
};
|
}
|
||||||
function addAprsPacket(pkt) {
|
function addAprsPacket(pkt) {
|
||||||
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
|
||||||
pkt._tsMs = tsMs;
|
pkt._tsMs = tsMs;
|
||||||
@@ -327,7 +327,7 @@
|
|||||||
function normalizeServerAprsPacket(pkt) {
|
function normalizeServerAprsPacket(pkt) {
|
||||||
return normalizeAprsPacket(pkt, aprsWindow.getDecodeRigMeta?.() ?? null);
|
return normalizeAprsPacket(pkt, aprsWindow.getDecodeRigMeta?.() ?? null);
|
||||||
}
|
}
|
||||||
aprsWindow.onServerAprsBatch = function(packets) {
|
function onServerAprsBatch(packets) {
|
||||||
if (!Array.isArray(packets) || packets.length === 0) return;
|
if (!Array.isArray(packets) || packets.length === 0) return;
|
||||||
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
||||||
const normalized = [];
|
const normalized = [];
|
||||||
@@ -348,16 +348,13 @@
|
|||||||
pruneAprsPacketHistory();
|
pruneAprsPacketHistory();
|
||||||
if (hasCrcOk) scheduleAprsBarUpdate();
|
if (hasCrcOk) scheduleAprsBarUpdate();
|
||||||
scheduleAprsHistoryRender();
|
scheduleAprsHistoryRender();
|
||||||
};
|
}
|
||||||
aprsWindow.restoreAprsHistory = function(packets) {
|
|
||||||
aprsWindow.onServerAprsBatch?.(packets);
|
|
||||||
};
|
|
||||||
document.getElementById("settings-clear-aprs-history")?.addEventListener("click", () => {
|
document.getElementById("settings-clear-aprs-history")?.addEventListener("click", () => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (!await aprsWindow.trxUi.confirm({ title: "Clear APRS history?", message: "All stored APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
|
if (!await aprsWindow.trxUi.confirm({ title: "Clear APRS history?", message: "All stored APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
|
||||||
try {
|
try {
|
||||||
await aprsWindow.postPath?.("/clear_aprs_decode");
|
await aprsWindow.postPath?.("/clear_aprs_decode");
|
||||||
aprsWindow.resetAprsHistoryView?.();
|
resetAprsHistoryView();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("APRS history clear failed", e);
|
console.error("APRS history clear failed", e);
|
||||||
}
|
}
|
||||||
@@ -395,10 +392,17 @@
|
|||||||
renderAprsHistory();
|
renderAprsHistory();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
aprsWindow.onServerAprs = function(pkt) {
|
function onServerAprs(pkt) {
|
||||||
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
||||||
addAprsPacket(normalizeServerAprsPacket(pkt));
|
addAprsPacket(normalizeServerAprsPacket(pkt));
|
||||||
};
|
}
|
||||||
renderAprsHistory();
|
renderAprsHistory();
|
||||||
aprsWindow._trxDrainPendingDecode?.("aprs");
|
window.trxPluginRuntime.registerDecoder({
|
||||||
|
id: "aprs",
|
||||||
|
onMessage: onServerAprs,
|
||||||
|
onBatch: onServerAprsBatch,
|
||||||
|
restore: onServerAprsBatch,
|
||||||
|
reset: resetAprsHistoryView,
|
||||||
|
prune: pruneAprsHistoryView
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -253,15 +253,15 @@
|
|||||||
updateHfAprsSummary();
|
updateHfAprsSummary();
|
||||||
updateHfAprsChipState();
|
updateHfAprsChipState();
|
||||||
}
|
}
|
||||||
hfAprsWindow.resetHfAprsHistoryView = function() {
|
function resetHfAprsHistoryView() {
|
||||||
if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
|
if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
|
||||||
hfAprsPacketHistory = [];
|
hfAprsPacketHistory = [];
|
||||||
renderHfAprsHistory();
|
renderHfAprsHistory();
|
||||||
};
|
}
|
||||||
hfAprsWindow.pruneHfAprsHistoryView = function() {
|
function pruneHfAprsHistoryView() {
|
||||||
pruneHfAprsPacketHistory();
|
pruneHfAprsPacketHistory();
|
||||||
renderHfAprsHistory();
|
renderHfAprsHistory();
|
||||||
};
|
}
|
||||||
function addHfAprsPacket(pkt) {
|
function addHfAprsPacket(pkt) {
|
||||||
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
|
||||||
pkt._tsMs = tsMs;
|
pkt._tsMs = tsMs;
|
||||||
@@ -273,7 +273,7 @@
|
|||||||
function normalizeServerHfAprsPacket(pkt) {
|
function normalizeServerHfAprsPacket(pkt) {
|
||||||
return normalizeAprsPacket(pkt, hfAprsWindow.getDecodeRigMeta?.() ?? null);
|
return normalizeAprsPacket(pkt, hfAprsWindow.getDecodeRigMeta?.() ?? null);
|
||||||
}
|
}
|
||||||
hfAprsWindow.onServerHfAprsBatch = function(packets) {
|
function onServerHfAprsBatch(packets) {
|
||||||
if (!Array.isArray(packets) || packets.length === 0) return;
|
if (!Array.isArray(packets) || packets.length === 0) return;
|
||||||
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
||||||
const normalized = [];
|
const normalized = [];
|
||||||
@@ -288,10 +288,7 @@
|
|||||||
hfAprsPacketHistory = normalized.concat(hfAprsPacketHistory);
|
hfAprsPacketHistory = normalized.concat(hfAprsPacketHistory);
|
||||||
pruneHfAprsPacketHistory();
|
pruneHfAprsPacketHistory();
|
||||||
scheduleHfAprsHistoryRender();
|
scheduleHfAprsHistoryRender();
|
||||||
};
|
}
|
||||||
hfAprsWindow.restoreHfAprsHistory = function(packets) {
|
|
||||||
hfAprsWindow.onServerHfAprsBatch?.(packets);
|
|
||||||
};
|
|
||||||
var hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn");
|
var hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn");
|
||||||
hfAprsDecodeToggleBtn?.addEventListener("click", () => {
|
hfAprsDecodeToggleBtn?.addEventListener("click", () => {
|
||||||
void (async () => {
|
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;
|
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 {
|
try {
|
||||||
await hfAprsWindow.postPath?.("/clear_hf_aprs_decode");
|
await hfAprsWindow.postPath?.("/clear_hf_aprs_decode");
|
||||||
hfAprsWindow.resetHfAprsHistoryView?.();
|
resetHfAprsHistoryView();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("HF APRS history clear failed", e);
|
console.error("HF APRS history clear failed", e);
|
||||||
}
|
}
|
||||||
@@ -346,10 +343,17 @@
|
|||||||
renderHfAprsHistory();
|
renderHfAprsHistory();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
hfAprsWindow.onServerHfAprs = function(pkt) {
|
function onServerHfAprs(pkt) {
|
||||||
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
||||||
addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
|
addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
|
||||||
};
|
}
|
||||||
renderHfAprsHistory();
|
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.updateVdesBar = updateVdesBar;
|
||||||
vdesWindow.clearVdesBar = function() {
|
vdesWindow.clearVdesBar = function() {
|
||||||
vdesWindow.resetVdesHistoryView?.();
|
resetVdesHistoryView();
|
||||||
};
|
};
|
||||||
vdesWindow.resetVdesHistoryView = function() {
|
function resetVdesHistoryView() {
|
||||||
if (vdesMessagesEl) vdesMessagesEl.innerHTML = "";
|
if (vdesMessagesEl) vdesMessagesEl.innerHTML = "";
|
||||||
vdesMessageHistory = [];
|
vdesMessageHistory = [];
|
||||||
updateVdesBar();
|
updateVdesBar();
|
||||||
renderVdesHistory();
|
renderVdesHistory();
|
||||||
};
|
}
|
||||||
function renderVdesHistory() {
|
function renderVdesHistory() {
|
||||||
pruneVdesMessageHistory();
|
pruneVdesMessageHistory();
|
||||||
if (!vdesMessagesEl) {
|
if (!vdesMessagesEl) {
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
rig_id: msg.rig_id || null
|
rig_id: msg.rig_id || null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
vdesWindow.onServerVdesBatch = function(messages) {
|
function onServerVdesBatch(messages) {
|
||||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||||
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
||||||
const normalized = [];
|
const normalized = [];
|
||||||
@@ -228,16 +228,13 @@
|
|||||||
pruneVdesMessageHistory();
|
pruneVdesMessageHistory();
|
||||||
scheduleVdesBarUpdate();
|
scheduleVdesBarUpdate();
|
||||||
scheduleVdesHistoryRender();
|
scheduleVdesHistoryRender();
|
||||||
};
|
}
|
||||||
vdesWindow.restoreVdesHistory = function(messages) {
|
|
||||||
vdesWindow.onServerVdesBatch?.(messages);
|
|
||||||
};
|
|
||||||
document.getElementById("settings-clear-vdes-history")?.addEventListener("click", () => {
|
document.getElementById("settings-clear-vdes-history")?.addEventListener("click", () => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (!await vdesWindow.trxUi.confirm({ title: "Clear VDES history?", message: "All stored VDES decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
|
if (!await vdesWindow.trxUi.confirm({ title: "Clear VDES history?", message: "All stored VDES decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
|
||||||
try {
|
try {
|
||||||
await vdesWindow.postPath?.("/clear_vdes_decode");
|
await vdesWindow.postPath?.("/clear_vdes_decode");
|
||||||
vdesWindow.resetVdesHistoryView?.();
|
resetVdesHistoryView();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("VDES history clear failed", e);
|
console.error("VDES history clear failed", e);
|
||||||
}
|
}
|
||||||
@@ -249,19 +246,26 @@
|
|||||||
renderVdesHistory();
|
renderVdesHistory();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
vdesWindow.onServerVdes = function(msg) {
|
function onServerVdes(msg) {
|
||||||
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
||||||
const next = normalizeServerVdesMessage(msg);
|
const next = normalizeServerVdesMessage(msg);
|
||||||
addVdesMessage(next);
|
addVdesMessage(next);
|
||||||
if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) {
|
if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) {
|
||||||
vdesWindow.vdesMapAddPoint(next);
|
vdesWindow.vdesMapAddPoint(next);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
vdesWindow.pruneVdesHistoryView = function() {
|
function pruneVdesHistoryView() {
|
||||||
pruneVdesMessageHistory();
|
pruneVdesMessageHistory();
|
||||||
updateVdesBar();
|
updateVdesBar();
|
||||||
renderVdesHistory();
|
renderVdesHistory();
|
||||||
};
|
}
|
||||||
updateVdesSummary();
|
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="/leaflet-ais-tracksymbol.js"></script>
|
||||||
<script defer src="/webgl-renderer.js"></script>
|
<script defer src="/webgl-renderer.js"></script>
|
||||||
<script defer src="/ui-core.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="/plugin-loader.js"></script>
|
||||||
<script defer src="/app.js"></script>
|
<script defer src="/app.js"></script>
|
||||||
<!-- Template cloning is handled by navigateToTab() in app.js -->
|
<!-- Template cloning is handled by navigateToTab() in app.js -->
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ await build({
|
|||||||
"ui-core": path.join(sourceDir, "ui-core.ts"),
|
"ui-core": path.join(sourceDir, "ui-core.ts"),
|
||||||
"map-core": path.join(sourceDir, "map-core.js"),
|
"map-core": path.join(sourceDir, "map-core.js"),
|
||||||
"plugin-loader": path.join(sourceDir, "plugin-loader.ts"),
|
"plugin-loader": path.join(sourceDir, "plugin-loader.ts"),
|
||||||
|
"plugin-runtime": path.join(sourceDir, "plugin-runtime.ts"),
|
||||||
screenshot: path.join(sourceDir, "screenshot.ts"),
|
screenshot: path.join(sourceDir, "screenshot.ts"),
|
||||||
"webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"),
|
"webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"),
|
||||||
bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"),
|
bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"),
|
||||||
|
|||||||
@@ -567,10 +567,10 @@ function currentDecodeHistoryRetentionMs() {
|
|||||||
window.getDecodeHistoryRetentionMs = currentDecodeHistoryRetentionMs;
|
window.getDecodeHistoryRetentionMs = currentDecodeHistoryRetentionMs;
|
||||||
|
|
||||||
window.applyDecodeHistoryRetention = function() {
|
window.applyDecodeHistoryRetention = function() {
|
||||||
if (typeof window.pruneAprsHistoryView === "function") window.pruneAprsHistoryView();
|
window.trxPluginRuntime.prune("aprs");
|
||||||
if (typeof window.pruneHfAprsHistoryView === "function") window.pruneHfAprsHistoryView();
|
window.trxPluginRuntime.prune("hf_aprs");
|
||||||
if (typeof window.pruneAisHistoryView === "function") window.pruneAisHistoryView();
|
window.trxPluginRuntime.prune("ais");
|
||||||
if (typeof window.pruneVdesHistoryView === "function") window.pruneVdesHistoryView();
|
window.trxPluginRuntime.prune("vdes");
|
||||||
if (typeof window.pruneFt8HistoryView === "function") window.pruneFt8HistoryView();
|
if (typeof window.pruneFt8HistoryView === "function") window.pruneFt8HistoryView();
|
||||||
if (typeof window.pruneWsprHistoryView === "function") window.pruneWsprHistoryView();
|
if (typeof window.pruneWsprHistoryView === "function") window.pruneWsprHistoryView();
|
||||||
};
|
};
|
||||||
@@ -615,41 +615,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -6250,10 +6215,9 @@ function updateDecodeStatus(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function dispatchDecodeMessage(msg, skipStats) {
|
function dispatchDecodeMessage(msg, skipStats) {
|
||||||
if (msg.type === "ais") { if (window.onServerAis) window.onServerAis(msg); else (_pendingDecodeLive.ais = _pendingDecodeLive.ais || []).push(msg); }
|
if (msg.type === "ais" || msg.type === "vdes" || msg.type === "aprs" || msg.type === "hf_aprs") {
|
||||||
if (msg.type === "vdes") { if (window.onServerVdes) window.onServerVdes(msg); else (_pendingDecodeLive.vdes = _pendingDecodeLive.vdes || []).push(msg); }
|
window.trxPluginRuntime.dispatch(msg.type, 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 === "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);
|
||||||
@@ -6281,20 +6245,8 @@ function dispatchDecodeBatch(batch) {
|
|||||||
const type = String(batch[0]?.type || "");
|
const type = String(batch[0]?.type || "");
|
||||||
const uniformType = batch.every((msg) => String(msg?.type || "") === type);
|
const uniformType = batch.every((msg) => String(msg?.type || "") === type);
|
||||||
if (uniformType) {
|
if (uniformType) {
|
||||||
if (type === "ais" && window.onServerAisBatch) {
|
if (type === "ais" || type === "vdes" || type === "aprs" || type === "hf_aprs") {
|
||||||
window.onServerAisBatch(batch);
|
window.trxPluginRuntime.dispatchBatch(type, 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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (type === "ft8" && window.onServerFt8Batch) {
|
if (type === "ft8" && window.onServerFt8Batch) {
|
||||||
@@ -6366,24 +6318,8 @@ function restoreDecodeHistoryGroup(kind, messages) {
|
|||||||
}
|
}
|
||||||
window.trx.modules.map?.scheduleStatsRender();
|
window.trx.modules.map?.scheduleStatsRender();
|
||||||
}
|
}
|
||||||
if (kind === "ais") {
|
if (kind === "ais" || kind === "vdes" || kind === "aprs" || kind === "hf_aprs") {
|
||||||
if (window.restoreAisHistory) { window.restoreAisHistory(messages); }
|
window.trxPluginRuntime.restore(kind, 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); }
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (kind === "cw" && window.restoreCwHistory) {
|
if (kind === "cw" && window.restoreCwHistory) {
|
||||||
@@ -6417,12 +6353,11 @@ function connectDecode() {
|
|||||||
terminateDecodeHistoryWorker();
|
terminateDecodeHistoryWorker();
|
||||||
decodeHistoryReplayActive = false;
|
decodeHistoryReplayActive = false;
|
||||||
decodeMapSyncPending = false;
|
decodeMapSyncPending = false;
|
||||||
// Clear any pending buffers from a previous connection cycle.
|
window.trxPluginRuntime.clearQueued();
|
||||||
for (const k in _pendingDecodeHistory) delete _pendingDecodeHistory[k];
|
window.trxPluginRuntime.reset("ais");
|
||||||
for (const k in _pendingDecodeLive) delete _pendingDecodeLive[k];
|
window.trxPluginRuntime.reset("vdes");
|
||||||
if (window.resetAisHistoryView) window.resetAisHistoryView();
|
window.trxPluginRuntime.reset("aprs");
|
||||||
if (window.resetVdesHistoryView) window.resetVdesHistoryView();
|
window.trxPluginRuntime.reset("hf_aprs");
|
||||||
if (window.resetAprsHistoryView) window.resetAprsHistoryView();
|
|
||||||
if (window.resetCwHistoryView) window.resetCwHistoryView();
|
if (window.resetCwHistoryView) window.resetCwHistoryView();
|
||||||
if (window.resetFt8HistoryView) window.resetFt8HistoryView();
|
if (window.resetFt8HistoryView) window.resetFt8HistoryView();
|
||||||
if (window.resetFt4HistoryView) window.resetFt4HistoryView();
|
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 {};
|
export {};
|
||||||
|
|
||||||
|
import type { PluginRuntimeWindow } from "./runtime-contract";
|
||||||
|
|
||||||
interface AisMessage {
|
interface AisMessage {
|
||||||
rig_id?: string | null;
|
rig_id?: string | null;
|
||||||
channel?: string | null;
|
channel?: string | null;
|
||||||
@@ -36,12 +38,6 @@ interface AisBridge {
|
|||||||
trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
|
trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
|
||||||
updateAisBar?: () => void;
|
updateAisBar?: () => void;
|
||||||
clearAisBar?: () => 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 aisWindow = window as unknown as AisBridge;
|
||||||
const escapeAisHtml = (input: string): string => aisWindow.escapeMapHtml?.(input) ?? input
|
const escapeAisHtml = (input: string): string => aisWindow.escapeMapHtml?.(input) ?? input
|
||||||
@@ -324,16 +320,16 @@ function updateAisBar() {
|
|||||||
}
|
}
|
||||||
aisWindow.updateAisBar = updateAisBar;
|
aisWindow.updateAisBar = updateAisBar;
|
||||||
aisWindow.clearAisBar = function() {
|
aisWindow.clearAisBar = function() {
|
||||||
aisWindow.resetAisHistoryView?.();
|
resetAisHistoryView();
|
||||||
};
|
};
|
||||||
|
|
||||||
aisWindow.resetAisHistoryView = function() {
|
function resetAisHistoryView(): void {
|
||||||
if (aisMessagesEl) aisMessagesEl.innerHTML = "";
|
if (aisMessagesEl) aisMessagesEl.innerHTML = "";
|
||||||
aisMessageHistory = [];
|
aisMessageHistory = [];
|
||||||
updateAisBar();
|
updateAisBar();
|
||||||
renderAisHistory();
|
renderAisHistory();
|
||||||
aisWindow.clearMapMarkersByType?.("ais");
|
aisWindow.clearMapMarkersByType?.("ais");
|
||||||
};
|
}
|
||||||
|
|
||||||
function renderAisHistory() {
|
function renderAisHistory() {
|
||||||
pruneAisMessageHistory();
|
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 (!Array.isArray(messages) || messages.length === 0) return;
|
||||||
if (aisStatus) aisStatus.textContent = "Receiving";
|
if (aisStatus) aisStatus.textContent = "Receiving";
|
||||||
const normalized: AisMessage[] = [];
|
const normalized: AisMessage[] = [];
|
||||||
@@ -398,23 +394,19 @@ aisWindow.onServerAisBatch = function(messages: AisMessage[]) {
|
|||||||
pruneAisMessageHistory();
|
pruneAisMessageHistory();
|
||||||
scheduleAisBarUpdate();
|
scheduleAisBarUpdate();
|
||||||
scheduleAisHistoryRender();
|
scheduleAisHistoryRender();
|
||||||
};
|
}
|
||||||
|
|
||||||
aisWindow.restoreAisHistory = function(messages: AisMessage[]) {
|
function pruneAisHistoryView(): void {
|
||||||
aisWindow.onServerAisBatch?.(messages);
|
|
||||||
};
|
|
||||||
|
|
||||||
aisWindow.pruneAisHistoryView = function() {
|
|
||||||
pruneAisMessageHistory();
|
pruneAisMessageHistory();
|
||||||
updateAisBar();
|
updateAisBar();
|
||||||
renderAisHistory();
|
renderAisHistory();
|
||||||
};
|
}
|
||||||
|
|
||||||
document.getElementById("settings-clear-ais-history")?.addEventListener("click", () => { void (async () => {
|
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;
|
if (!await aisWindow.trxUi.confirm({ title: "Clear AIS history?", message: "All stored AIS messages will be permanently removed.", confirmLabel: "Clear history" })) return;
|
||||||
try {
|
try {
|
||||||
await aisWindow.postPath?.("/clear_ais_decode");
|
await aisWindow.postPath?.("/clear_ais_decode");
|
||||||
aisWindow.resetAisHistoryView?.();
|
resetAisHistoryView();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("AIS history clear failed", 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";
|
if (aisStatus) aisStatus.textContent = "Receiving";
|
||||||
addAisMessage(normalizeServerAisMessage(msg));
|
addAisMessage(normalizeServerAisMessage(msg));
|
||||||
};
|
}
|
||||||
|
|
||||||
updateAisSummary();
|
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 AprsPacket,
|
||||||
type AprsTypeFilter,
|
type AprsTypeFilter,
|
||||||
} from "./aprs-shared";
|
} from "./aprs-shared";
|
||||||
|
import type { PluginRuntimeWindow } from "./runtime-contract";
|
||||||
interface AprsBridge {
|
interface AprsBridge {
|
||||||
getDecodeHistoryRetentionMs?: () => number;
|
getDecodeHistoryRetentionMs?: () => number;
|
||||||
trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
|
trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
|
||||||
@@ -31,12 +32,6 @@ interface AprsBridge {
|
|||||||
updateAprsBar?: () => void;
|
updateAprsBar?: () => void;
|
||||||
clearAprsBar?: () => void;
|
clearAprsBar?: () => void;
|
||||||
closeAprsBar?: () => 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 aprsWindow = window as unknown as AprsBridge;
|
||||||
const escapeAprsHtml = (input: string): string => aprsWindow.escapeMapHtml?.(input) ?? input
|
const escapeAprsHtml = (input: string): string => aprsWindow.escapeMapHtml?.(input) ?? input
|
||||||
@@ -288,7 +283,7 @@ function updateAprsBar() {
|
|||||||
}
|
}
|
||||||
aprsWindow.updateAprsBar = updateAprsBar;
|
aprsWindow.updateAprsBar = updateAprsBar;
|
||||||
aprsWindow.clearAprsBar = function() {
|
aprsWindow.clearAprsBar = function() {
|
||||||
aprsWindow.resetAprsHistoryView?.();
|
resetAprsHistoryView();
|
||||||
};
|
};
|
||||||
aprsWindow.closeAprsBar = function() {
|
aprsWindow.closeAprsBar = function() {
|
||||||
aprsBarDismissedAtMs = Date.now();
|
aprsBarDismissedAtMs = Date.now();
|
||||||
@@ -298,19 +293,19 @@ aprsWindow.closeAprsBar = function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
aprsWindow.resetAprsHistoryView = function() {
|
function resetAprsHistoryView(): void {
|
||||||
if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
|
if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
|
||||||
aprsPacketHistory = [];
|
aprsPacketHistory = [];
|
||||||
updateAprsBar();
|
updateAprsBar();
|
||||||
renderAprsHistory();
|
renderAprsHistory();
|
||||||
aprsWindow.clearMapMarkersByType?.("aprs");
|
aprsWindow.clearMapMarkersByType?.("aprs");
|
||||||
};
|
}
|
||||||
|
|
||||||
aprsWindow.pruneAprsHistoryView = function() {
|
function pruneAprsHistoryView(): void {
|
||||||
pruneAprsPacketHistory();
|
pruneAprsPacketHistory();
|
||||||
updateAprsBar();
|
updateAprsBar();
|
||||||
renderAprsHistory();
|
renderAprsHistory();
|
||||||
};
|
}
|
||||||
|
|
||||||
function addAprsPacket(pkt: AprsPacket): void {
|
function addAprsPacket(pkt: AprsPacket): void {
|
||||||
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
|
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);
|
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 (!Array.isArray(packets) || packets.length === 0) return;
|
||||||
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
||||||
const normalized: AprsPacket[] = [];
|
const normalized: AprsPacket[] = [];
|
||||||
@@ -354,17 +349,13 @@ aprsWindow.onServerAprsBatch = function(packets: AprsPacket[]) {
|
|||||||
pruneAprsPacketHistory();
|
pruneAprsPacketHistory();
|
||||||
if (hasCrcOk) scheduleAprsBarUpdate();
|
if (hasCrcOk) scheduleAprsBarUpdate();
|
||||||
scheduleAprsHistoryRender();
|
scheduleAprsHistoryRender();
|
||||||
};
|
}
|
||||||
|
|
||||||
aprsWindow.restoreAprsHistory = function(packets: AprsPacket[]) {
|
|
||||||
aprsWindow.onServerAprsBatch?.(packets);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById("settings-clear-aprs-history")?.addEventListener("click", () => { void (async () => {
|
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;
|
if (!await aprsWindow.trxUi.confirm({ title: "Clear APRS history?", message: "All stored APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
|
||||||
try {
|
try {
|
||||||
await aprsWindow.postPath?.("/clear_aprs_decode");
|
await aprsWindow.postPath?.("/clear_aprs_decode");
|
||||||
aprsWindow.resetAprsHistoryView?.();
|
resetAprsHistoryView();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("APRS history clear failed", e);
|
console.error("APRS history clear failed", e);
|
||||||
}
|
}
|
||||||
@@ -408,10 +399,17 @@ if (aprsFilterInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Server-side APRS decode handler ---
|
// --- Server-side APRS decode handler ---
|
||||||
aprsWindow.onServerAprs = function(pkt: AprsPacket) {
|
function onServerAprs(pkt: AprsPacket): void {
|
||||||
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
||||||
addAprsPacket(normalizeServerAprsPacket(pkt));
|
addAprsPacket(normalizeServerAprsPacket(pkt));
|
||||||
};
|
}
|
||||||
|
|
||||||
renderAprsHistory();
|
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 AprsPacket,
|
||||||
type AprsTypeFilter,
|
type AprsTypeFilter,
|
||||||
} from "./aprs-shared";
|
} from "./aprs-shared";
|
||||||
|
import type { PluginRuntimeWindow } from "./runtime-contract";
|
||||||
|
|
||||||
interface HfAprsBridge {
|
interface HfAprsBridge {
|
||||||
getDecodeHistoryRetentionMs?: () => number;
|
getDecodeHistoryRetentionMs?: () => number;
|
||||||
@@ -28,12 +29,6 @@ interface HfAprsBridge {
|
|||||||
takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<unknown>;
|
takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<unknown>;
|
||||||
postPath?: (path: string) => Promise<unknown>;
|
postPath?: (path: string) => Promise<unknown>;
|
||||||
trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
|
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 hfAprsWindow = window as unknown as HfAprsBridge;
|
||||||
const escapeHfAprsHtml = (input: string): string => hfAprsWindow.escapeMapHtml?.(input) ?? input
|
const escapeHfAprsHtml = (input: string): string => hfAprsWindow.escapeMapHtml?.(input) ?? input
|
||||||
@@ -247,16 +242,16 @@ function renderHfAprsHistory() {
|
|||||||
updateHfAprsChipState();
|
updateHfAprsChipState();
|
||||||
}
|
}
|
||||||
|
|
||||||
hfAprsWindow.resetHfAprsHistoryView = function() {
|
function resetHfAprsHistoryView(): void {
|
||||||
if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
|
if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
|
||||||
hfAprsPacketHistory = [];
|
hfAprsPacketHistory = [];
|
||||||
renderHfAprsHistory();
|
renderHfAprsHistory();
|
||||||
};
|
}
|
||||||
|
|
||||||
hfAprsWindow.pruneHfAprsHistoryView = function() {
|
function pruneHfAprsHistoryView(): void {
|
||||||
pruneHfAprsPacketHistory();
|
pruneHfAprsPacketHistory();
|
||||||
renderHfAprsHistory();
|
renderHfAprsHistory();
|
||||||
};
|
}
|
||||||
|
|
||||||
function addHfAprsPacket(pkt: AprsPacket): void {
|
function addHfAprsPacket(pkt: AprsPacket): void {
|
||||||
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
|
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);
|
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 (!Array.isArray(packets) || packets.length === 0) return;
|
||||||
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
||||||
const normalized: AprsPacket[] = [];
|
const normalized: AprsPacket[] = [];
|
||||||
@@ -288,11 +283,7 @@ hfAprsWindow.onServerHfAprsBatch = function(packets: AprsPacket[]) {
|
|||||||
hfAprsPacketHistory = normalized.concat(hfAprsPacketHistory);
|
hfAprsPacketHistory = normalized.concat(hfAprsPacketHistory);
|
||||||
pruneHfAprsPacketHistory();
|
pruneHfAprsPacketHistory();
|
||||||
scheduleHfAprsHistoryRender();
|
scheduleHfAprsHistoryRender();
|
||||||
};
|
}
|
||||||
|
|
||||||
hfAprsWindow.restoreHfAprsHistory = function(packets: AprsPacket[]) {
|
|
||||||
hfAprsWindow.onServerHfAprsBatch?.(packets);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn");
|
const hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn");
|
||||||
hfAprsDecodeToggleBtn?.addEventListener("click", () => { void (async () => {
|
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;
|
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 {
|
try {
|
||||||
await hfAprsWindow.postPath?.("/clear_hf_aprs_decode");
|
await hfAprsWindow.postPath?.("/clear_hf_aprs_decode");
|
||||||
hfAprsWindow.resetHfAprsHistoryView?.();
|
resetHfAprsHistoryView();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("HF APRS history clear failed", e);
|
console.error("HF APRS history clear failed", e);
|
||||||
}
|
}
|
||||||
@@ -352,10 +343,17 @@ if (hfAprsFilterInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Server-side HF APRS decode handler ---
|
// --- Server-side HF APRS decode handler ---
|
||||||
hfAprsWindow.onServerHfAprs = function(pkt: AprsPacket) {
|
function onServerHfAprs(pkt: AprsPacket): void {
|
||||||
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
|
||||||
addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
|
addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
|
||||||
};
|
}
|
||||||
|
|
||||||
renderHfAprsHistory();
|
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 {};
|
export {};
|
||||||
|
|
||||||
|
import type { PluginRuntimeWindow } from "./runtime-contract";
|
||||||
|
|
||||||
interface VdesMessage {
|
interface VdesMessage {
|
||||||
rig_id?: string | null;
|
rig_id?: string | null;
|
||||||
message_type?: number;
|
message_type?: number;
|
||||||
@@ -42,12 +44,6 @@ interface VdesBridge {
|
|||||||
vdesMapAddPoint?: (message: VdesMessage) => void;
|
vdesMapAddPoint?: (message: VdesMessage) => void;
|
||||||
updateVdesBar?: () => void;
|
updateVdesBar?: () => void;
|
||||||
clearVdesBar?: () => 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;
|
const vdesWindow = window as unknown as VdesBridge;
|
||||||
@@ -267,15 +263,15 @@ function updateVdesBar() {
|
|||||||
}
|
}
|
||||||
vdesWindow.updateVdesBar = updateVdesBar;
|
vdesWindow.updateVdesBar = updateVdesBar;
|
||||||
vdesWindow.clearVdesBar = function() {
|
vdesWindow.clearVdesBar = function() {
|
||||||
vdesWindow.resetVdesHistoryView?.();
|
resetVdesHistoryView();
|
||||||
};
|
};
|
||||||
|
|
||||||
vdesWindow.resetVdesHistoryView = function() {
|
function resetVdesHistoryView(): void {
|
||||||
if (vdesMessagesEl) vdesMessagesEl.innerHTML = "";
|
if (vdesMessagesEl) vdesMessagesEl.innerHTML = "";
|
||||||
vdesMessageHistory = [];
|
vdesMessageHistory = [];
|
||||||
updateVdesBar();
|
updateVdesBar();
|
||||||
renderVdesHistory();
|
renderVdesHistory();
|
||||||
};
|
}
|
||||||
|
|
||||||
function renderVdesHistory() {
|
function renderVdesHistory() {
|
||||||
pruneVdesMessageHistory();
|
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 (!Array.isArray(messages) || messages.length === 0) return;
|
||||||
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
||||||
const normalized: VdesMessage[] = [];
|
const normalized: VdesMessage[] = [];
|
||||||
@@ -336,17 +332,13 @@ vdesWindow.onServerVdesBatch = function(messages: VdesMessage[]) {
|
|||||||
pruneVdesMessageHistory();
|
pruneVdesMessageHistory();
|
||||||
scheduleVdesBarUpdate();
|
scheduleVdesBarUpdate();
|
||||||
scheduleVdesHistoryRender();
|
scheduleVdesHistoryRender();
|
||||||
};
|
}
|
||||||
|
|
||||||
vdesWindow.restoreVdesHistory = function(messages: VdesMessage[]) {
|
|
||||||
vdesWindow.onServerVdesBatch?.(messages);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById("settings-clear-vdes-history")?.addEventListener("click", () => { void (async () => {
|
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;
|
if (!await vdesWindow.trxUi.confirm({ title: "Clear VDES history?", message: "All stored VDES decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
|
||||||
try {
|
try {
|
||||||
await vdesWindow.postPath?.("/clear_vdes_decode");
|
await vdesWindow.postPath?.("/clear_vdes_decode");
|
||||||
vdesWindow.resetVdesHistoryView?.();
|
resetVdesHistoryView();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("VDES history clear failed", 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";
|
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
||||||
const next = normalizeServerVdesMessage(msg);
|
const next = normalizeServerVdesMessage(msg);
|
||||||
addVdesMessage(next);
|
addVdesMessage(next);
|
||||||
if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) {
|
if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) {
|
||||||
vdesWindow.vdesMapAddPoint(next);
|
vdesWindow.vdesMapAddPoint(next);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
vdesWindow.pruneVdesHistoryView = function() {
|
function pruneVdesHistoryView(): void {
|
||||||
pruneVdesMessageHistory();
|
pruneVdesMessageHistory();
|
||||||
updateVdesBar();
|
updateVdesBar();
|
||||||
renderVdesHistory();
|
renderVdesHistory();
|
||||||
};
|
}
|
||||||
|
|
||||||
updateVdesSummary();
|
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,
|
Map,
|
||||||
console,
|
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");
|
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);
|
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.length, 1);
|
||||||
assert.equal(forwarded[0].mmsi, 261000001);
|
assert.equal(forwarded[0].mmsi, 261000001);
|
||||||
assert.equal(forwarded[0].rig_id, null);
|
assert.equal(forwarded[0].rig_id, null);
|
||||||
|
|||||||
@@ -25,11 +25,14 @@ test("APRS entry normalizes positioned packets without remote symbol assets", as
|
|||||||
Reflect,
|
Reflect,
|
||||||
console,
|
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");
|
const source = await readFile(new URL("../../assets/web/generated/aprs.js", import.meta.url), "utf8");
|
||||||
assert.equal(source.includes("raw.githubusercontent.com"), false);
|
assert.equal(source.includes("raw.githubusercontent.com"), false);
|
||||||
|
new vm.Script(runtime).runInContext(context);
|
||||||
new vm.Script(source).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.length, 1);
|
||||||
assert.equal(forwarded[0][0], "SP0ABC");
|
assert.equal(forwarded[0][0], "SP0ABC");
|
||||||
assert.equal(forwarded[0][1], 54.5);
|
assert.equal(forwarded[0][1], 54.5);
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ test("HF APRS entry uses shared typed normalization and local symbols", async ()
|
|||||||
Reflect,
|
Reflect,
|
||||||
console,
|
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");
|
const source = await readFile(new URL("../../assets/web/generated/hf-aprs.js", import.meta.url), "utf8");
|
||||||
assert.equal(source.includes("raw.githubusercontent.com"), false);
|
assert.equal(source.includes("raw.githubusercontent.com"), false);
|
||||||
|
new vm.Script(runtime).runInContext(context);
|
||||||
new vm.Script(source).runInContext(context);
|
new vm.Script(source).runInContext(context);
|
||||||
|
|
||||||
window.onServerHfAprs({ src_call: "SP0ABC", dest_call: "APRS", crc_ok: true, symbol_table: "/", symbol_code: ">" });
|
window.trxPluginRuntime.dispatch("hf_aprs", { src_call: "SP0ABC", dest_call: "APRS", crc_ok: true, symbol_table: "/", symbol_code: ">" });
|
||||||
assert.equal(typeof window.restoreHfAprsHistory, "function");
|
assert.equal(window.onServerHfAprs, undefined);
|
||||||
assert.equal(typeof window.pruneHfAprsHistoryView, "function");
|
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,
|
Array,
|
||||||
console,
|
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");
|
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);
|
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.length, 1);
|
||||||
assert.equal(forwarded[0].callsign, "SP0ABC");
|
assert.equal(forwarded[0].callsign, "SP0ABC");
|
||||||
assert.equal(forwarded[0].rig_id, null);
|
assert.equal(forwarded[0].rig_id, null);
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ define_gz_cache!(
|
|||||||
status::PLUGIN_LOADER_JS,
|
status::PLUGIN_LOADER_JS,
|
||||||
"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_screenshot_js, status::SCREENSHOT_JS, "screenshot.js");
|
||||||
define_gz_cache!(
|
define_gz_cache!(
|
||||||
gz_decode_history_worker_js,
|
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)
|
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")]
|
#[get("/screenshot.js")]
|
||||||
pub(crate) async fn screenshot_js(req: HttpRequest) -> impl Responder {
|
pub(crate) async fn screenshot_js(req: HttpRequest) -> impl Responder {
|
||||||
let c = gz_screenshot_js();
|
let c = gz_screenshot_js();
|
||||||
|
|||||||
@@ -646,6 +646,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(assets::ui_core_js)
|
.service(assets::ui_core_js)
|
||||||
.service(assets::map_core_js)
|
.service(assets::map_core_js)
|
||||||
.service(assets::plugin_loader_js)
|
.service(assets::plugin_loader_js)
|
||||||
|
.service(assets::plugin_runtime_js)
|
||||||
.service(assets::screenshot_js)
|
.service(assets::screenshot_js)
|
||||||
.service(assets::decode_history_worker_js)
|
.service(assets::decode_history_worker_js)
|
||||||
.service(assets::webgl_renderer_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 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 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_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 SCREENSHOT_JS: &str = include_str!("../assets/web/generated/screenshot.js");
|
||||||
pub const DECODE_HISTORY_WORKER_JS: &str =
|
pub const DECODE_HISTORY_WORKER_JS: &str =
|
||||||
include_str!("../assets/web/generated/decode-history-worker.js");
|
include_str!("../assets/web/generated/decode-history-worker.js");
|
||||||
@@ -94,6 +95,14 @@ mod tests {
|
|||||||
.find("/plugin-loader.js")
|
.find("/plugin-loader.js")
|
||||||
.expect("typed plugin loader is loaded");
|
.expect("typed plugin loader is loaded");
|
||||||
assert!(plugin_loader < app, "plugin registry must load before app");
|
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!(PLUGIN_LOADER_JS.contains("import(path)"));
|
||||||
assert!(html.contains("<summary>Scheduler controls</summary>"));
|
assert!(html.contains("<summary>Scheduler controls</summary>"));
|
||||||
assert!(html.contains("<summary>Audio controls</summary>"));
|
assert!(html.contains("<summary>Audio controls</summary>"));
|
||||||
|
|||||||
Reference in New Issue
Block a user