Lot B2 du chantier server/client mode : le cœur backend émet désormais ses flux via une abstraction de sink agnostique, sans dépendre directement de tauri::ipc::Channel, préalable au futur serveur web + PTY WebSocket. - crates/backend : abstraction de sink (stream.rs) câblée dans lib.rs. - crates/app-tauri : implémentation Tauri du sink (stream.rs) et adaptation des surfaces lib.rs, pty.rs, chat.rs. - Nettoyage clippy des 2 warnings B2. Validé : cargo check --workspace vert, tests backend/app-tauri verts, cœur agnostique Tauri, clippy B2 propre. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
217 lines
5.4 KiB
Rust
217 lines
5.4 KiB
Rust
use std::collections::HashMap;
|
|
use std::hash::Hash;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum OutputSinkError {
|
|
Closed,
|
|
Full,
|
|
Other(String),
|
|
}
|
|
|
|
pub trait OutputSink<T>: Send + Sync + 'static {
|
|
fn send(&self, item: T) -> Result<(), OutputSinkError>;
|
|
}
|
|
|
|
type SharedOutputSink<T> = Arc<dyn OutputSink<T>>;
|
|
type SinkRegistry<K, T> = HashMap<K, (u64, SharedOutputSink<T>)>;
|
|
|
|
pub struct OutputBridge<K, T>
|
|
where
|
|
K: Copy + Eq + Hash,
|
|
{
|
|
sinks: Mutex<SinkRegistry<K, T>>,
|
|
}
|
|
|
|
impl<K, T> Default for OutputBridge<K, T>
|
|
where
|
|
K: Copy + Eq + Hash,
|
|
T: 'static,
|
|
{
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl<K, T> OutputBridge<K, T>
|
|
where
|
|
K: Copy + Eq + Hash,
|
|
T: 'static,
|
|
{
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
sinks: Mutex::new(HashMap::new()),
|
|
}
|
|
}
|
|
|
|
pub fn register(&self, key: K, sink: SharedOutputSink<T>) -> u64 {
|
|
if let Ok(mut map) = self.sinks.lock() {
|
|
let gen = map.get(&key).map_or(0, |(g, _)| g.wrapping_add(1));
|
|
map.insert(key, (gen, sink));
|
|
gen
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
pub fn unregister(&self, key: &K) {
|
|
if let Ok(mut map) = self.sinks.lock() {
|
|
map.remove(key);
|
|
}
|
|
}
|
|
|
|
pub fn unregister_if(&self, key: &K, gen: u64) {
|
|
if let Ok(mut map) = self.sinks.lock() {
|
|
if matches!(map.get(key), Some((g, _)) if *g == gen) {
|
|
map.remove(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn try_send_output(&self, key: &K, item: T) -> Result<(), OutputSinkError> {
|
|
let sink = {
|
|
let Ok(map) = self.sinks.lock() else {
|
|
return Err(OutputSinkError::Closed);
|
|
};
|
|
map.get(key)
|
|
.map(|(_, sink)| Arc::clone(sink))
|
|
.ok_or(OutputSinkError::Closed)?
|
|
};
|
|
sink.send(item)
|
|
}
|
|
|
|
pub fn send_output(&self, key: &K, item: T) -> bool {
|
|
self.try_send_output(key, item).is_ok()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn active_sessions(&self) -> usize {
|
|
self.sinks.lock().map(|m| m.len()).unwrap_or(0)
|
|
}
|
|
}
|
|
|
|
struct ReplayEntry<T> {
|
|
generation: u64,
|
|
sink: Option<SharedOutputSink<T>>,
|
|
scrollback: Vec<T>,
|
|
}
|
|
|
|
pub struct ReplayOutputBridge<K, T>
|
|
where
|
|
K: Copy + Eq + Hash,
|
|
{
|
|
entries: Mutex<HashMap<K, ReplayEntry<T>>>,
|
|
max_chunks: usize,
|
|
max_bytes: usize,
|
|
measure: fn(&T) -> usize,
|
|
}
|
|
|
|
impl<K, T> ReplayOutputBridge<K, T>
|
|
where
|
|
K: Copy + Eq + Hash,
|
|
T: Clone + 'static,
|
|
{
|
|
#[must_use]
|
|
pub fn new(max_chunks: usize, max_bytes: usize, measure: fn(&T) -> usize) -> Self {
|
|
Self {
|
|
entries: Mutex::new(HashMap::new()),
|
|
max_chunks,
|
|
max_bytes,
|
|
measure,
|
|
}
|
|
}
|
|
|
|
pub fn register(&self, key: K, sink: SharedOutputSink<T>) -> u64 {
|
|
if let Ok(mut map) = self.entries.lock() {
|
|
match map.get_mut(&key) {
|
|
Some(entry) => {
|
|
entry.generation = entry.generation.wrapping_add(1);
|
|
entry.sink = Some(sink);
|
|
entry.generation
|
|
}
|
|
None => {
|
|
map.insert(
|
|
key,
|
|
ReplayEntry {
|
|
generation: 0,
|
|
sink: Some(sink),
|
|
scrollback: Vec::new(),
|
|
},
|
|
);
|
|
0
|
|
}
|
|
}
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn scrollback(&self, key: &K) -> Vec<T> {
|
|
self.entries
|
|
.lock()
|
|
.ok()
|
|
.and_then(|m| m.get(key).map(|e| e.scrollback.clone()))
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn unregister(&self, key: &K) {
|
|
if let Ok(mut map) = self.entries.lock() {
|
|
map.remove(key);
|
|
}
|
|
}
|
|
|
|
pub fn detach_if(&self, key: &K, gen: u64) {
|
|
if let Ok(mut map) = self.entries.lock() {
|
|
if let Some(entry) = map.get_mut(key) {
|
|
if entry.generation == gen {
|
|
entry.sink = None;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn try_send_output(&self, key: &K, item: T) -> Result<(), OutputSinkError> {
|
|
let sink = {
|
|
let Ok(mut map) = self.entries.lock() else {
|
|
return Err(OutputSinkError::Closed);
|
|
};
|
|
let Some(entry) = map.get_mut(key) else {
|
|
return Err(OutputSinkError::Closed);
|
|
};
|
|
entry.scrollback.push(item.clone());
|
|
trim_scrollback(entry, self.max_chunks, self.max_bytes, self.measure);
|
|
entry.sink.as_ref().map(Arc::clone)
|
|
};
|
|
|
|
match sink {
|
|
Some(sink) => sink.send(item),
|
|
None => Err(OutputSinkError::Closed),
|
|
}
|
|
}
|
|
|
|
pub fn send_output(&self, key: &K, item: T) -> bool {
|
|
self.try_send_output(key, item).is_ok()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn active_sessions(&self) -> usize {
|
|
self.entries.lock().map(|m| m.len()).unwrap_or(0)
|
|
}
|
|
}
|
|
|
|
fn trim_scrollback<T>(
|
|
entry: &mut ReplayEntry<T>,
|
|
max_chunks: usize,
|
|
max_bytes: usize,
|
|
measure: fn(&T) -> usize,
|
|
) {
|
|
while !entry.scrollback.is_empty()
|
|
&& (entry.scrollback.len() > max_chunks
|
|
|| entry.scrollback.iter().map(measure).sum::<usize>() > max_bytes)
|
|
{
|
|
entry.scrollback.remove(0);
|
|
}
|
|
}
|