HubConnection.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741
  1. #if !BESTHTTP_DISABLE_SIGNALR_CORE && !BESTHTTP_DISABLE_WEBSOCKET
  2. using BestHTTP.Futures;
  3. using BestHTTP.SignalRCore.Authentication;
  4. using BestHTTP.SignalRCore.Messages;
  5. using System;
  6. using System.Collections.Generic;
  7. namespace BestHTTP.SignalRCore
  8. {
  9. public sealed class HubOptions
  10. {
  11. /// <summary>
  12. /// When this is set to true, the plugin will skip the negotiation request if the PreferedTransport is WebSocket. Its default value is false.
  13. /// </summary>
  14. public bool SkipNegotiation { get; set; }
  15. /// <summary>
  16. /// The preferred transport to choose when more than one available. Its default value is TransportTypes.WebSocket.
  17. /// </summary>
  18. public TransportTypes PreferedTransport { get; set; }
  19. /// <summary>
  20. /// A ping message is only sent if the interval has elapsed without a message being sent. Its default value is 15 seconds.
  21. /// </summary>
  22. public TimeSpan PingInterval { get; set; }
  23. /// <summary>
  24. /// The maximum count of redirect negoitiation result that the plugin will follow. Its default value is 100.
  25. /// </summary>
  26. public int MaxRedirects { get; set; }
  27. public HubOptions()
  28. {
  29. this.SkipNegotiation = false;
  30. this.PreferedTransport = TransportTypes.WebSocket;
  31. this.PingInterval = TimeSpan.FromSeconds(15);
  32. this.MaxRedirects = 100;
  33. }
  34. }
  35. public sealed class HubConnection : BestHTTP.Extensions.IHeartbeat
  36. {
  37. public static readonly object[] EmptyArgs = new object[0];
  38. /// <summary>
  39. /// Uri of the Hub endpoint
  40. /// </summary>
  41. public Uri Uri { get; private set; }
  42. /// <summary>
  43. /// Current state of this connection.
  44. /// </summary>
  45. public ConnectionStates State { get; private set; }
  46. /// <summary>
  47. /// Current, active ITransport instance.
  48. /// </summary>
  49. public ITransport Transport { get; private set; }
  50. /// <summary>
  51. /// The IProtocol implementation that will parse, encode and decode messages.
  52. /// </summary>
  53. public IProtocol Protocol { get; private set; }
  54. /// <summary>
  55. /// This event is called when the connection is redirected to a new uri.
  56. /// </summary>
  57. public event Action<HubConnection, Uri, Uri> OnRedirected;
  58. /// <summary>
  59. /// This event is called when successfully connected to the hub.
  60. /// </summary>
  61. public event Action<HubConnection> OnConnected;
  62. /// <summary>
  63. /// This event is called when an unexpected error happen and the connection is closed.
  64. /// </summary>
  65. public event Action<HubConnection, string> OnError;
  66. /// <summary>
  67. /// This event is called when the connection is gracefully terminated.
  68. /// </summary>
  69. public event Action<HubConnection> OnClosed;
  70. /// <summary>
  71. /// This event is called for every server-sent message. When returns false, no further processing of the message is done
  72. /// by the plugin.
  73. /// </summary>
  74. public event Func<HubConnection, Message, bool> OnMessage;
  75. /// <summary>
  76. /// An IAuthenticationProvider implementation that will be used to authenticate the connection.
  77. /// </summary>
  78. public IAuthenticationProvider AuthenticationProvider { get; set; }
  79. /// <summary>
  80. /// Negotiation response sent by the server.
  81. /// </summary>
  82. public NegotiationResult NegotiationResult { get; private set; }
  83. /// <summary>
  84. ///
  85. /// </summary>
  86. public HubOptions Options { get; private set; }
  87. /// <summary>
  88. /// How many times this connection is redirected.
  89. /// </summary>
  90. public int RedirectCount { get; private set; }
  91. /// <summary>
  92. /// This will be increment to add a unique id to every message the plugin will send.
  93. /// </summary>
  94. private long lastInvocationId = 0;
  95. /// <summary>
  96. /// Store the callback for all sent message that expect a return value from the server. All sent message has
  97. /// a unique invocationId that will be sent back from the server.
  98. /// </summary>
  99. private Dictionary<long, Action<Message>> invocations = new Dictionary<long, Action<Message>>();
  100. /// <summary>
  101. /// This is where we store the methodname => callback mapping.
  102. /// </summary>
  103. private Dictionary<string, Subscription> subscriptions = new Dictionary<string, Subscription>(StringComparer.OrdinalIgnoreCase);
  104. /// <summary>
  105. /// When we sent out the last message to the server.
  106. /// </summary>
  107. private DateTime lastMessageSent;
  108. public HubConnection(Uri hubUri, IProtocol protocol)
  109. : this(hubUri, protocol, new HubOptions())
  110. {
  111. }
  112. public HubConnection(Uri hubUri, IProtocol protocol, HubOptions options)
  113. {
  114. this.Uri = hubUri;
  115. this.State = ConnectionStates.Initial;
  116. this.Options = options;
  117. this.Protocol = protocol;
  118. this.Protocol.Connection = this;
  119. this.AuthenticationProvider = new DefaultAccessTokenAuthenticator(this);
  120. }
  121. public void StartConnect()
  122. {
  123. if (this.State != ConnectionStates.Initial && this.State != ConnectionStates.Redirected)
  124. {
  125. HTTPManager.Logger.Warning("HubConnection", "StartConnect - Expected Initial or Redirected state, got " + this.State.ToString());
  126. return;
  127. }
  128. HTTPManager.Logger.Verbose("HubConnection", "StartConnect");
  129. if (this.AuthenticationProvider != null && this.AuthenticationProvider.IsPreAuthRequired)
  130. {
  131. HTTPManager.Logger.Information("HubConnection", "StartConnect - Authenticating");
  132. SetState(ConnectionStates.Authenticating);
  133. this.AuthenticationProvider.OnAuthenticationSucceded += OnAuthenticationSucceded;
  134. this.AuthenticationProvider.OnAuthenticationFailed += OnAuthenticationFailed;
  135. // Start the authentication process
  136. this.AuthenticationProvider.StartAuthentication();
  137. }
  138. else
  139. StartNegotiation();
  140. }
  141. private void OnAuthenticationSucceded(IAuthenticationProvider provider)
  142. {
  143. HTTPManager.Logger.Verbose("HubConnection", "OnAuthenticationSucceded");
  144. this.AuthenticationProvider.OnAuthenticationSucceded -= OnAuthenticationSucceded;
  145. this.AuthenticationProvider.OnAuthenticationFailed -= OnAuthenticationFailed;
  146. StartNegotiation();
  147. }
  148. private void OnAuthenticationFailed(IAuthenticationProvider provider, string reason)
  149. {
  150. HTTPManager.Logger.Error("HubConnection", "OnAuthenticationFailed: " + reason);
  151. this.AuthenticationProvider.OnAuthenticationSucceded -= OnAuthenticationSucceded;
  152. this.AuthenticationProvider.OnAuthenticationFailed -= OnAuthenticationFailed;
  153. SetState(ConnectionStates.Closed, reason);
  154. }
  155. private void StartNegotiation()
  156. {
  157. HTTPManager.Logger.Verbose("HubConnection", "StartNegotiation");
  158. if (this.State == ConnectionStates.CloseInitiated)
  159. {
  160. SetState(ConnectionStates.Closed);
  161. return;
  162. }
  163. if (this.Options.SkipNegotiation)
  164. {
  165. HTTPManager.Logger.Verbose("HubConnection", "Skipping negotiation");
  166. ConnectImpl();
  167. return;
  168. }
  169. SetState(ConnectionStates.Negotiating);
  170. // https://github.com/aspnet/SignalR/blob/dev/specs/TransportProtocols.md#post-endpoint-basenegotiate-request
  171. // Send out a negotiation request. While we could skip it and connect right with the websocket transport
  172. // it might return with additional information that could be useful.
  173. UriBuilder builder = new UriBuilder(this.Uri);
  174. builder.Path += "/negotiate";
  175. var request = new HTTPRequest(builder.Uri, HTTPMethods.Post, OnNegotiationRequestFinished);
  176. if (this.AuthenticationProvider != null)
  177. this.AuthenticationProvider.PrepareRequest(request);
  178. request.Send();
  179. }
  180. private void ConnectImpl()
  181. {
  182. HTTPManager.Logger.Verbose("HubConnection", "ConnectImpl");
  183. switch (this.Options.PreferedTransport)
  184. {
  185. case TransportTypes.WebSocket:
  186. if (this.NegotiationResult != null && !IsTransportSupported("WebSockets"))
  187. {
  188. SetState(ConnectionStates.Closed, "The 'WebSockets' transport isn't supported by the server!");
  189. return;
  190. }
  191. this.Transport = new Transports.WebSocketTransport(this);
  192. this.Transport.OnStateChanged += Transport_OnStateChanged;
  193. break;
  194. default:
  195. SetState(ConnectionStates.Closed, "Unsupportted transport: " + this.Options.PreferedTransport);
  196. break;
  197. }
  198. this.Transport.StartConnect();
  199. }
  200. private bool IsTransportSupported(string transportName)
  201. {
  202. // https://github.com/aspnet/SignalR/blob/release/2.2/specs/TransportProtocols.md#post-endpoint-basenegotiate-request
  203. // If the negotiation response contains only the url and accessToken, no 'availableTransports' list is sent
  204. if (this.NegotiationResult.SupportedTransports == null)
  205. return true;
  206. for (int i = 0; i < this.NegotiationResult.SupportedTransports.Count; ++i)
  207. if (this.NegotiationResult.SupportedTransports[i].Name == transportName)
  208. return true;
  209. return false;
  210. }
  211. private void OnNegotiationRequestFinished(HTTPRequest req, HTTPResponse resp)
  212. {
  213. if (this.State == ConnectionStates.CloseInitiated)
  214. {
  215. SetState(ConnectionStates.Closed);
  216. return;
  217. }
  218. string errorReason = null;
  219. switch (req.State)
  220. {
  221. // The request finished without any problem.
  222. case HTTPRequestStates.Finished:
  223. if (resp.IsSuccess)
  224. {
  225. HTTPManager.Logger.Information("HubConnection", "Negotiation Request Finished Successfully! Response: " + resp.DataAsText);
  226. // Parse negotiation
  227. this.NegotiationResult = NegotiationResult.Parse(resp.DataAsText, out errorReason, this);
  228. // TODO: check validity of the negotiation result:
  229. // If url and accessToken is present, the other two must be null.
  230. // https://github.com/aspnet/SignalR/blob/dev/specs/TransportProtocols.md#post-endpoint-basenegotiate-request
  231. if (string.IsNullOrEmpty(errorReason))
  232. {
  233. if (this.NegotiationResult.Url != null)
  234. {
  235. this.SetState(ConnectionStates.Redirected);
  236. if (++this.RedirectCount >= this.Options.MaxRedirects)
  237. errorReason = string.Format("MaxRedirects ({0:N0}) reached!", this.Options.MaxRedirects);
  238. else
  239. {
  240. var oldUri = this.Uri;
  241. this.Uri = this.NegotiationResult.Url;
  242. if (this.OnRedirected != null)
  243. {
  244. try
  245. {
  246. this.OnRedirected(this, oldUri, Uri);
  247. }
  248. catch (Exception ex)
  249. {
  250. HTTPManager.Logger.Exception("HubConnection", "OnNegotiationRequestFinished - OnRedirected", ex);
  251. }
  252. }
  253. StartConnect();
  254. }
  255. }
  256. else
  257. ConnectImpl();
  258. }
  259. }
  260. else // Internal server error?
  261. errorReason = string.Format("Negotiation Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}",
  262. resp.StatusCode,
  263. resp.Message,
  264. resp.DataAsText);
  265. break;
  266. // The request finished with an unexpected error. The request's Exception property may contain more info about the error.
  267. case HTTPRequestStates.Error:
  268. errorReason = "Negotiation Request Finished with Error! " + (req.Exception != null ? (req.Exception.Message + "\n" + req.Exception.StackTrace) : "No Exception");
  269. break;
  270. // The request aborted, initiated by the user.
  271. case HTTPRequestStates.Aborted:
  272. errorReason = "Negotiation Request Aborted!";
  273. break;
  274. // Connecting to the server is timed out.
  275. case HTTPRequestStates.ConnectionTimedOut:
  276. errorReason = "Negotiation Request - Connection Timed Out!";
  277. break;
  278. // The request didn't finished in the given time.
  279. case HTTPRequestStates.TimedOut:
  280. errorReason = "Negotiation Request - Processing the request Timed Out!";
  281. break;
  282. }
  283. if (errorReason != null)
  284. SetState(ConnectionStates.Closed, errorReason);
  285. }
  286. public void StartClose()
  287. {
  288. HTTPManager.Logger.Verbose("HubConnection", "StartClose");
  289. SetState(ConnectionStates.CloseInitiated);
  290. if (this.Transport != null)
  291. this.Transport.StartClose();
  292. }
  293. public IFuture<StreamItemContainer<TResult>> Stream<TResult>(string target, params object[] args)
  294. {
  295. var future = new Future<StreamItemContainer<TResult>>();
  296. long id = InvokeImp(target,
  297. args,
  298. callback: (message) =>
  299. {
  300. switch (message.type)
  301. {
  302. // StreamItem message contains only one item.
  303. case MessageTypes.StreamItem:
  304. {
  305. var container = future.value;
  306. if (container.IsCanceled)
  307. break;
  308. container.AddItem((TResult)this.Protocol.ConvertTo(typeof(TResult), message.item));
  309. // (re)assign the container to raise OnItem event
  310. future.AssignItem(container);
  311. break;
  312. }
  313. case MessageTypes.Completion:
  314. {
  315. bool isSuccess = string.IsNullOrEmpty(message.error);
  316. if (isSuccess)
  317. {
  318. var container = future.value;
  319. // While completion message must not contain any result, this should be future-proof
  320. //if (!container.IsCanceled && message.Result != null)
  321. //{
  322. // TResult[] results = (TResult[])this.Protocol.ConvertTo(typeof(TResult[]), message.Result);
  323. //
  324. // container.AddItems(results);
  325. //}
  326. future.Assign(container);
  327. }
  328. else
  329. future.Fail(new Exception(message.error));
  330. break;
  331. }
  332. }
  333. },
  334. isStreamingInvocation: true);
  335. future.BeginProcess(new StreamItemContainer<TResult>(id));
  336. return future;
  337. }
  338. public void CancelStream<T>(StreamItemContainer<T> container)
  339. {
  340. Message message = new Message {
  341. type = MessageTypes.CancelInvocation,
  342. invocationId = container.id.ToString()
  343. };
  344. container.IsCanceled = true;
  345. SendMessage(message);
  346. }
  347. public IFuture<TResult> Invoke<TResult>(string target, params object[] args)
  348. {
  349. Future<TResult> future = new Future<TResult>();
  350. InvokeImp(target,
  351. args,
  352. (message) =>
  353. {
  354. bool isSuccess = string.IsNullOrEmpty(message.error);
  355. if (isSuccess)
  356. future.Assign((TResult)this.Protocol.ConvertTo(typeof(TResult), message.result));
  357. else
  358. future.Fail(new Exception(message.error));
  359. });
  360. return future;
  361. }
  362. public IFuture<bool> Send(string target, params object[] args)
  363. {
  364. Future<bool> future = new Future<bool>();
  365. InvokeImp(target,
  366. args,
  367. (message) =>
  368. {
  369. bool isSuccess = string.IsNullOrEmpty(message.error);
  370. if (isSuccess)
  371. future.Assign(true);
  372. else
  373. future.Fail(new Exception(message.error));
  374. });
  375. return future;
  376. }
  377. private long InvokeImp(string target, object[] args, Action<Message> callback, bool isStreamingInvocation = false)
  378. {
  379. if (this.State != ConnectionStates.Connected)
  380. return -1;
  381. long invocationId = System.Threading.Interlocked.Increment(ref this.lastInvocationId);
  382. var message = new Message
  383. {
  384. type = isStreamingInvocation ? MessageTypes.StreamInvocation : MessageTypes.Invocation,
  385. invocationId = invocationId.ToString(),
  386. target = target,
  387. arguments = args,
  388. nonblocking = callback == null,
  389. };
  390. SendMessage(message);
  391. if (callback != null)
  392. this.invocations.Add(invocationId, callback);
  393. return invocationId;
  394. }
  395. private void SendMessage(Message message)
  396. {
  397. byte[] encoded = this.Protocol.EncodeMessage(message);
  398. this.Transport.Send(encoded);
  399. this.lastMessageSent = DateTime.UtcNow;
  400. }
  401. public void On(string methodName, Action callback)
  402. {
  403. On(methodName, null, (args) => callback());
  404. }
  405. public void On<T1>(string methodName, Action<T1> callback)
  406. {
  407. On(methodName, new Type[] { typeof(T1) }, (args) => callback((T1)args[0]));
  408. }
  409. public void On<T1, T2>(string methodName, Action<T1, T2> callback)
  410. {
  411. On(methodName,
  412. new Type[] { typeof(T1), typeof(T2) },
  413. (args) => callback((T1)args[0], (T2)args[1]));
  414. }
  415. public void On<T1, T2, T3>(string methodName, Action<T1, T2, T3> callback)
  416. {
  417. On(methodName,
  418. new Type[] { typeof(T1), typeof(T2), typeof(T3) },
  419. (args) => callback((T1)args[0], (T2)args[1], (T3)args[2]));
  420. }
  421. public void On<T1, T2, T3, T4>(string methodName, Action<T1, T2, T3, T4> callback)
  422. {
  423. On(methodName,
  424. new Type[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4) },
  425. (args) => callback((T1)args[0], (T2)args[1], (T3)args[2], (T4)args[3]));
  426. }
  427. public void On(string methodName, Type[] paramTypes, Action<object[]> callback)
  428. {
  429. Subscription subscription = null;
  430. if (!this.subscriptions.TryGetValue(methodName, out subscription))
  431. this.subscriptions.Add(methodName, subscription = new Subscription());
  432. subscription.Add(paramTypes, callback);
  433. }
  434. internal void OnMessages(List<Message> messages)
  435. {
  436. for (int messageIdx = 0; messageIdx < messages.Count; ++messageIdx)
  437. {
  438. var message = messages[messageIdx];
  439. try
  440. {
  441. if (this.OnMessage != null && !this.OnMessage(this, message))
  442. return;
  443. }
  444. catch (Exception ex)
  445. {
  446. HTTPManager.Logger.Exception("HubConnection", "Exception in OnMessage user code!", ex);
  447. }
  448. switch (message.type)
  449. {
  450. case MessageTypes.Invocation:
  451. {
  452. Subscription subscribtion = null;
  453. if (this.subscriptions.TryGetValue(message.target, out subscribtion))
  454. {
  455. for (int i = 0; i < subscribtion.callbacks.Count; ++i)
  456. {
  457. var callbackDesc = subscribtion.callbacks[i];
  458. object[] realArgs = null;
  459. try
  460. {
  461. realArgs = this.Protocol.GetRealArguments(callbackDesc.ParamTypes, message.arguments);
  462. }
  463. catch (Exception ex)
  464. {
  465. HTTPManager.Logger.Exception("HubConnection", "OnMessages - Invocation - GetRealArguments", ex);
  466. }
  467. try
  468. {
  469. callbackDesc.Callback.Invoke(realArgs);
  470. }
  471. catch (Exception ex)
  472. {
  473. HTTPManager.Logger.Exception("HubConnection", "OnMessages - Invocation - Invoke", ex);
  474. }
  475. }
  476. }
  477. break;
  478. }
  479. case MessageTypes.StreamItem:
  480. {
  481. long invocationId;
  482. if (long.TryParse(message.invocationId, out invocationId))
  483. {
  484. Action<Message> callback;
  485. if (this.invocations.TryGetValue(invocationId, out callback) && callback != null)
  486. {
  487. try
  488. {
  489. callback(message);
  490. }
  491. catch (Exception ex)
  492. {
  493. HTTPManager.Logger.Exception("HubConnection", "OnMessages - StreamItem - callback", ex);
  494. }
  495. }
  496. }
  497. break;
  498. }
  499. case MessageTypes.Completion:
  500. {
  501. long invocationId;
  502. if (long.TryParse(message.invocationId, out invocationId))
  503. {
  504. Action<Message> callback;
  505. if (this.invocations.TryGetValue(invocationId, out callback) && callback != null)
  506. {
  507. try
  508. {
  509. callback(message);
  510. }
  511. catch (Exception ex)
  512. {
  513. HTTPManager.Logger.Exception("HubConnection", "OnMessages - Completion - callback", ex);
  514. }
  515. }
  516. this.invocations.Remove(invocationId);
  517. }
  518. break;
  519. }
  520. case MessageTypes.Close:
  521. SetState(ConnectionStates.Closed, message.error);
  522. break;
  523. }
  524. }
  525. }
  526. private void Transport_OnStateChanged(TransportStates oldState, TransportStates newState)
  527. {
  528. HTTPManager.Logger.Verbose("HubConnection", string.Format("Transport_OnStateChanged - oldState: {0} newState: {1}", oldState.ToString(), newState.ToString()));
  529. switch (newState)
  530. {
  531. case TransportStates.Connected:
  532. SetState(ConnectionStates.Connected);
  533. break;
  534. case TransportStates.Failed:
  535. SetState(ConnectionStates.Closed, this.Transport.ErrorReason);
  536. break;
  537. case TransportStates.Closed:
  538. SetState(ConnectionStates.Closed);
  539. break;
  540. }
  541. }
  542. private void SetState(ConnectionStates state, string errorReason = null)
  543. {
  544. if (string.IsNullOrEmpty(errorReason))
  545. HTTPManager.Logger.Information("HubConnection", "SetState - from State: '" + this.State.ToString() + "' to State: '" + state.ToString() + "'");
  546. else
  547. HTTPManager.Logger.Information("HubConnection", "SetState - from State: '" + this.State.ToString() + "' to State: '" + state.ToString() + "' errorReason: '" + errorReason + "'");
  548. if (this.State == state)
  549. return;
  550. this.State = state;
  551. switch (state)
  552. {
  553. case ConnectionStates.Initial:
  554. case ConnectionStates.Authenticating:
  555. case ConnectionStates.Negotiating:
  556. case ConnectionStates.CloseInitiated:
  557. break;
  558. case ConnectionStates.Connected:
  559. try
  560. {
  561. if (this.OnConnected != null)
  562. this.OnConnected(this);
  563. }
  564. catch(Exception ex)
  565. {
  566. HTTPManager.Logger.Exception("HubConnection", "Exception in OnConnected user code!", ex);
  567. }
  568. HTTPManager.Heartbeats.Subscribe(this);
  569. this.lastMessageSent = DateTime.UtcNow;
  570. break;
  571. case ConnectionStates.Closed:
  572. if (string.IsNullOrEmpty(errorReason))
  573. {
  574. if (this.OnClosed != null)
  575. {
  576. try
  577. {
  578. this.OnClosed(this);
  579. }
  580. catch(Exception ex)
  581. {
  582. HTTPManager.Logger.Exception("HubConnection", "Exception in OnClosed user code!", ex);
  583. }
  584. }
  585. }
  586. else
  587. {
  588. if (this.OnError != null)
  589. {
  590. try
  591. {
  592. this.OnError(this, errorReason);
  593. }
  594. catch(Exception ex)
  595. {
  596. HTTPManager.Logger.Exception("HubConnection", "Exception in OnError user code!", ex);
  597. }
  598. }
  599. }
  600. HTTPManager.Heartbeats.Unsubscribe(this);
  601. break;
  602. }
  603. }
  604. void BestHTTP.Extensions.IHeartbeat.OnHeartbeatUpdate(TimeSpan dif)
  605. {
  606. switch (this.State)
  607. {
  608. case ConnectionStates.Connected:
  609. if (this.Options.PingInterval != TimeSpan.Zero && DateTime.UtcNow - this.lastMessageSent >= this.Options.PingInterval)
  610. SendMessage(new Message() { type = MessageTypes.Ping });
  611. break;
  612. }
  613. }
  614. }
  615. }
  616. #endif