using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using System.Collections.Concurrent; using System.Net.ServerSentEvents; using System.Runtime.CompilerServices; namespace ModelContextProtocol.AspNetCore.Tests.Utils; /// /// In-memory event store for testing resumability. /// This is a simple implementation intended for testing, not for production use. /// public sealed class TestSseEventStreamStore : ISseEventStreamStore { private readonly ConcurrentDictionary _streams = new(); private readonly ConcurrentDictionary _eventLookup = new(); private readonly List _storedEventIds = []; private readonly List _storedReconnectionIntervals = []; private readonly object _storedEventIdsLock = new(); private int _storeEventCallCount; private long _globalSequence; /// /// Gets the number of times events have been stored. /// public int StoreEventCallCount => _storeEventCallCount; /// /// Gets the list of stored event IDs in order. /// public IReadOnlyList StoredEventIds { get { lock (_storedEventIdsLock) { return [.. _storedEventIds]; } } } /// /// Gets the list of stored reconnection intervals in order. /// public IReadOnlyList StoredReconnectionIntervals { get { lock (_storedEventIdsLock) { return [.. _storedReconnectionIntervals]; } } } /// public ValueTask CreateStreamAsync(SseEventStreamOptions options, CancellationToken cancellationToken = default) { var streamKey = GetStreamKey(options.SessionId, options.StreamId); var state = new StreamState(options.SessionId, options.StreamId, options.Mode); if (!_streams.TryAdd(streamKey, state)) { throw new InvalidOperationException($"A stream with key '{streamKey}' has already been created."); } var writer = new InMemoryEventStreamWriter(this, state); return new ValueTask(writer); } /// public ValueTask GetStreamReaderAsync(string lastEventId, CancellationToken cancellationToken = default) { // Look up the event by its ID to find which stream it belongs to if (!_eventLookup.TryGetValue(lastEventId, out var lookup)) { return new ValueTask((ISseEventStreamReader?)null); } var reader = new InMemoryEventStreamReader(lookup.Stream, lookup.Sequence); return new ValueTask(reader); } private string GenerateEventId() => Interlocked.Increment(ref _globalSequence).ToString(); private void TrackEvent(string eventId, StreamState stream, long sequence, TimeSpan? reconnectionInterval = null) { _eventLookup[eventId] = (stream, sequence); lock (_storedEventIdsLock) { _storedEventIds.Add(eventId); if (reconnectionInterval.HasValue) { _storedReconnectionIntervals.Add(reconnectionInterval.Value); } } Interlocked.Increment(ref _storeEventCallCount); } private static string GetStreamKey(string sessionId, string streamId) => $"{sessionId}:{streamId}"; /// /// Holds the state for a single stream. /// private sealed class StreamState { private readonly List<(SseItem Item, long Sequence)> _events = []; private readonly object _lock = new(); private TaskCompletionSource _newEventSignal = new(TaskCreationOptions.RunContinuationsAsynchronously); private long _sequence; public StreamState(string sessionId, string streamId, SseEventStreamMode mode) { SessionId = sessionId; StreamId = streamId; Mode = mode; } public string SessionId { get; } public string StreamId { get; } public SseEventStreamMode Mode { get; set; } public bool IsCompleted { get; private set; } public long NextSequence() => Interlocked.Increment(ref _sequence); public void AddEvent(SseItem item, long sequence) { lock (_lock) { if (IsCompleted) { throw new InvalidOperationException("Cannot add events to a completed stream."); } _events.Add((item, sequence)); var oldSignal = _newEventSignal; _newEventSignal = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); oldSignal.TrySetResult(); } } public (List> Events, long LastSequence, Task NewEventSignal) GetEventsAfter(long sequence) { lock (_lock) { var result = new List>(); long lastSequence = sequence; foreach (var (item, seq) in _events) { if (seq > sequence) { result.Add(item); lastSequence = seq; } } return (result, lastSequence, _newEventSignal.Task); } } public void Complete() { lock (_lock) { IsCompleted = true; _newEventSignal.TrySetResult(); } } } private sealed class InMemoryEventStreamWriter : ISseEventStreamWriter { private readonly TestSseEventStreamStore _store; private readonly StreamState _state; private bool _disposed; public InMemoryEventStreamWriter(TestSseEventStreamStore store, StreamState state) { _store = store; _state = state; } public ValueTask SetModeAsync(SseEventStreamMode mode, CancellationToken cancellationToken = default) { _state.Mode = mode; return default; } public ValueTask> WriteEventAsync(SseItem sseItem, CancellationToken cancellationToken = default) { // Skip if already has an event ID if (sseItem.EventId is not null) { return new ValueTask>(sseItem); } var sequence = _state.NextSequence(); var eventId = _store.GenerateEventId(); var newItem = sseItem with { EventId = eventId }; _state.AddEvent(newItem, sequence); _store.TrackEvent(eventId, _state, sequence, sseItem.ReconnectionInterval); return new ValueTask>(newItem); } public ValueTask DisposeAsync() { if (_disposed) { return default; } _disposed = true; _state.Complete(); return default; } } private sealed class InMemoryEventStreamReader : ISseEventStreamReader { private readonly StreamState _state; private readonly long _startSequence; public InMemoryEventStreamReader(StreamState state, long startSequence) { _state = state; _startSequence = startSequence; } public string SessionId => _state.SessionId; public string StreamId => _state.StreamId; public async IAsyncEnumerable> ReadEventsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) { long lastSeenSequence = _startSequence; while (true) { // Get events after the last seen sequence var (events, lastSequence, newEventSignal) = _state.GetEventsAfter(lastSeenSequence); foreach (var evt in events) { yield return evt; } // Update to the sequence we actually retrieved lastSeenSequence = lastSequence; // If in polling mode, stop after returning currently available events if (_state.Mode == SseEventStreamMode.Polling) { yield break; } // If the stream is completed, stop if (_state.IsCompleted) { yield break; } // Wait for new events or cancellation await newEventSignal.WaitAsync(cancellationToken).ConfigureAwait(false); } } } }