@@ -228,6 +228,8 @@ public async Task<Search> StartAsync(Guid id, SearchQuery query, SearchScope sco
228228
229229 var rateLimiter = new RateLimiter ( 250 ) ;
230230
231+ // initialize the search record, save it to the database, and broadcast the creation
232+ // we do this so the UI has some feedback to show to the user that we've gotten their request
231233 var search = new Search ( )
232234 {
233235 SearchText = query . SearchText ,
@@ -241,6 +243,10 @@ public async Task<Search> StartAsync(Guid id, SearchQuery query, SearchScope sco
241243 context . Add ( search ) ;
242244 context . SaveChanges ( ) ;
243245
246+ await SearchHub . BroadcastCreateAsync ( search ) ;
247+
248+ // initialize the list of responses that we'll use to accumulate them
249+ // populated by the responseHandler we pass to SearchAsync
244250 List < SearchResponse > responses = new ( ) ;
245251
246252 options ??= new SearchOptions ( ) ;
@@ -266,47 +272,75 @@ public async Task<Search> StartAsync(Guid id, SearchQuery query, SearchScope sco
266272 Update ( search ) ;
267273 } ) ) ;
268274
269- var soulseekSearchTask = Client . SearchAsync (
270- query ,
271- responseHandler : ( response ) => responses . Add ( response ) ,
272- scope ,
273- token ,
274- options ,
275- cancellationToken : cancellationTokenSource . Token ) ;
276-
277- _ = Task . Run ( async ( ) =>
275+ try
278276 {
279- try
280- {
281- var soulseekSearch = await soulseekSearchTask ;
282- search = search . WithSoulseekSearch ( soulseekSearch ) ;
283- }
284- finally
277+ // initiate the search. this can throw at invocation if there's a problem with
278+ // the client state (e.g. disconnected) or a problem with the search (e.g. no terms)
279+ var soulseekSearchTask = Client . SearchAsync (
280+ query ,
281+ responseHandler : ( response ) => responses . Add ( response ) ,
282+ scope ,
283+ token ,
284+ options ,
285+ cancellationToken : cancellationTokenSource . Token ) ;
286+
287+ // seach looks ok so far; let the rest of the logic run asynchronously
288+ // on a background thread. this logic needs to clean up after itself and
289+ // update the search record to accurately reflect the final state
290+ _ = Task . Run ( async ( ) =>
285291 {
286- rateLimiter . Dispose ( ) ;
287- CancellationTokens . TryRemove ( id , out _ ) ;
288-
289292 try
290293 {
291- search . EndedAt = DateTime . UtcNow ;
292- search . Responses = responses . Select ( r => Response . FromSoulseekSearchResponse ( r ) ) ;
293-
294- Update ( search ) ;
295-
296- // zero responses before broadcasting, as we don't want to blast this
297- // data out over the SignalR socket
298- await SearchHub . BroadcastUpdateAsync ( search with { Responses = [ ] } ) ;
294+ var soulseekSearch = await soulseekSearchTask ;
295+ search = search . WithSoulseekSearch ( soulseekSearch ) ;
299296 }
300297 catch ( Exception ex )
301298 {
302- Log . Error ( ex , "Failed to persist search for {SearchQuery} ({Id})" , query , id ) ;
299+ Log . Error ( ex , "Failed to execute search {Search}: {Message}" , new { query , scope , options } , ex . Message ) ;
300+ search . State = SearchStates . Completed | SearchStates . Errored ;
303301 }
304- }
305- } ) ;
302+ finally
303+ {
304+ rateLimiter . Dispose ( ) ;
305+ CancellationTokens . TryRemove ( id , out _ ) ;
306+
307+ try
308+ {
309+ search . EndedAt = DateTime . UtcNow ;
310+ search . Responses = responses . Select ( r => Response . FromSoulseekSearchResponse ( r ) ) ;
311+
312+ Update ( search ) ;
313+
314+ // zero responses before broadcasting, as we don't want to blast this
315+ // data out over the SignalR socket
316+ await SearchHub . BroadcastUpdateAsync ( search with { Responses = [ ] } ) ;
317+ }
318+ catch ( Exception ex )
319+ {
320+ // record may be left 'hanging' and will need to be cleaned up at the next boot
321+ Log . Error ( ex , "Failed to persist search for {SearchQuery} ({Id})" , query , id ) ;
322+ }
323+ }
324+ } ) ;
306325
307- await SearchHub . BroadcastCreateAsync ( search ) ;
326+ await SearchHub . BroadcastUpdateAsync ( search ) ;
327+
328+ return search ;
329+ }
330+ catch ( Exception ex )
331+ {
332+ // we'll end up here if the initial call throws for an ArgumentException, InvalidOperationException if
333+ // the app isn't connected, and a few other straightforward issues that arise before even requesting the search
334+ Log . Error ( ex , "Failed to execute search {Search}: {Message}" , new { query , scope , options } , ex . Message ) ;
335+
336+ search . State = SearchStates . Completed | SearchStates . Errored ;
337+ search . EndedAt = search . StartedAt ;
338+ Update ( search ) ;
339+
340+ await SearchHub . BroadcastUpdateAsync ( search with { Responses = [ ] } ) ;
308341
309- return search ;
342+ throw ;
343+ }
310344 }
311345
312346 /// <summary>
0 commit comments