using CryptoExchange.Net.Sockets.Default; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.Objects.Sockets { /// /// Subscription to a data stream /// public class UpdateSubscription { private readonly SocketConnection _connection; internal readonly Subscription _subscription; #if NET9_0_OR_GREATER private readonly Lock _eventLock = new Lock(); #else private readonly object _eventLock = new object(); #endif private bool _connectionEventsSubscribed = true; private List _connectionClosedEventHandlers = new List(); private List _connectionLostEventHandlers = new List(); private List> _resubscribeFailedEventHandlers = new List>(); private List> _connectionRestoredEventHandlers = new List>(); private List _activityPausedEventHandlers = new List(); private List _activityUnpausedEventHandlers = new List(); /// /// Event when the status of the subscription changes /// public event Action? SubscriptionStatusChanged; /// /// Event when the connection is lost. The socket will automatically reconnect when possible. /// public event Action ConnectionLost { add { lock (_eventLock) _connectionLostEventHandlers.Add(value); } remove { lock (_eventLock) _connectionLostEventHandlers.Remove(value); } } /// /// Event when the connection is closed and will not be reconnected /// public event Action ConnectionClosed { add { lock (_eventLock) _connectionClosedEventHandlers.Add(value); } remove { lock (_eventLock) _connectionClosedEventHandlers.Remove(value); } } /// /// Event when a lost connection is restored, but the resubscribing of update subscriptions failed /// public event Action ResubscribingFailed { add { lock (_eventLock) _resubscribeFailedEventHandlers.Add(value); } remove { lock (_eventLock) _resubscribeFailedEventHandlers.Remove(value); } } /// /// Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting. /// Note that when the executing code is suspended and resumed at a later period (for example, a laptop going to sleep) the disconnect time will be incorrect as the disconnect /// will only be detected after resuming the code, so the initial disconnect time is lost. Use the timespan only for informational purposes. /// public event Action ConnectionRestored { add { lock (_eventLock) _connectionRestoredEventHandlers.Add(value); } remove { lock (_eventLock) _connectionRestoredEventHandlers.Remove(value); } } /// /// Event when the connection to the server is paused based on a server indication. No operations can be performed while paused /// public event Action ActivityPaused { add { lock (_eventLock) _activityPausedEventHandlers.Add(value); } remove { lock (_eventLock) _activityPausedEventHandlers.Remove(value); } } /// /// Event when the connection to the server is unpaused after being paused /// public event Action ActivityUnpaused { add { lock (_eventLock) _activityUnpausedEventHandlers.Add(value); } remove { lock (_eventLock) _activityUnpausedEventHandlers.Remove(value); } } /// /// Event when an exception happens during the handling of the data /// public event Action Exception { add => _subscription.Exception += value; remove => _subscription.Exception -= value; } /// /// The id of the socket /// public int SocketId => _connection.SocketId; /// /// The id of the subscription /// public int Id => _subscription.Id; /// /// The last timestamp anything was received from the server /// public DateTime? LastReceiveTime => _connection.LastReceiveTime; /// /// The current websocket status /// public SocketStatus SocketStatus => _connection.Status; /// /// The current subscription status /// public SubscriptionStatus SubscriptionStatus => _subscription.Status; /// /// ctor /// /// The socket connection the subscription is on /// The subscription public UpdateSubscription(SocketConnection connection, Subscription subscription) { _connection = connection; _connection.ConnectionClosed += HandleConnectionClosedEvent; _connection.ConnectionLost += HandleConnectionLostEvent; _connection.ConnectionRestored += HandleConnectionRestoredEvent; _connection.ResubscribingFailed += HandleResubscribeFailedEvent; _connection.ActivityPaused += HandlePausedEvent; _connection.ActivityUnpaused += HandleUnpausedEvent; _subscription = subscription; _subscription.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x); } private void UnsubscribeConnectionEvents() { lock (_eventLock) { if (!_connectionEventsSubscribed) return; _connection.ConnectionClosed -= HandleConnectionClosedEvent; _connection.ConnectionLost -= HandleConnectionLostEvent; _connection.ConnectionRestored -= HandleConnectionRestoredEvent; _connection.ResubscribingFailed -= HandleResubscribeFailedEvent; _connection.ActivityPaused -= HandlePausedEvent; _connection.ActivityUnpaused -= HandleUnpausedEvent; _connectionEventsSubscribed = false; } } private void HandleConnectionClosedEvent() { UnsubscribeConnectionEvents(); // If we're not the subscription closing this connection don't bother emitting if (!_subscription.IsClosingConnection) return; List handlers; lock (_eventLock) handlers = _connectionClosedEventHandlers.ToList(); foreach(var callback in handlers) callback(); } private void HandleConnectionLostEvent() { if (!_subscription.Active) { UnsubscribeConnectionEvents(); return; } List handlers; lock (_eventLock) handlers = _connectionLostEventHandlers.ToList(); foreach (var callback in handlers) callback(); } private void HandleConnectionRestoredEvent(TimeSpan period) { if (!_subscription.Active) { UnsubscribeConnectionEvents(); return; } List> handlers; lock (_eventLock) handlers = _connectionRestoredEventHandlers.ToList(); foreach (var callback in handlers) callback(period); } private void HandleResubscribeFailedEvent(Error error) { if (!_subscription.Active) { UnsubscribeConnectionEvents(); return; } List> handlers; lock (_eventLock) handlers = _resubscribeFailedEventHandlers.ToList(); foreach (var callback in handlers) callback(error); } private void HandlePausedEvent() { if (!_subscription.Active) { UnsubscribeConnectionEvents(); return; } List handlers; lock (_eventLock) handlers = _activityPausedEventHandlers.ToList(); foreach (var callback in handlers) callback(); } private void HandleUnpausedEvent() { if (!_subscription.Active) { UnsubscribeConnectionEvents(); return; } List handlers; lock (_eventLock) handlers = _activityUnpausedEventHandlers.ToList(); foreach (var callback in handlers) callback(); } /// /// Close the subscription /// /// public Task CloseAsync() { return _connection.CloseAsync(_subscription); } /// /// Close the socket to cause a reconnect /// /// public Task ReconnectAsync() { return _connection.TriggerReconnectAsync(); } /// /// Unsubscribe a subscription /// /// internal async Task UnsubscribeAsync() { await _connection.UnsubscribeAsync(_subscription).ConfigureAwait(false); } /// /// Resubscribe this subscription /// /// internal async Task ResubscribeAsync() { return await _connection.ResubscribeAsync(_subscription).ConfigureAwait(false); } } }