From 3f0e4f360b2b7e56d25678cbf8688ee94ce0ab4a Mon Sep 17 00:00:00 2001 From: lilla28 Date: Thu, 5 Sep 2024 16:54:24 +0200 Subject: [PATCH] fix(fdc3) - AddContextListener fix if the channel has not been set --- .../Contracts/AddContextListenerRequest.cs | 40 ++++ .../Contracts/AddContextListenerResponse.cs | 36 ++++ .../Contracts/RemoveContextListenerRequest.cs | 34 +++ .../RemoveContextListenerResponse.cs | 31 +++ .../Contracts/SubscribeState.cs | 2 +- .../Exceptions/Fdc3DesktopAgentErrors.cs | 5 + .../Fdc3DesktopAgent.cs | 101 ++++++++- .../Fdc3Topic.cs | 4 +- .../Internal/ContextListener.cs | 36 ++++ .../Fdc3DesktopAgentMessageRouterService.cs | 14 ++ .../Internal/IFdc3DesktopAgentBridge.cs | 14 ++ .../EndToEndTests.cs | 203 ++++++++++++++++-- .../Fdc3DesktopAgentTests.cs | 144 +++++++++++++ ...3DesktopAgentMessageRouterService.Tests.cs | 144 +++++++++++++ .../src/ComposeUIChannel.spec.ts | 74 ++++++- .../src/ComposeUIContextListener.spec.ts | 26 ++- .../src/ComposeUIDesktopAgent.spec.ts | 17 +- .../src/ComposeUIDesktopAgent.ts | 45 ++-- .../src/infrastructure/ChannelFactory.ts | 3 +- .../src/infrastructure/ComposeUIChannel.ts | 8 +- .../ComposeUIContextListener.ts | 70 ++++-- .../src/infrastructure/ComposeUIErrors.ts | 4 +- .../src/infrastructure/ComposeUITopic.ts | 10 + .../MessageRouterChannelFactory.ts | 15 +- .../messages/Fdc3AddContextListenerRequest.ts | 22 ++ .../Fdc3AddContextListenerResponse.ts | 17 ++ .../Fdc3RemoveContextListenerRequest.ts | 19 ++ .../Fdc3RemoveContextListenerResponse.ts | 17 ++ 28 files changed, 1068 insertions(+), 87 deletions(-) create mode 100644 src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/AddContextListenerRequest.cs create mode 100644 src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/AddContextListenerResponse.cs create mode 100644 src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/RemoveContextListenerRequest.cs create mode 100644 src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/RemoveContextListenerResponse.cs create mode 100644 src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Infrastructure/Internal/ContextListener.cs create mode 100644 src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3AddContextListenerRequest.ts create mode 100644 src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3AddContextListenerResponse.ts create mode 100644 src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3RemoveContextListenerRequest.ts create mode 100644 src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3RemoveContextListenerResponse.ts diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/AddContextListenerRequest.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/AddContextListenerRequest.cs new file mode 100644 index 000000000..909fc9b9c --- /dev/null +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/AddContextListenerRequest.cs @@ -0,0 +1,40 @@ +/* +* 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 Finos.Fdc3; + +namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Contracts; + +internal sealed class AddContextListenerRequest +{ + /// + /// Instance id of the app that sent the request. + /// + public string Fdc3InstanceId { get; set; } + + /// + /// Type of the context that the listener should listen on. + /// + public string? ContextType { get; set; } + + /// + /// The id of the channel, that the current listener is listening on. + /// + public string ChannelId { get; set; } + + /// + /// The type of the channel that the current listener listens on. + /// + public ChannelType ChannelType { get; set; } +} \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/AddContextListenerResponse.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/AddContextListenerResponse.cs new file mode 100644 index 000000000..8138008d9 --- /dev/null +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/AddContextListenerResponse.cs @@ -0,0 +1,36 @@ +/* +* 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.Contracts; + +internal sealed class AddContextListenerResponse +{ + /// + /// The generated id of the context listener + /// + public string? Id { get; set; } + + /// + /// Indicates that exception was thrown during the execution. + /// + public string? Error { get; set; } + + /// + /// Indicates if the execution was successful. + /// + public bool Success { get; set; } + + public static AddContextListenerResponse Failure(string error) => new() { Error = error, Success = false }; + public static AddContextListenerResponse Added(string id) => new() { Id = id, Success = true }; +} \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/RemoveContextListenerRequest.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/RemoveContextListenerRequest.cs new file mode 100644 index 000000000..6a8222377 --- /dev/null +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/RemoveContextListenerRequest.cs @@ -0,0 +1,34 @@ +/* +* 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.Contracts; + +internal sealed class RemoveContextListenerRequest +{ + /// + /// Id of the instance that sent the request. + /// + public string Fdc3InstanceId { get; set; } + + /// + /// Id of the context listener. + /// + public string ListenerId { get; set; } + + /// + /// Indicates the type of the context for the subscription. + /// + public string? ContextType { get; set; } +} \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/RemoveContextListenerResponse.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/RemoveContextListenerResponse.cs new file mode 100644 index 000000000..c8d6ac6a7 --- /dev/null +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/RemoveContextListenerResponse.cs @@ -0,0 +1,31 @@ +/* +* 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.Contracts; + +internal sealed class RemoveContextListenerResponse +{ + /// + /// Indicates that error was thrown during the execution of the request. + /// + public string? Error { get; set; } + + /// + /// Indicates the state of the request. + /// + public bool Success { get; set; } + + public static RemoveContextListenerResponse Failure(string error) => new() {Error = error, Success = false}; + public static RemoveContextListenerResponse Executed() => new() { Success = true}; +} \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/SubscribeState.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/SubscribeState.cs index 1b7dbbb23..988af353b 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/SubscribeState.cs +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Contracts/SubscribeState.cs @@ -18,7 +18,7 @@ namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Contracts; /// -/// Indicates the state of the IntentListener which was sent to the DesktopAgent backend. +/// Indicates the state of the Listener which was sent to the DesktopAgent backend. /// internal enum SubscribeState { diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Exceptions/Fdc3DesktopAgentErrors.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Exceptions/Fdc3DesktopAgentErrors.cs index 843f35d1e..c1b51d1f9 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Exceptions/Fdc3DesktopAgentErrors.cs +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Exceptions/Fdc3DesktopAgentErrors.cs @@ -40,4 +40,9 @@ public static class Fdc3DesktopAgentErrors /// Indicates that no user channel set was configured. /// public const string NoUserChannelSetFound = $"{nameof(NoUserChannelSetFound)}"; + + /// + /// Indicates that the listener was not found for execution. + /// + public const string ListenerNotFound = $"{nameof(ListenerNotFound)}"; } \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Fdc3DesktopAgent.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Fdc3DesktopAgent.cs index 0b75fcda3..e0e7ea747 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Fdc3DesktopAgent.cs +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Fdc3DesktopAgent.cs @@ -56,7 +56,9 @@ internal class Fdc3DesktopAgent : IFdc3DesktopAgentBridge private readonly ConcurrentDictionary _runningModules = new(); private readonly ConcurrentDictionary _raisedIntentResolutions = new(); private readonly ConcurrentDictionary> _pendingStartRequests = new(); + private readonly ConcurrentDictionary> _contextListeners = new(); private IAsyncDisposable? _subscription; + private readonly object _contextListenerLock = new(); public Fdc3DesktopAgent( IAppDirectory appDirectory, @@ -102,7 +104,7 @@ public Fdc3DesktopAgent( { if (_logger.IsEnabled(LogLevel.Warning)) { - _logger.LogWarning(exception, $"{channelId} is already registed as service endpoint."); + _logger.LogWarning(exception, $"{channelId} is already registered as service endpoint."); } return userChannel; @@ -133,7 +135,7 @@ public async ValueTask AddPrivateChannel(Func addPrivate { if (_logger.IsEnabled(LogLevel.Warning)) { - _logger.LogWarning(exception, $"{privateChannelId} is already registed as service endpoint."); + _logger.LogWarning(exception, $"{privateChannelId} is already registered as service endpoint."); } } catch (Exception exception) @@ -174,7 +176,7 @@ public async ValueTask AddAppChannel(Func GetAppMetadata(GetAppMetadataRequ } } + public ValueTask AddContextListener(AddContextListenerRequest? request) + { + if (request == null) + { + return ValueTask.FromResult(AddContextListenerResponse.Failure(Fdc3DesktopAgentErrors.PayloadNull)); + } + + if (!Guid.TryParse(request.Fdc3InstanceId, out var originFdc3InstanceId) || !_runningModules.TryGetValue(originFdc3InstanceId, out _)) + { + return ValueTask.FromResult(AddContextListenerResponse.Failure(Fdc3DesktopAgentErrors.MissingId)); + } + + lock (_contextListenerLock) + { + var contextListener = new ContextListener( + request.ContextType, + request.ChannelId, + request.ChannelType); + + _contextListeners.AddOrUpdate( + originFdc3InstanceId, + _ => new List { contextListener }, + (_, contextListeners) => + { + contextListeners.Add(contextListener); + return contextListeners; + }); + + return ValueTask.FromResult(AddContextListenerResponse.Added(contextListener.Id.ToString())); + } + } + + public ValueTask RemoveContextListener(RemoveContextListenerRequest? request) + { + if (request == null) + { + return ValueTask.FromResult(RemoveContextListenerResponse.Failure(Fdc3DesktopAgentErrors.PayloadNull)); + } + + lock (_contextListenerLock) + { + if (!Guid.TryParse(request.Fdc3InstanceId, out var originFdc3InstanceId) + || !_runningModules.TryGetValue(originFdc3InstanceId, out _) + || !_contextListeners.TryGetValue(originFdc3InstanceId, out var listeners) + || request.ListenerId == null + || !Guid.TryParse(request.ListenerId, out var listenerId)) + { + return ValueTask.FromResult(RemoveContextListenerResponse.Failure(Fdc3DesktopAgentErrors.MissingId)); + } + + var listener = listeners.FirstOrDefault(x => x.Id == listenerId && x.ContextType == request.ContextType); + if (listener == null) + { + return ValueTask.FromResult(RemoveContextListenerResponse.Failure(Fdc3DesktopAgentErrors.ListenerNotFound)); + } + + listeners.Remove(listener); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("ContextListener has been successfully unsubscribed."); + } + + return ValueTask.FromResult(RemoveContextListenerResponse.Executed()); + } + } + public async ValueTask> RaiseIntent(RaiseIntentRequest? request) { if (request == null) @@ -1218,7 +1293,12 @@ private Task RemoveModuleAsync(IModuleInstance instance) return Task.CompletedTask; } - if (!_runningModules.TryRemove(new(fdc3InstanceId!), out _)) //At this point the fdc3InstanceId shouldn't be null + if (!Guid.TryParse(fdc3InstanceId, out var id)) + { + return Task.CompletedTask; + } + + if (!_runningModules.TryRemove(id, out _)) //At this point the fdc3InstanceId shouldn't be null { _logger.LogError($"Could not remove the closed window with instanceId: {fdc3InstanceId}."); } @@ -1228,6 +1308,19 @@ private Task RemoveModuleAsync(IModuleInstance instance) taskCompletionSource.SetException(ThrowHelper.TargetInstanceUnavailable()); } + lock (_contextListenerLock) + { + if (!_contextListeners.TryRemove(id, out _)) + { + _logger.LogError($"Could not remove the registered context listeners of id: {fdc3InstanceId}."); + } + } + + if (!_raisedIntentResolutions.TryRemove(id, out _)) + { + _logger.LogError($"Could not remove the stored intent resolutions of id: {fdc3InstanceId} which raised the intents."); + } + return Task.CompletedTask; } diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Fdc3Topic.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Fdc3Topic.cs index f87efe31a..9d732779e 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Fdc3Topic.cs +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Fdc3Topic.cs @@ -12,9 +12,7 @@ * and limitations under the License. */ -using System.Threading.Tasks.Dataflow; using Finos.Fdc3; -using MorganStanley.ComposeUI.Messaging.Protocol.Messages; namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent; @@ -36,6 +34,8 @@ internal static class Fdc3Topic internal static string GetInfo => TopicRoot + "getInfo"; internal static string FindInstances => TopicRoot + "findInstances"; internal static string GetAppMetadata => TopicRoot + "getAppMetadata"; + internal static string AddContextListener => TopicRoot + "addContextListener"; + internal static string RemoveContextListener => TopicRoot + "removeContextListener"; //IntentListeners will be listening at this endpoint internal static string RaiseIntentResolution(string intent, string instanceId) diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Infrastructure/Internal/ContextListener.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Infrastructure/Internal/ContextListener.cs new file mode 100644 index 000000000..375750c23 --- /dev/null +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Infrastructure/Internal/ContextListener.cs @@ -0,0 +1,36 @@ +/* +* 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 Finos.Fdc3; + +namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Infrastructure.Internal; + +internal class ContextListener +{ + private readonly string? _contextType; + private readonly Guid _instanceId; + private string? _channelId; + private ChannelType? _channelType; + + public Guid Id => _instanceId; + public string? ContextType => _contextType; + + public ContextListener(string? contextType, string channelId, ChannelType channelType) + { + _contextType = contextType; + _channelId = channelId; + _channelType = channelType; + _instanceId = Guid.NewGuid(); + } +} \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Infrastructure/Internal/Fdc3DesktopAgentMessageRouterService.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Infrastructure/Internal/Fdc3DesktopAgentMessageRouterService.cs index 6a733e71e..db6d54593 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Infrastructure/Internal/Fdc3DesktopAgentMessageRouterService.cs +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Infrastructure/Internal/Fdc3DesktopAgentMessageRouterService.cs @@ -196,6 +196,16 @@ internal async ValueTask HandleGetAppMetadata(GetAppMeta return await _desktopAgent.GetAppMetadata(request); } + internal async ValueTask HandleAddContextListener(AddContextListenerRequest? request, MessageContext? context) + { + return await _desktopAgent.AddContextListener(request); + } + + internal async ValueTask HandleRemoveContextListener(RemoveContextListenerRequest? request, MessageContext? context) + { + return await _desktopAgent.RemoveContextListener(request); + } + private async ValueTask SafeWaitAsync(IEnumerable tasks) { foreach (var task in tasks) @@ -238,6 +248,8 @@ await _messageRouter.RegisterServiceAsync(topic, await RegisterHandler(Fdc3Topic.GetInfo, HandleGetInfo); await RegisterHandler(Fdc3Topic.FindInstances, HandleFindInstances); await RegisterHandler(Fdc3Topic.GetAppMetadata, HandleGetAppMetadata); + await RegisterHandler(Fdc3Topic.AddContextListener, HandleAddContextListener); + await RegisterHandler(Fdc3Topic.RemoveContextListener, HandleRemoveContextListener); await _desktopAgent.StartAsync(cancellationToken); @@ -265,6 +277,8 @@ public async Task StopAsync(CancellationToken cancellationToken) _messageRouter.UnregisterServiceAsync(Fdc3Topic.GetInfo, cancellationToken), _messageRouter.UnregisterServiceAsync(Fdc3Topic.FindInstances, cancellationToken), _messageRouter.UnregisterServiceAsync(Fdc3Topic.GetAppMetadata, cancellationToken), + _messageRouter.UnregisterServiceAsync(Fdc3Topic.AddContextListener, cancellationToken), + _messageRouter.UnregisterServiceAsync(Fdc3Topic.RemoveContextListener, cancellationToken), }; await SafeWaitAsync(unregisteringTasks); diff --git a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Infrastructure/Internal/IFdc3DesktopAgentBridge.cs b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Infrastructure/Internal/IFdc3DesktopAgentBridge.cs index 63f5816ba..5e0818e22 100644 --- a/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Infrastructure/Internal/IFdc3DesktopAgentBridge.cs +++ b/src/fdc3/dotnet/DesktopAgent/src/MorganStanley.ComposeUI.DesktopAgent/Infrastructure/Internal/IFdc3DesktopAgentBridge.cs @@ -142,4 +142,18 @@ internal interface IFdc3DesktopAgentBridge /// /// public ValueTask GetAppMetadata(GetAppMetadataRequest? request); + + /// + /// Handles the AddContextListener call in the bridge. It enables tracking the added contextListeners using the `fdc3.addContextListener`. + /// + /// + /// + public ValueTask AddContextListener(AddContextListenerRequest? request); + + /// + /// Handles the ContextListener action (join/unsubscribe to a channel) call in the bridge. + /// + /// + /// + public ValueTask RemoveContextListener(RemoveContextListenerRequest? request); } \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.DesktopAgent.Tests/EndToEndTests.cs b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.DesktopAgent.Tests/EndToEndTests.cs index 952b98849..6d52e86b0 100644 --- a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.DesktopAgent.Tests/EndToEndTests.cs +++ b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.DesktopAgent.Tests/EndToEndTests.cs @@ -901,7 +901,6 @@ public async Task AddIntentListenerUnsubscribes() intentListenerResponse!.Error.Should().BeNull(); } - [Fact] public async Task AddAppChannelReturnsSuccessfully() { @@ -1082,7 +1081,7 @@ public async Task JoinUserChannelReturnsNoChannelFoundError() } [Fact] - public async Task JoinUserChannel_succeeds() + public async Task JoinUserChannelSucceeds() { //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id var origin = await _moduleLoader.StartModule(new StartRequest("appId1")); @@ -1248,7 +1247,7 @@ public async Task GetInfoSuccessfullyReturns() } [Fact] - public async Task FindInstances_returns_PayloadNull_error_as_no_request() + public async Task FindInstancesReturnsPayloadNullErrorAsNoRequest() { FindInstancesRequest? request = null; @@ -1264,7 +1263,7 @@ public async Task FindInstances_returns_PayloadNull_error_as_no_request() } [Fact] - public async Task FindInstances_returns_MissingId_as_invalid_id() + public async Task FindInstancesReturnsMissingIdAsInvalidId() { var request = new FindInstancesRequest { @@ -1287,7 +1286,7 @@ public async Task FindInstances_returns_MissingId_as_invalid_id() } [Fact] - public async Task FindInstances_returns_MissingId_error_as_no_instance_found_which_is_contained_by_the_container() + public async Task FindInstancesReturnsMissingIdErrorAsNoInstanceFound() { var request = new FindInstancesRequest { @@ -1310,7 +1309,7 @@ public async Task FindInstances_returns_MissingId_error_as_no_instance_found_whi } [Fact] - public async Task FindInstances_returns_NoAppsFound_error_as_no_appId_found() + public async Task FindInstancesReturnsNoAppsFound() { //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id var origin = await _moduleLoader.StartModule(new StartRequest("appId1")); @@ -1337,7 +1336,7 @@ public async Task FindInstances_returns_NoAppsFound_error_as_no_appId_found() } [Fact] - public async Task FindInstances_succeeds_with_one_app() + public async Task FindInstancesSucceedsWithOneApp() { //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id var origin = await _moduleLoader.StartModule(new StartRequest("appId1")); @@ -1365,7 +1364,7 @@ public async Task FindInstances_succeeds_with_one_app() } [Fact] - public async Task FindInstances_succeeds_with_empty_array() + public async Task FindInstancesSucceedsWithEmptyArray() { //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id var origin = await _moduleLoader.StartModule(new StartRequest("appId1")); @@ -1393,7 +1392,7 @@ public async Task FindInstances_succeeds_with_empty_array() } [Fact] - public async Task GetAppMetadata_returns_PayLoadNull_error_as_request_null() + public async Task GetAppMetadataReturnsPayLoadNull() { GetAppMetadataRequest? request = null; @@ -1410,7 +1409,7 @@ public async Task GetAppMetadata_returns_PayLoadNull_error_as_request_null() } [Fact] - public async Task GetAppMetadata_returns_MissingId_error_as_initiator_id_not_found() + public async Task GetAppMetadataReturnsMissingId() { var request = new GetAppMetadataRequest { @@ -1434,7 +1433,7 @@ public async Task GetAppMetadata_returns_MissingId_error_as_initiator_id_not_fou } [Fact] - public async Task GetAppMetadata_returns_MissingId_error_as_the_searched_instanceId_not_valid() + public async Task GetAppMetadataReturnsMissingIdErrorAsThSearchedInstanceIdIsNotValid() { //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id var origin = await _moduleLoader.StartModule(new StartRequest("appId1")); @@ -1463,7 +1462,7 @@ public async Task GetAppMetadata_returns_MissingId_error_as_the_searched_instanc } [Fact] - public async Task GetAppMetadata_returns_TargetInstanceUnavailable_error_as_the_searched_instanceId_not_found() + public async Task GetAppMetadataReturnsTargetInstanceUnavailableErrorAsTheSearchedInstanceIdNotFound() { //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id var origin = await _moduleLoader.StartModule(new StartRequest("appId1")); @@ -1492,7 +1491,7 @@ public async Task GetAppMetadata_returns_TargetInstanceUnavailable_error_as_the_ } [Fact] - public async Task GetAppMetadata_returns_AppMetadata_based_on_instanceId() + public async Task GetAppMetadataReturnsAppMetadataBasedOnInstanceId() { //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id var origin = await _moduleLoader.StartModule(new StartRequest("appId1")); @@ -1527,7 +1526,7 @@ public async Task GetAppMetadata_returns_AppMetadata_based_on_instanceId() } [Fact] - public async Task GetAppMetadata_returns_TargetAppUnavailable_error_as_the_searched_appId_not_found() + public async Task GetAppMetadataReturnsTargetAppUnavailableErrorAsTheSearchedAppIdNotFound() { //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id var origin = await _moduleLoader.StartModule(new StartRequest("appId1")); @@ -1555,7 +1554,7 @@ public async Task GetAppMetadata_returns_TargetAppUnavailable_error_as_the_searc } [Fact] - public async Task GetAppMetadata_returns_AppMetadata_based_on_appId() + public async Task GetAppMetadataReturnsAppMetadataBasedOnAppId() { //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id var origin = await _moduleLoader.StartModule(new StartRequest("appId1")); @@ -1586,6 +1585,180 @@ public async Task GetAppMetadata_returns_AppMetadata_based_on_appId() }); } + [Fact] + public async Task AddContextListenerReturnsPayloadNull() + { + AddContextListenerRequest? request = null; + + var result = await _messageRouter.InvokeAsync( + Fdc3Topic.AddContextListener, + MessageBuffer.Factory.CreateJson(request, _options)); + + var response = result!.ReadJson(_options); + + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.PayloadNull); + } + + [Fact] + public async Task AddContextListenerReturnsMissingId() + { + var request = new AddContextListenerRequest + { + Fdc3InstanceId = "dummyId", + ChannelId = "fdc3.channel.1", + ChannelType = ChannelType.User + }; + + var result = await _messageRouter.InvokeAsync( + Fdc3Topic.AddContextListener, + MessageBuffer.Factory.CreateJson(request, _options)); + + var response = result!.ReadJson(_options); + + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.MissingId); + } + + [Fact] + public async Task AddContextListenerSuccessfullyRegistersContextListener() + { + //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id + var origin = await _moduleLoader.StartModule(new StartRequest("appId1")); + var originFdc3InstanceId = Fdc3InstanceIdRetriever.Get(origin); + + var request = new AddContextListenerRequest + { + Fdc3InstanceId = originFdc3InstanceId, + ChannelId = "fdc3.channel.1", + ChannelType = ChannelType.User + }; + + var result = await _messageRouter.InvokeAsync( + Fdc3Topic.AddContextListener, + MessageBuffer.Factory.CreateJson(request, _options)); + + var response = result!.ReadJson(_options); + + response.Should().NotBeNull(); + response!.Success.Should().BeTrue(); + } + + [Fact] + public async Task RemoveContextListenerReturnsPayloadNullError() + { + RemoveContextListenerRequest? request = null; + + var result = await _messageRouter.InvokeAsync( + Fdc3Topic.RemoveContextListener, + MessageBuffer.Factory.CreateJson(request, _options)); + + var response = result!.ReadJson(_options); + + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.PayloadNull); + } + + [Fact] + public async Task RemoveContextListenerReturnsMissingIdError() + { + var request = new RemoveContextListenerRequest + { + ContextType = null, + Fdc3InstanceId = "dummyId", + ListenerId = Guid.NewGuid().ToString(), + }; + + var result = await _messageRouter.InvokeAsync( + Fdc3Topic.RemoveContextListener, + MessageBuffer.Factory.CreateJson(request, _options)); + + var response = result!.ReadJson(_options); + + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.MissingId); + } + + [Fact] + public async Task RemoveContextListenerReturnsListenerNotFoundError() + { + //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id + var origin = await _moduleLoader.StartModule(new StartRequest("appId1")); + var originFdc3InstanceId = Fdc3InstanceIdRetriever.Get(origin); + + var addContextListenerRequest = new AddContextListenerRequest + { + Fdc3InstanceId = originFdc3InstanceId, + ChannelId = "fdc3.channel.1", + ChannelType = ChannelType.User, + ContextType = "fdc3.instrument" + }; + + var addContextListenerResult = await _messageRouter.InvokeAsync( + Fdc3Topic.AddContextListener, + MessageBuffer.Factory.CreateJson(addContextListenerRequest, _options)); + + var addContextListenerResponse = addContextListenerResult!.ReadJson(_options); + + addContextListenerResponse.Should().NotBeNull(); + addContextListenerResponse!.Success.Should().BeTrue(); + + var request = new RemoveContextListenerRequest + { + ContextType = null, + Fdc3InstanceId = originFdc3InstanceId, + ListenerId = addContextListenerResponse.Id!, + }; + + var result = await _messageRouter.InvokeAsync( + Fdc3Topic.RemoveContextListener, + MessageBuffer.Factory.CreateJson(request, _options)); + + var response = result!.ReadJson(_options); + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.ListenerNotFound); + } + + [Fact] + public async Task RemoveContextListenerSuccessfullyRemovesContextListener() + { + //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id + var origin = await _moduleLoader.StartModule(new StartRequest("appId1")); + var originFdc3InstanceId = Fdc3InstanceIdRetriever.Get(origin); + + var addContextListenerRequest = new AddContextListenerRequest + { + Fdc3InstanceId = originFdc3InstanceId, + ChannelId = "fdc3.channel.1", + ChannelType = ChannelType.User, + ContextType = null + }; + + var addContextListenerResult = await _messageRouter.InvokeAsync( + Fdc3Topic.AddContextListener, + MessageBuffer.Factory.CreateJson(addContextListenerRequest, _options)); + + var addContextListenerResponse = addContextListenerResult!.ReadJson(_options); + addContextListenerResponse.Should().NotBeNull(); + addContextListenerResponse!.Success.Should().BeTrue(); + + var request = new RemoveContextListenerRequest + { + ContextType = null, + Fdc3InstanceId = originFdc3InstanceId, + ListenerId = addContextListenerResponse.Id!, + }; + + var result = await _messageRouter.InvokeAsync( + Fdc3Topic.RemoveContextListener, + MessageBuffer.Factory.CreateJson(request, _options)); + + var response = result!.ReadJson(_options); + + response.Should().NotBeNull(); + response!.Success.Should().BeTrue(); + } + private MessageBuffer GetContext() { return MessageBuffer.Factory.CreateJson( diff --git a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.DesktopAgent.Tests/Fdc3DesktopAgentTests.cs b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.DesktopAgent.Tests/Fdc3DesktopAgentTests.cs index bb742c34c..5b00e69eb 100644 --- a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.DesktopAgent.Tests/Fdc3DesktopAgentTests.cs +++ b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.DesktopAgent.Tests/Fdc3DesktopAgentTests.cs @@ -1209,4 +1209,148 @@ public async Task GetAppMetadata_returns_AppMetadata_based_on_appId() Name = "app1" }); } + + [Fact] + public async Task AddContextListener_returns_payload_null_error() + { + AddContextListenerRequest? request = null; + + var response = await _fdc3.AddContextListener(request); + + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.PayloadNull); + } + + [Fact] + public async Task AddContextListener_returns_missing_id_error() + { + var request = new AddContextListenerRequest + { + Fdc3InstanceId = "dummyId", + ChannelId = "fdc3.channel.1", + ChannelType = ChannelType.User + }; + + var response = await _fdc3.AddContextListener(request); + + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.MissingId); + } + + [Fact] + public async Task AddContextListener_successfully_registers_context_listener() + { + await _fdc3.StartAsync(CancellationToken.None); + + //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id + var origin = await _mockModuleLoader.Object.StartModule(new StartRequest("appId1")); + var originFdc3InstanceId = Fdc3InstanceIdRetriever.Get(origin); + + var request = new AddContextListenerRequest + { + Fdc3InstanceId = originFdc3InstanceId, + ChannelId = "fdc3.channel.1", + ChannelType = ChannelType.User + }; + + var response = await _fdc3.AddContextListener(request); + + response.Should().NotBeNull(); + response!.Success.Should().BeTrue(); + } + + [Fact] + public async Task RemoveContextListener_returns_payload_null_error() + { + RemoveContextListenerRequest? request = null; + + var response = await _fdc3.RemoveContextListener(request); + + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.PayloadNull); + } + + [Fact] + public async Task RemoveContextListener_returns_missing_id_error() + { + var request = new RemoveContextListenerRequest + { + ContextType = null, + Fdc3InstanceId = "dummyId", + ListenerId = Guid.NewGuid().ToString(), + }; + + var response = await _fdc3.RemoveContextListener(request); + + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.MissingId); + } + + [Fact] + public async Task RemoveContextListener_returns_listener_not_found_error() + { + await _fdc3.StartAsync(CancellationToken.None); + + //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id + var origin = await _mockModuleLoader.Object.StartModule(new StartRequest("appId1")); + var originFdc3InstanceId = Fdc3InstanceIdRetriever.Get(origin); + + var addContextListenerRequest = new AddContextListenerRequest + { + Fdc3InstanceId = originFdc3InstanceId, + ChannelId = "fdc3.channel.1", + ChannelType = ChannelType.User, + ContextType = "fdc3.instrument" + }; + + var addContextListenerResponse = await _fdc3.AddContextListener(addContextListenerRequest); + addContextListenerResponse.Should().NotBeNull(); + addContextListenerResponse!.Success.Should().BeTrue(); + + var request = new RemoveContextListenerRequest + { + ContextType = null, + Fdc3InstanceId = originFdc3InstanceId, + ListenerId = addContextListenerResponse.Id!, + }; + + var response = await _fdc3.RemoveContextListener(request); + + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.ListenerNotFound); + } + + [Fact] + public async Task RemoveContextListener_successfully_removes_context_listener() + { + await _fdc3.StartAsync(CancellationToken.None); + + //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id + var origin = await _mockModuleLoader.Object.StartModule(new StartRequest("appId1")); + var originFdc3InstanceId = Fdc3InstanceIdRetriever.Get(origin); + + var addContextListenerRequest = new AddContextListenerRequest + { + Fdc3InstanceId = originFdc3InstanceId, + ChannelId = "fdc3.channel.1", + ChannelType = ChannelType.User, + ContextType = null + }; + + var addContextListenerResponse = await _fdc3.AddContextListener(addContextListenerRequest); + addContextListenerResponse.Should().NotBeNull(); + addContextListenerResponse!.Success.Should().BeTrue(); + + var request = new RemoveContextListenerRequest + { + ContextType = null, + Fdc3InstanceId = originFdc3InstanceId, + ListenerId = addContextListenerResponse.Id!, + }; + + var response = await _fdc3.RemoveContextListener(request); + + response.Should().NotBeNull(); + response!.Success.Should().BeTrue(); + } } \ No newline at end of file diff --git a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.DesktopAgent.Tests/Infrastructure/Internal/Fdc3DesktopAgentMessageRouterService.Tests.cs b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.DesktopAgent.Tests/Infrastructure/Internal/Fdc3DesktopAgentMessageRouterService.Tests.cs index 752bed751..a9e6a41be 100644 --- a/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.DesktopAgent.Tests/Infrastructure/Internal/Fdc3DesktopAgentMessageRouterService.Tests.cs +++ b/src/fdc3/dotnet/DesktopAgent/test/MorganStanley.ComposeUI.DesktopAgent.Tests/Infrastructure/Internal/Fdc3DesktopAgentMessageRouterService.Tests.cs @@ -1540,6 +1540,150 @@ public async Task HandleGetAppMetadata_returns_AppMetadata_based_on_appId() }); } + [Fact] + public async Task HandleAddContextListener_returns_payload_null_error() + { + AddContextListenerRequest? request = null; + + var response = await _fdc3.HandleAddContextListener(request, new()); + + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.PayloadNull); + } + + [Fact] + public async Task HandleAddContextListener_returns_missing_id_error() + { + var request = new AddContextListenerRequest + { + Fdc3InstanceId = "dummyId", + ChannelId = "fdc3.channel.1", + ChannelType = ChannelType.User + }; + + var response = await _fdc3.HandleAddContextListener(request, new()); + + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.MissingId); + } + + [Fact] + public async Task HandleAddContextListener_successfully_registers_context_listener() + { + await _fdc3.StartAsync(CancellationToken.None); + + //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id + var origin = await _mockModuleLoader.Object.StartModule(new StartRequest("appId1")); + var originFdc3InstanceId = Fdc3InstanceIdRetriever.Get(origin); + + var request = new AddContextListenerRequest + { + Fdc3InstanceId = originFdc3InstanceId, + ChannelId = "fdc3.channel.1", + ChannelType = ChannelType.User + }; + + var response = await _fdc3.HandleAddContextListener(request, new()); + + response.Should().NotBeNull(); + response!.Success.Should().BeTrue(); + } + + [Fact] + public async Task HandleRemoveContextListener_returns_payload_null_error() + { + RemoveContextListenerRequest? request = null; + + var response = await _fdc3.HandleRemoveContextListener(request, new()); + + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.PayloadNull); + } + + [Fact] + public async Task HandleRemoveContextListener_returns_missing_id_error() + { + var request = new RemoveContextListenerRequest + { + ContextType = null, + Fdc3InstanceId = "dummyId", + ListenerId = Guid.NewGuid().ToString(), + }; + + var response = await _fdc3.HandleRemoveContextListener(request, new()); + + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.MissingId); + } + + [Fact] + public async Task HandleRemoveContextListener_returns_listener_not_found_error() + { + await _fdc3.StartAsync(CancellationToken.None); + + //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id + var origin = await _mockModuleLoader.Object.StartModule(new StartRequest("appId1")); + var originFdc3InstanceId = Fdc3InstanceIdRetriever.Get(origin); + + var addContextListenerRequest = new AddContextListenerRequest + { + Fdc3InstanceId = originFdc3InstanceId, + ChannelId = "fdc3.channel.1", + ChannelType = ChannelType.User, + ContextType = "fdc3.instrument" + }; + + var addContextListenerResponse = await _fdc3.HandleAddContextListener(addContextListenerRequest, new()); + addContextListenerResponse.Should().NotBeNull(); + addContextListenerResponse!.Success.Should().BeTrue(); + + var request = new RemoveContextListenerRequest + { + ContextType = null, + Fdc3InstanceId = originFdc3InstanceId, + ListenerId = addContextListenerResponse.Id!, + }; + + var response = await _fdc3.HandleRemoveContextListener(request, new()); + + response.Should().NotBeNull(); + response!.Error.Should().Be(Fdc3DesktopAgentErrors.ListenerNotFound); + } + + [Fact] + public async Task HandleRemoveContextListener_successfully_removes_context_listener() + { + await _fdc3.StartAsync(CancellationToken.None); + + //TODO: should add some identifier to the query => "fdc3:" + instance.Manifest.Id + var origin = await _mockModuleLoader.Object.StartModule(new StartRequest("appId1")); + var originFdc3InstanceId = Fdc3InstanceIdRetriever.Get(origin); + + var addContextListenerRequest = new AddContextListenerRequest + { + Fdc3InstanceId = originFdc3InstanceId, + ChannelId = "fdc3.channel.1", + ChannelType = ChannelType.User, + ContextType = null + }; + + var addContextListenerResponse = await _fdc3.HandleAddContextListener(addContextListenerRequest, new()); + addContextListenerResponse.Should().NotBeNull(); + addContextListenerResponse!.Success.Should().BeTrue(); + + var request = new RemoveContextListenerRequest + { + ContextType = null, + Fdc3InstanceId = originFdc3InstanceId, + ListenerId = addContextListenerResponse.Id!, + }; + + var response = await _fdc3.HandleRemoveContextListener(request, new()); + + response.Should().NotBeNull(); + response!.Success.Should().BeTrue(); + } + [Theory] [ClassData(typeof(FindIntentTheoryData))] public async Task HandleFindIntent_edge_case_tests(FindIntentTestCase testCase) diff --git a/src/fdc3/js/composeui-fdc3/src/ComposeUIChannel.spec.ts b/src/fdc3/js/composeui-fdc3/src/ComposeUIChannel.spec.ts index 52a9ae2e8..3f3609e44 100644 --- a/src/fdc3/js/composeui-fdc3/src/ComposeUIChannel.spec.ts +++ b/src/fdc3/js/composeui-fdc3/src/ComposeUIChannel.spec.ts @@ -18,6 +18,7 @@ import { ComposeUIContextListener } from './infrastructure/ComposeUIContextListe import { ComposeUITopic } from './infrastructure/ComposeUITopic'; import { Channel, ChannelError, Context } from '@finos/fdc3'; import { ComposeUIDesktopAgent } from './ComposeUIDesktopAgent'; +import { Fdc3AddContextListenerResponse } from './infrastructure/messages/Fdc3AddContextListenerResponse'; const dummyChannelId = "dummyId"; let messageRouterClient: MessageRouter; @@ -35,6 +36,22 @@ const contextMessageHandlerMock = jest.fn((_) => { describe('Tests for ComposeUIChannel implementation API', () => { beforeEach(() => { + + window.composeui = { + fdc3: { + config: { + appId: "testAppId", + instanceId: "testInstanceId" + }, + channelId : "test" + } + }; + + const response: Fdc3AddContextListenerResponse = { + success: true, + id: "testListenerId" + }; + messageRouterClient = { clientId: "dummy", subscribe: jest.fn(() => { @@ -47,7 +64,8 @@ describe('Tests for ComposeUIChannel implementation API', () => { unregisterEndpoint: jest.fn(() => { return Promise.resolve() }), registerService: jest.fn(() => { return Promise.resolve() }), unregisterService: jest.fn(() => { return Promise.resolve() }), - invoke: jest.fn(() => Promise.resolve("")) + invoke: jest.fn(() => { return Promise.resolve(`${JSON.stringify(undefined)}`) }) + .mockImplementationOnce(() => Promise.resolve(`${JSON.stringify(response)}`)) }; testChannel = new ComposeUIChannel(dummyChannelId, "user", messageRouterClient); @@ -60,27 +78,65 @@ describe('Tests for ComposeUIChannel implementation API', () => { }); it('broadcast will set the lastContext to test instrument', async () => { + const messageRouterClientMock = { + clientId: "dummy", + subscribe: jest.fn(() => { + return Promise.resolve({ unsubscribe: () => { } }); + }), + + publish: jest.fn(() => { return Promise.resolve() }), + connect: jest.fn(() => { return Promise.resolve() }), + registerEndpoint: jest.fn(() => { return Promise.resolve() }), + unregisterEndpoint: jest.fn(() => { return Promise.resolve() }), + registerService: jest.fn(() => { return Promise.resolve() }), + unregisterService: jest.fn(() => { return Promise.resolve() }), + invoke: jest.fn(() => { return Promise.resolve(`${JSON.stringify(testInstrument)}`) }) + }; + + testChannel = new ComposeUIChannel(dummyChannelId, "user", messageRouterClientMock); + await testChannel.broadcast(testInstrument); const resultContext = await testChannel.getCurrentContext(); - expect(messageRouterClient.publish).toHaveBeenCalledTimes(1); - expect(messageRouterClient.publish).toHaveBeenCalledWith(ComposeUITopic.broadcast(dummyChannelId, "user"), JSON.stringify(testInstrument)); + expect(messageRouterClientMock.publish).toHaveBeenCalledTimes(1); + expect(messageRouterClientMock.publish).toHaveBeenCalledWith(ComposeUITopic.broadcast(dummyChannelId, "user"), JSON.stringify(testInstrument)); expect(resultContext).toMatchObject(testInstrument); }); it('getCurrentContext will overwrite the lastContext of the same type', async () => { - await testChannel.broadcast(testInstrument); const testInstrument2 = { type: 'fdc3.instrument', id: { ticker: 'SMSN' } }; + + const messageRouterClientMock = { + clientId: "dummy", + subscribe: jest.fn(() => { + return Promise.resolve({ unsubscribe: () => { } }); + }), + + publish: jest.fn(() => { return Promise.resolve() }), + connect: jest.fn(() => { return Promise.resolve() }), + registerEndpoint: jest.fn(() => { return Promise.resolve() }), + unregisterEndpoint: jest.fn(() => { return Promise.resolve() }), + registerService: jest.fn(() => { return Promise.resolve() }), + unregisterService: jest.fn(() => { return Promise.resolve() }), + invoke: jest.fn(() => { return Promise.resolve(`${JSON.stringify(testInstrument)}`) }) + .mockImplementationOnce(() => {return Promise.resolve(`${JSON.stringify(testInstrument2)}`)}) + .mockImplementationOnce(() => {return Promise.resolve(`${JSON.stringify(testInstrument2)}`)}) + }; + + testChannel = new ComposeUIChannel(dummyChannelId, "user", messageRouterClientMock); + + await testChannel.broadcast(testInstrument); await testChannel.broadcast(testInstrument2); + const resultContext = await testChannel.getCurrentContext(); const resultContextWithContextType = await testChannel.getCurrentContext(testInstrument2.type); - expect(messageRouterClient.publish).toBeCalledTimes(2); - expect(messageRouterClient.publish).toHaveBeenCalledWith(ComposeUITopic.broadcast(dummyChannelId, "user"), JSON.stringify(testInstrument)); - expect(messageRouterClient.publish).toHaveBeenCalledWith(ComposeUITopic.broadcast(dummyChannelId, "user"), JSON.stringify(testInstrument2)); + expect(messageRouterClientMock.publish).toBeCalledTimes(2); + expect(messageRouterClientMock.publish).toHaveBeenCalledWith(ComposeUITopic.broadcast(dummyChannelId, "user"), JSON.stringify(testInstrument)); + expect(messageRouterClientMock.publish).toHaveBeenCalledWith(ComposeUITopic.broadcast(dummyChannelId, "user"), JSON.stringify(testInstrument2)); expect(resultContext).toMatchObject(testInstrument2); expect(resultContextWithContextType).toMatchObject>(testInstrument2); }); @@ -96,11 +152,11 @@ describe('Tests for ComposeUIChannel implementation API', () => { await testChannel.broadcast(testInstrument); const resultListener = await testChannel.addContextListener('fdc3.instrument', contextMessageHandlerMock); expect(resultListener).toBeInstanceOf(ComposeUIContextListener); - expect(contextMessageHandlerMock).toHaveBeenCalledTimes(0); //as per the standard + expect(contextMessageHandlerMock).toHaveBeenCalledTimes(0); }); // TODO: This doesn't test what it sais it tests - it('addContextListener will treat contexType is ContextHandler as all types', async () => { + it('addContextListener will treat contextType is ContextHandler as all types', async () => { const resultListener = await testChannel.addContextListener(null, contextMessageHandlerMock); expect(resultListener).toBeInstanceOf(ComposeUIContextListener); expect(messageRouterClient.subscribe).toBeCalledTimes(1); diff --git a/src/fdc3/js/composeui-fdc3/src/ComposeUIContextListener.spec.ts b/src/fdc3/js/composeui-fdc3/src/ComposeUIContextListener.spec.ts index d676bc15c..5b45f317a 100644 --- a/src/fdc3/js/composeui-fdc3/src/ComposeUIContextListener.spec.ts +++ b/src/fdc3/js/composeui-fdc3/src/ComposeUIContextListener.spec.ts @@ -14,6 +14,8 @@ import { jest } from '@jest/globals'; import { MessageRouter } from '@morgan-stanley/composeui-messaging-client'; import { ComposeUIContextListener } from './infrastructure/ComposeUIContextListener'; +import { Fdc3AddContextListenerResponse } from './infrastructure/messages/Fdc3AddContextListenerResponse'; +import { Fdc3RemoveContextListenerResponse } from './infrastructure/messages/Fdc3RemoveContextListenerResponse'; const dummyContext = { type: "dummyContextType" }; const dummyChannelId = "dummyId"; @@ -37,6 +39,21 @@ const contextMessageHandlerMock = jest.fn((something) => { describe('Tests for ComposeUIContextListener implementation API', () => { beforeEach(async () => { + window.composeui = { + fdc3: { + config: { + appId: "testAppId", + instanceId: "testInstanceId" + }, + channelId : "test" + } + }; + + const response: Fdc3AddContextListenerResponse = { + success: true, + id: "testListenerId" + }; + messageRouterClient = { clientId: "dummy", subscribe: jest.fn(() => { @@ -49,11 +66,13 @@ describe('Tests for ComposeUIContextListener implementation API', () => { unregisterEndpoint: jest.fn(() => { return Promise.resolve() }), registerService: jest.fn(() => { return Promise.resolve() }), unregisterService: jest.fn(() => { return Promise.resolve() }), - invoke: jest.fn(() => { return Promise.resolve(JSON.stringify({ context: "", payload: `${JSON.stringify(dummyContext)}` })) }) + invoke: jest.fn(() => { return Promise.resolve(`${JSON.stringify(undefined)}`) }) + .mockImplementationOnce(() => Promise.resolve(`${JSON.stringify(response)}`)) + .mockImplementationOnce(() => Promise.resolve(JSON.stringify({ context: "", payload: `${JSON.stringify(dummyContext)}` }))) }; - testListener = new ComposeUIContextListener(messageRouterClient, contextMessageHandlerMock, dummyChannelId, "user", "fdc3.instrument"); - await testListener.subscribe(); + testListener = new ComposeUIContextListener(messageRouterClient, contextMessageHandlerMock, "fdc3.instrument"); + await testListener.subscribe(dummyChannelId, "user"); }); it('subscribe will call messagerouter subscribe method', async () => { @@ -66,6 +85,7 @@ describe('Tests for ComposeUIContextListener implementation API', () => { }); it('handleContextMessage will be rejected with Error if unsubscribed', async () => { + testListener = new ComposeUIContextListener(messageRouterClient, contextMessageHandlerMock, "fdc3.instrument"); testListener.unsubscribe(); await expect(testListener.handleContextMessage(testInstrument)) .rejects diff --git a/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.spec.ts b/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.spec.ts index f8e47047a..2ecf30083 100644 --- a/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.spec.ts +++ b/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.spec.ts @@ -17,7 +17,7 @@ import { MessageRouter } from '@morgan-stanley/composeui-messaging-client'; import { ComposeUIContextListener } from './infrastructure/ComposeUIContextListener'; import { ComposeUIDesktopAgent } from './ComposeUIDesktopAgent'; import { ComposeUITopic } from './infrastructure/ComposeUITopic'; -import { Channel, ChannelError, DesktopAgent } from '@finos/fdc3'; +import { Channel, ChannelError, ContextHandler, DesktopAgent } from '@finos/fdc3'; import { ComposeUIErrors } from './infrastructure/ComposeUIErrors'; import { ChannelFactory } from './infrastructure/ChannelFactory'; import { ComposeUIPrivateChannel } from './infrastructure/ComposeUIPrivateChannel'; @@ -78,6 +78,7 @@ describe('Tests for ComposeUIDesktopAgent implementation API', () => { createAppChannel: jest.fn(() => Promise.reject("Not implemented")), joinUserChannel: jest.fn(() => Promise.resolve(new ComposeUIChannel(dummyChannelId, "user", messageRouterClient))), getUserChannels: jest.fn(() => Promise.reject("Not implemented")), + getContextListener: jest.fn((channel: Channel, handler: ContextHandler, contextType?: string) => {return Promise.resolve(new ComposeUIContextListener(messageRouterClient, handler, contextType))}) }; desktopAgent = new ComposeUIDesktopAgent(messageRouterClient, channelFactory); @@ -104,20 +105,6 @@ describe('Tests for ComposeUIDesktopAgent implementation API', () => { .toThrow("The current channel has not been set."); }); - it('addContextListener will trigger messageRouter subscribe method', async () => { - const resultListener = await desktopAgent.addContextListener("fdc3.instrument", contextMessageHandlerMock); - expect(resultListener).toBeInstanceOf(ComposeUIContextListener); - expect(messageRouterClient.subscribe).toBeCalledTimes(1); - }); - - it('addContextListener will fail as the current channel is not defined', async () => { - await desktopAgent.leaveCurrentChannel(); - await expect(desktopAgent.addContextListener("fdc3.instrument", contextMessageHandlerMock)) - .rejects - .toThrow("The current channel has not been set."); - expect(messageRouterClient.subscribe).toBeCalledTimes(0); - }); - it('default channel can be retrieved', async () => { var result = await desktopAgent.getCurrentChannel(); expect(result).toMatchObject>({ id: dummyChannelId, type: "user" }); diff --git a/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.ts b/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.ts index 5dd9de99d..aabba49fd 100644 --- a/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.ts +++ b/src/fdc3/js/composeui-fdc3/src/ComposeUIDesktopAgent.ts @@ -42,7 +42,7 @@ export class ComposeUIDesktopAgent implements DesktopAgent { private userChannels: Channel[] = []; private privateChannels: Channel[] = []; private currentChannel?: Channel; - private currentChannelListeners: ComposeUIContextListener[] = []; + private topLevelContextListeners: ComposeUIContextListener[] = []; private intentListeners: Listener[] = []; private channelFactory: ChannelFactory; private intentsClient: IntentsClient; @@ -101,25 +101,20 @@ export class ComposeUIDesktopAgent implements DesktopAgent { } public async addContextListener(contextType?: string | null | ContextHandler, handler?: ContextHandler): Promise { - if (!this.currentChannel) { - throw new Error(ComposeUIErrors.CurrentChannelNotSet); - } - if (contextType && typeof contextType != 'string') { handler = contextType; contextType = null; } - const listener = await this.currentChannel!.addContextListener(contextType ?? null, handler!); - - const lastContext = await this.currentChannel!.getCurrentContext(contextType ?? undefined) + const listener = await this.channelFactory.getContextListener(this.currentChannel, handler, contextType); + this.topLevelContextListeners.push(listener); - if (lastContext) { - //TODO: timing issue - setTimeout(async() => await listener.handleContextMessage(lastContext), 100); + if (!this.currentChannel) { + return listener; } - this.currentChannelListeners.push(listener); + await this.getLastContext(listener); + return listener; } @@ -127,7 +122,6 @@ export class ComposeUIDesktopAgent implements DesktopAgent { return await this.channelFactory.getUserChannels(); } - //TODO: add pending context listeners which were registered via the fdc3.addContextListener public async joinUserChannel(channelId: string): Promise { if (this.currentChannel) { //DesktopAgnet clients can listen on only one channel @@ -145,6 +139,11 @@ export class ComposeUIDesktopAgent implements DesktopAgent { this.addChannel(channel); this.currentChannel = channel; + + for (const listener of this.topLevelContextListeners) { + await listener.subscribe(this.currentChannel.id, this.currentChannel.type); + await this.getLastContext(listener); + } } public async getOrCreateChannel(channelId: string): Promise { @@ -167,14 +166,13 @@ export class ComposeUIDesktopAgent implements DesktopAgent { return this.currentChannel ?? null; } - //TODO: add messageRouter message that we are leaving the current channel to notify the backend. - //TODO: leave the current channel's listeners added via fdc3.addContextListener. public async leaveCurrentChannel(): Promise { + //The context listeners, that have been added through the `fdc3.addContextListener()` should unsubscribe + for (const listener of this.topLevelContextListeners) { + await listener.unsubscribe(); + } + this.currentChannel = undefined; - this.currentChannelListeners.forEach(listener => { - listener.unsubscribe(); - }); - this.currentChannelListeners = []; } public async getInfo(): Promise { @@ -211,4 +209,13 @@ export class ComposeUIDesktopAgent implements DesktopAgent { break; } } + + private async getLastContext(listener: ComposeUIContextListener) : Promise { + const lastContext = await this.currentChannel!.getCurrentContext(listener.contextType); + + if (lastContext) { + //TODO: timing issue + setTimeout(async() => await listener.handleContextMessage(lastContext), 100); + } + } } \ No newline at end of file diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/ChannelFactory.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/ChannelFactory.ts index d8df17228..7e984628e 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/ChannelFactory.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/ChannelFactory.ts @@ -11,7 +11,7 @@ * */ -import { Channel, IntentHandler, Listener, PrivateChannel } from "@finos/fdc3"; +import { Channel, ContextHandler, IntentHandler, Listener, PrivateChannel } from "@finos/fdc3"; import { ChannelType } from "./ChannelType"; export interface ChannelFactory { @@ -21,4 +21,5 @@ export interface ChannelFactory { joinUserChannel(channelId: string): Promise; getUserChannels(): Promise; getIntentListener(intent: string, handler: IntentHandler): Promise; + getContextListener(channel?: Channel, handler?: ContextHandler, contextType?: string | null): Promise; } \ No newline at end of file diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIChannel.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIChannel.ts index fe2dbfbcf..083deac01 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIChannel.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIChannel.ts @@ -17,6 +17,9 @@ import { ChannelType } from "./ChannelType"; import { ComposeUIContextListener } from "./ComposeUIContextListener"; import { Fdc3GetCurrentContextRequest } from "./messages/Fdc3GetCurrentContextRequest"; import { ComposeUITopic } from "./ComposeUITopic"; +import { ComposeUIErrors } from "./ComposeUIErrors"; +import { Fdc3AddContextListenerResponse } from "./messages/Fdc3AddContextListenerResponse"; +import { Fdc3AddContextListenerRequest } from "./messages/Fdc3AddContextListenerRequest"; export class ComposeUIChannel implements Channel { id: string; @@ -43,7 +46,6 @@ export class ComposeUIChannel implements Channel { await this.messageRouterClient.publish(topic, JSON.stringify(context)); } - //TODO add error public async getCurrentContext(contextType?: string | undefined): Promise { const message = JSON.stringify(new Fdc3GetCurrentContextRequest(contextType)); const response = await this.messageRouterClient.invoke(ComposeUITopic.getCurrentContext(this.id, this.type), message); @@ -77,8 +79,8 @@ export class ComposeUIChannel implements Channel { contextType = null; } - const listener = new ComposeUIContextListener(this.messageRouterClient, handler, this.id, this.type, contextType); - await listener.subscribe(); + const listener = new ComposeUIContextListener(this.messageRouterClient, handler, contextType); + await listener.subscribe(this.id, this.type); return listener; } } \ No newline at end of file diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIContextListener.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIContextListener.ts index 8e7a38c67..0b615d11d 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIContextListener.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIContextListener.ts @@ -11,34 +11,35 @@ * */ -import { Context, ContextHandler, Listener } from "@finos/fdc3"; +import { Context, ContextHandler, Listener, ResultError } from "@finos/fdc3"; import { MessageRouter, TopicMessage } from "@morgan-stanley/composeui-messaging-client"; import { ChannelType } from "./ChannelType"; import { Unsubscribable } from "rxjs"; import { ComposeUITopic } from "./ComposeUITopic"; +import { Fdc3RemoveContextListenerRequest } from "./messages/Fdc3RemoveContextListenerRequest"; +import { Fdc3RemoveContextListenerResponse } from "./messages/Fdc3RemoveContextListenerResponse"; +import { ComposeUIErrors } from "./ComposeUIErrors"; +import { Fdc3AddContextListenerRequest } from "./messages/Fdc3AddContextListenerRequest"; +import { Fdc3AddContextListenerResponse } from "./messages/Fdc3AddContextListenerResponse"; export class ComposeUIContextListener implements Listener { private readonly messageRouterClient: MessageRouter; private unsubscribable?: Unsubscribable; private readonly handler: ContextHandler; - private readonly channelId: string; - private readonly channelType: ChannelType; public readonly contextType?: string; private isSubscribed: boolean = false; + private id?: string; private unsubscribeCallback?: (x: ComposeUIContextListener) => void; - constructor(messageRouterClient: MessageRouter, handler: ContextHandler, channelId: string, channelType: ChannelType, contextType?: string) { + constructor(messageRouterClient: MessageRouter, handler: ContextHandler, contextType?: string) { this.messageRouterClient = messageRouterClient; this.handler = handler; - - this.channelId = channelId; - this.channelType = channelType; - this.contextType = contextType; } - public async subscribe(): Promise { - const subscribeTopic = ComposeUITopic.broadcast(this.channelId, this.channelType); + public async subscribe(channelId: string, channelType: ChannelType): Promise { + await this.registerContextListener(channelId, channelType); + const subscribeTopic = ComposeUITopic.broadcast(channelId, channelType); this.unsubscribable = await this.messageRouterClient.subscribe(subscribeTopic, (topicMessage: TopicMessage) => { if (topicMessage.context.sourceId == this.messageRouterClient.clientId) return; //TODO: integration test @@ -64,14 +65,59 @@ export class ComposeUIContextListener implements Listener { this.unsubscribeCallback = unsubscribeCallback; } - public unsubscribe(): void { + public async unsubscribe(): Promise { if (!this.unsubscribable || !this.isSubscribed) { return; } + + try { + await this.leaveChannel(); + } catch(err) { + console.log(err); + } + this.unsubscribable.unsubscribe(); this.isSubscribed = false; + if (this.unsubscribeCallback) { this.unsubscribeCallback(this); } } -} \ No newline at end of file + + private async registerContextListener(channelId: string, channelType: ChannelType) :Promise{ + const request = new Fdc3AddContextListenerRequest(window.composeui.fdc3.config?.instanceId!, this.contextType, channelId, channelType); + const response = await this.messageRouterClient.invoke(ComposeUITopic.addContextListener(), JSON.stringify(request)); + + if (!response) { + throw new Error(ComposeUIErrors.NoAnswerWasProvided); + } + + const result = JSON.parse(response); + if (result.error) { + throw new Error(result.error); + } else if (!result.success) { + throw new Error(ComposeUIErrors.SubscribeFailure); + } + + this.id = result.id! + } + + private async leaveChannel() : Promise { + const request = new Fdc3RemoveContextListenerRequest(window.composeui.fdc3.config?.instanceId!, this.id!, this.contextType); + const result = await this.messageRouterClient.invoke(ComposeUITopic.removeContextListener(), JSON.stringify(request)); + if (!result) { + throw new Error(ComposeUIErrors.NoAnswerWasProvided); + } + + const response = JSON.parse(result); + if (response.error) { + throw new Error(response.error); + } + + if (!response.success) { + throw new Error(ComposeUIErrors.UnsubscribeFailure); + } + + return; + } +} diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIErrors.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIErrors.ts index c9e61d52c..f9762881d 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIErrors.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUIErrors.ts @@ -15,6 +15,6 @@ export enum ComposeUIErrors { NoAnswerWasProvided = 'No answer was provided by the DesktopAgent backend.', InstanceIdNotFound = 'InstanceId was not found on window object. To run Fdc3\'s ComposeUI implementation instance config should be set on window config.', CurrentChannelNotSet = 'The current channel has not been set.', - UnsubscribeFailure = 'The IntentListener could not unsubscribe.', - SubscribeFailure = 'The IntentListener could not subscribe.' + UnsubscribeFailure = 'The Listener could not unsubscribe.', + SubscribeFailure = 'The Listener could not subscribe.' } \ No newline at end of file diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUITopic.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUITopic.ts index 5753013ac..c8cac912a 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUITopic.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/ComposeUITopic.ts @@ -36,6 +36,8 @@ export class ComposeUITopic { private static readonly getInfoSuffix = "getInfo"; private static readonly findInstancesSuffix = "findInstances"; private static readonly getAppMetadataSuffix = "getAppMetadata"; + private static readonly addContextListenerSuffix = "addContextListener"; + private static readonly removeContextListenerSuffix = "removeContextListener"; public static broadcast(channelId: string, channelType: ChannelType = "user"): string { return `${this.getChannelsTopicRootWithChannelId(channelId, channelType)}/${this.broadcastSuffix}`; @@ -112,6 +114,14 @@ export class ComposeUITopic { public static getAppMetadata(): string { return `${this.topicRoot}/${this.getAppMetadataSuffix}`; } + + public static addContextListener(): string { + return `${this.topicRoot}/${this.addContextListenerSuffix}`; + } + + public static removeContextListener(): string { + return `${this.topicRoot}/${this.removeContextListenerSuffix}`; + } private static getChannelsTopicRootWithChannelId(channelId: string, channelType: ChannelType): string { return `${this.getChannelsTopicRoot(channelType)}/${channelId}`; diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/MessageRouterChannelFactory.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/MessageRouterChannelFactory.ts index a5f1dc23b..1db0ff4a0 100644 --- a/src/fdc3/js/composeui-fdc3/src/infrastructure/MessageRouterChannelFactory.ts +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/MessageRouterChannelFactory.ts @@ -11,7 +11,7 @@ * */ -import { ChannelError, IntentHandler, Listener, PrivateChannel } from "@finos/fdc3"; +import { ChannelError, ContextHandler, IntentHandler, Listener, PrivateChannel } from "@finos/fdc3"; import { MessageRouter } from "@morgan-stanley/composeui-messaging-client"; import { Channel } from "@finos/fdc3"; import { ChannelFactory } from "./ChannelFactory"; @@ -33,7 +33,10 @@ import { Fdc3GetUserChannelsRequest } from "./messages/Fdc3GetUserChannelsReques import { Fdc3GetUserChannelsResponse } from "./messages/Fdc3GetUserChannelsResponse"; import { Fdc3JoinUserChannelRequest } from "./messages/Fdc3JoinUserChannelRequest"; import { Fdc3JoinUserChannelResponse } from "./messages/Fdc3JoinUserChannelResponse"; +import { Fdc3AddContextListenerRequest } from "./messages/Fdc3AddContextListenerRequest"; +import { Fdc3AddContextListenerResponse } from "./messages/Fdc3AddContextListenerResponse"; import { ChannelItem } from "./ChannelItem"; +import { ComposeUIContextListener } from "./ComposeUIContextListener"; export class MessageRouterChannelFactory implements ChannelFactory { private messageRouterClient: MessageRouter; @@ -163,4 +166,14 @@ export class MessageRouterChannelFactory implements ChannelFactory { return listener; } + + public async getContextListener(channel?: Channel, handler?: ContextHandler, contextType?: string | null): Promise { + if (channel) { + const listener = await channel.addContextListener(contextType ?? null, handler!); + return listener; + } + + const listener = new ComposeUIContextListener(this.messageRouterClient, handler!, contextType ?? undefined); + return listener; + } } \ No newline at end of file diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3AddContextListenerRequest.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3AddContextListenerRequest.ts new file mode 100644 index 000000000..bb872a11a --- /dev/null +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3AddContextListenerRequest.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +import { ChannelType } from "../ChannelType"; + +export class Fdc3AddContextListenerRequest { + constructor( + public readonly fdc3InstanceId: string, + public readonly contextType?: string | null, + public readonly channelId?: string, + public readonly channelType?: ChannelType + ) {} +} \ No newline at end of file diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3AddContextListenerResponse.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3AddContextListenerResponse.ts new file mode 100644 index 000000000..604916b6d --- /dev/null +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3AddContextListenerResponse.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export interface Fdc3AddContextListenerResponse { + id?: string; + error?: string; + success: boolean; +} \ No newline at end of file diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3RemoveContextListenerRequest.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3RemoveContextListenerRequest.ts new file mode 100644 index 000000000..d56ea547f --- /dev/null +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3RemoveContextListenerRequest.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export class Fdc3RemoveContextListenerRequest { + constructor( + public readonly fdc3InstanceId: string, + public readonly listenerId: string, + public readonly contextType?: string, + ) {} +} \ No newline at end of file diff --git a/src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3RemoveContextListenerResponse.ts b/src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3RemoveContextListenerResponse.ts new file mode 100644 index 000000000..b7e400af6 --- /dev/null +++ b/src/fdc3/js/composeui-fdc3/src/infrastructure/messages/Fdc3RemoveContextListenerResponse.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + + +export interface Fdc3RemoveContextListenerResponse { + error?: string; + success: boolean; +} \ No newline at end of file