diff --git a/src/ChromeProtocol.Core/DomainEventHandler.cs b/src/ChromeProtocol.Core/DomainEventHandler.cs index 1a2acc9..8e4ef0d 100644 --- a/src/ChromeProtocol.Core/DomainEventHandler.cs +++ b/src/ChromeProtocol.Core/DomainEventHandler.cs @@ -1,4 +1,7 @@ namespace ChromeProtocol.Core; -public delegate Task DomainEventHandler(TEvent @event) +public delegate void SyncDomainEventHandler(TEvent @event) + where TEvent : IEvent; + +public delegate Task AsyncDomainEventHandler(TEvent @event) where TEvent : IEvent; diff --git a/src/ChromeProtocol.Runtime/Messaging/IProtocolClient.cs b/src/ChromeProtocol.Runtime/Messaging/IProtocolClient.cs index 272d686..6a9884a 100644 --- a/src/ChromeProtocol.Runtime/Messaging/IProtocolClient.cs +++ b/src/ChromeProtocol.Runtime/Messaging/IProtocolClient.cs @@ -6,18 +6,24 @@ namespace ChromeProtocol.Runtime.Messaging; public interface IProtocolClient : IDisposable { event EventHandler OnConnected; - event EventHandler OnDisconnected; - event EventHandler> OnRequestSent; event EventHandler> OnResponseReceived; event EventHandler> OnEventReceived; Task ConnectAsync(CancellationToken token = default); + [Obsolete("Use Dispose of the ConnectAsync return object.")] Task DisconnectAsync(CancellationToken token = default); - void ListenEvent(DomainEventHandler handler) + [Obsolete("Use SubscribeSync or SubscribeAsync instead.")] + void ListenEvent(AsyncDomainEventHandler handler) + where TEvent : IEvent; + + IDisposable SubscribeAsync(AsyncDomainEventHandler handler) + where TEvent : IEvent; + + IDisposable SubscribeSync(SyncDomainEventHandler handler) where TEvent : IEvent; Task SendCommandAsync(ICommand command, string? sessionId = default, CancellationToken? token = default) @@ -26,4 +32,4 @@ Task SendCommandAsync(ICommand command, string? Task FireCommandAsync(ICommand command, string? sessionId = default, CancellationToken token = default); IScopedProtocolClient CreateScoped(string sessionId); -} \ No newline at end of file +} diff --git a/src/ChromeProtocol.Runtime/Messaging/IScopedProtocolClient.cs b/src/ChromeProtocol.Runtime/Messaging/IScopedProtocolClient.cs index 65adf87..0f37b9f 100644 --- a/src/ChromeProtocol.Runtime/Messaging/IScopedProtocolClient.cs +++ b/src/ChromeProtocol.Runtime/Messaging/IScopedProtocolClient.cs @@ -6,11 +6,18 @@ public interface IScopedProtocolClient : IDisposable { public string SessionId { get; } - void ListenEvent(DomainEventHandler handler) + [Obsolete("Use SubscribeSync or SubscribeAsync instead.")] + void ListenEvent(AsyncDomainEventHandler handler) + where TEvent : IEvent; + + IDisposable SubscribeAsync(AsyncDomainEventHandler handler) + where TEvent : IEvent; + + IDisposable SubscribeSync(SyncDomainEventHandler handler) where TEvent : IEvent; Task SendCommandAsync(ICommand command, CancellationToken? token = default) where TResponse : IType; Task FireCommandAsync(ICommand command, CancellationToken token = default); -} \ No newline at end of file +} diff --git a/src/ChromeProtocol.Runtime/Messaging/WebSockets/ScopedProtocolClient.cs b/src/ChromeProtocol.Runtime/Messaging/WebSockets/ScopedProtocolClient.cs index 2c60384..eaac0a5 100644 --- a/src/ChromeProtocol.Runtime/Messaging/WebSockets/ScopedProtocolClient.cs +++ b/src/ChromeProtocol.Runtime/Messaging/WebSockets/ScopedProtocolClient.cs @@ -14,8 +14,14 @@ public ScopedProtocolClient(IProtocolClient mainClient, string sessionId) } // TODO: Support session-specific events - public void ListenEvent(DomainEventHandler handler) where TEvent : IEvent => - _mainClient.ListenEvent(handler); + public void ListenEvent(AsyncDomainEventHandler handler) where TEvent : IEvent => + SubscribeAsync(handler); + + public IDisposable SubscribeAsync(AsyncDomainEventHandler handler) where TEvent : IEvent => + _mainClient.SubscribeAsync(handler); + + public IDisposable SubscribeSync(SyncDomainEventHandler handler) where TEvent : IEvent => + _mainClient.SubscribeSync(handler); public Task SendCommandAsync(ICommand command, CancellationToken? token = default) where TResponse : IType => _mainClient.SendCommandAsync(command, SessionId, token); @@ -26,4 +32,4 @@ public Task FireCommandAsync(ICommand command, CancellationToken token = default public void Dispose() { } -} \ No newline at end of file +} diff --git a/src/ChromeProtocol.Runtime/Messaging/WebSockets/WebSocketProtocolClient.cs b/src/ChromeProtocol.Runtime/Messaging/WebSockets/WebSocketProtocolClient.cs index 531aa59..4196b53 100644 --- a/src/ChromeProtocol.Runtime/Messaging/WebSockets/WebSocketProtocolClient.cs +++ b/src/ChromeProtocol.Runtime/Messaging/WebSockets/WebSocketProtocolClient.cs @@ -62,16 +62,33 @@ public async Task DisconnectAsync(CancellationToken token = default) OnDisconnected?.Invoke(this, EventArgs.Empty); } - public void ListenEvent(DomainEventHandler handler) where TEvent : IEvent + public void ListenEvent(AsyncDomainEventHandler handler) where TEvent : IEvent { - Func, Task> HandleProtocolEvent(DomainEventHandler eventHandler) => + SubscribeAsync(handler); + } + + public IDisposable SubscribeAsync(AsyncDomainEventHandler handler) where TEvent : IEvent + { + Func, Task> HandleProtocolEvent(AsyncDomainEventHandler eventHandler) => async rawEvent => { var eventItself = rawEvent.Params.ToObject(); await eventHandler(eventItself).ConfigureAwait(false); }; - _eventHandlers.AddOrUpdate(GetMethodName(typeof(TEvent)), HandleProtocolEvent(handler), (_, existing) => existing + HandleProtocolEvent(handler)); + return SubscribeInternal(HandleProtocolEvent(handler)); + } + + public IDisposable SubscribeSync(SyncDomainEventHandler handler) where TEvent : IEvent + { + Func, Task> HandleProtocolEvent(SyncDomainEventHandler eventHandler) => + rawEvent => + { + var eventItself = rawEvent.Params.ToObject(); + return Task.Run(() => eventHandler(eventItself)); + }; + + return SubscribeInternal(HandleProtocolEvent(handler)); } public async Task SendCommandAsync(ICommand command, @@ -123,6 +140,14 @@ private async Task FireInternalAsync(int id, string methodName, ICommand command } } + private IDisposable SubscribeInternal(Func, Task> rawHandler) where TEvent : IEvent + { + var eventName = GetMethodName(typeof(TEvent)); + var subscription = new ProtocolSubscription(eventName, rawHandler, this); + _eventHandlers.AddOrUpdate(GetMethodName(typeof(TEvent)), rawHandler, (_, existing) => existing + rawHandler); + return subscription; + } + private void StartOutgoingWorker(CancellationToken token) { new Thread(() => @@ -216,4 +241,35 @@ private async Task ProcessOutgoingRequest(ProtocolRequest request) private static string GetMethodName(MemberInfo type) => type.GetCustomAttribute()?.MethodName ?? throw new Exception($"{nameof(MethodNameAttribute)} is required on type {type.Name} but it is not presented."); -} \ No newline at end of file + + private class ProtocolSubscription : IDisposable where T : WebSocket + { + private readonly string _eventName; + private readonly Func, Task>? _wrappedHandler; + private readonly WebSocketProtocolClient _client; + + public ProtocolSubscription(string eventName, Func,Task>? wrappedHandler, WebSocketProtocolClient client) + { + _eventName = eventName; + _wrappedHandler = wrappedHandler; + _client = client; + } + + public void Dispose() + { + if (_client._eventHandlers.TryGetValue(_eventName, out var aggregatedHandlers)) + { + var updatedHandlers = aggregatedHandlers - _wrappedHandler; + if (updatedHandlers is null) + { + _client._eventHandlers.TryRemove(_eventName, out _); + } + else + { + _client._eventHandlers[_eventName] = updatedHandlers; + } + } + } + } +} + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 991a3d5..039c03f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - 1.0.0-preview3 + 1.0.0-preview4 seclerp true