diff --git a/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/DesktopAgentClient.cs b/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/DesktopAgentClient.cs index 2f4f09f98..50cbcf772 100644 --- a/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/DesktopAgentClient.cs +++ b/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/DesktopAgentClient.cs @@ -25,16 +25,16 @@ namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Infrastructure; /// -/// Desktop Agent client implementation. +/// Desktop Agent client implementation for native apps. /// -public class DesktopAgentClient : IDesktopAgent +public class DesktopAgentClient : IDesktopAgent, IAsyncDisposable { private readonly IMessaging _messaging; private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; private readonly string _appId; private readonly string _instanceId; - private IChannelFactory _channelFactory; + private IChannelHandler _channelHandler; private IMetadataClient _metadataClient; private IIntentsClient _intentsClient; private IOpenClient _openClient; @@ -48,12 +48,7 @@ public class DesktopAgentClient : IDesktopAgent private readonly SemaphoreSlim _currentChannelLock = new(1, 1); - private readonly ConcurrentDictionary _userChannels = new(); - private readonly ConcurrentDictionary _appChannels = new(); - private readonly SemaphoreSlim _appChannelsLock = new(1, 1); - private readonly TaskCompletionSource _initializationTaskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - private string? _openedAppContextId; public DesktopAgentClient( IMessaging messaging, @@ -74,7 +69,7 @@ public DesktopAgentClient( _metadataClient = new MetadataClient(_appId, _instanceId, _messaging, _loggerFactory.CreateLogger()); _openClient = new OpenClient(_instanceId, _messaging, this, _loggerFactory.CreateLogger()); - _ = Task.Run(() => InitializeAsync()); + _ = Task.Run(() => InitializeAsync().ConfigureAwait(false)); } /// @@ -105,12 +100,13 @@ private async Task InitializeAsync() if (!string.IsNullOrEmpty(openedAppContextId)) { - _openedAppContextId = openedAppContextId; await GetOpenedAppContextAsync(openedAppContextId!).ConfigureAwait(false); } - _channelFactory = new ChannelFactory(_messaging, _instanceId, _openedAppContext, _loggerFactory); - _intentsClient = new IntentsClient(_messaging, _channelFactory, _instanceId, _loggerFactory); + _channelHandler = new ChannelHandler(_messaging, _instanceId, this, _openedAppContext, _loggerFactory); + _intentsClient = new IntentsClient(_messaging, _channelHandler, _instanceId, _loggerFactory); + + await _channelHandler.ConfigureChannelSelectorAsync(CancellationToken.None).ConfigureAwait(false); _initializationTaskCompletionSource.SetResult(_instanceId); } @@ -142,21 +138,21 @@ public async Task AddContextListener(string? contextType, ContextH "Checking if the app was opened via fdc3.open. OpenedAppContext exists: {IsNotNull}, current context listener's context type: {ContextType}, received app context's type via open call: {OpenedAppContextType}.", _openedAppContext != null, contextType, _openedAppContext?.Type); } - listener = await _channelFactory.CreateContextListenerAsync(handler, _currentChannel, contextType); + listener = await _channelHandler.CreateContextListenerAsync(handler, _currentChannel, contextType).ConfigureAwait(false); _contextListeners.Add( listener, async (channelId, channelType, cancellationToken) => { - await listener.SubscribeAsync(channelId, channelType, cancellationToken); - await HandleLastContextAsync(listener); + await listener.SubscribeAsync(channelId, channelType, cancellationToken).ConfigureAwait(false); + await HandleLastContextAsync(listener).ConfigureAwait(false); }); return listener; } finally { - await HandleLastContextAsync(listener); + await HandleLastContextAsync(listener).ConfigureAwait(false); _currentChannelLock.Release(); } @@ -178,7 +174,7 @@ public async Task AddIntentListener(string intent, IntentHandler(intent, handler); + var listener = await _intentsClient.AddIntentListenerAsync(intent, handler).ConfigureAwait(false); if (!_intentListeners.TryAdd(intent, listener)) { @@ -209,7 +205,7 @@ public async Task Broadcast(IContext context) throw ThrowHelper.ClientNotConnectedToUserChannel(); } - await _currentChannel.Broadcast(context); + await _currentChannel.Broadcast(context).ConfigureAwait(false); } finally { @@ -224,7 +220,7 @@ public async Task Broadcast(IContext context) public async Task CreatePrivateChannel() { await _initializationTaskCompletionSource.Task.ConfigureAwait(false); - var privateChannel = await _channelFactory.CreatePrivateChannelAsync(); + var privateChannel = await _channelHandler.CreatePrivateChannelAsync().ConfigureAwait(false); return privateChannel; } @@ -238,7 +234,7 @@ public async Task> FindInstances(IAppIdentifier app) { await _initializationTaskCompletionSource.Task.ConfigureAwait(false); - var instances = await _metadataClient.FindInstancesAsync(app); + var instances = await _metadataClient.FindInstancesAsync(app).ConfigureAwait(false); return instances; } @@ -253,7 +249,7 @@ public async Task FindIntent(string intent, IContext? context = null { await _initializationTaskCompletionSource.Task.ConfigureAwait(false); - var result = await _intentsClient.FindIntentAsync(intent, context, resultType); + var result = await _intentsClient.FindIntentAsync(intent, context, resultType).ConfigureAwait(false); return result; } @@ -267,7 +263,7 @@ public async Task> FindIntentsByContext(IContext context { await _initializationTaskCompletionSource.Task.ConfigureAwait(false); - var appIntents = await _intentsClient.FindIntentsByContextAsync(context, resultType); + var appIntents = await _intentsClient.FindIntentsByContextAsync(context, resultType).ConfigureAwait(false); return appIntents; } @@ -280,7 +276,7 @@ public async Task GetAppMetadata(IAppIdentifier app) { await _initializationTaskCompletionSource.Task.ConfigureAwait(false); - var appMetadata = await _metadataClient.GetAppMetadataAsync(app); + var appMetadata = await _metadataClient.GetAppMetadataAsync(app).ConfigureAwait(false); return appMetadata; } @@ -303,7 +299,7 @@ public async Task GetInfo() { await _initializationTaskCompletionSource.Task.ConfigureAwait(false); - var implementationMetadata = await _metadataClient.GetInfoAsync(); + var implementationMetadata = await _metadataClient.GetInfoAsync().ConfigureAwait(false); return implementationMetadata; } @@ -314,32 +310,9 @@ public async Task GetInfo() /// public async Task GetOrCreateChannel(string channelId) { - try - { - await _initializationTaskCompletionSource.Task.ConfigureAwait(false); - await _appChannelsLock.WaitAsync().ConfigureAwait(false); - - if (_appChannels.TryGetValue(channelId, out var existingChannel)) - { - return existingChannel; - } - - var channel = await _channelFactory.CreateAppChannelAsync(channelId); - - if (!_appChannels.TryAdd(channelId, channel)) - { - if (_logger.IsEnabled(LogLevel.Warning)) - { - _logger.LogWarning("Failed to add app channel to the internal collection: {ChannelId}.", channelId); - } - } - - return channel; - } - finally - { - _appChannelsLock.Release(); - } + await _initializationTaskCompletionSource.Task.ConfigureAwait(false); + var channel = await _channelHandler.CreateAppChannelAsync(channelId).ConfigureAwait(false); + return channel; } /// @@ -349,8 +322,7 @@ public async Task GetOrCreateChannel(string channelId) public async Task> GetUserChannels() { await _initializationTaskCompletionSource.Task.ConfigureAwait(false); - - var channels = await _channelFactory.GetUserChannelsAsync(); + var channels = await _channelHandler.GetUserChannelsAsync().ConfigureAwait(false); return channels; } @@ -373,12 +345,7 @@ public async Task JoinUserChannel(string channelId) await LeaveCurrentChannel().ConfigureAwait(false); } - if (!_userChannels.TryGetValue(channelId, out var channel)) - { - channel = await _channelFactory.JoinUserChannelAsync(channelId).ConfigureAwait(false); - _userChannels[channelId] = channel; - } - + var channel = await _channelHandler.JoinUserChannelAsync(channelId).ConfigureAwait(false); _currentChannel = channel; } finally @@ -422,7 +389,10 @@ public async Task LeaveCurrentChannel() contextListener.Key.Unsubscribe(); } - _currentChannel = null; + if (_currentChannel != null) + { + _currentChannel = null; + } } finally { @@ -440,7 +410,7 @@ public async Task Open(IAppIdentifier app, IContext? context = n { await _initializationTaskCompletionSource.Task.ConfigureAwait(false); - var appIdentifier = await _openClient.OpenAsync(app, context); + var appIdentifier = await _openClient.OpenAsync(app, context).ConfigureAwait(false); return appIdentifier; } @@ -455,7 +425,7 @@ public async Task RaiseIntent(string intent, IContext context { await _initializationTaskCompletionSource.Task.ConfigureAwait(false); - var intentResolution = await _intentsClient.RaiseIntentAsync(intent, context, app); + var intentResolution = await _intentsClient.RaiseIntentAsync(intent, context, app).ConfigureAwait(false); return intentResolution; } @@ -469,7 +439,7 @@ public async Task RaiseIntentForContext(IContext context, IAp { await _initializationTaskCompletionSource.Task.ConfigureAwait(false); - var intentResolution = await _intentsClient.RaiseIntentForContextAsync(context, app); + var intentResolution = await _intentsClient.RaiseIntentForContextAsync(context, app).ConfigureAwait(false); return intentResolution; } @@ -482,7 +452,7 @@ internal async ValueTask GetOpenedAppContextAsync(string openedAppContextId) { try { - _openedAppContext = await _openClient.GetOpenAppContextAsync(openedAppContextId); + _openedAppContext = await _openClient.GetOpenAppContextAsync(openedAppContextId).ConfigureAwait(false); } catch (Fdc3DesktopAgentException exception) { @@ -513,7 +483,7 @@ private async Task HandleLastContextAsync( return; } - var lastContext = await _currentChannel.GetCurrentContext(listener.ContextType); + var lastContext = await _currentChannel.GetCurrentContext(listener.ContextType).ConfigureAwait(false); if (lastContext == null) { @@ -526,6 +496,26 @@ private async Task HandleLastContextAsync( _logger.LogDebug("Invoking context handler for the last context of type: {ContextType}.", listener.ContextType ?? "null"); } - await listener.HandleContextAsync(lastContext); + await listener.HandleContextAsync(lastContext).ConfigureAwait(false); + } + + public ValueTask DisposeAsync() + { + if (_channelHandler != null) + { + return _channelHandler.DisposeAsync(); + } + + foreach (var intentListener in _intentListeners.Values) + { + intentListener.Unsubscribe(); + } + + foreach (var contextListener in _contextListeners.Keys) + { + contextListener.Unsubscribe(); + } + + return new ValueTask(); } } \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/IChannelFactory.cs b/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/IChannelHandler.cs similarity index 90% rename from src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/IChannelFactory.cs rename to src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/IChannelHandler.cs index eab12afd2..3862ce12d 100644 --- a/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/IChannelFactory.cs +++ b/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/IChannelHandler.cs @@ -21,7 +21,7 @@ namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Infrastructure; /// /// Provides methods for creating context listeners and joining channels and doing other channel operations. /// -internal interface IChannelFactory +internal interface IChannelHandler : IAsyncDisposable { /// /// Creates a context listener for the specified context type. @@ -70,4 +70,11 @@ public ValueTask> CreateContextListenerAsync( /// /// public ValueTask CreatePrivateChannelAsync(); -} + + /// + /// Sets the handler when the user choose a user channel to join to from the UI. + /// + /// + /// + public ValueTask ConfigureChannelSelectorAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/ChannelFactory.cs b/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/ChannelHandler.cs similarity index 79% rename from src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/ChannelFactory.cs rename to src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/ChannelHandler.cs index a60eafb3b..32a57e729 100644 --- a/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/ChannelFactory.cs +++ b/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/ChannelHandler.cs @@ -23,31 +23,35 @@ using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Contracts; using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Exceptions; using MorganStanley.ComposeUI.Messaging.Abstractions; +using MorganStanley.ComposeUI.Messaging.Abstractions.Exceptions; namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Infrastructure.Internal; -//TODO: Rename this class and its interface to better reflect their responsibilities -internal class ChannelFactory : IChannelFactory +internal class ChannelHandler : IChannelHandler { private readonly IMessaging _messaging; private readonly string _instanceId; + private readonly IDesktopAgent _desktopAgent; private readonly IContext? _openedAppContext; private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly JsonSerializerOptions _jsonSerializerOptions = SerializerOptionsHelper.JsonSerializerOptionsWithContextSerialization; private readonly ConcurrentDictionary _privateChannels = new(); + private IAsyncDisposable _channelSelector; - public ChannelFactory( + public ChannelHandler( IMessaging messaging, string fdc3InstanceId, + IDesktopAgent desktopAgent, IContext? openedAppContext = null, ILoggerFactory? loggerFactory = null) { _messaging = messaging; _instanceId = fdc3InstanceId; + _desktopAgent = desktopAgent; _openedAppContext = openedAppContext; _loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; - _logger = _loggerFactory.CreateLogger(); + _logger = _loggerFactory.CreateLogger(); } public async ValueTask> CreateContextListenerAsync( @@ -67,7 +71,7 @@ public async ValueTask> CreateContextListenerAsync( logger: _loggerFactory.CreateLogger>()); } - if (await currentChannel.AddContextListener(contextType, contextHandler) is ContextListener contextListener) + if (await currentChannel.AddContextListener(contextType, contextHandler).ConfigureAwait(false) is ContextListener contextListener) { _logger.LogDebug("Added context listener to channel {CurrentChannelId} for context type {ContextType}.)", currentChannel.Id, contextType ?? "null"); @@ -90,7 +94,7 @@ public async ValueTask CreateAppChannelAsync(string channelId) var response = await _messaging.InvokeJsonServiceAsync( Fdc3Topic.CreateAppChannel, request, - _jsonSerializerOptions); + _jsonSerializerOptions).ConfigureAwait(false); if (response == null) { @@ -126,7 +130,7 @@ public async ValueTask> GetUserChannelsAsync() var response = await _messaging.InvokeJsonServiceAsync( Fdc3Topic.GetUserChannels, request, - _jsonSerializerOptions); + _jsonSerializerOptions).ConfigureAwait(false); if (response == null) { @@ -183,7 +187,7 @@ public async ValueTask JoinUserChannelAsync(string channelId) var response = await _messaging.InvokeJsonServiceAsync( serviceName: Fdc3Topic.JoinUserChannel, request, - _jsonSerializerOptions); + _jsonSerializerOptions).ConfigureAwait(false); if (response == null) { @@ -209,6 +213,17 @@ public async ValueTask JoinUserChannelAsync(string channelId) displayMetadata: response.DisplayMetadata, loggerFactory: _loggerFactory); + try + { + var result = await _messaging.InvokeServiceAsync(Fdc3Topic.ChannelSelectorFromAPI(_instanceId), channelId).ConfigureAwait(false); + + _logger.LogDebug("Triggered channel selector from API for module: {InstanceId}, with {ChannelId}, and backend returned result: {Result}", _instanceId, channel.Id, result); + } + catch (Exception exception) + { + _logger.LogWarning(exception, "Failed to trigger channel selector from API for module: {InstanceId}, with {ChannelId}.", channelId, _instanceId); + } + return channel; } @@ -223,7 +238,7 @@ public async ValueTask FindChannelAsync(string channelId, ChannelType var response = await _messaging.InvokeJsonServiceAsync( Fdc3Topic.FindChannel, request, - _jsonSerializerOptions); + _jsonSerializerOptions).ConfigureAwait(false); if (response == null) { @@ -242,7 +257,7 @@ public async ValueTask FindChannelAsync(string channelId, ChannelType if (channelType == ChannelType.Private) { - return await JoinPrivateChannelAsync(channelId); + return await JoinPrivateChannelAsync(channelId).ConfigureAwait(false); } //This is only called when raising an intent, the RaiseIntent logic. @@ -265,7 +280,7 @@ public async ValueTask CreatePrivateChannelAsync() var response = await _messaging.InvokeJsonServiceAsync( Fdc3Topic.CreatePrivateChannel, request, - _jsonSerializerOptions); + _jsonSerializerOptions).ConfigureAwait(false); if (response == null) { @@ -291,6 +306,35 @@ public async ValueTask CreatePrivateChannelAsync() return channel; } + public async ValueTask ConfigureChannelSelectorAsync(CancellationToken cancellationToken = default) + { + _channelSelector = await _messaging.RegisterServiceAsync( + Fdc3Topic.ChannelSelectorFromUI(_instanceId), + ChannelSelectorFromUI, + cancellationToken).ConfigureAwait(false); + } + + private async ValueTask ChannelSelectorFromUI(string? request) + { + if (string.IsNullOrEmpty(request)) + { + return null; + } + + var joinUserChannelRequest = JsonSerializer.Deserialize(request, _jsonSerializerOptions); + + if (joinUserChannelRequest == null || string.IsNullOrEmpty(joinUserChannelRequest.ChannelId)) + { + _logger.LogDebug("Channel selector from UI received invalid request: {Request}", request); + + return null; + } + + await _desktopAgent.JoinUserChannel(joinUserChannelRequest.ChannelId).ConfigureAwait(false); + + return joinUserChannelRequest.ChannelId; + } + private async ValueTask JoinPrivateChannelAsync(string channelId) { var request = new JoinPrivateChannelRequest @@ -302,7 +346,7 @@ private async ValueTask JoinPrivateChannelAsync(string channelI var response = await _messaging.InvokeJsonServiceAsync( Fdc3Topic.JoinPrivateChannel, request, - _jsonSerializerOptions); + _jsonSerializerOptions).ConfigureAwait(false); if (response == null) { @@ -337,4 +381,17 @@ private async ValueTask JoinPrivateChannelAsync(string channelI return channel; } + + public async ValueTask DisposeAsync() + { + if (_channelSelector != null) + { + await _channelSelector.DisposeAsync(); + } + + foreach (var privateChannel in _privateChannels.Values) + { + privateChannel.Disconnect(); + } + } } diff --git a/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/IntentsClient.cs b/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/IntentsClient.cs index a5c9a2f46..1b7e494b3 100644 --- a/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/IntentsClient.cs +++ b/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/IntentsClient.cs @@ -31,7 +31,7 @@ internal class IntentsClient : IIntentsClient private static int _messageIdCounter = 0; private readonly IMessaging _messaging; - private readonly IChannelFactory _channelFactory; + private readonly IChannelHandler _channelFactory; private readonly string _instanceId; private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; @@ -39,7 +39,7 @@ internal class IntentsClient : IIntentsClient public IntentsClient( IMessaging messaging, - IChannelFactory channelFactory, + IChannelHandler channelFactory, string instanceId, ILoggerFactory? loggerFactory = null) { @@ -59,7 +59,7 @@ public async ValueTask AddIntentListenerAsync(string intent, Inten handler, _loggerFactory.CreateLogger>()); - await listener.RegisterIntentHandlerAsync(); + await listener.RegisterIntentHandlerAsync().ConfigureAwait(false); var request = new IntentListenerRequest { @@ -71,7 +71,7 @@ public async ValueTask AddIntentListenerAsync(string intent, Inten var response = await _messaging.InvokeJsonServiceAsync( Fdc3Topic.AddIntentListener, request, - _jsonSerializerOptions); + _jsonSerializerOptions).ConfigureAwait(false); if (response == null) { @@ -114,7 +114,7 @@ public async ValueTask FindIntentAsync(string intent, IContext? cont var response = await _messaging.InvokeJsonServiceAsync( Fdc3Topic.FindIntent, request, - _jsonSerializerOptions); + _jsonSerializerOptions).ConfigureAwait(false); if (response == null) { @@ -151,7 +151,7 @@ public async ValueTask> FindIntentsByContextAsync(IConte var response = await _messaging.InvokeJsonServiceAsync( Fdc3Topic.FindIntentsByContext, request, - _jsonSerializerOptions); + _jsonSerializerOptions).ConfigureAwait(false); if (response == null) { @@ -200,7 +200,7 @@ public async ValueTask RaiseIntentAsync(string intent, IConte var response = await _messaging.InvokeJsonServiceAsync( Fdc3Topic.RaiseIntent, request, - _jsonSerializerOptions); + _jsonSerializerOptions).ConfigureAwait(false); if (response == null) { @@ -258,7 +258,7 @@ public async ValueTask RaiseIntentForContextAsync(IContext co var response = await _messaging.InvokeJsonServiceAsync( Fdc3Topic.RaiseIntentForContext, request, - _jsonSerializerOptions); + _jsonSerializerOptions).ConfigureAwait(false); if (response == null) { diff --git a/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/Protocol/Channel.cs b/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/Protocol/Channel.cs index 867bd6366..36ce83d39 100644 --- a/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/Protocol/Channel.cs +++ b/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/Protocol/Channel.cs @@ -80,7 +80,7 @@ public virtual async Task AddContextListener(string? contextType, openedAppContext: _openedAppContext, logger: _loggerFactory.CreateLogger>()); - await listener.SubscribeAsync(_channelId, _channelType); + await listener.SubscribeAsync(_channelId, _channelType).ConfigureAwait(false); return listener; } @@ -100,7 +100,7 @@ public virtual async Task Broadcast(IContext context) await _messaging.PublishJsonAsync( new ChannelTopics(_channelId, _channelType).Broadcast, context, - _jsonSerializerOptions); + _jsonSerializerOptions).ConfigureAwait(false); } finally { diff --git a/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/Protocol/IntentResolution.cs b/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/Protocol/IntentResolution.cs index a90951d74..bf207572a 100644 --- a/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/Protocol/IntentResolution.cs +++ b/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/Protocol/IntentResolution.cs @@ -29,13 +29,13 @@ internal class IntentResolution : IIntentResolution { private readonly string _messageId; private readonly IMessaging _messaging; - private readonly IChannelFactory _channelFactory; + private readonly IChannelHandler _channelFactory; private readonly JsonSerializerOptions _jsonSerializerOptions = SerializerOptionsHelper.JsonSerializerOptionsWithContextSerialization; public IntentResolution( string messageId, IMessaging messaging, - IChannelFactory channelFactory, + IChannelHandler channelFactory, string intent, IAppIdentifier source, ILogger? logger = null) @@ -73,7 +73,7 @@ public IntentResolution( var response = await _messaging.InvokeJsonServiceAsync( Fdc3Topic.GetIntentResult, request, - _jsonSerializerOptions); + _jsonSerializerOptions).ConfigureAwait(false); if (response == null) { @@ -88,7 +88,7 @@ public IntentResolution( if (!string.IsNullOrEmpty(response.ChannelId) && response.ChannelType != null) { - var channel = await _channelFactory.FindChannelAsync(response.ChannelId!, response.ChannelType.Value); + var channel = await _channelFactory.FindChannelAsync(response.ChannelId!, response.ChannelType.Value).ConfigureAwait(false); return channel; } else if (!string.IsNullOrEmpty(response.Context)) diff --git a/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/Protocol/PrivateChannel.cs b/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/Protocol/PrivateChannel.cs index a8fca2423..ab31a43b1 100644 --- a/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/Protocol/PrivateChannel.cs +++ b/src/fdc3/dotnet/DesktopAgent.Client/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client/Infrastructure/Internal/Protocol/PrivateChannel.cs @@ -75,7 +75,7 @@ private async ValueTask InitializeAsync() _logger.LogDebug("Subscribing to private channel internal events for channel {ChannelId}, instance {InstanceId}, topic: {Topic}.", Id, InstanceId, _internalEventsTopic); } - _subscription = await Messaging.SubscribeAsync(_internalEventsTopic, HandleInternalEvent); + _subscription = await Messaging.SubscribeAsync(_internalEventsTopic, HandleInternalEvent).ConfigureAwait(false); var topic = _privateChannelTopics.GetContextHandlers(_isOriginalCreator); @@ -84,7 +84,7 @@ private async ValueTask InitializeAsync() _logger.LogDebug("Registering remote private channel context handlers service for channel {ChannelId}, instance {InstanceId}, service: {Service}.", Id, InstanceId, topic); } - _serviceRegistration = await Messaging.RegisterServiceAsync(topic, HandleRemoteContextListener); + _serviceRegistration = await Messaging.RegisterServiceAsync(topic, HandleRemoteContextListener).ConfigureAwait(false); _initializationTaskCompletionSource.SetResult(Id); } @@ -128,7 +128,7 @@ public override async Task AddContextListener(string? contextType, throw ThrowHelper.PrivateChannelDisconnected(Id, InstanceId); } - var listener = await base.AddContextListener(contextType, handler) as ContextListener; + var listener = await base.AddContextListener(contextType, handler).ConfigureAwait(false) as ContextListener; if (listener != null) { @@ -146,7 +146,7 @@ public override async Task AddContextListener(string? contextType, return listener; } - throw ThrowHelper.PrivatChannelSubscribeFailure(contextType, Id, InstanceId); + throw ThrowHelper.PrivateChannelSubscribeFailure(contextType, Id, InstanceId); } finally { @@ -204,19 +204,26 @@ public async void Disconnect() foreach (var listener in _contextHandlers) { - //Fire and forget - listener.Unsubscribe(); + try + { + //Fire and forget + listener.Unsubscribe(); - if (_logger.IsEnabled(LogLevel.Trace)) + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("Unsubscribed context listener on private channel {ChannelId}.", Id); + } + } + catch (Exception exception) { - _logger.LogTrace("Unsubscribed context listener on private channel {ChannelId}.", Id); + _logger.LogWarning(exception, "Error unsubscribing context listener on private channel {ChannelId}.", Id); } } var request = PrivateChannelInternalEvents.Disconnected(InstanceId); var serializedRequest = JsonSerializer.Serialize(request, _jsonSerializerOptions); - await Messaging.PublishAsync(_internalEventsTopic, serializedRequest); + await Messaging.PublishAsync(_internalEventsTopic, serializedRequest).ConfigureAwait(false); if (_logger.IsEnabled(LogLevel.Debug)) { @@ -225,6 +232,10 @@ public async void Disconnect() _onDisconnect(); } + catch (Exception exception) + { + _logger.LogError(exception, "Error during disconnecting private channel {ChannelId}, instance {InstanceId}.", Id, InstanceId); + } finally { _lock.Release(); @@ -415,7 +426,7 @@ private async ValueTask FireContextHandlerAdded(string? contextType) var serializedRequest = JsonSerializer.Serialize(request, _jsonSerializerOptions); - await Messaging.PublishAsync(_internalEventsTopic, serializedRequest); + await Messaging.PublishAsync(_internalEventsTopic, serializedRequest).ConfigureAwait(false); if (_logger.IsEnabled(LogLevel.Debug)) { @@ -458,7 +469,7 @@ private async ValueTask FireUnsubscribed(string? contextType) var serializedRequest = JsonSerializer.Serialize(request, _jsonSerializerOptions); - await Messaging.PublishAsync(_internalEventsTopic, serializedRequest); + await Messaging.PublishAsync(_internalEventsTopic, serializedRequest).ConfigureAwait(false); if (_logger.IsEnabled(LogLevel.Debug)) { diff --git a/src/fdc3/dotnet/DesktopAgent.Client/test/IntegrationTests/EndToEndTests.cs b/src/fdc3/dotnet/DesktopAgent.Client/test/IntegrationTests/EndToEndTests.cs index 7bda5af23..3a02be51a 100644 --- a/src/fdc3/dotnet/DesktopAgent.Client/test/IntegrationTests/EndToEndTests.cs +++ b/src/fdc3/dotnet/DesktopAgent.Client/test/IntegrationTests/EndToEndTests.cs @@ -11,6 +11,7 @@ using DisplayMetadata = MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Protocol.DisplayMetadata; using AppIdentifier = MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Protocol.AppIdentifier; using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.IntegrationTests.Helpers; +using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared; namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.IntegrationTests; @@ -26,6 +27,7 @@ public class EndToEndTests : IAsyncLifetime private readonly List _runningApps = []; private IDisposable _runningAppsObserver; private IDesktopAgent _desktopAgent; + public EndToEndTests() { var repoRoot = RootPathResolver.GetRepositoryRoot(); @@ -199,7 +201,9 @@ public async Task JoinUserChannel_joins_to_a_user_channel_and_registers_already_ { var resultContexts = new List(); - var module = await _moduleLoader.StartModule(new StartRequest("appId1-native", new Dictionary() { { "Fdc3InstanceId", Guid.NewGuid().ToString() } })); // This will ensure that the DesktopAgent backend knows its an FDC3 enabled module. The app broadcasts an instrument context after it joined to the fdc3.channel.1. + var instanceId = Guid.NewGuid().ToString(); + + var module = await _moduleLoader.StartModule(new StartRequest("appId1-native", new Dictionary() { { "Fdc3InstanceId", instanceId } })); // This will ensure that the DesktopAgent backend knows its an FDC3 enabled module. The app broadcasts an instrument context after it joined to the fdc3.channel.1. //We need to wait somehow for the module to finish up the broadcast await Task.Delay(2000); @@ -320,7 +324,9 @@ public async Task GetOrCreateChannel_creates_channel_and_client_able_to_broadcas resultContexts.Add(context); }); - var module = await _moduleLoader.StartModule(new StartRequest("appId1-native", new Dictionary() { { "Fdc3InstanceId", Guid.NewGuid().ToString() } })); // This will ensure that the DesktopAgent backend knows its an FDC3 enabled module. For test only + var instanceId = Guid.NewGuid().ToString(); + + var module = await _moduleLoader.StartModule(new StartRequest("appId1-native", new Dictionary() { { "Fdc3InstanceId", instanceId } })); // This will ensure that the DesktopAgent backend knows its an FDC3 enabled module. For test only //We need to wait somehow for the module to finish up the broadcast await Task.Delay(2000); @@ -331,6 +337,8 @@ public async Task GetOrCreateChannel_creates_channel_and_client_able_to_broadcas resultContexts.Should().BeEquivalentTo(new List() { new Instrument(new InstrumentID { Ticker = $"test-instrument-2" }, "test-name2") }); } + private void ChannelSelectorBehavior(string? obj) { } + [Fact] public async Task FindIntent_returns_AppIntent() { diff --git a/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/DesktopAgentClient.Tests.cs b/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/DesktopAgentClient.Tests.cs index c32da578d..f4dd10b7a 100644 --- a/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/DesktopAgentClient.Tests.cs +++ b/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/DesktopAgentClient.Tests.cs @@ -231,6 +231,7 @@ public async Task JoinUserChannel_handles_last_context_for_all_the_registered_to It.IsAny(), It.IsAny())) .Returns(new ValueTask(JsonSerializer.Serialize(new JoinUserChannelResponse { Success = true, DisplayMetadata = new DisplayMetadata() { Name = "test-channelId" } }, _jsonSerializerOptions))) + .Returns(new ValueTask("test-channelId")) .Returns(new ValueTask(JsonSerializer.Serialize(new AddContextListenerResponse { Success = true, Id = Guid.NewGuid().ToString() }, _jsonSerializerOptions))) .Returns(new ValueTask(JsonSerializer.Serialize(new Instrument(new InstrumentID { Ticker = "test-instrument" }, "test-name"), _jsonSerializerOptions))) //GetCurrentContext response after joining to the channels when iterating through the context listeners .Returns(new ValueTask(JsonSerializer.Serialize(new AddContextListenerResponse { Success = true, Id = Guid.NewGuid().ToString() }, _jsonSerializerOptions))) @@ -311,6 +312,7 @@ public async Task AddContextListener_handles_last_context_on_the_channel() var subscriptionMock = new Mock(); var messagingMock = new Mock(); + messagingMock.Setup( _ => _.SubscribeAsync( It.IsAny(), @@ -324,6 +326,7 @@ public async Task AddContextListener_handles_last_context_on_the_channel() It.IsAny(), It.IsAny())) .Returns(new ValueTask(JsonSerializer.Serialize(new JoinUserChannelResponse { Success = true, DisplayMetadata = new DisplayMetadata() { Name = "test-channelId" } }, _jsonSerializerOptions))) + .Returns(new ValueTask("test-channelId")) .Returns(new ValueTask(JsonSerializer.Serialize(new AddContextListenerResponse { Success = true, Id = Guid.NewGuid().ToString() }, _jsonSerializerOptions))) .Returns(new ValueTask(JsonSerializer.Serialize(new Instrument(new InstrumentID { Ticker = "test-instrument" }, "test-name"), _jsonSerializerOptions))) .Returns(new ValueTask(JsonSerializer.Serialize(new AddContextListenerResponse { Success = true, Id = Guid.NewGuid().ToString() }, _jsonSerializerOptions))) @@ -363,6 +366,7 @@ public async Task AddContextListener_receives_messages() It.IsAny(), It.IsAny())) .Returns(new ValueTask(JsonSerializer.Serialize(new JoinUserChannelResponse { Success = true, DisplayMetadata = new DisplayMetadata() { Name = "test-channelId" } }, _jsonSerializerOptions))) + .Returns(new ValueTask("test-channelId")) .Returns(new ValueTask(JsonSerializer.Serialize(new AddContextListenerResponse { Success = true, Id = Guid.NewGuid().ToString() }, _jsonSerializerOptions))) .Returns(new ValueTask(JsonSerializer.Serialize(new Instrument(new InstrumentID { Ticker = "test-instrument" }, "test-name"), _jsonSerializerOptions))) .Returns(new ValueTask(JsonSerializer.Serialize(new AddContextListenerResponse { Success = true, Id = Guid.NewGuid().ToString() }, _jsonSerializerOptions))) @@ -904,7 +908,7 @@ public async Task RaiseIntentForContext_returns_channel_as_IntentResolution() MessageId = Guid.NewGuid().ToString() }; - var channelFactoryMock = new Mock(); + var channelFactoryMock = new Mock(); channelFactoryMock .Setup(_ => _.FindChannelAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new Channel("test-channel-id", ChannelType.User, messagingMock.Object, It.IsAny(), null, It.IsAny(), It.IsAny())); @@ -1017,7 +1021,7 @@ public async Task RaiseIntentForContext_returns_channel_if_everything_is_defined MessageId = Guid.NewGuid().ToString() }; - var channelFactoryMock = new Mock(); + var channelFactoryMock = new Mock(); channelFactoryMock .Setup(_ => _.FindChannelAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new Channel("test-channel-id", ChannelType.App, messagingMock.Object, It.IsAny(), null, It.IsAny(), It.IsAny())); diff --git a/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/ChannelFactory.Tests.cs b/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/ChannelHandler.Tests.cs similarity index 82% rename from src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/ChannelFactory.Tests.cs rename to src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/ChannelHandler.Tests.cs index 42975bad2..e7ba9ae4f 100644 --- a/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/ChannelFactory.Tests.cs +++ b/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/ChannelHandler.Tests.cs @@ -25,7 +25,7 @@ namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests.Infrastructure.Internal; -public class ChannelFactoryTests +public class ChannelHandlerTests { private readonly JsonSerializerOptions _jsonSerializerOptions = SerializerOptionsHelper.JsonSerializerOptionsWithContextSerialization; @@ -37,11 +37,13 @@ public async Task CreateContextListenerAsync_returns_listener_from_channel_when_ var handler = new ContextHandler((ctx, _)=> { }); var expectedListener = new ContextListener("instanceId", handler, messagingMock.Object, "fdc3.instrument"); + var desktopAgentClientMock = new Mock(); + channelMock .Setup(c => c.AddContextListener("fdc3.instrument", handler)) .ReturnsAsync(expectedListener); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var result = await factory.CreateContextListenerAsync(handler, channelMock.Object, "fdc3.instrument"); @@ -53,7 +55,8 @@ public async Task CreateContextListenerAsync_creates_new_listener_when_currentCh { var messagingMock = new Mock(); var handler = new ContextHandler((ctx, _) => { }); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var result = await factory.CreateContextListenerAsync(handler, null, "fdc3.instrument"); @@ -78,7 +81,8 @@ public async Task JoinUserChannelAsync_returns_channel_when_successful() It.IsAny())) .Returns(new ValueTask(JsonSerializer.Serialize(response, _jsonSerializerOptions))); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var result = await factory.JoinUserChannelAsync("channelId"); @@ -97,7 +101,8 @@ public async Task JoinUserChannelAsync_returns_error_when_response_is_null() It.IsAny())) .Returns(new ValueTask((string?) null)); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var act = async () => await factory.JoinUserChannelAsync("channelId"); @@ -122,7 +127,8 @@ public async Task JoinUserChannelAsync_returns_error_when_response_has_error() It.IsAny())) .Returns(new ValueTask(JsonSerializer.Serialize(response, _jsonSerializerOptions))); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var act = async () => await factory.JoinUserChannelAsync("channelId"); @@ -147,7 +153,8 @@ public async Task JoinUserChannelAsync_returns_error_when_response_is_unsuccessf It.IsAny())) .Returns(new ValueTask(JsonSerializer.Serialize(response, _jsonSerializerOptions))); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var act = async() => await factory.JoinUserChannelAsync("channelId"); @@ -167,7 +174,8 @@ public async Task JoinUserChannelAsync_returns_error_when_messaging_throws() It.IsAny())) .ThrowsAsync(new InvalidOperationException("Messaging error")); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var act = async () => await factory.JoinUserChannelAsync("channelId"); @@ -192,7 +200,8 @@ public async Task CreateAppChannelAsync_returns_channel_when_successful() It.IsAny())) .Returns(new ValueTask(JsonSerializer.Serialize(response, _jsonSerializerOptions))); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var result = await factory.CreateAppChannelAsync("channelId"); @@ -210,7 +219,8 @@ public async Task CreateAppChannelAsync_returns_error_when_response_is_null() It.IsAny())) .Returns(new ValueTask((string?) null)); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var act = async () => await factory.CreateAppChannelAsync("channelId"); @@ -235,7 +245,8 @@ public async Task CreateAppChannelAsync_returns_error_when_response_has_error() It.IsAny())) .Returns(new ValueTask(JsonSerializer.Serialize(response, _jsonSerializerOptions))); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var act = async () => await factory.CreateAppChannelAsync("channelId"); @@ -260,7 +271,8 @@ public async Task CreateAppChannelAsync_returns_error_when_response_is_unsuccess It.IsAny())) .Returns(new ValueTask(JsonSerializer.Serialize(response, _jsonSerializerOptions))); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var act = async () => await factory.CreateAppChannelAsync("channelId"); @@ -280,7 +292,8 @@ public async Task CreateAppChannelAsync_returns_error_when_messaging_throws() It.IsAny())) .ThrowsAsync(new InvalidOperationException("Messaging error")); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var act = async () => await factory.CreateAppChannelAsync("channelId"); @@ -307,7 +320,8 @@ public async Task FindChannelAsync_returns_Channel_when_found() It.IsAny())) .ReturnsAsync(responseJson); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var result = await factory.FindChannelAsync("myChannel", ChannelType.User); result.Id.Should().Be("myChannel"); @@ -325,7 +339,8 @@ public async Task FindChannelAsync_throws_when_response_is_null() It.IsAny())) .ReturnsAsync((string?) null); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var act = async () => await factory.FindChannelAsync("myChannel", ChannelType.User); await act.Should().ThrowAsync() @@ -350,7 +365,8 @@ public async Task FindChannelAsync_throws_when_response_has_error() It.IsAny())) .ReturnsAsync(responseJson); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var act = async () => await factory.FindChannelAsync("myChannel", ChannelType.User); await act.Should().ThrowAsync() @@ -375,7 +391,8 @@ public async Task FindChannelAsync_throws_when_channel_not_found() It.IsAny())) .ReturnsAsync(responseJson); - var factory = new ChannelFactory(messagingMock.Object, "instanceId"); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var act = async () => await factory.FindChannelAsync("myChannel", ChannelType.User); await act.Should().ThrowAsync() @@ -386,7 +403,6 @@ await act.Should().ThrowAsync() public async Task FindChannelAsync_joins_private_channel() { var messagingMock = new Mock(); - var instanceId = "test-instance"; var channelId = "private-channel"; var findChannelResponse = new FindChannelResponse { Found = true }; @@ -400,7 +416,8 @@ public async Task FindChannelAsync_joins_private_channel() .ReturnsAsync(JsonSerializer.Serialize(findChannelResponse, _jsonSerializerOptions)) .ReturnsAsync(JsonSerializer.Serialize(joinPrivateChannelResponse, _jsonSerializerOptions)); - var factory = new ChannelFactory(messagingMock.Object, instanceId); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var result = await factory.FindChannelAsync(channelId, ChannelType.Private); @@ -425,7 +442,8 @@ public async Task FindChannelAsync_try_to_join_private_channel_but_throws_when_m .ReturnsAsync(JsonSerializer.Serialize(findChannelResponse, _jsonSerializerOptions)) .ReturnsAsync(JsonSerializer.Serialize((string?)null)); - var factory = new ChannelFactory(messagingMock.Object, instanceId); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var act = async() => await factory.FindChannelAsync(channelId, ChannelType.Private); @@ -451,7 +469,8 @@ public async Task FindChannelAsync_try_to_join_private_channel_but_throws_when_e .ReturnsAsync(JsonSerializer.Serialize(findChannelResponse, _jsonSerializerOptions)) .ReturnsAsync(JsonSerializer.Serialize(joinPrivateChannelResponse, _jsonSerializerOptions)); - var factory = new ChannelFactory(messagingMock.Object, instanceId); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var act = async () => await factory.FindChannelAsync(channelId, ChannelType.Private); @@ -477,7 +496,8 @@ public async Task FindChannelAsync_try_to_join_private_channel_but_throws_when_n .ReturnsAsync(JsonSerializer.Serialize(findChannelResponse, _jsonSerializerOptions)) .ReturnsAsync(JsonSerializer.Serialize(joinPrivateChannelResponse, _jsonSerializerOptions)); - var factory = new ChannelFactory(messagingMock.Object, instanceId); + var desktopAgentClientMock = new Mock(); + var factory = new ChannelHandler(messagingMock.Object, "instanceId", desktopAgentClientMock.Object); var act = async () => await factory.FindChannelAsync(channelId, ChannelType.Private); diff --git a/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/IntentsClient.Tests.cs b/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/IntentsClient.Tests.cs index 703638048..5ba362b7c 100644 --- a/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/IntentsClient.Tests.cs +++ b/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/IntentsClient.Tests.cs @@ -25,10 +25,7 @@ using AppMetadata = MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Protocol.AppMetadata; using AppIntent = MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Protocol.AppIntent; using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Infrastructure; -using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Protocol; -using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Infrastructure.Internal.Protocol; using Finos.Fdc3; -using IntentResolution = MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Infrastructure.Internal.Protocol.IntentResolution; using AppIdentifier = MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Protocol.AppIdentifier; namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests.Infrastructure.Internal; @@ -37,12 +34,12 @@ public class IntentsClientTests { private readonly JsonSerializerOptions _jsonSerializerOptions = SerializerOptionsHelper.JsonSerializerOptionsWithContextSerialization; private readonly Mock _messagingMock = new(); - private readonly Mock _channelFactoryMock = new(); + private readonly Mock _channelHandlerMock = new(); private readonly IntentsClient _client; public IntentsClientTests() { - _client = new IntentsClient(_messagingMock.Object, _channelFactoryMock.Object, "testInstance"); + _client = new IntentsClient(_messagingMock.Object, _channelHandlerMock.Object, "testInstance"); } [Fact] diff --git a/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/OpenClient.Tests.cs b/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/OpenClient.Tests.cs index 79ad3dbf3..5f94913e3 100644 --- a/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/OpenClient.Tests.cs +++ b/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/OpenClient.Tests.cs @@ -32,7 +32,7 @@ namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests.Infrastructure. public class OpenClientTests { private readonly Mock _messagingMock = new(); - private readonly Mock _channelFactoryMock = new(); + private readonly Mock _channelHandlerMock = new(); private readonly Mock _listenerMock = new(); private readonly Mock _desktopAgentMock = new(); private readonly JsonSerializerOptions _jsonSerializerOptions = SerializerOptionsHelper.JsonSerializerOptionsWithContextSerialization; diff --git a/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/Protocol/IntentResolution.Tests.cs b/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/Protocol/IntentResolution.Tests.cs index 3e2ff1665..9222c696e 100644 --- a/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/Protocol/IntentResolution.Tests.cs +++ b/src/fdc3/dotnet/DesktopAgent.Client/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests/Infrastructure/Internal/Protocol/IntentResolution.Tests.cs @@ -31,7 +31,7 @@ namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Tests.Infrastructure. public class IntentResolutionTests { private readonly Mock _messagingMock = new(); - private readonly Mock _channelFactoryMock = new(); + private readonly Mock _channelHandleryMock = new(); private readonly Mock> _loggerMock = new(); private readonly string _messageId = "msg-1"; private readonly string _intent = "ViewChart"; @@ -43,7 +43,7 @@ private IntentResolution CreateIntentResolution() return new IntentResolution( _messageId, _messagingMock.Object, - _channelFactoryMock.Object, + _channelHandleryMock.Object, _intent, _source, _loggerMock.Object); @@ -67,7 +67,7 @@ public async Task GetResult_returns_channel_when_channelId_and_type_are_present( It.IsAny())) .ReturnsAsync(JsonSerializer.Serialize(response, _jsonOptions)); - _channelFactoryMock + _channelHandleryMock .Setup(f => f.FindChannelAsync("ch1", ChannelType.User)) .ReturnsAsync(channelMock.Object); diff --git a/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Contracts/JoinUserChannelRequest.cs b/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Contracts/JoinUserChannelRequest.cs index 47467d245..b92c2cfbb 100644 --- a/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Contracts/JoinUserChannelRequest.cs +++ b/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Contracts/JoinUserChannelRequest.cs @@ -14,7 +14,7 @@ namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Contracts; -internal sealed class JoinUserChannelRequest +public sealed class JoinUserChannelRequest { /// /// Uniques identifier of the channel. diff --git a/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Contracts/JoinUserChannelResponse.cs b/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Contracts/JoinUserChannelResponse.cs index bdf987a8e..f70ef5731 100644 --- a/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Contracts/JoinUserChannelResponse.cs +++ b/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Contracts/JoinUserChannelResponse.cs @@ -16,7 +16,7 @@ namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Contracts; -internal sealed class JoinUserChannelResponse +public sealed class JoinUserChannelResponse { /// /// Error while executing the JoinUserChannel call. diff --git a/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Exceptions/ThrowHelper.cs b/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Exceptions/ThrowHelper.cs index d952f9908..1b94dbe2b 100644 --- a/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Exceptions/ThrowHelper.cs +++ b/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Exceptions/ThrowHelper.cs @@ -122,9 +122,12 @@ internal static Fdc3DesktopAgentException PrivateChannelCreationFailed() => internal static Fdc3DesktopAgentException PrivateChannelDisconnected(string channelId, string instanceId) => new($"Private channel is disconnected. ChannelId: {channelId}; instance id: {instanceId}."); - internal static Fdc3DesktopAgentException PrivatChannelSubscribeFailure(string? contextType, string channelId, string instanceId) => + internal static Fdc3DesktopAgentException PrivateChannelSubscribeFailure(string? contextType, string channelId, string instanceId) => new($"Private channel was not able to add context listener. ChannelId: {channelId}; instance id: {instanceId}; context type: {contextType}."); internal static Fdc3DesktopAgentException PrivateChannelJoiningFailed(string channelId) => new($"Client was not able to join to private channel. ChannelId: {channelId}."); + + internal static Fdc3DesktopAgentException MissingInstanceId(string methodName) => + new($"Fdc3InstanceId was missing before executing: {methodName}."); } \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Fdc3StartupProperties.cs b/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Fdc3StartupProperties.cs index 78dedc8e6..c9f8040a2 100644 --- a/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Fdc3StartupProperties.cs +++ b/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Fdc3StartupProperties.cs @@ -14,7 +14,10 @@ namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared; -internal class Fdc3StartupProperties +/// +/// FDC3 specific startup properties for the application/module. +/// +public class Fdc3StartupProperties { /// /// Fdc3 DesktopAgent's specific identifier for the created application instance. @@ -27,7 +30,7 @@ internal class Fdc3StartupProperties public string? ChannelId { get; init; } /// - /// This implies that the opened app was started via using the fdc3.open() call. Thi id ensures that if the app opens and it's available on the object then the opened app can request the context and handle it when its context listener is being registered for the right context type. + /// This implies that the opened app was started via using the fdc3.open() call. This id ensures that if the app opens and it's available on the object then the opened app can request the context and handle it when its context listener is being registered for the right context type. /// public string? OpenedAppContextId { get; set; } } diff --git a/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Fdc3Topic.cs b/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Fdc3Topic.cs index 1ccdbe6d5..7d664e5c1 100644 --- a/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Fdc3Topic.cs +++ b/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/Fdc3Topic.cs @@ -53,6 +53,26 @@ public static class Fdc3Topic /// public static string ResolverUIIntent => TopicRoot + "resolverUIIntent"; + /// + /// Topic for handling join user channel request from the UI (per container) + /// + /// + /// + public static string ChannelSelectorFromUI(string fdc3InstanceId) + { + return $"{TopicRoot}channelSelector/UI/{fdc3InstanceId}"; + } + + /// + /// Topic for handling join user channel request from the API (per container) + /// + /// + /// + public static string ChannelSelectorFromAPI(string fdc3InstanceId) + { + return $"{TopicRoot}channelSelector/API/{fdc3InstanceId}"; + } + //IntentListeners will be listening at this endpoint internal static string RaiseIntentResolution(string intent, string instanceId) { diff --git a/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/IModuleChannelSelector.cs b/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/IModuleChannelSelector.cs new file mode 100644 index 000000000..7b49b5aa0 --- /dev/null +++ b/src/fdc3/dotnet/DesktopAgent.Shared/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared/IModuleChannelSelector.cs @@ -0,0 +1,39 @@ +/* + * Morgan Stanley makes this available to you under the Apache License, + * Version 2.0 (the "License"). You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0. + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Unless required by applicable law or agreed + * to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared; + +/// +/// Responsible for registering endpoint to handle channel selection requests from the API. +/// +public interface IModuleChannelSelector : IAsyncDisposable +{ + /// + /// Registers a handler to process channel selection requests initiated from desktop agent clients. If the client send a leaveCurrentChannel request then the onChannelJoined callback will be invoked with null or empty string. + /// + /// + /// + /// + /// + public ValueTask RegisterChannelSelectorHandlerInitiatedFromClientsAsync(string fdc3InstanceId, Action onChannelJoined, CancellationToken cancellationToken = default); + + /// + /// Invokes the client library to handle the channel join request initiated from the UI. + /// + /// + /// + /// + /// + public ValueTask InvokeJoinUserChannelFromUIAsync(string fdc3InstanceId, string channelId, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/DependencyInjection/ServiceCollectionExtensions.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/DependencyInjection/ServiceCollectionExtensions.cs index f33de3603..5ca765461 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/DependencyInjection/ServiceCollectionExtensions.cs @@ -14,6 +14,7 @@ using MorganStanley.ComposeUI.Fdc3.DesktopAgent; using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Infrastructure.Internal; +using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared; using MorganStanley.ComposeUI.ModuleLoader; // ReSharper disable once CheckNamespace @@ -49,6 +50,7 @@ public static IServiceCollection AddFdc3DesktopAgent( serviceCollection.AddSingleton(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); return serviceCollection; } diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Fdc3DesktopAgentService.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Fdc3DesktopAgentService.cs index d47c0cc69..ffbacbdb2 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Fdc3DesktopAgentService.cs +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Fdc3DesktopAgentService.cs @@ -25,6 +25,7 @@ using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared; using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Contracts; using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Exceptions; +using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Protocol; using MorganStanley.ComposeUI.Messaging.Abstractions.Exceptions; using MorganStanley.ComposeUI.ModuleLoader; using AppChannel = MorganStanley.ComposeUI.Fdc3.DesktopAgent.Channels.AppChannel; @@ -70,6 +71,7 @@ public Fdc3DesktopAgentService( IOptions options, IResolverUICommunicator resolverUI, IUserChannelSetReader userChannelSetReader, + IChannelSelector? channelSelector = null, ILoggerFactory? loggerFactory = null) { _appDirectory = appDirectory; @@ -91,7 +93,7 @@ public Fdc3DesktopAgentService( return null; } - var userChannelSet = await _userChannelSetReader.GetUserChannelSet(); + var userChannelSet = await _userChannelSetReader.GetUserChannelSet().ConfigureAwait(false); if (!userChannelSet.TryGetValue(channelId, out var channelItem) || channelItem == null) { @@ -108,7 +110,7 @@ public Fdc3DesktopAgentService( try { - await userChannel!.Connect(); + await userChannel!.Connect().ConfigureAwait(false); return userChannel; } catch (DuplicateServiceNameException exception) @@ -147,7 +149,7 @@ public async ValueTask CreateOrJoinPrivateChannel(Func a SafeAddToPrivateChannelsDictionary(instanceId, privateChannel); - await privateChannel!.Connect(); + await privateChannel!.Connect().ConfigureAwait(false); } catch (DuplicateServiceNameException exception) { @@ -232,7 +234,7 @@ public async ValueTask AddAppChannel(Func x.Value.DisposeAsync()).ToArray(); var appChannelDisposeTasks = _appChannels.Select(x => x.Value.DisposeAsync()).ToArray(); - await SafeWaitAsync(privateChannelDisposeTasks); - await SafeWaitAsync(userChannelDisposeTasks); - await SafeWaitAsync(appChannelDisposeTasks); + await SafeWaitAsync(privateChannelDisposeTasks).ConfigureAwait(false); + await SafeWaitAsync(userChannelDisposeTasks).ConfigureAwait(false); + await SafeWaitAsync(appChannelDisposeTasks).ConfigureAwait(false); _startedLifetimeEventSubscription?.Dispose(); _stoppedLifetimeEventSubscription?.Dispose(); @@ -291,7 +293,7 @@ public async Task StopAsync(CancellationToken cancellationToken) _pendingStartRequests.Clear(); _userChannels.Clear(); _privateChannels.Clear(); - _privateChannelsByInstanceId.Clear(); + _privateChannelsByInstanceId?.Clear(); _appChannels.Clear(); lock (_contextListenerLock) @@ -321,7 +323,7 @@ public async ValueTask FindIntent(FindIntentRequest? request return FindIntentResponse.Failure(ResolveError.IntentDeliveryFailed); } - var result = await GetAppIntentsByRequest(request.Intent, contextType, request.ResultType, null); + var result = await GetAppIntentsByRequestAsync(request.Intent, contextType, request.ResultType, null).ConfigureAwait(false); return result.AppIntents.TryGetValue(request.Intent, out var appIntent) ? FindIntentResponse.Success(appIntent) @@ -335,7 +337,7 @@ public async ValueTask FindIntentsByContext(FindIn return FindIntentsByContextResponse.Failure(ResolveError.IntentDeliveryFailed); } - var result = await GetAppIntentsByRequest(contextType: contextType, resultType: request.ResultType); + var result = await GetAppIntentsByRequestAsync(contextType: contextType, resultType: request.ResultType).ConfigureAwait(false); return !result.AppIntents.Any() ? FindIntentsByContextResponse.Failure(ResolveError.NoAppsFound) @@ -357,9 +359,9 @@ public async ValueTask GetIntentResult(GetIntentResultR try { var intentResolutionTask = GetIntentResolutionResult(request); - if (await Task.WhenAny(intentResolutionTask, Task.Delay(_options.IntentResultTimeout)) == intentResolutionTask) + if (await Task.WhenAny(intentResolutionTask, Task.Delay(_options.IntentResultTimeout)).ConfigureAwait(false) == intentResolutionTask) { - var intentResolution = await intentResolutionTask; // Completed in time + var intentResolution = await intentResolutionTask.ConfigureAwait(false); // Completed in time // Use intentResolution as needed if (intentResolution == null) @@ -485,7 +487,7 @@ public async ValueTask GetUserChannels(GetUserChannelsR return GetUserChannelsResponse.Failure(ChannelError.AccessDenied); } - var result = (await _userChannelSetReader.GetUserChannelSet()).Values; + var result = (await _userChannelSetReader.GetUserChannelSet().ConfigureAwait(false)).Values; if (result == null) { return GetUserChannelsResponse.Failure(Fdc3DesktopAgentErrors.NoUserChannelSetFound); @@ -501,7 +503,7 @@ public async ValueTask GetUserChannels(GetUserChannelsR return JoinUserChannelResponse.Failed(Fdc3DesktopAgentErrors.MissingId); } - var userChannelSet = await _userChannelSetReader.GetUserChannelSet(); + var userChannelSet = await _userChannelSetReader.GetUserChannelSet().ConfigureAwait(false); if (!userChannelSet.TryGetValue(request.ChannelId, out var channelItem)) { return JoinUserChannelResponse.Failed(ChannelError.NoChannelFound); @@ -509,7 +511,7 @@ public async ValueTask GetUserChannels(GetUserChannelsR try { - await AddUserChannel(addUserChannelFactory, request.ChannelId); + await AddUserChannel(addUserChannelFactory, request.ChannelId).ConfigureAwait(false); } catch (Exception exception) { @@ -537,7 +539,7 @@ public async ValueTask GetInfo(GetInfoRequest? request) return GetInfoResponse.Failure(Fdc3DesktopAgentErrors.MissingId); } - var result = await GetAppInfo(request.AppIdentifier); + var result = await GetAppInfo(request.AppIdentifier).ConfigureAwait(false); return result; } @@ -556,7 +558,7 @@ public async ValueTask FindInstances(FindInstancesRequest try { - await _appDirectory.GetApp(request.AppIdentifier.AppId!); + await _appDirectory.GetApp(request.AppIdentifier.AppId!).ConfigureAwait(false); } catch (AppNotFoundException) { @@ -607,7 +609,7 @@ public async ValueTask GetAppMetadata(GetAppMetadataRequ try { - var app = await _appDirectory.GetApp(request.AppIdentifier.AppId); + var app = await _appDirectory.GetApp(request.AppIdentifier.AppId).ConfigureAwait(false); var appMetadata = app.ToAppMetadata(); return GetAppMetadataResponse.Success(appMetadata); } @@ -707,7 +709,7 @@ public async ValueTask GetAppMetadata(GetAppMetadataRequ try { - var fdc3App = await _appDirectory.GetApp(request.AppIdentifier!.AppId); + var fdc3App = await _appDirectory.GetApp(request.AppIdentifier!.AppId).ConfigureAwait(false); var appMetadata = fdc3App.ToAppMetadata(); var parameters = new Dictionary(); @@ -722,7 +724,7 @@ public async ValueTask GetAppMetadata(GetAppMetadataRequ parameters.Add(Fdc3StartupParameters.Fdc3ChannelId, request.ChannelId); } - var target = await StartModule(appMetadata, parameters); + var target = await StartModuleAsync(appMetadata, parameters).ConfigureAwait(false); if (!Guid.TryParse(target.InstanceId, out var targetInstanceId)) { @@ -736,11 +738,11 @@ public async ValueTask GetAppMetadata(GetAppMetadataRequ } var cancellationToken = new CancellationToken(); - var contextListenerTask = GetContextListener(targetInstanceId, contextType!, cancellationToken); - if (await Task.WhenAny(contextListenerTask, Task.Delay(_options.ListenerRegistrationTimeout, cancellationToken)) == contextListenerTask) + var contextListenerTask = GetContextListenerAsync(targetInstanceId, contextType!, cancellationToken); + if (await Task.WhenAny(contextListenerTask, Task.Delay(_options.ListenerRegistrationTimeout, cancellationToken)).ConfigureAwait(false) == contextListenerTask) { // Task completed within timeout - _ = await contextListenerTask; + _ = await contextListenerTask.ConfigureAwait(false); } else { @@ -807,7 +809,7 @@ public async ValueTask> RaiseIntentForCon _logger.LogWarning("Source app did not register its raiseable intent(s) for context: {ContextType} in the `raises` section of AppDirectory.", contextType); } - var filteredAppIntents = await GetAppIntentsByRequest(contextType: contextType, targetAppIdentifier: request.TargetAppIdentifier); + var filteredAppIntents = await GetAppIntentsByRequestAsync(contextType: contextType, targetAppIdentifier: request.TargetAppIdentifier).ConfigureAwait(false); if (filteredAppIntents.AppIntents == null || !filteredAppIntents.AppIntents.Any()) { @@ -821,7 +823,7 @@ public async ValueTask> RaiseIntentForCon if (filteredAppIntents.AppIntents.Count > 1) { using var resolverUIIntentCancellationSource = new CancellationTokenSource(TimeSpan.FromMinutes(2)); - var resolverUIIntentResponse = await _resolverUI.SendResolverUIIntentRequest(filteredAppIntents.AppIntents.Select(x => x.Value.Intent.Name), resolverUIIntentCancellationSource.Token); + var resolverUIIntentResponse = await _resolverUI.SendResolverUIIntentRequestAsync(filteredAppIntents.AppIntents.Select(x => x.Value.Intent.Name), resolverUIIntentCancellationSource.Token).ConfigureAwait(false); if (resolverUIIntentResponse == null) { @@ -885,16 +887,16 @@ public async ValueTask> RaiseIntentForCon if (appIntent.Apps.Count() == 1) { raiseIntentSpecification.TargetAppMetadata = appIntent.Apps.ElementAt(0); - return await RaiseIntentToApplication(raiseIntentSpecification); + return await RaiseIntentToApplicationAsync(raiseIntentSpecification).ConfigureAwait(false); } using var resolverUICancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(2)); - var resolverUIResult = await _resolverUI.SendResolverUIRequest(appIntent.Apps, resolverUICancellationTokenSource.Token); + var resolverUIResult = await _resolverUI.SendResolverUIRequestAsync(appIntent.Apps, resolverUICancellationTokenSource.Token).ConfigureAwait(false); if (resolverUIResult != null && resolverUIResult.Error == null) { raiseIntentSpecification.TargetAppMetadata = (AppMetadata) resolverUIResult.AppMetadata!; - return await RaiseIntentToApplication(raiseIntentSpecification); + return await RaiseIntentToApplicationAsync(raiseIntentSpecification).ConfigureAwait(false); } if (resolverUIResult?.Error != null) @@ -939,7 +941,7 @@ public async ValueTask> RaiseIntent(Raise _logger.LogWarning("Source app did not register its raiseable intent(s) for context: {ContextType} in the `raises` section of AppDirectory.", contextType); } - var intentQueryResult = await GetAppIntentsByRequest(request.Intent, contextType, targetAppIdentifier: request.TargetAppIdentifier); + var intentQueryResult = await GetAppIntentsByRequestAsync(request.Intent, contextType, targetAppIdentifier: request.TargetAppIdentifier).ConfigureAwait(false); if (intentQueryResult.Error != null) { @@ -970,17 +972,17 @@ public async ValueTask> RaiseIntent(Raise { raiseIntentSpecification.TargetAppMetadata = appIntent.Apps.ElementAt(0); - return await RaiseIntentToApplication(raiseIntentSpecification); + return await RaiseIntentToApplicationAsync(raiseIntentSpecification).ConfigureAwait(false); } var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(2)); //Resolve to one app via ResolverUI. - var result = await _resolverUI.SendResolverUIRequest(appIntent.Apps, cancellationTokenSource.Token); + var result = await _resolverUI.SendResolverUIRequestAsync(appIntent.Apps, cancellationTokenSource.Token).ConfigureAwait(false); if (result != null && result.Error == null) { raiseIntentSpecification.TargetAppMetadata = (AppMetadata) result.AppMetadata!; - return await RaiseIntentToApplication(raiseIntentSpecification); + return await RaiseIntentToApplicationAsync(raiseIntentSpecification).ConfigureAwait(false); } if (result?.Error != null) @@ -1037,9 +1039,9 @@ private static IEnumerable FilterAppIntentsByAppId(IEnumerable> RaiseIntentToApplication(RaiseIntentSpecification raiseIntentSpecification) + private async ValueTask> RaiseIntentToApplicationAsync(RaiseIntentSpecification raiseIntentSpecification) { - async Task?> GetRaiseIntentResponse(RaiseIntentSpecification raiseIntentSpecification, string messageId) + async Task?> GetRaiseIntentResponseAsync(RaiseIntentSpecification raiseIntentSpecification, string messageId) { RaiseIntentResult? response = null; @@ -1048,11 +1050,11 @@ private async ValueTask> RaiseIntentToApp if (!_raisedIntentResolutions.TryGetValue(new(raiseIntentSpecification.TargetAppMetadata.InstanceId!), out var registeredFdc3App) || !registeredFdc3App.IsIntentListenerRegistered(raiseIntentSpecification.Intent)) { - await Task.Delay(1); + await Task.Delay(1).ConfigureAwait(false); continue; } - var resolution = await GetRaiseIntentResolutionMessage(messageId, raiseIntentSpecification); + var resolution = await GetRaiseIntentResolutionMessageAsync(messageId, raiseIntentSpecification).ConfigureAwait(false); response = new() { Response = RaiseIntentResponse.Success(messageId, raiseIntentSpecification.Intent, raiseIntentSpecification.TargetAppMetadata), @@ -1071,11 +1073,11 @@ private async ValueTask> RaiseIntentToApp { var raisedIntentMessageId = StoreRaisedIntentForTarget(raiseIntentSpecification); - var responseTask = GetRaiseIntentResponse(raiseIntentSpecification, raisedIntentMessageId); - var completedTask = await Task.WhenAny(responseTask, Task.Delay(_options.ListenerRegistrationTimeout)); + var responseTask = GetRaiseIntentResponseAsync(raiseIntentSpecification, raisedIntentMessageId); + var completedTask = await Task.WhenAny(responseTask, Task.Delay(_options.ListenerRegistrationTimeout)).ConfigureAwait(false); if (completedTask == responseTask) { - var response = await responseTask; + var response = await responseTask.ConfigureAwait(false); if (response != null) { return response; @@ -1090,7 +1092,7 @@ private async ValueTask> RaiseIntentToApp try { - var module = await StartModule(raiseIntentSpecification.TargetAppMetadata); + var module = await StartModuleAsync(raiseIntentSpecification.TargetAppMetadata).ConfigureAwait(false); raiseIntentSpecification.TargetAppMetadata = module; @@ -1104,11 +1106,11 @@ private async ValueTask> RaiseIntentToApp }; } - var responseTask = GetRaiseIntentResponse(raiseIntentSpecification, raisedIntentMessageId); - var completedTask = await Task.WhenAny(responseTask, Task.Delay(_options.ListenerRegistrationTimeout)); + var responseTask = GetRaiseIntentResponseAsync(raiseIntentSpecification, raisedIntentMessageId); + var completedTask = await Task.WhenAny(responseTask, Task.Delay(_options.ListenerRegistrationTimeout)).ConfigureAwait(false); if (completedTask == responseTask) { - var response = await responseTask; + var response = await responseTask.ConfigureAwait(false); if (response != null) { @@ -1140,7 +1142,7 @@ private async ValueTask> RaiseIntentToApp } } - private async ValueTask StartModule(AppMetadata targetAppMetadata, IEnumerable>? additionalStartupParameters = null) + private async ValueTask StartModuleAsync(AppMetadata targetAppMetadata, IEnumerable>? additionalStartupParameters = null) { var startupParameters = additionalStartupParameters?.ToDictionary(x => x.Key, y => y.Value) ?? []; @@ -1170,7 +1172,7 @@ private async ValueTask StartModule(AppMetadata targetAppMetadata, }; } - var moduleInstance = await _moduleLoader.StartModule(startRequest); + var moduleInstance = await _moduleLoader.StartModule(startRequest).ConfigureAwait(false); if (moduleInstance == null) { @@ -1184,7 +1186,7 @@ private async ValueTask StartModule(AppMetadata targetAppMetadata, taskCompletionSource.TrySetException(exception); } - await taskCompletionSource.Task; + await taskCompletionSource.Task.ConfigureAwait(false); return new AppMetadata { @@ -1212,7 +1214,7 @@ private async ValueTask StartModule(AppMetadata targetAppMetadata, && resolution.ResultContext == null && resolution.ResultVoid == null)) { - await Task.Delay(100); + await Task.Delay(100).ConfigureAwait(false); } return resolution; @@ -1241,7 +1243,7 @@ private string StoreRaisedIntentForTarget(RaiseIntentSpecification raiseIntentSp } //Publishing intent resolution request to the fdc3 clients, they will receive the message and start their IntentHandler appropriately, and send a store request back to the backend. - private Task GetRaiseIntentResolutionMessage( + private Task GetRaiseIntentResolutionMessageAsync( string raisedIntentMessageId, RaiseIntentSpecification raiseIntentSpecification) { @@ -1273,7 +1275,7 @@ private string StoreRaisedIntentForTarget(RaiseIntentSpecification raiseIntentSp }); } - private async Task GetAppIntentsByRequest( + private async Task GetAppIntentsByRequestAsync( string? intent = null, string? contextType = null, string? resultType = null, @@ -1286,7 +1288,7 @@ private async Task GetAppIntentsByRequest( { try { - var filteredApps = await _intentResolver.GetMatchingAppsFromAppDirectory(intent, contextType, resultType, targetAppIdentifier?.AppId); + var filteredApps = await _intentResolver.GetMatchingAppsFromAppDirectoryAsync(intent, contextType, resultType, targetAppIdentifier?.AppId).ConfigureAwait(false); var appIntents = GetAppIntentsFromIntentMetadataCollection(filteredApps); result.AppIntents = appIntents; @@ -1305,7 +1307,7 @@ private async Task GetAppIntentsByRequest( try { - var filteredInstances = await _intentResolver.GetMatchingAppInstances(intent, contextType, resultType, targetAppIdentifier?.AppId, targetAppIdentifier?.InstanceId == null ? null : instanceId); + var filteredInstances = await _intentResolver.GetMatchingAppInstancesAsync(intent, contextType, resultType, targetAppIdentifier?.AppId, targetAppIdentifier?.InstanceId == null ? null : instanceId).ConfigureAwait(false); var appIntents = GetAppIntentsFromIntentMetadataCollection(filteredInstances); foreach (var app in appIntents) { @@ -1394,7 +1396,7 @@ private ValueTask GetAppInfo(IAppIdentifier appIdentifier) return new ValueTask(GetInfoResponse.Success(implementationMetadata)); } - private async Task GetContextListener(Guid instanceId, string contextType, CancellationToken cancellationToken = default) + private async Task GetContextListenerAsync(Guid instanceId, string contextType, CancellationToken cancellationToken = default) { while (true) { @@ -1417,7 +1419,7 @@ private async Task GetContextListener(Guid instanceId, string c } } - await Task.Delay(1, cancellationToken); + await Task.Delay(1, cancellationToken).ConfigureAwait(false); } } @@ -1474,7 +1476,7 @@ private async Task AddOrUpdateModuleAsync(IModuleInstance instance) Fdc3App fdc3App; try { - fdc3App = await _appDirectory.GetApp(instance.Manifest.Id); + fdc3App = await _appDirectory.GetApp(instance.Manifest.Id).ConfigureAwait(false); if (_pendingStartRequests.TryRemove(instance.StartRequest, out var taskCompletionSource)) { @@ -1517,7 +1519,7 @@ private async ValueTask SafeWaitAsync(IEnumerable tasks) { try { - await task; + await task.ConfigureAwait(false); } catch (Exception exception) { diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Fdc3StartupAction.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Fdc3StartupAction.cs index bdac95cc4..5a7d78a61 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Fdc3StartupAction.cs +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Fdc3StartupAction.cs @@ -50,7 +50,7 @@ public async Task InvokeAsync(StartupContext startupContext, Func next) try { var appId = (await _appDirectory.GetApp(startupContext.StartRequest.ModuleId)).AppId; - var userChannelSet = await _userChannelSetReader.GetUserChannelSet(); + var userChannelSet = await _userChannelSetReader.GetUserChannelSet().ConfigureAwait(false); var fdc3InstanceId = startupContext .StartRequest @@ -74,7 +74,7 @@ public async Task InvokeAsync(StartupContext startupContext, Func next) if (Handlers.TryGetValue(startupContext.ModuleInstance.Manifest.ModuleType, out var handler)) { - await handler.HandleAsync(startupContext, appId, fdc3InstanceId, channelId, openedAppContextId); + await handler.HandleAsync(startupContext, appId, fdc3InstanceId, channelId, openedAppContextId).ConfigureAwait(false); } } catch (AppNotFoundException exception) diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/IChannelSelector.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/IChannelSelector.cs new file mode 100644 index 000000000..666adb33d --- /dev/null +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/IChannelSelector.cs @@ -0,0 +1,28 @@ +// Morgan Stanley makes this available to you under the Apache License, +// Version 2.0 (the "License"). You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0. +// +// See the NOTICE file distributed with this work for additional information +// regarding copyright ownership. Unless required by applicable law or agreed +// to in writing, software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Protocol; + +namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent; + +/// +/// Adds ability to select user channels from the FDC3 windows/applications by showing on its UI - depends on the implementation. +/// +public interface IChannelSelector +{ + /// + /// Sends a request to the FDC3 service to retrieve the user channels set by the user. + /// + /// + /// + public Task> GetUserChannelsAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/IResolverUICommunicator.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/IResolverUICommunicator.cs index ec4fb8d38..66bdd4103 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/IResolverUICommunicator.cs +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/IResolverUICommunicator.cs @@ -25,7 +25,7 @@ public interface IResolverUICommunicator /// /// /// - public Task SendResolverUIIntentRequest(IEnumerable intents, CancellationToken cancellationToken = default); + public Task SendResolverUIIntentRequestAsync(IEnumerable intents, CancellationToken cancellationToken = default); /// /// Sends a request for the shell to show a window, aka ResolverUI, with the appropriate AppMetadata that can solve the raised intent. @@ -33,5 +33,5 @@ public interface IResolverUICommunicator /// /// /// - public Task SendResolverUIRequest(IEnumerable appMetadata, CancellationToken cancellationToken = default); + public Task SendResolverUIRequestAsync(IEnumerable appMetadata, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/Fdc3DesktopAgentMessagingService.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/Fdc3DesktopAgentMessagingService.cs index a90055d22..0e26b384c 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/Fdc3DesktopAgentMessagingService.cs +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/Fdc3DesktopAgentMessagingService.cs @@ -57,7 +57,7 @@ public Fdc3DesktopAgentMessagingService( public async ValueTask HandleAddUserChannel(string id) { - var userChannel = await _desktopAgent.AddUserChannel((channelId) => new UserChannel(channelId, _messaging, _jsonSerializerOptions, _loggerFactory.CreateLogger()), id); + var userChannel = await _desktopAgent.AddUserChannel((channelId) => new UserChannel(channelId, _messaging, _jsonSerializerOptions, _loggerFactory.CreateLogger()), id).ConfigureAwait(false); return userChannel; } @@ -72,27 +72,27 @@ public Fdc3DesktopAgentMessagingService( internal async ValueTask HandleFindIntent(FindIntentRequest? request) { var contextType = request?.Context != null ? JsonSerializer.Deserialize(request.Context, _jsonSerializerOptions)?.Type : null; - return await _desktopAgent.FindIntent(request, contextType); + return await _desktopAgent.FindIntent(request, contextType).ConfigureAwait(false); } internal async ValueTask HandleFindIntentsByContext(FindIntentsByContextRequest? request) { var contextType = request?.Context != null ? JsonSerializer.Deserialize(request.Context, _jsonSerializerOptions)?.Type : null; - return await _desktopAgent.FindIntentsByContext(request, contextType); + return await _desktopAgent.FindIntentsByContext(request, contextType).ConfigureAwait(false); } internal async ValueTask HandleRaiseIntent(RaiseIntentRequest? request) { var contextType = request?.Context != null ? JsonSerializer.Deserialize(request.Context, _jsonSerializerOptions)?.Type : null; - var result = await _desktopAgent.RaiseIntent(request, contextType!); + var result = await _desktopAgent.RaiseIntent(request, contextType!).ConfigureAwait(false); if (result.RaiseIntentResolutionMessages.Any()) { foreach (var message in result.RaiseIntentResolutionMessages) { await _messaging.PublishJsonAsync( Fdc3Topic.RaiseIntentResolution(message.Intent, message.TargetModuleInstanceId), - message.Request, _jsonSerializerOptions); + message.Request, _jsonSerializerOptions).ConfigureAwait(false); } } @@ -101,17 +101,17 @@ await _messaging.PublishJsonAsync( internal async ValueTask HandleAddIntentListener(IntentListenerRequest? request) { - return await _desktopAgent.AddIntentListener(request); + return await _desktopAgent.AddIntentListener(request).ConfigureAwait(false); } internal async ValueTask HandleStoreIntentResult(StoreIntentResultRequest? request) { - return await _desktopAgent.StoreIntentResult(request); + return await _desktopAgent.StoreIntentResult(request).ConfigureAwait(false); } internal async ValueTask HandleGetIntentResult(GetIntentResultRequest? request) { - return await _desktopAgent.GetIntentResult(request); + return await _desktopAgent.GetIntentResult(request).ConfigureAwait(false); } internal async ValueTask HandleCreatePrivateChannel(CreatePrivateChannelRequest? request) @@ -124,7 +124,7 @@ await _messaging.PublishJsonAsync( try { var privateChannelId = Guid.NewGuid().ToString(); - await _desktopAgent.CreateOrJoinPrivateChannel((channelId) => new PrivateChannel(channelId, _messaging, _jsonSerializerOptions, _loggerFactory.CreateLogger()), privateChannelId, request.InstanceId); + await _desktopAgent.CreateOrJoinPrivateChannel((channelId) => new PrivateChannel(channelId, _messaging, _jsonSerializerOptions, _loggerFactory.CreateLogger()), privateChannelId, request.InstanceId).ConfigureAwait(false); return CreatePrivateChannelResponse.Created(privateChannelId); } @@ -143,7 +143,7 @@ await _messaging.PublishJsonAsync( } try { - await _desktopAgent.CreateOrJoinPrivateChannel((channelId) => throw new Fdc3DesktopAgentException(Fdc3DesktopAgentErrors.PrivateChannelNotFound, "The private channel could not be found"), request.ChannelId, request.InstanceId); + await _desktopAgent.CreateOrJoinPrivateChannel((channelId) => throw new Fdc3DesktopAgentException(Fdc3DesktopAgentErrors.PrivateChannelNotFound, "The private channel could not be found"), request.ChannelId, request.InstanceId).ConfigureAwait(false); return JoinPrivateChannelResponse.Joined; } catch (Exception ex) @@ -160,18 +160,18 @@ await _messaging.PublishJsonAsync( return CreateAppChannelResponse.Failed(Fdc3DesktopAgentErrors.PayloadNull); } - return await _desktopAgent.AddAppChannel((channelId) => new AppChannel(channelId, _messaging, _jsonSerializerOptions, _loggerFactory.CreateLogger()), request); + return await _desktopAgent.AddAppChannel((channelId) => new AppChannel(channelId, _messaging, _jsonSerializerOptions, _loggerFactory.CreateLogger()), request).ConfigureAwait(false); } internal async ValueTask HandleGetUserChannels( GetUserChannelsRequest? request) { - return await _desktopAgent.GetUserChannels(request); + return await _desktopAgent.GetUserChannels(request).ConfigureAwait(false); } internal async ValueTask HandleGetInfo(GetInfoRequest? request) { - return await _desktopAgent.GetInfo(request); + return await _desktopAgent.GetInfo(request).ConfigureAwait(false); } internal async ValueTask HandleJoinUserChannel(JoinUserChannelRequest? request) @@ -181,27 +181,27 @@ await _messaging.PublishJsonAsync( return JoinUserChannelResponse.Failed(Fdc3DesktopAgentErrors.PayloadNull); } - return await _desktopAgent.JoinUserChannel((channelId) => new UserChannel(channelId, _messaging, _jsonSerializerOptions, _loggerFactory.CreateLogger()), request); + return await _desktopAgent.JoinUserChannel((channelId) => new UserChannel(channelId, _messaging, _jsonSerializerOptions, _loggerFactory.CreateLogger()), request).ConfigureAwait(false); } internal async ValueTask HandleFindInstances(FindInstancesRequest? request) { - return await _desktopAgent.FindInstances(request); + return await _desktopAgent.FindInstances(request).ConfigureAwait(false); } internal async ValueTask HandleGetAppMetadata(GetAppMetadataRequest? request) { - return await _desktopAgent.GetAppMetadata(request); + return await _desktopAgent.GetAppMetadata(request).ConfigureAwait(false); } internal async ValueTask HandleAddContextListener(AddContextListenerRequest? request) { - return await _desktopAgent.AddContextListener(request); + return await _desktopAgent.AddContextListener(request).ConfigureAwait(false); } internal async ValueTask HandleRemoveContextListener(RemoveContextListenerRequest? request) { - return await _desktopAgent.RemoveContextListener(request); + return await _desktopAgent.RemoveContextListener(request).ConfigureAwait(false); } internal async ValueTask HandleOpen(OpenRequest? request) @@ -211,7 +211,8 @@ await _messaging.PublishJsonAsync( { contextType = JsonObject.Parse(request.Context)?["type"]?.GetValue(); } - return await _desktopAgent.Open(request, contextType); + + return await _desktopAgent.Open(request, contextType).ConfigureAwait(false); } public async ValueTask HandleRaiseIntentForContext(RaiseIntentForContextRequest? request) @@ -223,24 +224,23 @@ await _messaging.PublishJsonAsync( var contextType = request?.Context != null ? JsonSerializer.Deserialize(request.Context, _jsonSerializerOptions)?.Type : null; - var result = await _desktopAgent.RaiseIntentForContext(request, contextType!); + var result = await _desktopAgent.RaiseIntentForContext(request, contextType!).ConfigureAwait(false); if (result.RaiseIntentResolutionMessages.Any()) { foreach (var message in result.RaiseIntentResolutionMessages) { await _messaging.PublishJsonAsync( Fdc3Topic.RaiseIntentResolution(message.Intent, message.TargetModuleInstanceId), - message.Request, _jsonSerializerOptions); + message.Request, _jsonSerializerOptions).ConfigureAwait(false); } } return result.Response; } - internal async ValueTask HandleGetOpenedAppContext( - GetOpenedAppContextRequest? request) + internal async ValueTask HandleGetOpenedAppContext(GetOpenedAppContextRequest? request) { - return await _desktopAgent.GetOpenedAppContext(request); + return await _desktopAgent.GetOpenedAppContext(request).ConfigureAwait(false); } private async ValueTask SafeWaitAsync(IEnumerable tasks) @@ -249,7 +249,7 @@ private async ValueTask SafeWaitAsync(IEnumerable tasks) { try { - await task; + await task.ConfigureAwait(false); } catch (Exception exception) { diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/IntentResolver.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/IntentResolver.cs index 5ce0a3c62..297033cb4 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/IntentResolver.cs +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/IntentResolver.cs @@ -31,7 +31,7 @@ public IntentResolver(IAppDirectory appDirectory, ConcurrentDictionary> GetMatchingAppsFromAppDirectory(string? intent = null, string? contextType = null, string? resultType = null, string? appIdentifier = null) + public async Task> GetMatchingAppsFromAppDirectoryAsync(string? intent = null, string? contextType = null, string? resultType = null, string? appIdentifier = null) { IEnumerable appIntents; @@ -67,7 +67,7 @@ public async Task> GetMatchingAppsFromAppDirectory(st return appIntents; } - public async Task> GetMatchingAppInstances( + public async Task> GetMatchingAppInstancesAsync( string? intent = null, string? contextType = null, string? resultType = null, @@ -77,7 +77,7 @@ public async Task> GetMatchingAppInstances( { if (instanceId != null) { - return await MatchSpecificInstance(instanceId.Value, intent, contextType, resultType, appIdentifier); + return await MatchSpecificInstance(instanceId.Value, intent, contextType, resultType, appIdentifier).ConfigureAwait(false); } var apps = Enumerable.Empty(); @@ -160,7 +160,7 @@ private async Task> MatchSpecificInstance( { try { - _ = await _appDirectory.GetApp(appIdentifier); + _ = await _appDirectory.GetApp(appIdentifier).ConfigureAwait(false); throw ThrowHelper.NoAppsFound(); } catch (AppNotFoundException) diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/ResolverUICommunicator.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/ResolverUICommunicator.cs index d7b1c77fd..a6264b8c4 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/ResolverUICommunicator.cs +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/ResolverUICommunicator.cs @@ -39,11 +39,11 @@ public ResolverUICommunicator( _logger = logger ?? NullLogger.Instance; } - public async Task SendResolverUIRequest(IEnumerable appMetadata, CancellationToken cancellationToken = default) + public async Task SendResolverUIRequestAsync(IEnumerable appMetadata, CancellationToken cancellationToken = default) { try { - return await SendResolverUIRequestCore(appMetadata, cancellationToken); + return await SendResolverUIRequestCore(appMetadata, cancellationToken).ConfigureAwait(false); } catch (TimeoutException ex) { @@ -70,18 +70,18 @@ public ResolverUICommunicator( Fdc3Topic.ResolverUI, request, _jsonSerializerOptions, - cancellationToken); + cancellationToken).ConfigureAwait(false); return response; } - public async Task SendResolverUIIntentRequest(IEnumerable intents, CancellationToken cancellationToken = default) + public async Task SendResolverUIIntentRequestAsync(IEnumerable intents, CancellationToken cancellationToken = default) { //TODO: use the same ResolverUI try { - return await SendResolverUIIntentRequestCore(intents, cancellationToken); + return await SendResolverUIIntentRequestCore(intents, cancellationToken).ConfigureAwait(false); } catch (TimeoutException ex) { @@ -106,8 +106,9 @@ public ResolverUICommunicator( var response = await _messaging.InvokeJsonServiceAsync( Fdc3Topic.ResolverUIIntent, - request, _jsonSerializerOptions, - cancellationToken: cancellationToken); + request, + _jsonSerializerOptions, + cancellationToken: cancellationToken).ConfigureAwait(false); return response; } diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/UserChannelSetReader.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/UserChannelSetReader.cs index 86599f84f..53c1cd0d9 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/UserChannelSetReader.cs +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/Infrastructure/Internal/UserChannelSetReader.cs @@ -67,7 +67,7 @@ public async Task> GetUserChannelSet(Ca using var stream = assembly.GetManifestResourceStream(ResourceNames.DefaultUserChannelSet); if (stream != null) { - var userChannels = await JsonSerializer.DeserializeAsync(stream, _jsonSerializerOptions); + var userChannels = await JsonSerializer.DeserializeAsync(stream, _jsonSerializerOptions).ConfigureAwait(false); _userChannelSet = userChannels?.ToDictionary(x => x.Id, y => y); } } @@ -84,14 +84,14 @@ public async Task> GetUserChannelSet(Ca if (_fileSystem.File.Exists(path)) { using var stream = _fileSystem.File.OpenRead(path); - _userChannelSet = (await JsonSerializer.DeserializeAsync(stream, _jsonSerializerOptions))?.ToDictionary(x => x.Id, y => y); + _userChannelSet = (await JsonSerializer.DeserializeAsync(stream, _jsonSerializerOptions).ConfigureAwait(false))?.ToDictionary(x => x.Id, y => y); } } else if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) { - var response = await _httpClient.GetAsync(uri, cancellationToken); - using var stream = await response.Content.ReadAsStreamAsync(); - _userChannelSet = (await JsonSerializer.DeserializeAsync(stream, _jsonSerializerOptions))?.ToDictionary(x => x.Id, y => y); + var response = await _httpClient.GetAsync(uri, cancellationToken).ConfigureAwait(false); + using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + _userChannelSet = (await JsonSerializer.DeserializeAsync(stream, _jsonSerializerOptions).ConfigureAwait(false))?.ToDictionary(x => x.Id, y => y); } } diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/ModuleChannelSelector.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/ModuleChannelSelector.cs new file mode 100644 index 000000000..05059b887 --- /dev/null +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/ModuleChannelSelector.cs @@ -0,0 +1,84 @@ +/* + * Morgan Stanley makes this available to you under the Apache License, + * Version 2.0 (the "License"). You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0. + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Unless required by applicable law or agreed + * to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared; +using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Contracts; +using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Exceptions; +using MorganStanley.ComposeUI.Messaging.Abstractions; + +namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent; + +internal class ModuleChannelSelector : IModuleChannelSelector +{ + private readonly ILogger _logger; + private readonly IMessaging _messaging; + private IAsyncDisposable? _handler; + private readonly JsonSerializerOptions _jsonSerializerOptions = SerializerOptionsHelper.JsonSerializerOptionsWithContextSerialization; + + public ModuleChannelSelector( + IMessaging messaging, + ILogger? logger = null) + { + _messaging = messaging; + _logger = logger ?? NullLogger.Instance; + } + + public async ValueTask RegisterChannelSelectorHandlerInitiatedFromClientsAsync( + string fdc3InstanceId, + Action onChannelJoined, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(fdc3InstanceId)) + { + throw ThrowHelper.MissingInstanceId(nameof(RegisterChannelSelectorHandlerInitiatedFromClientsAsync)); + } + + _handler = await _messaging.RegisterServiceAsync( + Fdc3Topic.ChannelSelectorFromAPI(fdc3InstanceId), + (channelId) => + { + _logger.LogDebug("Request for instance: {InstanceId} was received with content: {ChannelId}", fdc3InstanceId, channelId); + + onChannelJoined(channelId); + + return new ValueTask(channelId); + }, + cancellationToken).ConfigureAwait(false); + } + + public async ValueTask InvokeJoinUserChannelFromUIAsync(string fdc3InstanceId, string channelId, CancellationToken cancellationToken = default) + { + var request = new JoinUserChannelRequest + { + InstanceId = fdc3InstanceId, + ChannelId = channelId + }; + + var result = await _messaging.InvokeServiceAsync(Fdc3Topic.ChannelSelectorFromUI(fdc3InstanceId), JsonSerializer.Serialize(request, _jsonSerializerOptions), cancellationToken).ConfigureAwait(false); + return result; + } + + public ValueTask DisposeAsync() + { + if (_handler != null) + { + return _handler.DisposeAsync(); + } + + return new ValueTask(); + } +} \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/README.md b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/README.md index a3f533276..d540e053f 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/README.md +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.Fdc3.DesktopAgent/README.md @@ -79,6 +79,7 @@ All options can be set via the `Fdc3DesktopAgentOptions` class or in your config - [MorganStanley.ComposeUI.Messaging.Abstractions](https://www.nuget.org/packages/MorganStanley.ComposeUI.Messaging.Abstractions) - [MorganStanley.ComposeUI.ModuleLoader.Abstractions](https://www.nuget.org/packages/MorganStanley.ComposeUI.ModuleLoader.Abstractions) - [MorganStanley.ComposeUI.Utilities] (https://www.nuget.org/packages/MorganStanley.ComposeUI.Utilities) +- [MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared] (https://www.nuget.org/packages/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared) - [Finos.Fdc3](https://www.nuget.org/packages/Finos.Fdc3) - [Finos.Fdc3.AppDirectory](https://www.nuget.org/packages/Finos.Fdc3.AppDirectory) - [Microsoft.Bcl.AsyncInterfaces](https://www.nuget.org/packages/Microsoft.Bcl.AsyncInterfaces) @@ -115,6 +116,68 @@ services.AddFdc3DesktopAgent(); These dependencies are required for the Desktop Agent to function correctly and enable features such as app discovery, intent resolution, and inter-application messaging. +### Channel Selector Integration + +## Channel Selector Integration +Our TypeScript and C# FDC3 clients let a module’s container expose a user-channel selector. On startup, each client subscribes to `ComposeUI/fdc3/${fdc3Version}/channelSelector/UI/${fdc3InstanceIdPerModule}`. This allows the client library to receive channel-selection updates from the UI that handles user channel joining logic. The UI should publish updates to that topic, for example: +```csharp +if (string.IsNullOrEmpty(_fdc3InstanceId)) +{ + return; +} + +JoinUserChannelRequest? request = null; + +if (sender is MyHeaderToolItem item) +{ + var channelId = item.Header as string; + + if (string.IsNullOrEmpty(channelId)) + { + return; + } + + var result = await _moduleChannelSelector.InvokeJoinUserChannelFromUIAsync(_fdc3InstanceId, channelId); + + if (string.IsNullOrEmpty(result)) + { + return; + } +} +``` + +When an update arrives on that topic, the client libraries are calling `fdc3.joinUserChannel(channelId)` and wires up the top-level context listeners for the selected channel. +The UI must also subscribe to `ComposeUI/fdc3/${fdc3Version}/channelSelector/API/${fdc3InstanceIdPerModule}` (`Fdc3Topic.ChannelSelectorFromAPI(fdc3InstanceIdPerModule)`) to reflect changes when `fdc3.joinUserChannel` is invoked by the client libraries. + +To support this, the module’s container should expose an endpoint that receives these API-originated updates and updates the UI to show the current channel. For that each FDC3 enabled module should ensure the module’s container resolves `IModuleChannelSelector` to register the handler for API updates. + +Example usage: +```csharp +await _moduleChannelSelector.RegisterChannelSelectorHandlerInitiatedFromClientsAsync(_fdc3InstanceId, + (channelId) => + { + var channel = _userChannels.FirstOrDefault(c => c.Id == channelId); + if (channel != null) + { + _logger.Debug($"Instance {_fdc3InstanceId} joined channel {channelId} via Channel Selector API."); + + Application.Current.Dispatcher.Invoke(() => + { + if (myContainer.Header is { Content: Border { Child: TextBlock textBlock } }) + { + textBlock.ToolTip = $"Current Channel: {channel.Id}"; + var drawingColor = System.Drawing.Color.FromName(channel.DisplayMetadata.Color); + var mediaColor = System.Windows.Media.Color.FromArgb(drawingColor.A, drawingColor.R, drawingColor.G, drawingColor.B); + var brush = new SolidColorBrush(mediaColor); + textBlock.Background = brush; + textBlock.Width = 30; + textBlock.Margin = new Thickness(5, 0, 5, 0); + } + }); + } +}).ConfigureAwait(false); +``` + ## License This project is licensed under the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0). diff --git a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/Fdc3DesktopAgentServiceTestsBase.cs b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/Fdc3DesktopAgentServiceTestsBase.cs index 59bdc77d7..d116cd77a 100644 --- a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/Fdc3DesktopAgentServiceTestsBase.cs +++ b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/Fdc3DesktopAgentServiceTestsBase.cs @@ -33,6 +33,7 @@ public abstract class Fdc3DesktopAgentServiceTestsBase : IAsyncLifetime protected Mock ResolverUICommunicator { get; } = new(); internal Mock> Logger { get; } = new(); protected Mock LoggerFactory { get; } = new(); + protected Mock ChannelSelector { get; } = new(); private readonly ConcurrentDictionary _modules = new(); private IDisposable? _disposable; @@ -65,6 +66,7 @@ public Fdc3DesktopAgentServiceTestsBase(string appDirectorySource) options, ResolverUICommunicator.Object, new UserChannelSetReader(options), + ChannelSelector.Object, LoggerFactory.Object); } diff --git a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/Infrastructure/Internal/Fdc3DesktopAgentMessagingServiceTests.cs b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/Infrastructure/Internal/Fdc3DesktopAgentMessagingServiceTests.cs index 18a51a9d1..fff44362b 100644 --- a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/Infrastructure/Internal/Fdc3DesktopAgentMessagingServiceTests.cs +++ b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/Infrastructure/Internal/Fdc3DesktopAgentMessagingServiceTests.cs @@ -54,10 +54,11 @@ public partial class Fdc3DesktopAgentMessagingServiceTests : IAsyncLifetime private readonly MockModuleLoader _mockModuleLoader = new(); private readonly ConcurrentDictionary _modules = new(); private IDisposable? _disposable; + private readonly Mock _mockChannelSelector; public Fdc3DesktopAgentMessagingServiceTests() { - + _mockChannelSelector = new Mock(); _mockMessaging.Setup(x => x.RegisterServiceAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns(() => ValueTask.FromResult(new Mock().Object)); var options = new Fdc3DesktopAgentOptions() @@ -74,6 +75,7 @@ public Fdc3DesktopAgentMessagingServiceTests() options, _mockResolverUICommunicator.Object, new UserChannelSetReader(options), + _mockChannelSelector.Object, NullLoggerFactory.Instance), new Fdc3DesktopAgentOptions(), NullLoggerFactory.Instance); @@ -303,7 +305,7 @@ public async Task HandleRaiseIntent_calls_ResolverUI_by_Context_filter() }; var result = await _fdc3.HandleRaiseIntent(raiseIntentRequest); - _mockResolverUICommunicator.Verify(_ => _.SendResolverUIRequest(It.IsAny>(), It.IsAny())); + _mockResolverUICommunicator.Verify(_ => _.SendResolverUIRequestAsync(It.IsAny>(), It.IsAny())); } [Fact] @@ -322,7 +324,7 @@ public async Task HandleRaiseIntent_calls_ResolverUI_by_Context_filter_if_fdc3_n }; var result = await _fdc3.HandleRaiseIntent(raiseIntentRequest); - _mockResolverUICommunicator.Verify(_ => _.SendResolverUIRequest(It.IsAny>(), It.IsAny())); + _mockResolverUICommunicator.Verify(_ => _.SendResolverUIRequestAsync(It.IsAny>(), It.IsAny())); } [Fact] diff --git a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/Infrastructure/Internal/ResolverUICommunicatorTests.cs b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/Infrastructure/Internal/ResolverUICommunicatorTests.cs index 12a841adb..28b207d4d 100644 --- a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/Infrastructure/Internal/ResolverUICommunicatorTests.cs +++ b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/Infrastructure/Internal/ResolverUICommunicatorTests.cs @@ -42,7 +42,7 @@ public async Task SendResolverUIRequest_will_return_null() var resolverUICommunicator = new ResolverUICommunicator(messagingMock.Object, null); - var response = await resolverUICommunicator.SendResolverUIRequest(It.IsAny>()); + var response = await resolverUICommunicator.SendResolverUIRequestAsync(It.IsAny>()); response.Should().BeNull(); } @@ -64,7 +64,7 @@ public async Task SendResolverUIRequest_will_return_response() var resolverUICommunicator = new ResolverUICommunicator(messagingMock.Object, null); - var response = await resolverUICommunicator.SendResolverUIRequest(It.IsAny>()); + var response = await resolverUICommunicator.SendResolverUIRequestAsync(It.IsAny>()); response.Should().NotBeNull(); response!.AppMetadata.Should().NotBeNull(); diff --git a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/ModuleChannelSelectorTests.cs b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/ModuleChannelSelectorTests.cs new file mode 100644 index 000000000..1b89a9b3c --- /dev/null +++ b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/ModuleChannelSelectorTests.cs @@ -0,0 +1,92 @@ +/* + * Morgan Stanley makes this available to you under the Apache License, + * Version 2.0 (the "License"). You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0. + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Unless required by applicable law or agreed + * to in writing, software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared; +using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Exceptions; +using MorganStanley.ComposeUI.Messaging.Abstractions; +using ServiceHandler = MorganStanley.ComposeUI.Messaging.Abstractions.ServiceHandler; + +namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests; + +public class ModuleChannelSelectorTests +{ + private readonly Mock _mockMessaging = new(); + + [Fact] + public async Task RegisterChannelSelectorHandlerInitiatedFromClientsAsync_registers_service() + { + var moduleChannelSelector = new ModuleChannelSelector(_mockMessaging.Object); + var fdc3InstanceId = "test-instance-id"; + + Action onChannelJoined = (channelId) => { }; + + _mockMessaging + .Setup(m => m.RegisterServiceAsync( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(Mock.Of()); + + await moduleChannelSelector.RegisterChannelSelectorHandlerInitiatedFromClientsAsync( + fdc3InstanceId, + onChannelJoined); + + _mockMessaging.Verify(m => m.RegisterServiceAsync( + It.Is(s => s == Fdc3Topic.ChannelSelectorFromAPI(fdc3InstanceId)), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task RegisterChannelSelectorHandlerInitiatedFromClientsAsync_throws_exception_when_instanceId_is_null_or_empty() + { + var moduleChannelSelector = new ModuleChannelSelector(_mockMessaging.Object); + Action onChannelJoined = (channelId) => { }; + + var action1 = async() => await moduleChannelSelector.RegisterChannelSelectorHandlerInitiatedFromClientsAsync( + null!, + onChannelJoined); + + await action1.Should().ThrowAsync(); + + var action2 = async() => await moduleChannelSelector.RegisterChannelSelectorHandlerInitiatedFromClientsAsync( + string.Empty, + onChannelJoined); + + await action2.Should().ThrowAsync(); + } + + [Fact] + public async Task InvokeJoinUserChannelFromUIAsync_invokes_service_and_returns_channelId() + { + var moduleChannelSelector = new ModuleChannelSelector(_mockMessaging.Object); + var fdc3InstanceId = "test-instance-id"; + var channelId = "test-channel-id"; + + _mockMessaging + .Setup(m => m.InvokeServiceAsync( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(channelId); + + var result = await moduleChannelSelector.InvokeJoinUserChannelFromUIAsync( + fdc3InstanceId, + channelId); + + result.Should().NotBeNull(); + result.Should().Be(channelId); + } +} diff --git a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/RaiseIntentForContextTests.cs b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/RaiseIntentForContextTests.cs index eff4ecc40..89ef059dd 100644 --- a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/RaiseIntentForContextTests.cs +++ b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/RaiseIntentForContextTests.cs @@ -52,7 +52,7 @@ public async Task RaiseIntentForContext_with_multiple_possibilities_calls_Resolv var originFdc3InstanceId = Fdc3InstanceIdRetriever.GetFdc3InstanceId(origin); ResolverUICommunicator - .Setup(x => x.SendResolverUIIntentRequest(It.IsAny>(), It.IsAny())) + .Setup(x => x.SendResolverUIIntentRequestAsync(It.IsAny>(), It.IsAny())) .ReturnsAsync(new ResolverUIIntentResponse() { SelectedIntent = Intent2.Name @@ -66,8 +66,8 @@ public async Task RaiseIntentForContext_with_multiple_possibilities_calls_Resolv var result = await Fdc3.RaiseIntentForContext(request, MultipleContext.Type); - ResolverUICommunicator.Verify(_ => _.SendResolverUIIntentRequest(It.IsAny>(), It.IsAny())); - ResolverUICommunicator.Verify(_ => _.SendResolverUIRequest(It.IsAny>(), It.IsAny())); + ResolverUICommunicator.Verify(_ => _.SendResolverUIIntentRequestAsync(It.IsAny>(), It.IsAny())); + ResolverUICommunicator.Verify(_ => _.SendResolverUIRequestAsync(It.IsAny>(), It.IsAny())); ResolverUICommunicator.VerifyNoOtherCalls(); } @@ -78,7 +78,7 @@ public async Task RaiseIntentForContext_with_single_intent_but_multiple_apps_cal var originFdc3InstanceId = Fdc3InstanceIdRetriever.GetFdc3InstanceId(origin); ResolverUICommunicator - .Setup(x => x.SendResolverUIIntentRequest(It.IsAny>(), It.IsAny())) + .Setup(x => x.SendResolverUIIntentRequestAsync(It.IsAny>(), It.IsAny())) .Returns(Task.FromResult(new ResolverUIIntentResponse() { SelectedIntent = IntentWithNoResult.Name @@ -92,8 +92,8 @@ public async Task RaiseIntentForContext_with_single_intent_but_multiple_apps_cal var result = await Fdc3.RaiseIntentForContext(request, ContextTypes.Nothing); - ResolverUICommunicator.Verify(_ => _.SendResolverUIIntentRequest(It.IsAny>(), It.IsAny())); - ResolverUICommunicator.Verify(_ => _.SendResolverUIRequest(It.IsAny>(), It.IsAny())); + ResolverUICommunicator.Verify(_ => _.SendResolverUIIntentRequestAsync(It.IsAny>(), It.IsAny())); + ResolverUICommunicator.Verify(_ => _.SendResolverUIRequestAsync(It.IsAny>(), It.IsAny())); ResolverUICommunicator.VerifyNoOtherCalls(); } @@ -104,7 +104,7 @@ public async Task RaiseIntentForContext_with_multiple_intents_but_single_app_cal var originFdc3InstanceId = Fdc3InstanceIdRetriever.GetFdc3InstanceId(origin); ResolverUICommunicator - .Setup(x => x.SendResolverUIIntentRequest(It.IsAny>(), It.IsAny())) + .Setup(x => x.SendResolverUIIntentRequestAsync(It.IsAny>(), It.IsAny())) .ReturnsAsync(new ResolverUIIntentResponse() { SelectedIntent = IntentWithNoResult.Name @@ -118,7 +118,7 @@ public async Task RaiseIntentForContext_with_multiple_intents_but_single_app_cal var result = await Fdc3.RaiseIntentForContext(request, OnlyApp3Context.Type); - ResolverUICommunicator.Verify(_ => _.SendResolverUIIntentRequest(It.IsAny>(), It.IsAny())); + ResolverUICommunicator.Verify(_ => _.SendResolverUIIntentRequestAsync(It.IsAny>(), It.IsAny())); ResolverUICommunicator.VerifyNoOtherCalls(); } diff --git a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/RaiseIntentTests.cs b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/RaiseIntentTests.cs index 8cd9417ca..1cb896e1e 100644 --- a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/RaiseIntentTests.cs +++ b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.Fdc3.DesktopAgent.Tests/RaiseIntentTests.cs @@ -63,7 +63,7 @@ public async Task RaiseIntent_calls_ResolverUI() }; var result = await Fdc3.RaiseIntent(request, MultipleContext.Type); - ResolverUICommunicator.Verify(_ => _.SendResolverUIRequest(It.IsAny>(), It.IsAny())); + ResolverUICommunicator.Verify(_ => _.SendResolverUIRequestAsync(It.IsAny>(), It.IsAny())); } [Fact] diff --git a/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.spec.ts b/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.spec.ts index a49fa8a8d..179aa9340 100644 --- a/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.spec.ts +++ b/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.spec.ts @@ -18,7 +18,7 @@ import { ComposeUIDesktopAgent } from './ComposeUIDesktopAgent'; import { ComposeUITopic } from './infrastructure/ComposeUITopic'; import { Channel, ChannelError, ContextHandler } from '@finos/fdc3'; import { ComposeUIErrors } from './infrastructure/ComposeUIErrors'; -import { ChannelFactory } from './infrastructure/ChannelFactory'; +import { ChannelHandler } from './infrastructure/ChannelHandler'; import { ComposeUIPrivateChannel } from './infrastructure/ComposeUIPrivateChannel'; import { ChannelType } from './infrastructure/ChannelType'; import { Fdc3GetOpenedAppContextResponse } from './infrastructure/messages/Fdc3GetOpenedAppContextResponse'; @@ -36,7 +36,7 @@ const testInstrument = { const contextMessageHandlerMock = vi.fn((_ctx) => 'dummy'); -const buildChannelFactory = (jm: JsonMessaging): ChannelFactory => ({ +const buildChannelFactory = (jm: JsonMessaging): ChannelHandler => ({ createPrivateChannel: vi.fn(() => Promise.resolve(new ComposeUIPrivateChannel('privateId', 'localInstance', jm, true)) ), @@ -51,7 +51,10 @@ const buildChannelFactory = (jm: JsonMessaging): ChannelFactory => ({ getContextListener: vi.fn( (_openHandled: boolean, _channel: Channel, handler: ContextHandler, contextType?: string) => Promise.resolve(new ComposeUIContextListener(true, jm, handler, contextType)) - ) + ), + leaveCurrentChannel: vi.fn(() => Promise.resolve()), + configureChannelSelectorFromUI: vi.fn(() => Promise.resolve()), + [Symbol.asyncDispose]: vi.fn(() => Promise.resolve()) }); describe('Tests for ComposeUIDesktopAgent implementation API', () => { diff --git a/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.ts b/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.ts index ef5230fe0..ac0a8721f 100644 --- a/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.ts +++ b/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.ts @@ -29,8 +29,8 @@ import { import { IMessaging, JsonMessaging } from "@morgan-stanley/composeui-messaging-abstractions"; import { ComposeUIContextListener } from './infrastructure/ComposeUIContextListener'; import { ComposeUIErrors } from './infrastructure/ComposeUIErrors'; -import { ChannelFactory } from './infrastructure/ChannelFactory'; -import { MessagingChannelFactory } from './infrastructure/MessagingChannelFactory'; +import { ChannelHandler } from './infrastructure/ChannelHandler'; +import { MessagingChannelHandler } from './infrastructure/MessagingChannelHandler'; import { MessagingIntentsClient } from './infrastructure/MessagingIntentsClient'; import { IntentsClient } from './infrastructure/IntentsClient'; import { MetadataClient } from './infrastructure/MetadataClient'; @@ -38,14 +38,11 @@ import { MessagingMetadataClient } from './infrastructure/MessagingMetadataClien import { OpenClient } from "./infrastructure/OpenClient"; import { MessagingOpenClient } from "./infrastructure/MessagingOpenClient"; -export class ComposeUIDesktopAgent implements DesktopAgent { - private appChannels: Channel[] = []; - private userChannels: Channel[] = []; - private privateChannels: Channel[] = []; +export class ComposeUIDesktopAgent implements DesktopAgent, AsyncDisposable { private currentChannel?: Channel; private topLevelContextListeners: ComposeUIContextListener[] = []; private intentListeners: Listener[] = []; - private channelFactory: ChannelFactory; + private channelHandler: ChannelHandler; private intentsClient: IntentsClient; private metadataClient: MetadataClient; private openClient: OpenClient; @@ -55,7 +52,7 @@ export class ComposeUIDesktopAgent implements DesktopAgent { //TODO: we should enable passing multiple channelId to the ctor. constructor( messaging: IMessaging, - channelFactory?: ChannelFactory, + channelHandler?: ChannelHandler, intentsClient?: IntentsClient, metadataClient?: MetadataClient, openClient?: OpenClient) { @@ -67,12 +64,35 @@ export class ComposeUIDesktopAgent implements DesktopAgent { const jsonMessaging: JsonMessaging = new JsonMessaging(messaging); // TODO: inject this directly instead of the messageRouter - this.channelFactory = channelFactory ?? new MessagingChannelFactory(jsonMessaging, window.composeui.fdc3.config.instanceId); - this.intentsClient = intentsClient ?? new MessagingIntentsClient(jsonMessaging, this.channelFactory); + this.channelHandler = channelHandler ?? new MessagingChannelHandler(jsonMessaging, window.composeui.fdc3.config.instanceId); + this.intentsClient = intentsClient ?? new MessagingIntentsClient(jsonMessaging, this.channelHandler); this.metadataClient = metadataClient ?? new MessagingMetadataClient(jsonMessaging, window.composeui.fdc3.config); this.openClient = openClient ?? new MessagingOpenClient(window.composeui.fdc3.config.instanceId!, jsonMessaging, window.composeui.fdc3.openAppIdentifier); } + public async [Symbol.asyncDispose](): Promise { + console.debug("Disposing ComposeUIDesktopAgent"); + + await this.channelHandler[Symbol.asyncDispose](); + + for (const listener of this.intentListeners) { + await listener.unsubscribe(); + } + + this.intentListeners = []; + + for (const listener of this.topLevelContextListeners) { + await listener.unsubscribe(); + } + + this.topLevelContextListeners = []; + } + + // This regiters an endpoint to listen when an action was initiated from the UI to select a user channel to join to. + public async init(): Promise { + await this.channelHandler.configureChannelSelectorFromUI(); + } + public async open(app?: string | AppIdentifier, context?: Context): Promise { return await this.openClient.open(app, context); } @@ -106,7 +126,7 @@ export class ComposeUIDesktopAgent implements DesktopAgent { } public async addIntentListener(intent: string, handler: IntentHandler): Promise { - var listener = await this.channelFactory.getIntentListener(intent, handler); + var listener = await this.channelHandler.getIntentListener(intent, handler); this.intentListeners.push(listener); return listener; } @@ -132,7 +152,7 @@ export class ComposeUIDesktopAgent implements DesktopAgent { this.openedAppContextHandled = true; } - const listener = await this.channelFactory.getContextListener(this.openedAppContextHandled, this.currentChannel, handler, contextType); + const listener = await this.channelHandler.getContextListener(this.openedAppContextHandled, this.currentChannel, handler, contextType); this.topLevelContextListeners.push(listener); if (!this.currentChannel) { @@ -147,24 +167,22 @@ export class ComposeUIDesktopAgent implements DesktopAgent { } public async getUserChannels(): Promise> { - return await this.channelFactory.getUserChannels(); + return await this.channelHandler.getUserChannels(); } public async joinUserChannel(channelId: string): Promise { if (this.currentChannel) { //DesktopAgnet clients can listen on only one channel + console.debug("Leaving current channel: ", this.currentChannel.id); await this.leaveCurrentChannel(); } - let channel = this.userChannels.find(innerChannel => innerChannel.id == channelId); + let channel = await this.channelHandler.joinUserChannel(channelId); + + console.debug("Joined to user channel: ", channelId); + if (!channel) { - channel = await this.channelFactory.joinUserChannel(channelId); - - if (!channel) { - throw new Error(ChannelError.NoChannelFound); - } - - this.addChannel(channel); + throw new Error(ChannelError.NoChannelFound); } this.currentChannel = channel; @@ -178,19 +196,12 @@ export class ComposeUIDesktopAgent implements DesktopAgent { } public async getOrCreateChannel(channelId: string): Promise { - let appChannel = this.appChannels.find(channel => channel.id == channelId); - if (appChannel) { - return appChannel; - } - - appChannel = await this.channelFactory.createAppChannel(channelId); - - this.addChannel(appChannel!); + let appChannel = await this.channelHandler.createAppChannel(channelId); return appChannel!; } public async createPrivateChannel(): Promise { - return await this.channelFactory.createPrivateChannel(); + return await this.channelHandler.createPrivateChannel(); } public async getCurrentChannel(): Promise { @@ -199,11 +210,15 @@ export class ComposeUIDesktopAgent implements DesktopAgent { public async leaveCurrentChannel(): Promise { //The context listeners, that have been added through the `fdc3.addContextListener()` should unsubscribe + console.debug("Unsubscribing top level context listeners: ", this.topLevelContextListeners); for (const listener of this.topLevelContextListeners) { await listener.unsubscribe(); } - this.currentChannel = undefined; + if (this.currentChannel) { + await this.channelHandler.leaveCurrentChannel(); + this.currentChannel = undefined; + } } public async getInfo(): Promise { @@ -234,21 +249,6 @@ export class ComposeUIDesktopAgent implements DesktopAgent { } } - private addChannel(channel: Channel): void { - if (channel == null) return; - switch (channel.type) { - case "app": - this.appChannels.push(channel); - break; - case "user": - this.userChannels.push(channel); - break; - case "private": - this.privateChannels.push(channel); - break; - } - } - private async callHandlerOnChannelsCurrentContext(listener: ComposeUIContextListener) : Promise { const lastContext = await this.currentChannel!.getCurrentContext(listener.contextType); diff --git a/src/fdc3/js/composeui-fdc3/src/ComposeUIMessagingChannelFactory.spec.ts b/src/fdc3/js/composeui-fdc3/src/ComposeUIMessagingChannelHandler.spec.ts similarity index 87% rename from src/fdc3/js/composeui-fdc3/src/ComposeUIMessagingChannelFactory.spec.ts rename to src/fdc3/js/composeui-fdc3/src/ComposeUIMessagingChannelHandler.spec.ts index 316d50a80..1292382b5 100644 --- a/src/fdc3/js/composeui-fdc3/src/ComposeUIMessagingChannelFactory.spec.ts +++ b/src/fdc3/js/composeui-fdc3/src/ComposeUIMessagingChannelHandler.spec.ts @@ -12,7 +12,7 @@ import { describe, it, expect, vi } from 'vitest'; import { ChannelError } from '@finos/fdc3'; -import { MessagingChannelFactory } from './infrastructure/MessagingChannelFactory'; +import { MessagingChannelHandler } from './infrastructure/MessagingChannelHandler'; import { Fdc3JoinUserChannelResponse } from './infrastructure/messages/Fdc3JoinUserChannelResponse'; import { Fdc3GetUserChannelsResponse } from './infrastructure/messages/Fdc3GetUserChannelsResponse'; import { ComposeUIChannel } from './infrastructure/ComposeUIChannel'; @@ -30,11 +30,11 @@ const baseMessagingMock = (): IMessaging => ({ invokeService: vi.fn(() => Promise.resolve(null)) }); -describe('MessagingChannelFactory tests', () => { +describe('MessagingChannelHandler tests', () => { it('joinUserChannel rejects CreationFailed when no response', async () => { const messagingMock = baseMessagingMock(); const jsonMessaging = new JsonMessaging(messagingMock); - const factory = new MessagingChannelFactory(jsonMessaging, 'localInstance'); + const factory = new MessagingChannelHandler(jsonMessaging, 'localInstance'); await expect(factory.joinUserChannel('dummyId')).rejects.toThrow(ChannelError.CreationFailed); expect(messagingMock.invokeService).toHaveBeenCalledTimes(1); }); @@ -44,7 +44,7 @@ describe('MessagingChannelFactory tests', () => { const messagingMock = baseMessagingMock(); messagingMock.invokeService = vi.fn(() => Promise.resolve(JSON.stringify(response))); const jsonMessaging = new JsonMessaging(messagingMock); - const factory = new MessagingChannelFactory(jsonMessaging, 'localInstance'); + const factory = new MessagingChannelHandler(jsonMessaging, 'localInstance'); await expect(factory.joinUserChannel('dummyId')).rejects.toThrow('testError'); expect(messagingMock.invokeService).toHaveBeenCalledTimes(1); }); @@ -57,7 +57,7 @@ describe('MessagingChannelFactory tests', () => { const messagingMock = baseMessagingMock(); messagingMock.invokeService = vi.fn(() => Promise.resolve(JSON.stringify(response))); const jsonMessaging = new JsonMessaging(messagingMock); - const factory = new MessagingChannelFactory(jsonMessaging, 'localInstance'); + const factory = new MessagingChannelHandler(jsonMessaging, 'localInstance'); await expect(factory.joinUserChannel('dummyId')).rejects.toThrow(ChannelError.CreationFailed); expect(messagingMock.invokeService).toHaveBeenCalledTimes(1); }); @@ -70,16 +70,16 @@ describe('MessagingChannelFactory tests', () => { const messagingMock = baseMessagingMock(); messagingMock.invokeService = vi.fn(() => Promise.resolve(JSON.stringify(response))); const jsonMessaging = new JsonMessaging(messagingMock); - const factory = new MessagingChannelFactory(jsonMessaging, 'localInstance'); + const factory = new MessagingChannelHandler(jsonMessaging, 'localInstance'); const result = await factory.joinUserChannel('dummyId'); expect(result).toBeInstanceOf(ComposeUIChannel); - expect(messagingMock.invokeService).toHaveBeenCalledTimes(1); + expect(messagingMock.invokeService).toHaveBeenCalledTimes(2); //This will trigger the channel selector logic as well. }); it('getUserChannels rejects NoChannelFound when no response', async () => { const messagingMock = baseMessagingMock(); const jsonMessaging = new JsonMessaging(messagingMock); - const factory = new MessagingChannelFactory(jsonMessaging, 'localInstance'); + const factory = new MessagingChannelHandler(jsonMessaging, 'localInstance'); await expect(factory.getUserChannels()).rejects.toThrow(ChannelError.NoChannelFound); expect(messagingMock.invokeService).toHaveBeenCalledTimes(1); }); @@ -89,7 +89,7 @@ describe('MessagingChannelFactory tests', () => { const messagingMock = baseMessagingMock(); messagingMock.invokeService = vi.fn(() => Promise.resolve(JSON.stringify(response))); const jsonMessaging = new JsonMessaging(messagingMock); - const factory = new MessagingChannelFactory(jsonMessaging, 'localInstance'); + const factory = new MessagingChannelHandler(jsonMessaging, 'localInstance'); await expect(factory.getUserChannels()).rejects.toThrow('testError'); expect(messagingMock.invokeService).toHaveBeenCalledTimes(1); }); @@ -101,7 +101,7 @@ describe('MessagingChannelFactory tests', () => { const messagingMock = baseMessagingMock(); messagingMock.invokeService = vi.fn(() => Promise.resolve(JSON.stringify(response))); const jsonMessaging = new JsonMessaging(messagingMock); - const factory = new MessagingChannelFactory(jsonMessaging, 'localInstance'); + const factory = new MessagingChannelHandler(jsonMessaging, 'localInstance'); const result = await factory.getUserChannels(); expect(result).toBeDefined(); expect(result.length).toBe(1); diff --git a/src/fdc3/js/composeui-fdc3/src/index.ts b/src/fdc3/js/composeui-fdc3/src/index.ts index e82bdb55d..942a64c91 100644 --- a/src/fdc3/js/composeui-fdc3/src/index.ts +++ b/src/fdc3/js/composeui-fdc3/src/index.ts @@ -39,6 +39,33 @@ async function initialize(): Promise { const openAppIdentifier: OpenAppIdentifier | undefined = window.composeui.fdc3.openAppIdentifier; const messaging = window.composeui.messaging.communicator as IMessaging; const fdc3 = new ComposeUIDesktopAgent(messaging); + await fdc3.init(); + + let _disposed = false; + + const disposeAgent = () => { + if (_disposed) { + return; + } + + _disposed = true; + + try { + const agent: ComposeUIDesktopAgent = (window.fdc3 as ComposeUIDesktopAgent) || fdc3; + if (agent) { + agent[Symbol.asyncDispose]() + } + } catch (err) { + console.warn("Error disposing FDC3 agent", err); + } finally { + // remove handlers after first run + window.removeEventListener("beforeunload", disposeAgent); + window.removeEventListener("unload", disposeAgent); + } + }; + + window.addEventListener("beforeunload", disposeAgent); + window.addEventListener("unload", disposeAgent); if (channelId) { await fdc3.joinUserChannel(channelId) @@ -47,10 +74,12 @@ async function initialize(): Promise { await fdc3.getOpenedAppContext() .then(() => { window.fdc3 = fdc3; + console.log("FDC3 initialized, handled initial context which initiates that the app was opened via `fdc3.open` and joined to channel: ", channelId, window.fdc3); window.dispatchEvent(new Event("fdc3Ready")); }) } else { window.fdc3 = fdc3; + console.log("FDC3 initialized and joined to channel: ", channelId, window.fdc3); window.dispatchEvent(new Event("fdc3Ready")); } }); @@ -58,10 +87,12 @@ async function initialize(): Promise { if (openAppIdentifier) { await fdc3.getOpenedAppContext().then(() => { window.fdc3 = fdc3; + console.log("FDC3 initialized, handled initial context which initiates that the app was opened via `fdc3.open`: ", window.fdc3); window.dispatchEvent(new Event("fdc3Ready")); }) } else { window.fdc3 = fdc3; + console.log("FDC3 initialized: ", window.fdc3); window.dispatchEvent(new Event("fdc3Ready")); } } diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/ChannelFactory.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/ChannelHandler.ts similarity index 53% rename from src/fdc3/js/composeui-fdc3/src/infrastructure/ChannelFactory.ts rename to src/fdc3/js/composeui-fdc3/src/infrastructure/ChannelHandler.ts index 46bf158d5..ac973a533 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/ChannelFactory.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/ChannelHandler.ts @@ -14,12 +14,49 @@ import { Channel, ContextHandler, IntentHandler, Listener, PrivateChannel } from "@finos/fdc3"; import { ChannelType } from "./ChannelType"; -export interface ChannelFactory { +export interface ChannelHandler extends AsyncDisposable { + /* + * Gets a channel by sending a request to the backend using its ID and type + */ getChannel(channelId: string, channelType: ChannelType): Promise; + + /* + * Creates a private channel by sending a request to the backend + */ createPrivateChannel(): Promise; + + /* + * Creates an app channel by sending a request to the backend using its ID + */ createAppChannel(channelId: string): Promise; + + /* + * Joins a user channel by sending a request to the backend using its ID + */ joinUserChannel(channelId: string): Promise; + + /* + * Gets all the user channels by sending a request to the backend + */ getUserChannels(): Promise; + + /* + * Gets all the app channels by sending a request to the backend + */ getIntentListener(intent: string, handler: IntentHandler): Promise; + + /* + * Gets a context listener by sending a request to the backend. This should reflect if the initial context sent by the fdc3.open call was handled or not. + */ getContextListener(openHandled: boolean, channel?: Channel, handler?: ContextHandler, contextType?: string | null): Promise; + + /* + * Configures the channel selector to allow the user to select a channel from the UI, by registering an endpoint to listen to UI initiated actions. + */ + configureChannelSelectorFromUI(): Promise; + + /* + * Leaves the current channel by sending a request to the backend using its ID + */ + leaveCurrentChannel(): Promise; } \ No newline at end of file diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/ChannelItem.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/ChannelItem.ts index fa2ef7ce9..9d130df74 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/ChannelItem.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/ChannelItem.ts @@ -13,6 +13,9 @@ import { DisplayMetadata } from "@finos/fdc3"; import { ChannelType } from "./ChannelType"; +/* +* Represents a channel item containing its id, type and optional display metadata +*/ export interface ChannelItem { id: string; type: ChannelType; diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIChannel.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIChannel.ts index d5a8631a7..fe7c6e10e 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIChannel.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIChannel.ts @@ -35,7 +35,7 @@ export class ComposeUIChannel implements Channel { this.displayMetadata = displayMetadata; } - //Broadcasting on the composeui/fdc3/v2.0/broadcast topic + //Broadcasting on the composeui/fdc3/v2.0//broadcast topic public async broadcast(context: Context): Promise { //Setting the last published context message. this.lastContexts.set(context.type, context); diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIContextListener.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIContextListener.ts index e39b7564a..ccf4cb59a 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIContextListener.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIContextListener.ts @@ -42,6 +42,9 @@ export class ComposeUIContextListener implements Listener { public async subscribe(channelId: string, channelType: ChannelType): Promise { await this.registerContextListener(channelId, channelType); + + console.debug("Subscribed context listener with id: ", this.id, ", to channel: ", channelId, ", of type: ", channelType, ", for context type: ", this.contextType); + const subscribeTopic = ComposeUITopic.broadcast(channelId, channelType); this.unsubscribable = await this.jsonMessaging.subscribeJson(subscribeTopic, async (context: Context) => { @@ -54,6 +57,7 @@ export class ComposeUIContextListener implements Listener { } }); + console.log("Registered context listener with id: ", this.id, ", to topic: ", subscribeTopic); this.isSubscribed = true; } @@ -94,16 +98,18 @@ export class ComposeUIContextListener implements Listener { public async unsubscribe(): Promise { if (!this.unsubscribable || !this.isSubscribed) { + console.debug("The current listener is not subscribed."); return; } try { await this.leaveChannel(); + console.debug("Unsubscribed context listener with id: ", this.id); } catch(err) { console.log(err); } - this.unsubscribable.unsubscribe(); + await this.unsubscribable.unsubscribe(); this.isSubscribed = false; if (this.unsubscribeCallback) { @@ -129,6 +135,7 @@ export class ComposeUIContextListener implements Listener { } private async leaveChannel() : Promise { + console.debug("Removing context listener with id: ", this.id); const request = new Fdc3RemoveContextListenerRequest(window.composeui.fdc3.config?.instanceId!, this.id!, this.contextType); const response = await this.jsonMessaging.invokeJsonService(ComposeUITopic.removeContextListener(), request); if (!response) { diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIIntentResolution.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIIntentResolution.ts index 514e32d44..5cafc71f9 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIIntentResolution.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIIntentResolution.ts @@ -12,7 +12,7 @@ */ import { AppMetadata, IntentResolution, IntentResult } from "@finos/fdc3"; import { JsonMessaging } from "@morgan-stanley/composeui-messaging-abstractions"; -import { ChannelFactory } from "./ChannelFactory"; +import { ChannelHandler } from "./ChannelHandler"; import { ComposeUIErrors } from "./ComposeUIErrors"; import { ComposeUITopic } from "./ComposeUITopic"; import { Fdc3GetIntentResultRequest } from "./messages/Fdc3GetIntentResultRequest"; @@ -20,18 +20,18 @@ import { Fdc3GetIntentResultResponse } from "./messages/Fdc3GetIntentResultRespo export class ComposeUIIntentResolution implements IntentResolution { private jsonMessaging: JsonMessaging; - private channelFactory: ChannelFactory; + private channelHandler: ChannelHandler; public source: AppMetadata; public intent: string public messageId: string; - constructor(messageId: string, jsonMessaging: JsonMessaging, channelFactory: ChannelFactory, intent: string, source: AppMetadata) { + constructor(messageId: string, jsonMessaging: JsonMessaging, channelHandler: ChannelHandler, intent: string, source: AppMetadata) { this.messageId = messageId; this.intent = intent; this.source = source; this.jsonMessaging = jsonMessaging; - this.channelFactory = channelFactory; + this.channelHandler = channelHandler; } async getResult(): Promise { @@ -47,7 +47,7 @@ export class ComposeUIIntentResolution implements IntentResolution { } if (result.channelId && result.channelType) { - const channel = this.channelFactory.getChannel(result.channelId, result.channelType) + const channel = this.channelHandler.getChannel(result.channelId, result.channelType) return channel; } else if (result.context) { return result.context; diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIPrivateChannel.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIPrivateChannel.ts index 8205635f4..e3a18a44e 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIPrivateChannel.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIPrivateChannel.ts @@ -104,7 +104,7 @@ export class ComposeUIPrivateChannel extends ComposeUIChannel implements Private if (this.disconnected) { throw new Error("Channel disconnected"); } - + return super.broadcast(context) } diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUITopic.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUITopic.ts index 67aa33449..9efcc9ddc 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUITopic.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUITopic.ts @@ -107,6 +107,14 @@ export class ComposeUITopic { return `${this.topicRoot}/${this.joinUserChannelSuffix}`; } + public static channelSelectorFromUI(instanceId: string): string { + return `${this.topicRoot}/channelSelector/UI/${instanceId}`; + } + + public static channelSelectorFromAPI(instanceId: string): string { + return `${this.topicRoot}/channelSelector/API/${instanceId}`; + } + public static getInfo(): string { return `${this.topicRoot}/${this.getInfoSuffix}`; } diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/MessagingChannelFactory.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/MessagingChannelHandler.ts similarity index 68% rename from src/fdc3/js/composeui-fdc3/src/infrastructure/MessagingChannelFactory.ts rename to src/fdc3/js/composeui-fdc3/src/infrastructure/MessagingChannelHandler.ts index e72b7cbf0..7a463f818 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/MessagingChannelFactory.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/MessagingChannelHandler.ts @@ -14,7 +14,7 @@ import { ChannelError, ContextHandler, IntentHandler, Listener, PrivateChannel } from "@finos/fdc3"; import { JsonMessaging } from "@morgan-stanley/composeui-messaging-abstractions"; import { Channel } from "@finos/fdc3"; -import { ChannelFactory } from "./ChannelFactory"; +import { ChannelHandler } from "./ChannelHandler"; import { ComposeUIPrivateChannel } from "./ComposeUIPrivateChannel"; import { Fdc3CreatePrivateChannelRequest } from "./messages/Fdc3CreatePrivateChannelRequest"; import { Fdc3CreatePrivateChannelResponse } from "./messages/Fdc3CreatePrivateChannelResponse"; @@ -38,14 +38,26 @@ import { Fdc3JoinUserChannelResponse } from "./messages/Fdc3JoinUserChannelRespo import { ChannelItem } from "./ChannelItem"; import { ComposeUIContextListener } from "./ComposeUIContextListener"; -export class MessagingChannelFactory implements ChannelFactory { +export class MessagingChannelHandler implements ChannelHandler { private jsonMessaging: JsonMessaging; private fdc3instanceId: string; + private channelSelector: AsyncDisposable | undefined = undefined; constructor(jsonMessaging: JsonMessaging,fdc3instanceId: string) { this.jsonMessaging = jsonMessaging; this.fdc3instanceId = fdc3instanceId; } + + [Symbol.asyncDispose](): PromiseLike { + return this.channelSelector + ? this.channelSelector[Symbol.asyncDispose]() + : Promise.resolve(); + } + + public async configureChannelSelectorFromUI(): Promise { + this.channelSelector = await this.jsonMessaging.registerService(ComposeUITopic.channelSelectorFromUI(this.fdc3instanceId), this.selectUserChannelFromUIHandler); + console.debug("Configured channel selector for module: ", this.fdc3instanceId); + } public async getChannel(channelId: string, channelType: ChannelType): Promise { const topic = ComposeUITopic.findChannel(); @@ -117,24 +129,43 @@ export class MessagingChannelFactory implements ChannelFactory { } public async joinUserChannel(channelId: string): Promise { - const topic: string = ComposeUITopic.joinUserChannel(); - const request: Fdc3JoinUserChannelRequest = new Fdc3JoinUserChannelRequest(channelId, this.fdc3instanceId); - const response = await this.jsonMessaging.invokeJsonService(topic, request); + try { + const topic: string = ComposeUITopic.joinUserChannel(); + const request: Fdc3JoinUserChannelRequest = new Fdc3JoinUserChannelRequest(channelId, this.fdc3instanceId); + const response = await this.jsonMessaging.invokeJsonService(topic, request); - if (!response) { - throw new Error(ChannelError.CreationFailed); - } + console.debug("Received joinUserChannel response: ", response); - if (response.error) { - throw new Error(response.error); - } + if (!response) { + throw new Error(ChannelError.CreationFailed); + } - if (!response.success) { - throw new Error(ChannelError.CreationFailed); + if (response.error) { + throw new Error(response.error); + } + + if (!response.success) { + throw new Error(ChannelError.CreationFailed); + } + + var channel = new ComposeUIChannel(channelId, "user", this.jsonMessaging, response.displayMetadata); + + this.triggerChannelJoinedEvent(channel.id); + + return channel; + } catch (error) { + console.error("Error joining user channel: ", error); + throw error; } + } - var channel = new ComposeUIChannel(channelId, "user", this.jsonMessaging, response.displayMetadata); - return channel; + private async triggerChannelJoinedEvent(id: string | undefined) : Promise { + try { + var result = await this.jsonMessaging.invokeService(ComposeUITopic.channelSelectorFromAPI(this.fdc3instanceId), id); + console.debug("Triggered channel selector of container: ", this.fdc3instanceId, ", with: ", id, ", and got result:", result); + } catch (error) { + console.error("Error triggering channel joined event for module: ", this.fdc3instanceId, ", with channel id: ", id, ", error: ", error); + } } public async getUserChannels(): Promise { @@ -193,4 +224,36 @@ export class MessagingChannelFactory implements ChannelFactory { const listener = new ComposeUIContextListener(openHandled, this.jsonMessaging, handler!, contextType ?? undefined); return listener; } + + public async leaveCurrentChannel(): Promise { + await this.triggerChannelJoinedEvent(undefined); + } + + private async selectUserChannelFromUIHandler(request?: string | null | undefined): Promise { + try { + if (!request) { + console.debug("Empty request received when the user channel selection was requested from UI."); + return null; + } + + var objectRequest = JSON.parse(request); + var joinUserChannelRequest = objectRequest as Fdc3JoinUserChannelRequest; + console.debug("Parsed the request from the UI when user selected a user channel to join: ", joinUserChannelRequest); + + if (!joinUserChannelRequest || !joinUserChannelRequest.channelId) { + console.debug("Invalid request received when user selected a user channel to join to from the UI: ", request); + return null; + } + + //We should join the channel requested by the user from the UI. -> this will trigger the handler of the module/container to show which channel the app is joined to. + await window.fdc3.joinUserChannel(joinUserChannelRequest.channelId); + + return joinUserChannelRequest.channelId; + + } catch (error) { + console.error("Error processing request when channel selector was invoked: ", request, ", error: ", error); + return null; + } + } } + diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/MessagingIntentsClient.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/MessagingIntentsClient.ts index a2e7c18ee..074a5952e 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/MessagingIntentsClient.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/MessagingIntentsClient.ts @@ -13,7 +13,7 @@ import { AppIdentifier, AppIntent, AppMetadata, Context, IntentResolution } from "@finos/fdc3"; import { JsonMessaging } from "@morgan-stanley/composeui-messaging-abstractions"; -import { ChannelFactory } from "./ChannelFactory"; +import { ChannelHandler } from "./ChannelHandler"; import { ComposeUIErrors } from "./ComposeUIErrors"; import { ComposeUIIntentResolution } from "./ComposeUIIntentResolution"; import { ComposeUITopic } from "./ComposeUITopic"; @@ -27,15 +27,15 @@ import { Fdc3RaiseIntentResponse } from "./messages/Fdc3RaiseIntentResponse"; import { Fdc3RaiseIntentForContextRequest } from "./messages/Fdc3RaiseIntentForContextRequest"; export class MessagingIntentsClient implements IntentsClient { - private channelFactory: ChannelFactory; + private channelHandler: ChannelHandler; private jsonMessaging: JsonMessaging; - constructor( jsonMessaging: JsonMessaging, channelFactory: ChannelFactory, ) { + constructor( jsonMessaging: JsonMessaging, channelFactory: ChannelHandler, ) { if (!window.composeui.fdc3.config || !window.composeui.fdc3.config.instanceId) { throw new Error(ComposeUIErrors.InstanceIdNotFound); } - this.channelFactory = channelFactory; + this.channelHandler = channelFactory; this.jsonMessaging = jsonMessaging; } @@ -69,7 +69,7 @@ export class MessagingIntentsClient implements IntentsClient { } public async getIntentResolution(messageId: string, intent: string, source: AppMetadata): Promise { - return new ComposeUIIntentResolution(messageId, this.jsonMessaging, this.channelFactory, intent, source); + return new ComposeUIIntentResolution(messageId, this.jsonMessaging, this.channelHandler, intent, source); } public async raiseIntent(intent: string, context: Context, app?: string | AppIdentifier): Promise { @@ -88,7 +88,7 @@ export class MessagingIntentsClient implements IntentsClient { throw new Error(response.error); } - const intentResolution = new ComposeUIIntentResolution(response.messageId, this.jsonMessaging, this.channelFactory, response.intent!, response.appMetadata!); + const intentResolution = new ComposeUIIntentResolution(response.messageId, this.jsonMessaging, this.channelHandler, response.intent!, response.appMetadata!); return intentResolution; } @@ -113,7 +113,7 @@ export class MessagingIntentsClient implements IntentsClient { throw new Error(response.error); } - const intentResolution = new ComposeUIIntentResolution(response.messageId!, this.jsonMessaging, this.channelFactory, response.intent!, response.appMetadata!); + const intentResolution = new ComposeUIIntentResolution(response.messageId!, this.jsonMessaging, this.channelHandler, response.intent!, response.appMetadata!); return intentResolution; } } diff --git a/src/messaging/dotnet/src/Client/Client/MessageRouterClient.cs b/src/messaging/dotnet/src/Client/Client/MessageRouterClient.cs index cd4ca9f47..9d4c5a674 100644 --- a/src/messaging/dotnet/src/Client/Client/MessageRouterClient.cs +++ b/src/messaging/dotnet/src/Client/Client/MessageRouterClient.cs @@ -1,4 +1,4 @@ -// Morgan Stanley makes this available to you under the Apache License, +// Morgan Stanley makes this available to you under the Apache License, // Version 2.0 (the "License"). You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0. @@ -392,6 +392,7 @@ await SendMessageAsync( e, $"Unhandled exception while processing an {nameof(InvokeRequest)}: {{ExceptionMessage}}", e.Message); + OnRequestStop(message, e); } }, diff --git a/src/shell/dotnet/src/Shell/Fdc3/ResolverUI/ResolverUIService.cs b/src/shell/dotnet/src/Shell/Fdc3/ResolverUI/ResolverUIService.cs index a565de576..dd052bdec 100644 --- a/src/shell/dotnet/src/Shell/Fdc3/ResolverUI/ResolverUIService.cs +++ b/src/shell/dotnet/src/Shell/Fdc3/ResolverUI/ResolverUIService.cs @@ -19,6 +19,7 @@ using Finos.Fdc3; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared; using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Contracts; using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Converters; using MorganStanley.ComposeUI.Messaging.Abstractions; @@ -33,10 +34,7 @@ internal class ResolverUIService : IHostedService private readonly List> _disposeTask = new(); private readonly IHost _host; - private readonly JsonSerializerOptions _jsonSerializerOptions = new() - { - Converters = { new AppMetadataJsonConverter(), new IconJsonConverter() } - }; + private readonly JsonSerializerOptions _jsonSerializerOptions = SerializerOptionsHelper.JsonSerializerOptionsWithContextSerialization; private readonly IResolverUIProjector _resolverUIWindow; diff --git a/src/shell/dotnet/src/Shell/Shell.csproj b/src/shell/dotnet/src/Shell/Shell.csproj index e014b7894..2e156c3bc 100644 --- a/src/shell/dotnet/src/Shell/Shell.csproj +++ b/src/shell/dotnet/src/Shell/Shell.csproj @@ -66,6 +66,7 @@ + @@ -79,10 +80,7 @@ - + \ No newline at end of file