[feat](trx-frontend): add context types for explicit initialization

Create explicit context types for frontend registration and runtime:

FrontendRegistrationContext:
- register_frontend(name, spawner) - register a frontend
- is_frontend_registered(name) - check if registered
- registered_frontends() -> Vec<String> - list all frontends
- spawn_frontend(name, ...) -> DynResult - spawn a frontend

FrontendRuntimeContext (NEW):
- audio_rx: broadcast channel for audio RX
- audio_tx: mpsc channel for audio TX
- audio_info: watch channel for audio stream metadata
- decode_rx: broadcast channel for decoded messages
- aprs_history: Arc<Mutex<VecDeque>> for APRS decode history
- cw_history: Arc<Mutex<VecDeque>> for CW decode history
- ft8_history: Arc<Mutex<VecDeque>> for FT8 decode history
- auth_tokens: HashSet for authentication

Replaces global mutable state with explicit context that can be
threaded through bootstrap. Maintains global API for compatibility.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-12 20:47:23 +01:00
parent ef6d45b878
commit d8e9397cf6
2 changed files with 108 additions and 4 deletions
+1
View File
@@ -8,5 +8,6 @@ version = "0.1.0"
edition = "2021"
[dependencies]
bytes = "1"
trx-core = { path = "../../trx-core" }
tokio = { workspace = true, features = ["sync"] }
+107 -4
View File
@@ -2,13 +2,17 @@
//
// SPDX-License-Identifier: BSD-2-Clause
use std::collections::HashMap;
use std::collections::{HashMap, VecDeque, HashSet};
use std::net::SocketAddr;
use std::sync::{Mutex, OnceLock};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Instant;
use tokio::sync::{mpsc, watch};
use bytes::Bytes;
use tokio::sync::{broadcast, mpsc, watch};
use tokio::task::JoinHandle;
use trx_core::audio::AudioStreamInfo;
use trx_core::decode::{AprsPacket, CwEvent, DecodedMessage, Ft8Message};
use trx_core::{DynResult, RigRequest, RigState};
/// Trait implemented by concrete frontends to expose a runner entrypoint.
@@ -21,13 +25,112 @@ pub trait FrontendSpawner {
) -> JoinHandle<()>;
}
type FrontendSpawnFn = fn(
pub type FrontendSpawnFn = fn(
watch::Receiver<RigState>,
mpsc::Sender<RigRequest>,
Option<String>,
SocketAddr,
) -> JoinHandle<()>;
/// Context for registering and spawning frontends.
pub struct FrontendRegistrationContext {
spawners: HashMap<String, FrontendSpawnFn>,
}
impl FrontendRegistrationContext {
/// Create a new empty registration context.
pub fn new() -> Self {
Self {
spawners: HashMap::new(),
}
}
/// Register a frontend spawner under a stable name (e.g. "http").
pub fn register_frontend(&mut self, name: &str, spawner: FrontendSpawnFn) {
let key = normalize_name(name);
self.spawners.insert(key, spawner);
}
/// Check whether a frontend name is registered.
pub fn is_frontend_registered(&self, name: &str) -> bool {
let key = normalize_name(name);
self.spawners.contains_key(&key)
}
/// List registered frontend names.
pub fn registered_frontends(&self) -> Vec<String> {
let mut names: Vec<String> = self.spawners.keys().cloned().collect();
names.sort();
names
}
/// Spawn a registered frontend by name.
pub fn spawn_frontend(
&self,
name: &str,
state_rx: watch::Receiver<RigState>,
rig_tx: mpsc::Sender<RigRequest>,
callsign: Option<String>,
listen_addr: SocketAddr,
) -> DynResult<JoinHandle<()>> {
let key = normalize_name(name);
let spawner = self
.spawners
.get(&key)
.ok_or_else(|| format!("Unknown frontend: {}", name))?;
Ok(spawner(state_rx, rig_tx, callsign, listen_addr))
}
}
impl Default for FrontendRegistrationContext {
fn default() -> Self {
Self::new()
}
}
/// Runtime context for frontend operation, containing audio channels and decode state.
pub struct FrontendRuntimeContext {
/// Audio RX broadcast channel (server → browser)
pub audio_rx: Option<broadcast::Sender<Bytes>>,
/// Audio TX channel (browser → server)
pub audio_tx: Option<mpsc::Sender<Bytes>>,
/// Audio stream info watch channel
pub audio_info: Option<watch::Receiver<Option<AudioStreamInfo>>>,
/// Decode message broadcast channel
pub decode_rx: Option<broadcast::Sender<DecodedMessage>>,
/// APRS decode history (timestamp, packet)
pub aprs_history: Arc<Mutex<VecDeque<(Instant, AprsPacket)>>>,
/// CW decode history (timestamp, event)
pub cw_history: Arc<Mutex<VecDeque<(Instant, CwEvent)>>>,
/// FT8 decode history (timestamp, message)
pub ft8_history: Arc<Mutex<VecDeque<(Instant, Ft8Message)>>>,
/// Authentication tokens for HTTP-JSON frontend
pub auth_tokens: HashSet<String>,
}
impl FrontendRuntimeContext {
/// Create a new empty runtime context.
pub fn new() -> Self {
Self {
audio_rx: None,
audio_tx: None,
audio_info: None,
decode_rx: None,
aprs_history: Arc::new(Mutex::new(VecDeque::new())),
cw_history: Arc::new(Mutex::new(VecDeque::new())),
ft8_history: Arc::new(Mutex::new(VecDeque::new())),
auth_tokens: HashSet::new(),
}
}
}
impl Default for FrontendRuntimeContext {
fn default() -> Self {
Self::new()
}
}
// Legacy global registry for plugin compatibility
struct FrontendRegistry {
spawners: HashMap<String, FrontendSpawnFn>,
}