// ReSharper disable InconsistentNaming namespace ElectronNET.API { using Common; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading.Tasks; public abstract class ApiBase { protected enum SocketTaskEventNameTypes { DashesLowerFirst, NoDashUpperFirst } protected enum SocketTaskMessageNameTypes { DashesLowerFirst, NoDashUpperFirst } protected enum SocketEventNameTypes { DashedLower, CamelCase, } private const int InvocationTimeout = 1000; private readonly string objectName; private readonly ConcurrentDictionary invocators; private readonly ConcurrentDictionary invocationEventNames = new(); private readonly ConcurrentDictionary invocationMessageNames = new(); private readonly ConcurrentDictionary methodMessageNames = new(); private static readonly ConcurrentDictionary eventContainers = new(); private static readonly ConcurrentDictionary> AllInvocators = new(); private readonly object objLock = new object(); public virtual int Id { get => -1; // ReSharper disable once ValueParameterNotUsed protected set { } } protected abstract SocketTaskEventNameTypes SocketTaskEventNameType { get; } protected virtual SocketTaskMessageNameTypes SocketTaskMessageNameType => SocketTaskMessageNameTypes.NoDashUpperFirst; protected virtual SocketEventNameTypes SocketEventNameType => SocketEventNameTypes.DashedLower; protected ApiBase() { this.objectName = this.GetType().Name.LowerFirst(); this.invocators = AllInvocators.GetOrAdd(this.objectName, _ => new ConcurrentDictionary()); } protected void CallMethod0([CallerMemberName] string callerName = null) { var messageName = this.methodMessageNames.GetOrAdd(callerName, s => this.objectName + s); if (this.Id >= 0) { BridgeConnector.Socket.Emit(messageName, this.Id); } else { BridgeConnector.Socket.Emit(messageName); } } protected void CallMethod1(object val1, [CallerMemberName] string callerName = null) { var messageName = this.methodMessageNames.GetOrAdd(callerName, s => this.objectName + s); if (this.Id >= 0) { BridgeConnector.Socket.Emit(messageName, this.Id, val1); } else { BridgeConnector.Socket.Emit(messageName, val1); } } protected void CallMethod2(object val1, object val2, [CallerMemberName] string callerName = null) { var messageName = this.methodMessageNames.GetOrAdd(callerName, s => this.objectName + s); if (this.Id >= 0) { BridgeConnector.Socket.Emit(messageName, this.Id, val1, val2); } else { BridgeConnector.Socket.Emit(messageName, val1, val2); } } protected void CallMethod3(object val1, object val2, object val3, [CallerMemberName] string callerName = null) { var messageName = this.methodMessageNames.GetOrAdd(callerName, s => this.objectName + s); if (this.Id >= 0) { BridgeConnector.Socket.Emit(messageName, this.Id, val1, val2, val3); } else { BridgeConnector.Socket.Emit(messageName, val1, val2, val3); } } protected Task InvokeAsync(object arg = null, [CallerMemberName] string callerName = null) { Debug.Assert(callerName != null, nameof(callerName) + " != null"); lock (this.objLock) { return this.invocators.GetOrAdd(callerName, _ => { var getter = new Invocator(this, callerName, InvocationTimeout, arg); getter.Task().ContinueWith(_ => { lock (this.objLock) { return this.invocators.TryRemove(callerName, out var _); } }); return getter; }).Task(); } } protected void AddEvent(Action value, int? id = null, [CallerMemberName] string callerName = null) { Debug.Assert(callerName != null, nameof(callerName) + " != null"); var eventName = this.EventName(callerName); var eventKey = this.EventKey(eventName, id); lock (this.objLock) { var container = eventContainers.GetOrAdd(eventKey, _ => { var container = new EventContainer(); BridgeConnector.Socket.On(eventKey, container.OnEventAction); BridgeConnector.Socket.Emit($"register-{eventName}", id); return container; }); container.Register(value); } } protected void RemoveEvent(Action value, int? id = null, [CallerMemberName] string callerName = null) { Debug.Assert(callerName != null, nameof(callerName) + " != null"); var eventName = this.EventName(callerName); var eventKey = this.EventKey(eventName, id); lock (this.objLock) { if (eventContainers.TryGetValue(eventKey, out var container) && !container.Unregister(value)) { BridgeConnector.Socket.Off(eventKey); eventContainers.TryRemove(eventKey, out _); } } } protected void AddEvent(Action value, int? id = null, [CallerMemberName] string callerName = null) { Debug.Assert(callerName != null, nameof(callerName) + " != null"); var eventName = this.EventName(callerName); var eventKey = this.EventKey(eventName, id); lock (this.objLock) { var container = eventContainers.GetOrAdd(eventKey, _ => { var container = new EventContainer(); BridgeConnector.Socket.On(eventKey, container.OnEventActionT); BridgeConnector.Socket.Emit($"register-{eventName}", id); return container; }); container.Register(value); } } protected void RemoveEvent(Action value, int? id = null, [CallerMemberName] string callerName = null) { Debug.Assert(callerName != null, nameof(callerName) + " != null"); var eventName = this.EventName(callerName); var eventKey = this.EventKey(eventName, id); lock (this.objLock) { if (eventContainers.TryGetValue(eventKey, out var container) && !container.Unregister(value)) { BridgeConnector.Socket.Off(eventKey); eventContainers.TryRemove(eventKey, out _); } } } private string EventName(string callerName) { switch (this.SocketEventNameType) { case SocketEventNameTypes.DashedLower: return $"{this.objectName}-{callerName.ToDashedEventName()}"; case SocketEventNameTypes.CamelCase: return $"{this.objectName}-{callerName.ToCamelCaseEventName()}"; default: throw new ArgumentOutOfRangeException(); } } private string EventKey(string eventName, int? id) { return string.Format(CultureInfo.InvariantCulture, "{0}{1:D}", eventName, id); } internal abstract class Invocator { public abstract Task Task(); } internal class Invocator : Invocator { private readonly Task tcsTask; private TaskCompletionSource tcs; public Invocator(ApiBase apiBase, string callerName, int timeoutMs, object arg = null) { this.tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); this.tcsTask = this.tcs.Task; string eventName; string messageName; switch (apiBase.SocketTaskEventNameType) { case SocketTaskEventNameTypes.DashesLowerFirst: eventName = apiBase.invocationEventNames.GetOrAdd(callerName, s => $"{apiBase.objectName}-{s.StripAsync().LowerFirst()}-completed"); break; case SocketTaskEventNameTypes.NoDashUpperFirst: eventName = apiBase.invocationEventNames.GetOrAdd(callerName, s => $"{apiBase.objectName}{s.StripAsync()}Completed"); break; default: throw new ArgumentOutOfRangeException(); } switch (apiBase.SocketTaskMessageNameType) { case SocketTaskMessageNameTypes.DashesLowerFirst: messageName = apiBase.invocationMessageNames.GetOrAdd(callerName, s => $"{apiBase.objectName}-{s.StripAsync().LowerFirst()}"); break; case SocketTaskMessageNameTypes.NoDashUpperFirst: messageName = apiBase.invocationMessageNames.GetOrAdd(callerName, s => apiBase.objectName + s.StripAsync()); break; default: throw new ArgumentOutOfRangeException(); } BridgeConnector.Socket.Once(eventName, (result) => { lock (this) { try { var value = result; this.tcs?.SetResult(value); } catch (Exception ex) { this.tcs?.TrySetException(ex); } finally { this.tcs = null; } } }); if (arg != null) { _ = apiBase.Id >= 0 ? BridgeConnector.Socket.Emit(messageName, apiBase.Id, arg) : BridgeConnector.Socket.Emit(messageName, arg); } else { _ = apiBase.Id >= 0 ? BridgeConnector.Socket.Emit(messageName, apiBase.Id) : BridgeConnector.Socket.Emit(messageName); } System.Threading.Tasks.Task.Delay(InvocationTimeout).ContinueWith(_ => { if (this.tcs != null) { lock (this) { if (this.tcs != null) { var ex = new TimeoutException($"No response after {timeoutMs:D}ms trying to retrieve value {apiBase.objectName}.{callerName}()"); this.tcs.TrySetException(ex); this.tcs = null; } } } }); } public override Task Task() { return this.tcsTask as Task; } } [SuppressMessage("ReSharper", "InconsistentlySynchronizedField")] private class EventContainer { private Action eventAction; private Delegate eventActionT; private Action GetEventActionT() { return (Action)this.eventActionT; } private void SetEventActionT(Action actionT) { this.eventActionT = actionT; } public void OnEventAction() => this.eventAction?.Invoke(); public void OnEventActionT(T p) => this.GetEventActionT()?.Invoke(p); public void Register(Action receiver) { this.eventAction += receiver; } public void Register(Action receiver) { var actionT = this.GetEventActionT(); actionT += receiver; this.SetEventActionT(actionT); } public bool Unregister(Action receiver) { this.eventAction -= receiver; return this.eventAction != null; } public bool Unregister(Action receiver) { var actionT = this.GetEventActionT(); actionT -= receiver; this.SetEventActionT(actionT); return actionT != null; } } } }