using System.Collections.Concurrent; using System.Threading; namespace Gott.Services; public class StreamingTracker { private readonly ConcurrentDictionary _streaming = new ConcurrentDictionary(); public StreamingTokens Start(string sessionId, string chainId) { var stopCts = new CancellationTokenSource(); var hardCts = new CancellationTokenSource(); var state = new StreamingState(chainId, stopCts, hardCts); _streaming[sessionId] = state; return new StreamingTokens(stopCts.Token, hardCts.Token); } public void Stop(string sessionId) { if (_streaming.TryRemove(sessionId, out var state)) { state.StopCts.Dispose(); state.HardCts.Dispose(); } } /// /// Cancels the running stream for a session. /// First call = graceful (laufende Iteration wird fertig). /// Second call = hard (sofortiger Abbruch aus while-Loop). /// Returns "graceful", "hard" or null if not streaming. /// public string Cancel(string sessionId) { if (_streaming.TryGetValue(sessionId, out var state)) { if (!state.StopCts.IsCancellationRequested) { // Erster Klick → graceful stop try { state.StopCts.Cancel(); } catch { } return "graceful"; } else if (!state.HardCts.IsCancellationRequested) { // Zweiter Klick → hard stop try { state.HardCts.Cancel(); } catch { } return "hard"; } return "hard"; // Bereits beides gecancelled } return null; } public bool IsStreaming(string sessionId) => _streaming.ContainsKey(sessionId); public string GetChainId(string sessionId) => _streaming.TryGetValue(sessionId, out var state) ? state.ChainId : null; public record StreamingTokens(CancellationToken StopToken, CancellationToken HardToken); private record StreamingState(string ChainId, CancellationTokenSource StopCts, CancellationTokenSource HardCts); }