2024-06-30 13:26:28 +00:00
using System.Collections.Concurrent ;
2026-08-25 16:53:09 +00:00
using AIStudio.Tools.Services ;
2024-06-30 13:26:28 +00:00
using Microsoft.AspNetCore.Components ;
// ReSharper disable RedundantRecordClassKeyword
namespace AIStudio.Tools ;
public sealed class MessageBus
{
public static readonly MessageBus INSTANCE = new ( ) ;
private readonly ConcurrentDictionary < IMessageBusReceiver , ComponentBase [ ] > componentFilters = new ( ) ;
private readonly ConcurrentDictionary < IMessageBusReceiver , Event [ ] > componentEvents = new ( ) ;
2026-08-25 16:53:09 +00:00
private readonly ConcurrentDictionary < IMessageBusReceiver , CircuitStateService > receiverCircuits = new ( ) ;
2024-08-18 10:32:18 +00:00
private readonly ConcurrentDictionary < Event , ConcurrentQueue < Message > > deferredMessages = new ( ) ;
2024-06-30 13:26:28 +00:00
private readonly ConcurrentQueue < Message > messageQueue = new ( ) ;
private readonly SemaphoreSlim sendingSemaphore = new ( 1 , 1 ) ;
2025-06-02 18:08:25 +00:00
private static ILogger < MessageBus > ? LOG ;
2024-06-30 13:26:28 +00:00
private MessageBus ( )
{
}
2025-06-02 18:08:25 +00:00
public void Initialize ( ILogger < MessageBus > logger )
{
LOG = logger ;
LOG . LogInformation ( "Message bus initialized." ) ;
}
2024-06-30 13:26:28 +00:00
2025-04-24 11:50:14 +00:00
/// <summary>
/// Define for which components and events you want to receive messages.
/// </summary>
/// <param name="receiver">That's you, the receiver.</param>
/// <param name="filterComponents">A list of components for which you want to receive messages. Use an empty list to receive messages from all components.</param>
/// <param name="events">A list of events for which you want to receive messages.</param>
2026-03-23 12:57:26 +00:00
public void ApplyFilters ( IMessageBusReceiver receiver , ComponentBase [ ] filterComponents , HashSet < Event > events )
2024-06-30 13:26:28 +00:00
{
2025-04-24 11:50:14 +00:00
this . componentFilters [ receiver ] = filterComponents ;
2026-03-23 12:57:26 +00:00
this . componentEvents [ receiver ] = events . ToArray ( ) ;
2024-06-30 13:26:28 +00:00
}
2026-08-25 16:53:09 +00:00
/// <summary>
/// Registers a receiver at the bus.
/// </summary>
/// <param name="receiver">That's you, the receiver.</param>
/// <param name="circuitState">The circuit this receiver belongs to. Components hand over their circuit
/// so the bus can let them go when that circuit ends. Services which live longer than any circuit,
/// such as hosted services, hand over nothing.</param>
public void RegisterComponent ( IMessageBusReceiver receiver , CircuitStateService ? circuitState = null )
2024-06-30 13:26:28 +00:00
{
this . componentFilters . TryAdd ( receiver , [ ] ) ;
this . componentEvents . TryAdd ( receiver , [ ] ) ;
2026-08-25 16:53:09 +00:00
if ( circuitState is not null )
this . receiverCircuits [ receiver ] = circuitState ;
2024-06-30 13:26:28 +00:00
}
2026-08-25 16:53:09 +00:00
2024-06-30 13:26:28 +00:00
public void Unregister ( IMessageBusReceiver receiver )
{
this . componentFilters . TryRemove ( receiver , out _ ) ;
this . componentEvents . TryRemove ( receiver , out _ ) ;
2026-08-25 16:53:09 +00:00
this . receiverCircuits . TryRemove ( receiver , out _ ) ;
}
/// <summary>
/// Removes all receivers which belong to one circuit.
/// </summary>
/// <remarks>
/// The circuit handler calls this when a circuit ends. Components deregister themselves when they get
/// disposed, but a circuit which was retained and then dropped does not give all of them that chance.
/// Since the bus holds a strong reference to every receiver, those leftovers would stay and would be
/// served forever.
/// </remarks>
/// <param name="circuitState">The circuit whose receivers must go.</param>
/// <returns>The number of removed receivers.</returns>
public int UnregisterCircuit ( CircuitStateService circuitState )
{
var numRemovedReceivers = 0 ;
foreach ( var ( receiver , receiverCircuit ) in this . receiverCircuits )
{
if ( ! ReferenceEquals ( receiverCircuit , circuitState ) )
continue ;
this . Unregister ( receiver ) ;
numRemovedReceivers + + ;
}
return numRemovedReceivers ;
2024-06-30 13:26:28 +00:00
}
private record class Message ( ComponentBase ? SendingComponent , Event TriggeredEvent , object? Data ) ;
public async Task SendMessage < T > ( ComponentBase ? sendingComponent , Event triggeredEvent , T ? data = default )
{
this . messageQueue . Enqueue ( new Message ( sendingComponent , triggeredEvent , data ) ) ;
2025-06-02 18:08:25 +00:00
2024-06-30 13:26:28 +00:00
try
{
await this . sendingSemaphore . WaitAsync ( ) ;
while ( this . messageQueue . TryDequeue ( out var message ) )
{
foreach ( var ( receiver , componentFilter ) in this . componentFilters )
{
2026-05-25 18:48:26 +00:00
if ( componentFilter . Length > 0 & & message . SendingComponent is not null & & ! componentFilter . Contains ( message . SendingComponent ) )
2024-06-30 13:26:28 +00:00
continue ;
var eventFilter = this . componentEvents [ receiver ] ;
2026-05-25 18:48:26 +00:00
if ( eventFilter . Length = = 0 | | eventFilter . Contains ( message . TriggeredEvent ) )
2025-06-02 18:08:25 +00:00
2024-06-30 13:26:28 +00:00
// We don't await the task here because we don't want to block the message bus:
2026-08-25 16:53:09 +00:00
_ = DeliverMessage ( receiver , message ) ;
2024-06-30 13:26:28 +00:00
}
}
}
2025-06-02 18:08:25 +00:00
catch ( Exception e )
{
LOG ? . LogError ( e , "Error while sending message." ) ;
}
2024-06-30 13:26:28 +00:00
finally
{
this . sendingSemaphore . Release ( ) ;
}
}
2025-04-04 14:52:33 +00:00
2026-08-25 16:53:09 +00:00
/// <summary>
/// Hands one message to one receiver and observes how that went.
/// </summary>
/// <remarks>
/// The bus must not wait for a receiver, since one slow receiver would hold up everybody else. Not
/// waiting is not the same as not caring, though: a receiver whose circuit is gone fails with a
/// disconnect or disposal exception, and nobody would ever see where it came from. Such a task
/// carries its fault until the finalizer reports it as an unobserved task exception — naming a task
/// type instead of the receiver and the event. This is where we give those failures a name.
/// </remarks>
/// <param name="receiver">The receiver of the message.</param>
/// <param name="message">The message to deliver.</param>
private static async Task DeliverMessage ( IMessageBusReceiver receiver , Message message )
{
try
{
await receiver . ProcessMessage ( message . SendingComponent , message . TriggeredEvent , message . Data ) ;
}
catch ( Exception exception ) when ( exception is JSDisconnectedException or ObjectDisposedException or OperationCanceledException )
{
//
// Expected whenever the browser connection of a receiver is gone: the app keeps circuits
// of reloaded or sleeping windows around, and their components still receive events.
//
LOG ? . LogDebug ( "The receiver '{ReceiverName}' did not process the event '{Event}' because its circuit was gone: {Reason}" , receiver . GetType ( ) . Name , message . TriggeredEvent , exception . Message ) ;
}
catch ( Exception exception )
{
LOG ? . LogError ( exception , "The receiver '{ReceiverName}' failed while processing the event '{Event}'." , receiver . GetType ( ) . Name , message . TriggeredEvent ) ;
}
}
2025-05-29 12:01:56 +00:00
public Task SendError ( DataErrorMessage dataErrorMessage ) = > this . SendMessage ( null , Event . SHOW_ERROR , dataErrorMessage ) ;
public Task SendWarning ( DataWarningMessage dataWarningMessage ) = > this . SendMessage ( null , Event . SHOW_WARNING , dataWarningMessage ) ;
public Task SendSuccess ( DataSuccessMessage dataSuccessMessage ) = > this . SendMessage ( null , Event . SHOW_SUCCESS , dataSuccessMessage ) ;
2025-04-04 14:52:33 +00:00
2026-08-02 19:41:12 +00:00
public Task SendInfo ( DataInfoMessage dataInfoMessage ) = > this . SendMessage ( null , Event . SHOW_INFO , dataInfoMessage ) ;
2026-08-23 19:15:37 +00:00
/// <summary>
/// Stores a message until someone asks for it, cf. TakeDeferredMessages. This is how a
/// component hands data to a component which does not exist yet, e.g. an assistant which
/// sends its result to the chat before the user gets there.
/// </summary>
/// <param name="sendingComponent">That's you, the sender.</param>
/// <param name="triggeredEvent">The event this message belongs to.</param>
/// <param name="data">The data to hand over.</param>
2024-08-18 10:32:18 +00:00
public void DeferMessage < T > ( ComponentBase ? sendingComponent , Event triggeredEvent , T ? data = default )
{
2026-08-23 19:15:37 +00:00
var queue = this . deferredMessages . GetOrAdd ( triggeredEvent , _ = > new ( ) ) ;
queue . Enqueue ( new Message ( sendingComponent , triggeredEvent , data ) ) ;
2024-08-18 10:32:18 +00:00
}
2026-08-23 19:15:37 +00:00
/// <summary>
/// Takes all deferred messages of an event out of the bus.
/// </summary>
/// <remarks>
/// This empties the queue and returns what was in it. It used to be a lazy iterator, which
/// meant that a caller stopping after the first message left the rest of the queue behind:
/// those messages were never delivered, and the data they carry — a complete chat thread, for
/// instance — stayed alive for as long as the app ran. Returning a list makes that impossible.
/// Callers who expect a single message take the last one, since that is the most recent thing
/// the user asked for.
/// </remarks>
/// <param name="triggeredEvent">The event whose messages you want.</param>
/// <returns>The deferred messages, oldest first. Empty when there are none.</returns>
public IReadOnlyList < T ? > TakeDeferredMessages < T > ( Event triggeredEvent )
2024-08-18 10:32:18 +00:00
{
2026-08-23 19:15:37 +00:00
//
// Removing the queue along with its messages is what keeps the dictionary from growing:
// otherwise, every event which ever deferred a message would keep an empty queue forever.
//
if ( ! this . deferredMessages . TryRemove ( triggeredEvent , out var queue ) )
return [ ] ;
var messages = new List < T ? > ( ) ;
while ( queue . TryDequeue ( out var message ) )
messages . Add ( message . Data is T data ? data : default ) ;
return messages ;
2024-08-18 10:32:18 +00:00
}
2024-07-13 08:37:57 +00:00
public async Task < TResult ? > SendMessageUseFirstResult < TPayload , TResult > ( ComponentBase ? sendingComponent , Event triggeredEvent , TPayload ? data = default )
{
foreach ( var ( receiver , componentFilter ) in this . componentFilters )
{
if ( componentFilter . Length > 0 & & sendingComponent is not null & & ! componentFilter . Contains ( sendingComponent ) )
continue ;
var eventFilter = this . componentEvents [ receiver ] ;
if ( eventFilter . Length = = 0 | | eventFilter . Contains ( triggeredEvent ) )
{
var result = await receiver . ProcessMessageWithResult < TPayload , TResult > ( sendingComponent , triggeredEvent , data ) ;
if ( result is not null )
return ( TResult ) result ;
}
}
return default ;
}
2024-06-30 13:26:28 +00:00
}