using System; namespace Network.NetworkApplication { public sealed class SessionManager { private readonly Func utcNowProvider; private DateTimeOffset? lastAcceptedLivenessUtc; private DateTimeOffset? lastTransportActivityUtc; private DateTimeOffset? lastHeartbeatSentUtc; private DateTimeOffset? nextReconnectAtUtc; public SessionManager( SessionReconnectPolicy reconnectPolicy = null, Func utcNowProvider = null) { ReconnectPolicy = reconnectPolicy ?? SessionReconnectPolicy.Default; this.utcNowProvider = utcNowProvider ?? (() => DateTimeOffset.UtcNow); State = ConnectionState.Disconnected; } public event Action LifecycleChanged; public ConnectionState State { get; private set; } public SessionReconnectPolicy ReconnectPolicy { get; } public DateTimeOffset? LastLivenessUtc => lastAcceptedLivenessUtc; public DateTimeOffset? LastTransportActivityUtc => lastTransportActivityUtc; public DateTimeOffset? LastHeartbeatSentUtc => lastHeartbeatSentUtc; public DateTimeOffset? NextReconnectAtUtc => nextReconnectAtUtc; public TimeSpan? LastRoundTripTime { get; private set; } public string LastFailureReason { get; private set; } public bool CanSendHeartbeat => State == ConnectionState.LoggedIn; public bool IsReconnectDue { get { if (State != ConnectionState.ReconnectPending || nextReconnectAtUtc == null) { return false; } return utcNowProvider() >= nextReconnectAtUtc.Value; } } public bool IsHeartbeatDue { get { if (!CanSendHeartbeat) { return false; } if (lastHeartbeatSentUtc == null) { return true; } return utcNowProvider() - lastHeartbeatSentUtc.Value >= ReconnectPolicy.HeartbeatInterval; } } public void NotifyTransportConnected() { var now = utcNowProvider(); lastTransportActivityUtc = now; lastHeartbeatSentUtc = null; nextReconnectAtUtc = null; LastFailureReason = null; TransitionTo(ConnectionState.TransportConnected, SessionEventKind.TransportConnected, now); } public void NotifyTransportActivity() { lastTransportActivityUtc = utcNowProvider(); } public void NotifyLoginStarted() { TransitionTo(ConnectionState.LoginPending, SessionEventKind.LoginStarted, utcNowProvider()); } public void NotifyLoginSucceeded() { var now = utcNowProvider(); lastTransportActivityUtc = now; lastAcceptedLivenessUtc = now; LastFailureReason = null; TransitionTo(ConnectionState.LoggedIn, SessionEventKind.LoginSucceeded, now); } public void NotifyLoginFailed(string reason = null) { LastFailureReason = reason; TransitionTo(ConnectionState.LoginFailed, SessionEventKind.LoginFailed, utcNowProvider(), reason); } public void NotifyHeartbeatSent() { lastHeartbeatSentUtc = utcNowProvider(); RaiseEvent(SessionEventKind.HeartbeatSent, State, State, lastHeartbeatSentUtc.Value); } public void NotifyHeartbeatReceived() { var now = utcNowProvider(); lastTransportActivityUtc = now; lastAcceptedLivenessUtc = now; if (lastHeartbeatSentUtc.HasValue) { LastRoundTripTime = now - lastHeartbeatSentUtc.Value; } RaiseEvent(SessionEventKind.HeartbeatReceived, State, State, now); } public void NotifyInboundActivity() { var now = utcNowProvider(); lastTransportActivityUtc = now; lastAcceptedLivenessUtc = now; } public void NotifyTransportDisconnected(string reason = null) { LastFailureReason = reason; nextReconnectAtUtc = null; TransitionTo(ConnectionState.Disconnected, SessionEventKind.Disconnected, utcNowProvider(), reason); } public void Evaluate() { var now = utcNowProvider(); if (ShouldTimeout(now)) { LastFailureReason = "Heartbeat timeout"; TransitionTo(ConnectionState.TimedOut, SessionEventKind.TimedOut, now, LastFailureReason); if (ReconnectPolicy.AutoReconnect) { nextReconnectAtUtc = now + ReconnectPolicy.ReconnectDelay; TransitionTo(ConnectionState.ReconnectPending, SessionEventKind.ReconnectScheduled, now, LastFailureReason); } return; } if (State == ConnectionState.ReconnectPending && nextReconnectAtUtc.HasValue && now >= nextReconnectAtUtc.Value) { TransitionTo(ConnectionState.Reconnecting, SessionEventKind.ReconnectStarted, now, LastFailureReason); } } private bool ShouldTimeout(DateTimeOffset now) { switch (State) { case ConnectionState.TransportConnected: case ConnectionState.LoginPending: return lastTransportActivityUtc.HasValue && now - lastTransportActivityUtc.Value >= ReconnectPolicy.HeartbeatTimeout; case ConnectionState.LoggedIn: return lastAcceptedLivenessUtc.HasValue && now - lastAcceptedLivenessUtc.Value >= ReconnectPolicy.HeartbeatTimeout; default: return false; } } private void TransitionTo( ConnectionState newState, SessionEventKind eventKind, DateTimeOffset occurredAtUtc, string reason = null) { if (State == newState) { RaiseEvent(eventKind, State, State, occurredAtUtc, reason); return; } var previousState = State; State = newState; RaiseEvent(eventKind, previousState, newState, occurredAtUtc, reason); } private void RaiseEvent( SessionEventKind kind, ConnectionState previousState, ConnectionState currentState, DateTimeOffset occurredAtUtc, string reason = null) { LifecycleChanged?.Invoke(new SessionLifecycleEvent(kind, previousState, currentState, occurredAtUtc, reason)); } } }