@@ -65,7 +65,7 @@ public Task HandleAddEvent(EventLogAction.AddEvent action, IDispatcher dispatche
6565
6666 var full = updatedBuffer . Count >= EventLogState . MaxNewEvents ;
6767
68- dispatcher . Dispatch ( new EventLogAction . AddEventBuffered ( updatedBuffer , full ) ) ;
68+ dispatcher . Dispatch ( new EventLogAction . AddEventBuffered ( updatedBuffer . AsReadOnly ( ) , full ) ) ;
6969 }
7070
7171 return Task . CompletedTask ;
@@ -151,7 +151,7 @@ public async Task HandleOpenLog(EventLogAction.OpenLog action, IDispatcher dispa
151151 List < DisplayEventModel > events = [ ] ;
152152
153153 await using Timer timer = new (
154- _ => { dispatcher . Dispatch ( new StatusBarAction . SetEventsLoading ( activityId , Volatile . Read ( ref resolved ) , failed ) ) ; } ,
154+ _ => { dispatcher . Dispatch ( new StatusBarAction . SetEventsLoading ( activityId , Volatile . Read ( ref resolved ) , Volatile . Read ( ref failed ) ) ) ; } ,
155155 null ,
156156 TimeSpan . Zero ,
157157 TimeSpan . FromSeconds ( 3 ) ) ;
@@ -200,6 +200,9 @@ await Parallel.ForEachAsync(
200200
201201 try
202202 {
203+ List < DisplayEventModel > localBatch = new ( batch . Length ) ;
204+ int localResolved = 0 ;
205+
203206 foreach ( var @event in batch )
204207 {
205208 token . ThrowIfCancellationRequested ( ) ;
@@ -215,17 +218,21 @@ await Parallel.ForEachAsync(
215218 continue ;
216219 }
217220
218- var resolvedEvent = eventResolver . ResolveEvent ( @event ) ;
219-
220- lock ( events ) { events . Add ( resolvedEvent ) ; }
221-
222- Interlocked . Increment ( ref resolved ) ;
221+ localBatch . Add ( eventResolver . ResolveEvent ( @event ) ) ;
222+ localResolved ++ ;
223223 }
224224 catch ( Exception ex )
225225 {
226226 _logger ? . Warn ( $ "Failed to resolve RecordId: { @event . RecordId } , { ex . Message } ") ;
227227 }
228228 }
229+
230+ if ( localBatch . Count > 0 )
231+ {
232+ lock ( events ) { events . AddRange ( localBatch ) ; }
233+
234+ Interlocked . Add ( ref resolved , localResolved ) ;
235+ }
229236 }
230237 finally
231238 {
@@ -237,17 +244,31 @@ await Parallel.ForEachAsync(
237244
238245 lastEvent = reader . LastBookmark ;
239246 }
240- catch ( TaskCanceledException )
247+ catch ( OperationCanceledException )
241248 {
249+ await StopProducerAsync ( producerTask ) ;
250+
242251 dispatcher . Dispatch ( new EventLogAction . CloseLog ( logData . Id , logData . Name ) ) ;
243252 dispatcher . Dispatch ( new StatusBarAction . ClearStatus ( activityId ) ) ;
244253
245254 return ;
246255 }
256+ catch ( Exception ex )
257+ {
258+ _logger ? . Error ( $ "Failed to load log { action . LogName } : { ex . Message } ") ;
259+
260+ await StopProducerAsync ( producerTask ) ;
261+
262+ dispatcher . Dispatch ( new EventLogAction . CloseLog ( logData . Id , logData . Name ) ) ;
263+ dispatcher . Dispatch ( new StatusBarAction . ClearStatus ( activityId ) ) ;
264+ dispatcher . Dispatch ( new StatusBarAction . SetResolverStatus ( $ "Error: Failed to load { action . LogName } ") ) ;
265+
266+ return ;
267+ }
247268
248269 events . Sort ( ( a , b ) => Comparer < long ? > . Default . Compare ( b . RecordId , a . RecordId ) ) ;
249270
250- dispatcher . Dispatch ( new EventLogAction . LoadEvents ( logData , events ) ) ;
271+ dispatcher . Dispatch ( new EventLogAction . LoadEvents ( logData , events . AsReadOnly ( ) ) ) ;
251272
252273 dispatcher . Dispatch ( new StatusBarAction . SetEventsLoading ( activityId , 0 , 0 ) ) ;
253274
@@ -285,7 +306,7 @@ private static EventLogData AddEventsToOneLog(EventLogData logData, List<Display
285306 {
286307 eventsToAdd . AddRange ( logData . Events ) ;
287308
288- return logData with { Events = eventsToAdd } ;
309+ return logData with { Events = eventsToAdd . AsReadOnly ( ) } ;
289310 }
290311
291312 private static ImmutableDictionary < string , EventLogData > DistributeEventsToManyLogs (
@@ -320,6 +341,17 @@ private static ImmutableDictionary<string, EventLogData> DistributeEventsToManyL
320341 return newLogs ;
321342 }
322343
344+ /// <summary>
345+ /// Awaits the producer task, suppressing all exceptions.
346+ /// The sole purpose is to ensure the producer has fully stopped before
347+ /// the reader is disposed. Any meaningful errors are handled by the caller.
348+ /// </summary>
349+ private static async Task StopProducerAsync ( Task producerTask )
350+ {
351+ try { await producerTask ; }
352+ catch { /* Intentionally swallowed — caller handles error reporting */ }
353+ }
354+
323355 private void ProcessNewEventBuffer ( EventLogState state , IDispatcher dispatcher )
324356 {
325357 var activeLogs = DistributeEventsToManyLogs ( state . ActiveLogs , state . NewEventBuffer ) ;
0 commit comments